详解从Vue.js源码看异步更新DOM策略及nextTick(3)
它是一个立即执行函数,返回一个queueNextTick接口。
传入的cb会被push进callbacks中存放起来,然后执行timerFunc(pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次)。
timerFunc是什么?
看了源码发现timerFunc会检测当前环境而不同实现,其实就是按照Promise,MutationObserver,setTimeout优先级,哪个存在使用哪个,最不济的环境下使用setTimeout。
这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。
如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
为什么要优先使用microtask?我在顾轶灵在知乎的回答中学习到:
JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。
setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。
要创建一个新的 microtask,优先使用 Promise,如果浏览器不支持,再尝试 MutationObserver。
实在不行,只能用 setTimeout 创建 task 了。
为啥要用 microtask?
根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。
反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。
参考顾轶灵知乎的回答
首先是Promise,(Promise.resolve()).then()可以在microtask中加入它的回调,
MutationObserver新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入microtask,即textNode.data = String(counter)时便会加入该回调。
setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
flushSchedulerQueue
/*Github:https://github.com/answershuto*/ /** * Flush both queues and run the watchers. */ /*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/ function flushSchedulerQueue () { flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. /* 给queue排序,这样做可以保证: 1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。 2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建 3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。 */ queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/ for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id /*将has的标记删除*/ has[id] = null /*执行watcher*/ watcher.run() // in dev build, check and stop circular updates. /* 在测试环境中,检测watch是否在死循环中 比如这样一种情况 watch: { test () { this.test++; } } 持续执行了一百次watch代表可能存在死循环 */ if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state /**/ /*得到队列的拷贝*/ const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() /*重置调度者的状态*/ resetSchedulerState() // call component updated and activated hooks /*使子组件状态都改编成active同时调用activated钩子*/ callActivatedHooks(activatedQueue) /*调用updated钩子*/ callUpdateHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }
内容版权声明:除非注明,否则皆为本站原创文章。