Open zhangzheng-zz opened 3 years ago
定义: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
1、DOM 的异步更新怎么做到?如何组件防止重复渲染? 2、nextTick 的延迟执行? 3、watch watchEffect 的区别和原理? 4、遗留 queuePostRenderEffect 在各个地方的调用?生命周期钩子的调用?是否也是异步?
源码:scheduler.ts 重点在 currentFlushPromise
// packages\runtime-core\src\scheduler.ts 第51行
export function nextTick(fn?: () => void): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(fn) : p
}
queueJob
维护job
列队,去重,保证任务的唯一性,每次调用去执行 queueFlush
queueCb
维护cb
列队,去重,保证任务的唯一性,每次调用去执行 queueFlush
// queueFlush
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush
注意这里就给currentFlushPromise
赋值了,nextTick
之后的回调会慢于 currentFlushPromise
的调用,接下来只要找到哪里调用了 queueJob
queueCb
就行。
接下来我们看 flushJobs
的内容。
先明确两个队列:
queueJob
保存了任务队列 queue
queueCb
是通过 queuePreFlushCb
queuePostFlushCb
调用,分别保存任务队列 pendingPreFlushCbs
pendingPostFlushCbs
function flushJobs(seen?: CountMap) {
// 递归调用排序好的 pendingPreFlushCbs
flushPreFlushCbs(seen)
// 排序
queue.sort((a, b) => getId(a!) - getId(b!))
//调用排序好的 queue
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job) {
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
// 递归调用排序好的 pendingPostFlushCbs
flushPostFlushCbs(seen)
// 递归 queue
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
这里的 update 其实就是 组件的更新方法,同时也是一个 effect,componentEffect。在组件响应式数据更新的时候,componentEffect 成为数据依赖的 effect 副作用函数调用 ,从而更新视图。
// componentPublicInstance.ts
$forceUpdate: i => () => queueJob(i.update)
// renderer.ts
instance.update = effect(function componentEffect() {
// update 方法
}, { scheduler: queueJob })
// effect.ts // queueJob 在这里 将 effect push 到 queue if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() }
#### 小结:经过以上过程,组件会在`render`阶段将`componentEffect`方法添加进`queue`中,这就保证了`nextTick`可以在数据更新之后被调用。
### queuePreFlushCb 的调用
- **watch 的回调里面**
```js
// apiWatch.ts
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
设置 ref
// doSet 是设置 ref
queuePostRenderEffect(doSet, parentSuspense)
调用 mounted
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
调用 update
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
mounted
queuePostRenderEffect(m, parentSuspense)
invokeVNodeHook
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, initialVNode)
}, parentSuspense)
activated
queuePostRenderEffect(a, parentSuspense)
updated
queuePostRenderEffect(u, parentSuspense)
transition
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
调用 unmounted
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
}, parentSuspense)
unmounted
queuePostRenderEffect(um, parentSuspense)
#### 总结:nextTick 将回调函数放到微任务队列里面(currentFlushPromise 的 resolve), 在当前工作 currentFlushPromise 执行完之后调用。而当前工作 currentFlushPromise 由 queueFlush 调用,queueFlush 会按队列里面任务的顺序循环执行:pendingPreFlushCbs、 queue、pendingPostFlushCbs 三个队列。
#### 在不同的时机,vue会通过 queuePreFlushCb、queueCb、queuePostFlushCb 三个函数将不同的任务压入队列,其中
#### queuePreFlushCb 会将 watch 的回调压入 pendingPreFlushCbs 队列;
#### queueCb 会在 render 阶段将 componentEffect 即组件渲染的副作用函数压入 queue 队列;
#### queuePostFlushCb 在很多个地方调用,将必要的回调任务压入 queuePostFlushCb 队列以保证在 nextTick 之前调用。
```js
// nextTick 原理
// 定义当前任务
let c
// 定义任务队列
let a = []
// 设置调度标志位
let isFlushPending = false
// 简易的 nextTick
const nT = (fn) => {
c.then(fn)
}
// 1、c 首先是一个微任务才能保证利用 js 的调度机制
// 2、判断是否在入队阶段,是的话只要给 c 赋值一次
const queueFlush = () => {
if (!isFlushPending) {
isFlushPending = true
c = Promise.resolve().then(callFn)
}
}
// 按顺序执行队列中的任务
const callFn = () => {
isFlushPending = false
// 任务去重
const jobs = [...new Set(a)]
jobs.forEach(cb => {
cb()
})
}
// 将任务入队的函数 入队后的任务会在 nextTick 之前执行
const queueJob = (job) => {
a.push(job)
queueFlush()
}
// 测试 nextTick 的作用
queueJob(() => {
console.log('job_1')
})
nT(() => {
console.log('nextTick')
})
// 假设这里数据改变 nT 永远保证在 queue 之后执行
queueJob(() => {
console.log('job_2---data change')
})
// 测试 DOM 更新是异步的 假设 effect 是渲染组件的副作用函数
const effect = () => {
console.log('effect')
}
// 多次改变数据
for (let index = 0; index < 10000; index++) {
queueJob(effect)
}
任务调度相关:https://juejin.cn/post/6947296766433705998
响应式都是通过effect来实现的
先赋值sheduler,有三种情况
flush: sync:直接赋值job,也就是直接触发effect执行
flush: post:赋值一个函数,函数会执行queuePostRenderEffect,即将job放入postFlushCbs队列
flush: pre:赋值一个函数,函数会执行queuePreRenderEffect,即将job放入preFlushCbs队列
创建effect,以实现响应式 并将scheduler传入。当响应式数据发生变化,就会调用scheduler函数
一开始:
let isFlushing = false 清空队列的控制
let isFlushPending = false 入队过程中的控制
只要有一次入队就开启微任务 并关闭开关
function queueFlush() {
if (!isFlushing && !isFlushPending) {
// 关闭开关
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
清空队列的时候也是关闭开关 isFlushing = true
flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
// 清空 pre
flushPreFlushCbs(seen)
try {
// 清空 queue componentEffect 里面执行了 queuePostFlushCb
// queuePostFlushCb 通过 queueCb 入队到 post 里面,此时 post 里面的任务还未成为微任务,等待下次微任务开启
} finally {
// 清空 post
flushPostFlushCbs(seen)
// 开启开关 下一次重新开启新的微任务
isFlushing = false
// 递归 重新开启新的微任务 所以这里还会判断 post 是不是空的
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
响应式系统:
https://vue3js.cn/reactivity/reactive.html
createApp 到 mounted