共计 7128 个字符,预计需要花费 18 分钟才能阅读完成。
nextTick
vue 版本
2.6.11
源码剖析(nextTick)
nextTick 源码调用过程总结:
init->timerFunc = (Promise/MutationObserver/setImmediate)
初始化阶段为 timerFunc 的执行形式赋值,一般来说在 Windows 浏览器环境下运行 timerFunc 函数的执行形式都会是 Promise.then 的形式,应用微工作队列的形式。
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) ||
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);
};
}
$nextTick(fn)->callbacks.push(function(){fn.call(this)})->timerFunc()
应用 nextTick 的源码如下:
function nextTick (cb, ctx) {console.log('vue nexttick')
var _resolve;
callbacks.push(function () { // 全局变量 callbacks
if (cb) {
try {cb.call(ctx); // 这里调用回调
} catch (e) {handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {_resolve(ctx);
}
});
if (!pending) {
pending = true; // 只执行一次 timerFunc 函数
timerFunc();}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {return new Promise(function (resolve) {_resolve = resolve;})
}
}
如上所示,在一次宏工作中执行屡次 nextTick 只会调用一次 timerFunc(),timerFunc()会将 flushCallbacks 函数放入 JavaScript 的微工作队列中,待顺序调用。
......
var p = Promise.resolve();
timerFunc = function () {p.then(flushCallbacks);
if (isIOS) {setTimeout(noop); }
};
......
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {copies[i]();}
}
其次,$nextTick()是即时调用的, 并且会将传入的函数的 this 值变成以后 Vue 实例
Vue.prototype.$nextTick = function (fn) {return nextTick(fn, this)
};
源码剖析(set 过程)
Vue 对每个组件中的 data 都做了数据代理(截持), 对 data 对象中的数据进行赋值操作,理论就会调用 defineProperty 中的 reactiveSetter 函数,进行一系列操作,包含告诉 Watcher 数据扭转了等等。
其中 setter 源代码如下,不止进行赋值操作,还会调用 dep.notify()告诉数据扭转了:
set: function reactiveSetter (newVal) {var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {return}
if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter();
}
if (getter && !setter) {return}
if (setter) {setter.call(obj, newVal); // 这里进行赋值操作
} else {val = newVal;}
childOb = !shallow && observe(newVal);
dep.notify(); // 这里进行告诉}
Dep 对象次要作用是记录以后组件依赖的 Watcher(? 不分明,之后再来看)
总而言之,调用了 Dep 原型上的 notify 函数, 再接着调用 Watcher 原型上的 update 办法
Dep.prototype.notify = function notify () {var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {subs.sort(function (a, b) {return a.id - b.id;});
}
for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}
};
update 办法, 这里调用了要害的 queueWatcher 函数
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {this.dirty = true;} else if (this.sync) {this.run();
} else {queueWatcher(this);
}
};
queueWatcher 函数做了两件要害的事
1、向 queue 变量中 push watcher
2、调用一次 nextTick, 将 flushSchedulerQueue 塞进微工作队列。
重要:也就是说,只有在宏工作运行过程中对 data 进行了一次赋值, 就会往微工作队列中塞一个 flushSchedulerQueue 函数的微工作(个别是 Promise)。waiting 只会在 flushSchedulerQueue 执行之后再次赋为 false
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {has[id] = true;
if (!flushing) {queue.push(watcher); // 退出 queue
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {i--;}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue); // nextTick flushSchedulerQueue
}
}
}
要害 :flushSchedulerQueue 函数做了什么:
1、遍历 queue 变量,获得 watcher
2、watcher.before() 调用,这个时候就是组件生命周期中的 beforeUpdate 回调告诉的时候。
3、watcher.run()调用,如果 watcher 对应的组件有配置 watch,就是这个时候执行回调,并且进行数据和 DOM 更新。
4、resetSchedulerState()调用,将 waiting=false,此时数据曾经更新结束,下次触发 reactiveSetter,则从新调用 nextTick
5、callUpdatedHooks,callActivatedHooks 调用,别离对应生命周期中的 activated 和 updated
function flushSchedulerQueue () {
// debugger
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) {return a.id - b.id;});
for (index = 0; index < queue.length; index++) {watcher = queue[index];
if (watcher.before) {watcher.before(); // beforeUpdate 回调(如果 before 属性存在的话)
}
id = watcher.id;
has[id] = null;
watcher.run(); // 如果有配置 watche 监督属性
// .... loop 报错揭示 ....
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {devtools.emit('flush');
}
}
实例剖析
这是我本人刚刚碰到的案例,组件中触发以下代码:
const pro = new Promise((resolve, reject)=>{console.log('promise immediate 111')
resolve('ok')
}).then(()=>{console.log('promise then 111')
})
this.$nextTick(()=>{console.log('nexcTick 111')
})
new Promise((resolve, reject)=>{console.log('promise immediate 222')
resolve('ok')
}).then(()=>{console.log('promise then 222')
})
this.$nextTick(()=>{console.log('nexcTick 222')
})
this.visible = false // 数据操作
this.$nextTick(()=>{console.log('nexcTick 333')
})
以上代码,依照我一开始的认知,输入程序应该是:
promise immediate 111
promise immediate 222
nexcTick 111
promise then 222
nexcTick 222
nexcTick 333
但其实不然,操作数据触发了 reactiveSetter,它理论退出微工作队列的程序是:
1、promise then 111 微工作 1
2、nexcTick 111 -> callbacks
3、flushCallbacks 函数 微工作 2
4、promise then 222 微工作 3
5、nexcTick 222 -> callbacks
6、setter 调用, flushSchedulerQueue -> callbacks
7、nexcTick 333 -> callbacks
在 Vue 源码 nextTick 函数中退出 console 输入,验证猜测:
function nextTick (cb, ctx) {
var _resolve;
console.log(`${cb.name?cb.name:'箭头函数'}退出了 callbacks`)
callbacks.push(...);
if (!pending) {
pending = true;
timerFunc();}
}
后果:
总结
setter 触发时的总过程 :
1、reactiveSetter。这里首先扭转 data 对象中的值, 然而 DOM 尚未更新,能够说先存着
2、dep.notify。这里告诉该组件[依赖] 的每个 watcher
3、Watcher.update。这里调用 queueWatcher,让 wacher 入队列,为更新做筹备。另:如果强制同步更新 DOM 的话,这里就执行 this.run(), 执行对应的 DOM 更新操作。
4、queueWatcher。这里让 watcher 入待执行队列,并且如果是本次更新操作第一次 setter,则调用 nextTick 函数,让 flushSchedulerQueue 函数退出微工作队列。
5、flushSchedulerQueue。这里函数开始执行,代表宏工作曾经执行结束,开始执行微工作队列,这里将通过 beforeUpdate-> 更新 DOM->updated 的过程
nextTick 触发时的总过程:
0、timerFunc 赋值。依据操作系统不同,个别是 Promise 形式执行异步工作。
1、nextTick。往 callbacks 队列中退出一个待执行的回调,如果是一个更新周期中首次执行该函数,则调用 timerFunc,将 flushCallbacks 函数退出微工作队列。
2、flushCallbacks。这里顺次遍历 callbacks 队列中的待执行工作,程序执行,此时可能有用户本人调用的 nextTick 回调,也有可能中途执行了 setter 操作,插入了 flushSchedulerQueue 回调。在 flushSchedulerQueue 工作前后执行代码,状况齐全不同,这也是为什么在编写代码的过程中可能呈现不合乎预期的状况。
总结:
Vue 中对于微工作的解决,尽管只插入一个微工作,然而数组形式存的待执行工作,就算是后执行的 setter 或者 nextTick,都能排在第一个 nextTick 或者 setter 调用时的优先程序执行。有种插队的感觉。
局部未提及源码
flushSchedulerQueue 中 watcher.before 函数, 对应 beforeUpdate 生命周期
new Watcher(vm, updateComponent, noop, {before: function before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
flushSchedulerQueue 中 watcher.run 函数, 此时进行数据更新
Watcher.prototype.run = function run () {if (this.active) {var value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {var info = "callback for watcher \"" + (this.expression) + "\"";
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
} else {this.cb.call(this.vm, value, oldValue); // 这里是 watch 回调
}
}
}
};
flushSchedulerQueue 中 callUpdatedHooks 函数,生命周期 updated
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {callHook(vm, 'updated');
}
}
}