nextTick的官方定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
源码
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
😵 就一个promise,搞定了。 就这!就这!就这!
兄台莫急,神奇的在下面。
JS执行机制
我们都知道 js 是单线程语言,即指某一时间内只能干一件事。单线程就意味着我们所有的任务都需要排队,后面的任务必须等待前面的任务完成才能执行,如果前面的任务耗时很长,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以js中就出现了异步的概念。
概念
- 同步 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步 不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
所以在源码中,我们创建了一个异步任务,那么_nextTick自然就会在同步任务完成后执行。_
调度程序
在nextTick的机制里,有几个兄弟函数,这几个函数就起了关键作用。
它们分别是 queueJob queueFlush queuePostFlushCb queueCb flushPreFlushCbs flushPostFlushCbs
我们先用几个单元测试看看这些函数的用法:
单测1
it('nextTick', async () => {
const calls: string[] = []
const dummyThen = Promise.resolve().then()
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
nextTick(job1)
job2()
expect(calls.length).toBe(1)
await dummyThen
// 微任务队列被清空后,job1被推送到下一个任务
expect(calls.length).toBe(2)
expect(calls).toMatchObject(['job2', 'job1'])
})
剖析:
- nextTick 创建一个微任务队列。
- 执行job2,calls的个数是1
- 微任务队列被清空后,job1被推送到下一个任务
- calls的个数是2
单测2 queueJob
it('basic usage', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
queueJob(job1)
queueJob(job2)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})
queueJob 接收一个函数作为参数,会将参数按顺序保存到一个队列中,
当宏任务执行完成后,微任务再依次执行。
queuePreFlushCb
基本
it('basic usage', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2'])
})
queuePreFlushCb 函数目前看跟 queueJob 类似。
单测 should dedupe queued preFlushCb
it('should dedupe queued preFlushCb', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
const cb3 = () => {
calls.push('cb3')
}
queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
queuePreFlushCb(cb3)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
})
queuePreFlushCb 函数会过滤重复参数。
单测 queueJob inside preFlushCb
it('queueJob inside preFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
// queueJob 在 preFlushCb 里
calls.push('cb1')
queueJob(job1)
}
queuePreFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})
单测 queueJob & preFlushCb
it('queueJob & preFlushCb inside preFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
calls.push('cb1')
queueJob(job1)
// cb2 在job1 之前执行
queuePreFlushCb(cb2)
}
const cb2 = () => {
calls.push('cb2')
}
queuePreFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
})
当 queueJob 和 queuePreFlushCb 相互嵌套时,queuePreFlushCb 执行顺序高于 queueJob 。
queuePostFlushCb
基本
it('basic usage', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
const cb3 = () => {
calls.push('cb3')
}
queuePostFlushCb([cb1, cb2])
queuePostFlushCb(cb3)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
})
queuePostFlushCb 也会去重。
单测 queueJob inside postFlushCb
it('queueJob inside postFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
// queueJob 在 postFlushCb 里
calls.push('cb1')
queueJob(job1)
}
queuePostFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})
queueJob 可以嵌套在 queuePostFlushCb 中。
单测 queueJob & postFlushCb inside postFlushCb
it('queueJob & postFlushCb inside postFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
calls.push('cb1')
queuePostFlushCb(cb2)
queueJob(job1)
}
const cb2 = () => {
calls.push('cb2')
}
queuePostFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1', 'cb2'])
})
queueJob 的执行顺序高于 queuePostFlushCb 。
单测 sort job based on id
test('sort job based on id', async () => {
const calls: string[] = []
const job1 = () => calls.push('job1')
// job1 has no id
const job2 = () => calls.push('job2')
job2.id = 2
const job3 = () => calls.push('job3')
job3.id = 1
queueJob(job1)
queueJob(job2)
queueJob(job3)
await nextTick()
expect(calls).toEqual(['job3', 'job2', 'job1'])
})
queueJob 和 queuePostFlushCb 的执行顺序可以用id排序。
单测 should prevent self-triggering jobs by default
test('should prevent self-triggering jobs by default', async () => {
let count = 0
const job = () => {
if (count < 3) {
count++
queueJob(job)
}
}
queueJob(job)
await nextTick()
expect(count).toBe(1)
})
只运行一次,job 不能自行加入队列。
单测 should allow explicitly marked jobs to trigger itself
test('should allow explicitly marked jobs to trigger itself', async () => {
// normal job
let count = 0
const job = () => {
if (count < 3) {
count++
queueJob(job)
}
}
job.allowRecurse = true
queueJob(job)
await nextTick()
expect(count).toBe(3)
// post cb
const cb = () => {
if (count < 5) {
count++
queuePostFlushCb(cb)
}
}
cb.allowRecurse = true
queuePostFlushCb(cb)
await nextTick()
expect(count).toBe(5)
})
允许标记的 job 触发自身,并加入队列。
总结
- nextTick接受函数作为参数,同时nextTick会创建一个微任务。
- queueJob接受函数作为参数,queueJob会将参数push到queue队列中,在当前宏任务执行结束之后,清空队列。
- queuePostFlushCb接受函数或者由函数组成的数组作为参数,queuePostFlushCb会将将参数push到postFlushCbs队列中,在当前宏任务执行结束之后,清空队列。
- queuePreFlushCb 执行的优先级高于 queueJob 。
- queueJob执行的优先级高于queuePostFlushCb。
源码分析
queueJob
维护job队列,有去重逻辑,以保住其唯一性。
再执行 queueFlush 方法。
const queue: SchedulerJob[] = []
function queueJob(job) {
if ((!queue.length ||!queue.includes( job )) && job !== currentPreFlushParentJob) {
queue.push(job)
queueFlush()
}
}
queueFlush
队列任务的刷新处理。
- isFlushPending 是否正在等待刷新
- isFlushing 是否正在刷新 ```javascript / 是否正在刷新 */ let isFlushing = false / 是否正在等待刷新 */ let isFlushPending = false const resolvedPromise = Promise.resolve() let currentFlushPromise = null
function queueFlush() { // 排队刷新队列,也就是多次调用queueFlush,只执行一个队列刷新。 // 带队列全部刷新完成后,清空isFlushPending才能再次进入。 if (!isFlushing && !isFlushPending) { isFlushPending = true // 当前需要刷新的异步任务,flushJobs在下面讲解 currentFlushPromise = resolvedPromise.then(flushJobs) } }
<a name="icDFl"></a>
### flushJobs
处理队列任务。
```javascript
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true // 正在刷新
if (__DEV__) {
seen = seen || new Map()
}
// 刷新 pre 队列
flushPreFlushCbs(seen)
// 队列可以根据id排序
queue.sort((a, b) => getId(a) - getId(b))
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job) {
if (__DEV__) {
checkRecursiveUpdates(seen!, job)
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
// 刷新 队列
flushPostFlushCbs(seen)
isFlushing = false // 刷新完成
currentFlushPromise = null
// 如果队列没有被清空,继续执行
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
总结:
- 先处理 pre 的队列,
- 队列根据 id 排序
- 刷新队列,并清空
- 如果还有,继续执行
flushPostFlushCbs
刷新队列。
let activePostFlushCbs = null
export function flushPostFlushCbs(seen?: CountMap) {
// 队列中有值
// pendingPostFlushCbs 是
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
// 判断如果已经有队列在活动
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 循环刷新队列里的任务
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (__DEV__) {
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
}
activePostFlushCbs[postFlushIndex]()
}
// 清空当前活动的刷新任务
activePostFlushCbs = null
postFlushIndex = 0
}
}
总结:
- 判断队列中是否有值
- 判断当前是否有正在活动的队列
- 赋值给 activePostFlushCbs 激活当前队列, 并按 id 排序
- 循环执行当前队列
- 清空当前活动的队列
nextTick
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
总结:
以上函数的执行顺序 queueJob > queueFlush > flushJobs > nextTick的参数fn执行
当我们在页面调用 nextTick 时,会执行该函数,而我们的参数是个 function 赋值给了 p.then(fn)
所以在队列任务完成后(同步任务),fn 就执行了。
队列如何跟effect关联的
要解决这个问题,我们要知道 queueJob 是怎么被调用的。
// renderer.ts
function createDevEffectOptions(
instance: ComponentInternalInstance
): ReactiveEffectOptions {
return {
scheduler: queueJob,
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc, e) : void 0,
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg, e) : void 0
}
}
// renderer.ts
const setupRenderEffect = (instance) => {
instance.update = effect(function componentEffect() {
...
}, createDevEffectOptions(instance))
}
// effect.ts
const run = (effect: ReactiveEffect) => {
...
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
上面的代码在前几篇文章里面都有讲解。
_
简单总结如下:
- 当响应式数据发送变化后执行其 effect;
- 其 effect 是有 scheduler 这个参数的(调度程序),然 scheduler 就是 queueJob;
- 执行 queueJob 方法,参数就是 effect;
相信看到了这里,大家都有一种恍然大悟的感觉吧。哈哈哈!!!
😜 😜 😜
完!