vue nextTick原理
后面谈到了vue2.x的响应式原理,vue.js在视图更新采纳的是异步更新策略,咱们来看看它是怎么做到的。
/** ? */for(let i = 0; i < 100; i++) { this.count++;}/** ? */在dom更新后执行一些操作this.$nextTick(fn)
先抛出两个问题:
- for循环更新count数值,dom会被更新100次吗?
- nextTick是如何做到监听dom更新结束的?
异步更新波及到js的运行机制,具体的可看这里
【event loop机制】
这篇文章呢咱们次要从源码角度来剖析nextTick的原理实现。
这是咱们响应式外面的watcher类
<!--观察者Watcher类-->class Watcher { constructor () { Dep.target = this // new Watcher的时候把观察者寄存到Dep.target外面 } update () { queueWatcher(this) // 异步更新策略 } run () { // dom在这里执行真正的更新 }}
watcher对象在进行更新执行update,外部次要执行了一个queueWatcher函数,将watcher对象作为this进行传递,所以咱们便从queueWatcher这个口子开始。
queueWatcher
queueWatcher函数在scheduler文件外面
/** queueWatcher函数*/let has = {};let queue = [];let waiting = false;function queueWatcher (watcher: Watcher) { const id = watcher.id // 避免queue队列wachter对象反复 if (has[id] == null) { has[id] = true queue.push(watcher) // 传递本次的更新工作 if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } }}/** flushSchedulerQueue函数 */function flushSchedulerQueue () { let watcher, id; for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; // 执行更新 watcher.run(); } // 更新结束复原标记位 waiting = false;}
- queue外面寄存着咱们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,雷同的watcher对象只会被退出到queue队列一次。
- flushSchedulerQueue函数顺次调用了wacther对象的run办法执行更新。并作为回调传递给了nextTick函数。
- waiting这个标记位代表咱们是否曾经向nextTick函数传递了更新工作,nextTick会在以后task完结后再去解决传入的回掉,只须要传递一次,更新结束再重置这个标记位。
next-tick
let callbacks = [];let pending = false;let timerFunc;/**----- nextTick -----*/function nextTick (cb) { // 把传进来的回调函数放到callbacks队列里 callbacks.push(cb); // pending代表一个期待状态 等这个tick执行 if (!pending) { pending = true timerFunc() } // 如果没传递回调 提供一个Promise化的调用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }}/**----- timerFunc ----*/// 1、优先思考Promise实现if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {// 2、降级到MutationObserver实现 let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// 3、降级到setImmediate实现 timerFunc = () => { setImmediate(flushCallbacks) }} else {// 4、如果以上都不反对就用setTimeout来兜底了 timerFunc = () => { setTimeout(flushCallbacks, 0) }}function flushCallbacks () { // 将callbacks中的cb顺次执行 pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() }}
传进来的回调函数会被保留到callbacks队列外面,这里应用callbacks 而没有在nextTick中间接执行回调函数,是因为这样能够保障在同一个tick 内屡次执行nextTick,在一个tick外面实现渲染,不会开启多个异步工作。
// 举个栗子????// 如果咱们间接在nexttick外面间接执行回调function nextTick (cb) { setTimeout(cb)}nextTick(cb1)nextTick(cb2)这种状况下就会开启两个异步工作,也就是两次事件循环,造成了页面不必要的渲染
- timerFunc是实现的外围,它会优先应用Promise等microtask,保障在同一个事件循环外面执行,这样页面只须要渲染一次。切实不行的话用setTimeout来兜底,尽管会造成二次渲染,但这也是最差的状况。vue在这里用了降级解决的策略。
$nextTick
最初再把nexttick函数挂到Vue原型上就OK了
Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this)}
小结
vue异步更新,实质上是js事件机制的一种使用,优先思考了具备高优先级的microtask,为了兼容,又做了降级策略。
当初再回头看结尾的那两个问题
- for循环更新count数值,dom会被更新100次吗?
不会,因为queueWatcher函数做了过滤,雷同的watcher对象不会被反复增加。
- nextTick是如何做到监听dom更新结束的?
vue用异步队列的形式来管制DOM更新和nextTick回调先后执行,保障了能在dom更新后在执行回调。