1、为什么需要异步渲染?
-
从用户体验角度,如果每次修改值后都要进行渲染,页面会有闪烁效果,会造成不好的用户体验
-
从性能角度,每次修改后进行渲染,同一个值多次修改,最后一次修改前还需要一次无用的渲染,增加了性能的消耗
-
对于浏览器来说,每次的渲染都会引起的重绘或者回流,进行性能消耗
异步渲染和熟悉的节流函数最终目的是一致的,将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验。
2、vue中如何实现异步渲染?数据为什么频繁变化但只会更新一次?
在vue中主线程执行过程就是一个tick,在一个tick中只会更新一次。vue每次更新不会每次都重新渲染,会将变化放入到异步更新队列中,在下一个tick批量处理这些,所以在一个事件处理程序中多次修改数据,只会更新一次组件。
- 检测到数据变化
- 开启一个队列
- 在同一事件循环中缓冲所有数据
- 如果同一个watcher被多次触发,只会推入到队列中一次
3、在源码层面梳理的Vue的异步渲染过程?
关于vue2渲染过程:juejin.cn/post/710428…
1、当我们修改赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。
defineReactive() {
set: function reactiveSetter (newVal) {
dep.notify();
}
}
2、notify函数中,将所有的订阅组件watcher中的update方法执行一遍。
Dep.prototype.notify = function notify () {
// 拷贝所有组件的watcher
var subs = this.subs.slice();
...
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
3、update函数得到执行后,默认情况下lazy是false,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
4、queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。
默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。
function queueWatcher (watcher) {
...
// 在全局队列里存储将要响应的变化update函数
queue.push(watcher);
...
// 当async配置是false的时候,页面更新是同步的
if (!config.async) {
flushSchedulerQueue();
return
}
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}
5、nextTick函数的执行后,传入的flushSchedulerQueue函数又一次push进callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。
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;
})
}
}
6、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)) {
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 {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
7、flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际是第5步的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]();
}
}
8、callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第4步中的存储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进去
for (index = 0; index < queue.length; index++) {
...
watcher.run();
// 渲染 ...
}
}
9、watcher.run的实现在构造函数Watcher原型链上,初始状态下active属性为true,直接执行Watcher原型链的set方法。
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
...
}
};
10、get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。
Watcher.prototype.get = function get () {
pushTarget(this);
// 将实例push到全局数组targetStack
var vm = this.vm;
value = this.getter.call(vm, vm);
...
}
11、实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。
function () {
vm._update(vm._render(), hydrating);
};
12、实例的_update函数执行后,将会把两次的虚拟节点传入vm的 patch 方法执行渲染操作。
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);
}
...
};
4、nextTick实现原理
-
nextTick并不是浏览器本身提供的一个异步API,而是Vue中使用由浏览器本身提供的原生异步API封装而成的一个异步封装方法,上面第5第6段是它的实现源码。
-
它对于浏览器异步API的选用规则如下,Promise存在取Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。
-
nextTick即有可能是微任务,也有可能是宏任务,从优先Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
5、vue能不能同步渲染
1. Vue.config.async = false
当然是可以的,在第四段源码里,我们能看到如下一段,当config里的async的值为false的情况下,并没有将flushSchedulerQueue加到nextTick里,而是直接执行了flushSchedulerQueue,就相当于把本次data里的值变化时,页面做了同步渲染。
function queueWatcher (watcher) {
...
// 在全局队列里存储将要响应的变化update函数
queue.push(watcher);
...
// 当async配置是false的时候,页面更新是同步的
if (!config.async) {
flushSchedulerQueue();
return
}
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}
在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。
import Vue from 'Vue'
Vue.config.async = false
2. this._watcher.sync = true
在Watch的update方法执行源码里,可以看到当this.sync为true时,这时候的渲染也是同步的。
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
- 在开发代码中,需要将本次watcher的sync属性修改为true,对于watcher的sync属性变化只需要在需要同步渲染的数据变化操作前执行this._watcher.sync=true,这时候则会同步执行页面渲染动作。
像下面的写法中,页面会渲染出val为1,而不会渲染出2,最终渲染的结果是3,但是官网未推荐该用法,请慎用。
new Vue({
el: '#app',
sync: true,
template: '<div>{{val}}</div>',
data () { return { val: 0 } },
mounted () {
this._watcher.sync = true
this.val = 1
debugger
this._watcher.sync = false
this.val = 2
this.val = 3
}
})
我们了解了Vue中为什么采用异步渲染页面的原因,并且从源码的角度剖析了整个渲染前的操作链路,同时剖析出Vue中的异步方法nextTick的实现与原生的异步API直接的联系。最后也从源码角度下了解到,Vue并非不能同步渲染,当我们的页面中需要同步渲染时,做适当的配置即可满足。