事件循环&nextTick原理&异步渲染
1.事件循环机制
众所周知,js是单线程的,即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback任务队列,进行后续的操作,最大程度的利用CPU。
任务队列
一. 分类
1.microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;
典型:Promise、Object.observe、MutationObserver
2.macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
典型:整体代码script,setTimeout,setInterval、I/O、UI render
二. 流程
如下图,初始化运行script是一个宏任务,此过程中出现的新的宏任务setTimeout被放到macrotask队列,微任务Promise.then放置到microtask队列,并且将比setTimeout优先执行,如果Promise.then执行时又产生了微任务,微任务将在插入当前微任务队列下,直到所有微任务队列执行完毕才会开始执行setTimeout(在Promise.then加入死循环页面将卡住,一直停留在微任务)
参考视频:https://www.bilibili.com/video/BV1VE411u7Xx?t=602
2.nextTick
原理
简化来说,nextTick的作用就是将一堆任务放到一个异步函数中,当主线程代码全部执行完就将这些任务按照执行,根据浏览器兼容性的不同,nextTick选用了四种异步api,优先级(Promise > MutationObserver > setImmediate > setTimeout)前两种是微任务,后两种是宏任务,根据以上任务队列的知识可知,nextTick为了更快执行,首先选用微任务,只有当浏览器不兼容才会采取宏任务方式,这是Vue2.6.11版本的源码,之前的版本对dom操作事件强行使用宏任务api。
/* 执行回调队列的任务 */
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => { /* 将任务放入回调队列 */
if (cb) {
try { cb.call(ctx) } catch (e) {
handleError(e, ctx, 'nextTick')
}
}
else if (_resolve) { _resolve(ctx) }
})
if (!pending) {/* 上一个callback数组已经清空,任务已经作为同步代码在执行了 */
pending = true
timerFunc()/* 将flushCallbacks放到到任务队列中 */
}
if (!cb && typeof Promise !== 'undefined') {/* 没有cb则返回Promise实例*/
return new Promise(resolve => {
_resolve = resolve
})
}
}
先举个例子,看下面输出:
const $name = this.$refs.name
this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
this.name = ' name改喽 '
console.log('数据1:' + this.name);
console.log(' 同步方式setter后:' + this.$refs.name.innerHTML)
setTimeout(() => console.log("setTimeout方式:" + this.$refs.name.innerHTML))
this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
/* 结果:
数据1: name改喽
同步方式setter后:SHERlocked93
setter前:SHERlocked93
setter后: name改喽
setTimeout方式: name改喽
*/
原因:
第1-2行:
数据更改是同步的,dom更改操作异步且此时还没有在主线程执行,所以输出为更改后的数据和更改前的dom。
setter前:
nextTick的第一个任务,dom更改任务在它之后。
setter后:
在此之前,name进行了更新,然后会有一个触发页面渲染的回调被加入到nextTick的callbacks中,然后setter后的输出函数也将加入,当主线程任务未执行完时,callback任务数组=[setter前输出、页面更新函数、setter后输出]。
setTimeout
由于setTimeout是宏任务并且在代码顺序后面,不管nextTick使用的是微任务api还是宏任务api,都将在前面执行。
3.异步渲染
在Vue中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里(Promise、MutationObserver、setImmidiate、setTimeout),直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,执行一次渲染操作,具体步骤如下图
上面的例子可以用异步渲染进行深入,name进行了更新(数据是同步更新),会触发name对应的Object.defineProperty里面的set()函数,然后通过dep.notify通知name的所有订阅者watcher执行update,watcher会根据设置的sync属性确定是否直接执行更新,默认sync=false
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
然后调用queueWatcher对这个watcher添加到全局数组queue里并且进行处理,当不是waiting态的时候nextTick传递flushSchedulerQueue(更新页面函数)任务,将此任务存入callback数组,利用异步API执行这个函数。
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {/* 判断是否进入过队列了 */
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) { i-- }/* */
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
轮到flushSchedulerQueue执行时对之前存入的queue进行排序,然后逐个调用渲染的run函数,run函数调用get函数,get函数将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。
function flushSchedulerQueue ()
var watcher, id;
// 安装id从小到大开始排序,越小的越前触发的update
queue.sort(function (a, b) { return a.id - b.id; });
// queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去
for (index = 0; index < queue.length; index++) {
...
watcher.run(); // 渲染
...
}
}
Watcher.prototype.get = function get () {
pushTarget(this); // 将实例push到全局数组targetStack
var vm = this.vm;
value = this.getter.call(vm, vm);
...
}
getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update,_update函数执行后,将会把两次的虚拟节点传入vm的patch方法执行渲染操作。
function () {
vm._update(vm._render(), hydrating);
};
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
...
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
};
参考:https://blog.csdn.net/qq_27053493/article/details/105213003