nextick原理
我丢,我这个居然忘了写,要不是重新看我的面经的话差点就忘了。啧啧啧,补上。(我发现我之前理解的nextick有点毛病,重新去看了一下技术博客和书总结了一下。
什么是异步渲染
对于上面的代码,首先假如是直接去渲染的话肯定是要渲染两次:第一次渲染,然后变成第二次渲染。这样的话就会造成性能浪费,因为我们实际上只需要渲染最后一次就行了。所以vue就使用了异步渲染,去达到只渲染一次这种效果。也就是说它将第一次赋值和第二次赋值带来的响应进行了合并,然后对最后的datas进行了一次页面渲染。
为什么需要异步渲染
从用户体验,你看第二次重新改变的话肯定会闪烁一下,那肯定用户就不得舒服了晒。然后就是性能,渲染一次肯定要比渲染两次的性能消耗要低一点。
怎么实现vue异步渲染呢?
在Vue中的异步渲染实际上是在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有需要渲染变化的部分合并起来,最终执行一次渲染操作。比如上面的例子,首先当datas第一次赋值,页面渲染文字,但是这个渲染会暂存,datas第二次赋值时,再次暂存将要引起的变化,这些变化会被丢到异步API,promise.then的回调函数中(微任务队列),等到所有同步代码执行完之后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将数组里的所有数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作。
那么在遍历数组的时候进行了什么操作呢?就是会对数组里面的数据进行一些筛查操作,将重复操作国的数据进行处理,实际就是先复制的丢弃不渲染,然后按照优先级组合成一套数据渲染。这样就大概是原理的全部内容,接下来从原理入手分析。
异步渲染原理
首先当我们给datas赋值的时候this.datas = '第一次渲染',datas所绑定的Object.defineProperty的setter函数触发,然后触发所订阅的notify函数。(vue响应式原理)
defineReactive() {
...
set: function reactiveSetter(newVal) {
dep.notify()
}
}
在notify中,会将收集的所有订阅组件watcher中的update方法执行一遍。
Dep.prototype.notify = function notify() {
...
let subs = this.subs.slice();
for(let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
update函数得到执行后,默认情况下lazy是fasle,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。
Watcher.prototype.update = function update() {
...
if(this.lazy) { //如果是懒加载
this.dirty = true;
} else if (this.sync) { //如果是同步,立即响应
this.run()
} else {
queueWatcher(this)
}
}
queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。
function queueWatcher(watcher) {
...
queue.push(watcher) //在全局队列里存储将要响应的变化update函数
if(!config.async) { //如果页面更新是同步
flushSchedulerQueue()
return
}
//将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}
nextTick函数执行后,传入的flushSchedulerQueue又一次被push进callbacks全局数组中,pending在初始情况下是false,这时候将触发timerFunc。
function nextTick(cb, ctx) {
let _resolve;
callbacks.push(function() {
if(cb) {
try {
ca.call(ctx)
} catch(e) {handleError(e, ctx, 'nextTick)}
} else if(_resolve){
_resolve(ctx)
}
})
if(!pending) {
pending = true
timerFunc();
}
if(!cb && typeof Promise !== 'undefined) {
return new Promise(resolve => {_resolve = resolve})
}
}
timerFunc函数是由浏览器的Promise、MutationObserve、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。(为了简便这里只写Promsie)
let timerFunc;
if(typeof Promise !== 'undefined' && isNative(Promise)) {
let p = Promise.resolve();
timerFunc = function() {
p.then(flushCallbacks);
if(isIos) {setTimeout(noop)}
}
ifUsingMicroTask = true
}...
flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际上是是之前push的flushScheduleQueue的执行函数
//将nextTick里push进去的flushScheduleQueue函数依次调用
function flushCallbacks() {
pending = false;
let copies = callbacks.slice(0) //复制
callbacks.length = 0
for( let i = 0; i < copies.length; i++) {
copies[i]();
}
}
callback遍历执行的flushScheduleQueue函数中,flushScheduleQueue里先按照id进行了优先级排序,接下来将之前存储的watcher对象全局queue遍历执行,触发渲染函数watcher.run
function flushScheduleQueue() {
let watcher, id;
//按照id从小到大开始排序,越小的越前触发
updatequeue.sort((a, b) => a.id - b.id)
//queue是全局数组,它在queueWatcher函数里,每次updates触发的时候将当时的watcher给push进去
for(let i = 0; i < queue.length; i++) {
...
watcher.run()
}
//渲染
}
watcher.run的实现在watcher原型链上,初始状态下active属性为true,直接执行watcher原型链的set方法。
Watcher.prototype.run = function run() {
if(this.active) {
let value = this.get()
...
}
}
get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。
Watcher.prototype.get = function get() {
pushTarget(this);
//将实例push到全局数组targetStack
let VM = this.vm
value = this.getter.call(vm, vm);
...
}
实例的getter方法实际是在实例化的时候传入的函数,也激素下面vm的真正更新函数_update。
function () {
vm._update(vm.render(), hydrating)
}
实例的_update函数执行后,将会把两次的虚拟节点传入vm的patch方法执行渲染操作
Vue.prototype._update = function (vnode, hydrating) {
let vm = this
...
let prevVnode = vm._node
vm._vnode = vnode
if(!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
所以上面就是所有的源码了。然后提几点注意的,首先nextTick既有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserve可以看出nextTIck优先微任务,其次是setImmediate和setTimeout宏任务。(我们只写了promsie)
同时,vue也能进行同步渲染,只需要将设置Vue.config.async = false就行了。
行百里者半九十