3.2.37
在之前的Vue2分析中,提到了Vue2的nextTick是维护了一个callbacks数组,每次更新过程中只插入一个微任务,执行放在callbacks数组中的回调。
而Vue3不同,Vue3的nextTick和Promise基本没什么区别,set过程的更新看似也不再依赖,nextTick进行。仅仅只是将创建一个resovled状态的Promise,将传入的函数放入回调中罢了。以下是vue3的nextTick源码:
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise;
return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
正因为如此,同样的代码,在Vue2和Vue3中会有不同的表现。参考以下代码:
Promise.resolve().then(()=>{
console.log('开始的Promise回调')
})
this.$nextTick(()=>{
console.log('第一次nextTick的回调')
})
Promise.resolve().then(()=>{
console.log('修改数据之前的Promise回调')
})
this.name = 'kirito' // 这里进行赋值操作
Promise.resolve().then(()=>{
console.log('修改数据之后的Promise回调')
})
this.$nextTick(()=>{
console.log('最后的nextTick的回调')
})
以上代码的运行结果,在Vue2中是:
开始的Promise回调
第一次nextTick的回调
最后的nextTick的回调
修改数据之前的Promise回调
修改数据之后的Promise回调
只要调用了nextTick或者对数据进行了变更,那么放在之后的Promise回调,一定是排在后面执行的。
而同样的代码:
const name = ref('yuuki')
const test2 = () => {
Promise.resolve().then(()=>{
console.log('开始的Promise回调')
})
nextTick(()=>{
console.log('第一次nextTick的回调')
})
Promise.resolve().then(()=>{
console.log('修改数据之前的Promise回调')
})
name.value = 'kirito' // 这里进行赋值操作
Promise.resolve().then(()=>{
console.log('修改数据之后的Promise回调')
})
nextTick(()=>{
console.log('最后的nextTick的回调')
})
}
在Vue3中的执行结果是:
开始的Promise回调
第一次nextTick的回调
修改数据之前的Promise回调
修改数据之后的Promise回调
最后的nextTick的回调
看上去完全是按照Promise加入微任务队列的逻辑,一次nextTick就是插入一个微任务队列,不维护callbacks数组。
上述示例代码,赋值操作如果稍稍改变一下位置,又会与预想的输出截然不同:
const name = ref('yuuki')
const age = ref(18)
const test2 = () => {
Promise.resolve().then(()=>{
console.log('开始的Promise回调')
})
name.value = 'kirito' // 这里进行赋值操作
nextTick(()=>{
console.log('第一次nextTick的回调')
})
Promise.resolve().then(()=>{
console.log('修改数据之前的Promise回调')
})
Promise.resolve().then(()=>{
console.log('修改数据之后的Promise回调')
})
nextTick(()=>{
console.log('最后的nextTick的回调')
})
}
以上的代码,执行了之后输出变成:
开始的Promise回调
修改数据之前的Promise回调
修改数据之后的Promise回调
第一次nextTick的回调
最后的nextTick的回调
似乎在进行赋值操作之后,nextTick的优先顺序又发生了变化,就算在后面的Promise回调也会在nextTick之前调用,与Vue2的赋值操作之后,nextTick优先级升级比起来,Vue3中进行了赋值操作,也就是说数据更新之后,nextTick回调的优先级反而下降了一个等级。
不同于Vue2的nextTick的实现,Vue3相比起来,nextTick的实现代码异常简单,只有短短几行:
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
let currentPreFlushParentJob = null;
const RECURSION_LIMIT = 100;
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise;
return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
resolvedPromise只是一个处于fulfilled状态的Promise对象,也就是说如果p变量是resolvedPromise,那么只会立即执行then回调并加入到微任务队列中。那么要关心的就是currentFlushPromise变量。也就是说正是由于currentFlushPromise的值不为null了,导致的nextTick执行优先级下降。
那么逐步分析响应式数据的set过程,一定能找到currentFlushPromise何时发生了变化。
对ref创建的响应式数据,进行赋值操作:首先会进入RefImpl的set函数
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) { // 判断是否变化
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal); //
}
}
}
值更新后进入triggerRefValue函数(追踪ref值变化),这里将ref转为普通数据或者说原始对象,这意味着取消了数据代理,不会因为值的读取和修改而造成额外开销,
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
if ((process.env.NODE_ENV !== 'production')) {
triggerEffects(ref.dep, {
target: ref,
type: "set" /* SET */,
key: 'value',
newValue: newVal
});
}
else {
triggerEffects(ref.dep);
}
}
}
之后进入triggerEffects函数,ref.dep是一个ReactiveEffect类的Set集合
function triggerEffects(dep, debuggerEventExtraInfo) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep];
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
进入triggerEffect函数,且不管onTrigger和run是何种情况会调用,这里进入的是effect.scheduler函数
function triggerEffect(effect, debuggerEventExtraInfo) {
if (effect !== activeEffect || effect.allowRecurse) {
if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
effect是一个ReactiveEffect类型对象,其构造函数以及,effect创建过程如下:
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
this.parent = undefined;
recordEffectScope(this, scope);
}
.......
}
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope
));
const update = (instance.update = () => effect.run());
也就是说,调用queueJob函数,如果全局变量queue为空或者queue中不包含该job并且当前job不等于正在准备flush的job,则往queue推入一个job
function queueJob(job) {
if ((!queue.length ||
!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
job !== currentPreFlushParentJob) {
if (job.id == null) {
queue.push(job);
}
else {
queue.splice(findInsertionIndex(job.id), 0, job);
}
queueFlush();
}
}
接着执行queueFlush函数,这里将flushJobs函数加入微任务队列,并且关键的标志bool变量,置为true,代表如果不是第一次进行数据更新就跳过这个操作,currentFlushPromise是一个状态为pending 的Promise对象,等待回调执行成功,这也解释了为什么之前的示例代码中,nextTick的优先级会降低一级。
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
接着来看回调的flushJobs函数干了什么事:
1、将标识isFlushPending还原,代表这次的回调已经成功开始执行了;将isFlushing标识置为true,代表接下来要进行更新操作,其他一些操作无法生效。
2、对更新队列进行排序。
3、调用callWithErrorHandling执行更新的具体操作
4、最后重置标识,以及一些全局变量等,为下一次更新做好准备,其中包括currentFlushPromise置为null。
function flushJobs(seen) {
isFlushPending = false;
isFlushing = true;
if ((process.env.NODE_ENV !== 'production')) {
seen = seen || new Map();
}
flushPreFlushCbs(seen);
// 排序队列的意义:1、保证组件的更新是从父到子组件,因为父组件一定先于子组件创建,所以父组件的渲染优先级更小。2、如果父组件在更新期间卸载组件,能够跳过他的更新。
queue.sort((a, b) => getId(a) - getId(b));
const check = (process.env.NODE_ENV !== 'production')
? (job) => checkRecursiveUpdates(seen, job)
: NOOP;
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
if ((process.env.NODE_ENV !== 'production') && check(job)) {
continue;
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, 14 /* SCHEDULER */);
}
}
}
finally {
flushIndex = 0;
queue.length = 0;
flushPostFlushCbs(seen);
isFlushing = false;
currentFlushPromise = null;
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length) {
flushJobs(seen);
}
}
}
后面没太看明白了,总之会进入ReactiveEffect的run函数中,接着this.fn()的函数调用
class ReactiveEffect {
.......
run() {
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
}
else {
cleanupEffect(this);
}
return this.fn(); // 函数调用
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
}
然后调用一个两百多行的componentUpdateFn函数,这里主要进行生命周期钩子回调的调用,以及进行虚拟DOM树的对比,以及实际DOM树的更新操作。这里会调用beforeMount,mount,activated,beforeUpdate以及updated这些hook和生命周期钩子函数。
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)
) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
toggleRecurse(instance, true)
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeMounted)
) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
if (
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
(parent &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense)
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:activated'),
parentSuspense
)
}
}
instance.isMounted = true
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (__DEV__) {
pushWarningContext(next || instance.vnode)
}
// Disallow component effect recursion during pre-lifecycle hooks.
toggleRecurse(instance, false)
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeUpdate')
}
toggleRecurse(instance, true)
// render
if (__DEV__) {
startMeasure(instance, `render`)
}
const nextTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
const prevTree = instance.subTree
instance.subTree = nextTree
if (__DEV__) {
startMeasure(instance, `patch`)
}
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
next.el = nextTree.el
if (originNext === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
// to child component's vnode
updateHOCHostEl(instance, nextTree.el)
}
// updated hook
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:updated'),
parentSuspense
)
}
}
}
到此为止的分析,也只是浅显的了解了响应式数据set过程中运行逻辑。
Vue2与Vue3,在nextTick和响应式数据更新过程中一样的点在于:
1、使用Promise方式放入更新的回调。
2、回调中使用通过全局变量等方式,使一次更新回调中,能访问到这次所有对数据的操作,即自第一次数据更新操作后,就不再使用Promise加入更多的微任务。
不同之处在于:
1、数据代理的方式不同了,都是拦截对数据的get和set操作,Vue3通过Proxy方式,Vue2通过Object.defineProperty的方式
2、nextTick的实现方式不同了,Vue2中的nextTick如果在数据赋值操作执行之前调用,是能够影响更新逻辑在微任务队列中的执行顺序的。
也就是说,如果执行了nextTick->Promise.then->赋值操作。那么实际的回调顺序会是nextTick,组件更新,Promise.then。
而Vue3中不同,即使nextTick赋值操作之前执行也不会影响到赋值操作加入的回调的执行顺序,并且在执行了赋值操作之后,再执行nextTick,则会等到组件实际的更新操作完成之后,才会将nextTick加入微任务队列中,准备执行。
然而分析了这么一通,其实按照Vue官方文档中推荐的写法,一般是不会出什么问题的。实际开发中也很少有需要nextTick和Promise混着并且重复用的地方。