vue的执行机制
首先我们知道vue是异步渲染的,至于为什么要采用异步渲染,是因为如果不采取异步更新,那么每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue 会在本轮数据更新后,再去异步更新数据。
随之而来的问题
如果是异步渲染,我们在修改数据时,如果立即去获取更新后的数据的话,虽然DOM更新了但是拿到的还是之前的数据,并且在下一轮渲染时,才会拿到上一轮更新的数据。
NextTick是什么
根据官方对其的定义:在下次 DOM 更新循环结束之后执行延迟回调 。在修改数据之后立即使用这个方法,获取更新后的 DOM
通俗来说就是使用NextTick可以立即获取dom更新后的数据进行更新。
如何使用及使用场景
该api的使用场景就是当dom发生变化时,想要立即获取更新后的数据时,就可以使用该api。
创建模板
首先我们用ul
和li
来当作列表来渲染。然后给li
标签使用v-for来循环渲染一个列表。并且在增添一个添加按钮并绑定点击事件。
<template>
<div class="next_tick">
<ul ref="ulRef">
<li v-for="(item,index) in listArr" :key="index">{{ item}}</li>
</ul>
<button @click="add">添加</button>
</div>
</template>
实例
在data里面定义一个数组供li标签循环。
data () {
return {
listArr: ['随机数1','随机数2','随机数3']
}
},
方法
此时我们创建点击事件并且实现每点击一次按钮就新增一条随机数,并且渲染到ul这个列表里。然后在控制台打印列表的长度。
methods: {
add() {
this.listArr.push(Math.random())
const ulele = this.$refs.ulRef;
const length = ulele.childNodes.length;
console.log('length',length)
}
}
遇到的问题
当我们点击新增按钮时,我们可以看到此时页面上已经渲染上去了,新增了一条随机数,但是我们尝试获取列表长度的时候,还是初始值3。
使用nextTick
使用nextTick之后,在去尝试获取列表长度。
methods: {
add() {
this.items.push(Math.random())
this.$nextTick(() => {
const ulele = this.$refs.ulRef;
const length = ulele.childNodes.length;
console.log('length',length)
})
}
}
可以发现使用nextTick之后,可以实时获取长度了。
批量更新
发现一个有意思的地方就是vue会批量更新数据,并不会一条一条更新而是批量更新。
methods: {
add() {
this.items.push(Math.random())
this.items.push(Math.random())
this.items.push(Math.random())
this.$nextTick(() => {
const ulele = this.$refs.ulRef;
const length = ulele.childNodes.length;
console.log('length',length)
})
}
}
同时新增了三个随机数,并没有打印三次,而是直接一次性打印出了一轮的数据更新结果。
原理
涉及到的四个函数nextTick、timeFun、flushCallbacks
、setTimeout
。
let pending=false;
let callBacks=[];//存放的是回调函数,存放的第一个回调函数是数据更新的回调函数
//调用this.$nextTick时执行的函数
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
//在数据首次修改时,pending为false,修改后,pending变成true
if (!pending) {
pending = true;
//在这里用到了事件循环
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
//定义eventLoop函数
let timeFunc=null;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
//清空事件队列中的回调函数,第一个回调函数是flushSchedulerQueue ()
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
//flushSchedulerQueue的核心代码,执行数据更新操作
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
//调用beforeUpdate()钩子函数
watcher.before();
}
id = watcher.id;
has[id] = null;
//执行更新
watcher.run();
}
}
在下次DOM
更新循环结束之后执行的延迟回调。nextTick
主要使用了宏任务和微任务。根据执行环境分别尝试采用
Vue
的 nextTick
的实现原理总结如下
nextTick
是Vue
提供的一个全局的API
,由于Vue
的异步更新策略导致我们对数据的修改不会立马体现到都没变化上,此时如果想要立即获取更新后的dom
的状态,就需要使用这个方法。Vue
在更新dom
时是异步执行的。只要监听到数据变化,Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher
被多次触发,只会被推入到队列中一次。这种在缓存时去重对于避免不必要的计算和dom操作是非常重要的。nextTick
方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。 用一张图可以很好的描述下这个过程。
无论是微任务还是宏任务,都会放到flushCallbacks使用
这里将callbacks里面的函数复制一份,同时callbacks置空。依次执行callbacks里面的函数。
pending =false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++){
copies[ i ]( )
}
}