1. 什么是异步渲染?
环境补充:当数据在同步变化的时候,页面订阅的响应操作为什么不会与数据变化完全对应,而是在所有的数据变化操作做完之后,页面才会得到响应,完成页面渲染。
从一个例子体验一下异步渲染机制:
import Vue from 'Vue'
new Vue({
el: '#app',
template: '<div>{{val}}</div>',
data () { return { val: 'init' } },
mounted () {
this.val = '我是第一次页面渲染' // debugger
this.val = '我是第二次页面渲染'
const st = Date.now()
while(Date.now() - st < 3000) {}
}
})
上面这一段代码中,在 mounted
里给 val
属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在 mounted
里有两次渲染。
而由于 Vue
内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的 val
只做一次页面渲染。
而且页面是在执行所有的同步代码执行完后才能得到渲染,在上述例子里的 while
阻塞代码之后,页面才会得到渲染,就像在熟悉的 setTimeout
里的回调函数的执行一样,这就是的异步渲染。
如果熟悉 React
,应该很快能想到多次执行 setState
函数时,页面 render
的渲染触发,实际上与上面所说的 Vue
的异步渲染有异曲同工之妙。
2. Vue 中如何实现异步渲染?
先总结一下原理,在 Vue
中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步 API 的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。
拿上面例子来说,当 val
第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val
第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then
的回调函数中,等到所有同步代码执行完后,then
函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。
异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。
这里触发渲染的异步 API 优先考虑 Promise
,其次MutationObserver
,如果没有MutationObserver
的话,会考虑setImmediate
,没有setImmediate
的话最后考虑是setTimeout
。
接下来在源码层面梳理一下的 Vue
的异步渲染过程。
2.1 流程图
2.2 源码分析
-
属性发生变化,会触发属性
setter
函数,会调用dep.notify
,对dep
里面收集的所有watcher
进行调用其update
的操作。// Observer.defineReactive function defineReactive () { const dep = new Dep() Object.defineProperty(obj, key, { get: function reactiveGetter () { //...省略... return value }, set: function reactiveSetter (newVal) { //...省略... dep.notify(); } }) }
// Dep.notify Dep.prototype.notify = function notify () { // ...省略... // 拷贝所有组件的watcher var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
-
update
函数得到执行后,默认情况下lazy
是false
,sync
也是false
,直接进入把所有响应变化存储进全局数组queueWatcher
函数下。// Watcher.update Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
-
queueWatcher
函数里,首先获取了watcher
的id
,然后判断是否存在,如果不存在,那么就标记成true
,而后继续了一个if
判断,这个flushing
就是队列是否正在更新的标志,当队列正在更新的时候,flushing
为true
。所以,如果这时候队列没有更新,会先将组件的watcher
存进全局数组变量queue
里。默认情况下config.async
是true
,直接进入nextTick
的函数执行,nextTick
是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue
函数。function queueWatcher (watcher) { // 在全局队列里存储将要响应的变化update函数 queue.push(watcher); const id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher) } else { // ...省略... } if (!waiting) { waiting = true; //无论调用多少次queueWatcher方法,该if内代码只执行一次 // 当async配置是false的时候,页面更新是同步的 if (!config.async) { flushSchedulerQueue(); return; } // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数 nextTick(flushSchedulerQueue); } }
-
nextTick
函数的执行后,传入的flushSchedulerQueue
函数又一次push
进callbacks
全局数组里,pending
在初始情况下是false
,这时候将触发timerFunc
。// 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); } }); if (!pending) { pending = true; timerFunc(); //触发 } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }); } }
-
timerFunc
函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks
函数。var timerFunc;// 这里Vue内部对于异步API的选用,由Promise、MutationObserver、setImmediate、setTimeout里取一个 //取用的规则是 Promise存在取由Promise,不存在取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在setTimeout。 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]") ) { 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)) { timerFunc = () => { setImmediate(flushCallbacks); }; } else { timerFunc = () => { setTimeout(flushCallbacks, 0); }; }
-
flushCallbacks
函数中将遍历执行nextTick
里push
的callback
全局数组,全局callback
数组中实际是第4步的push
的flushSchedulerQueue
的执行函数。// 将nextTick里push进去的flushSchedulerQueue函数进行for循环依次调用 function flushCallbacks () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } }
-
callback
遍历执行的flushSchedulerQueue
函数中,flushSchedulerQueue
里先按照id
进行了优先级排序,接下来将第 3 步中的存储watcher
对象全局queue
遍历执行,触发渲染函数watcher.run
。function flushSchedulerQueue () { var watcher, id;// 按照id从小到大开始排序,越小的越前触发的 //updatequeue.sort(function (a, b) { // return a.id - b.id; //});// queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去 // ...省略... flushing = true; // 表明正在更新中 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { //beforeUpdate watcher.before() } id = watcher.id has[id] = null // 响应式 watcher.run() //...省略 }
watcher.run
的实现在构造函数Watcher
原型链上,初始状态下active
属性为true
,直接执行Watcher
原型链的set
方法。Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); } };
3. nextTick的实现原理
首先nextTick
并不是浏览器本身提供的一个异步API,而是Vue中,用过由浏览器本身提供的原生异步API封装而成的一个异步封装方法,上面第 4 第 5 段是它的实现源码。
它对于浏览器异步API的选用规则如下,Promise
存在取由Promise.then
,不存在Promise
则取MutationObserver
,MutationObserver
不存在setImmediate
,setImmediate
不存在最后取setTimeout
来实现。
从上面的取用规则也可以看出来,nextTick
既有可能是微任务,也有可能是宏任务,从优先去Promise
和MutationObserver
可以看出nextTick
优先微任务,其次是setImmediate
和setTimeout
宏任务。
记得同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。
4. Vue可以同步渲染嘛?
-
Vue.config.async = false
在 2.2 的第3段源码里,当
config
里的async
的值为为false
的情况下,并没有将flushSchedulerQueue
加到nextTick
里,而是直接执行了flushSchedulerQueue
,就相当于把本次data
里的值变化时,页面做了同步渲染。if (!config.async) { flushSchedulerQueue(); return; } // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数 nextTick(flushSchedulerQueue);
import Vue from 'Vue' Vue.config.async = false //加入下一句即可让你的页面渲染同步进行
-
this._watcher.sync = true
在
Watch
的update
方法执行源码里(2.2 部分第 2 段源码),可以看到当this.sync
为true
时,这时候的渲染也是同步的。if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); }
参考文档: