nextTick的官方定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

那么nextTick是怎么做到的呢?我们来看源码:

源码

  1. const resolvedPromise = Promise.resolve()
  2. let currentFlushPromise = null
  3. function nextTick(fn) {
  4. const p = currentFlushPromise || resolvedPromise
  5. return fn ? p.then(this ? fn.bind(this) : fn) : p
  6. }

😵 就一个promise,搞定了。 就这!就这!就这!

兄台莫急,神奇的在下面。

JS执行机制

我们都知道 js 是单线程语言,即指某一时间内只能干一件事。单线程就意味着我们所有的任务都需要排队,后面的任务必须等待前面的任务完成才能执行,如果前面的任务耗时很长,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以js中就出现了异步的概念。

概念

  • 同步 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步 不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

所以在源码中,我们创建了一个异步任务,那么_nextTick自然就会在同步任务完成后执行。_

调度程序

在nextTick的机制里,有几个兄弟函数,这几个函数就起了关键作用。
它们分别是 queueJob queueFlush queuePostFlushCb queueCb flushPreFlushCbs flushPostFlushCbs

我们先用几个单元测试看看这些函数的用法:

单测1

  1. it('nextTick', async () => {
  2. const calls: string[] = []
  3. const dummyThen = Promise.resolve().then()
  4. const job1 = () => {
  5. calls.push('job1')
  6. }
  7. const job2 = () => {
  8. calls.push('job2')
  9. }
  10. nextTick(job1)
  11. job2()
  12. expect(calls.length).toBe(1)
  13. await dummyThen
  14. // 微任务队列被清空后,job1被推送到下一个任务
  15. expect(calls.length).toBe(2)
  16. expect(calls).toMatchObject(['job2', 'job1'])
  17. })

剖析:

  1. nextTick 创建一个微任务队列。
  2. 执行job2,calls的个数是1
  3. 微任务队列被清空后,job1被推送到下一个任务
  4. 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 触发自身,并加入队列。

总结

  1. nextTick接受函数作为参数,同时nextTick会创建一个微任务。
  2. queueJob接受函数作为参数,queueJob会将参数push到queue队列中,在当前宏任务执行结束之后,清空队列。
  3. queuePostFlushCb接受函数或者由函数组成的数组作为参数,queuePostFlushCb会将将参数push到postFlushCbs队列中,在当前宏任务执行结束之后,清空队列。
  4. queuePreFlushCb 执行的优先级高于 queueJob 。
  5. 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)
    }
  }
}

总结:

  1. 先处理 pre 的队列,
  2. 队列根据 id 排序
  3. 刷新队列,并清空
  4. 如果还有,继续执行

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
  }
}

总结:

  1. 判断队列中是否有值
  2. 判断当前是否有正在活动的队列
  3. 赋值给 activePostFlushCbs 激活当前队列, 并按 id 排序
  4. 循环执行当前队列
  5. 清空当前活动的队列

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()
  }
}

上面的代码在前几篇文章里面都有讲解。
_
简单总结如下:

  1. 当响应式数据发送变化后执行其 effect;
  2. 其 effect 是有 scheduler 这个参数的(调度程序),然 scheduler 就是 queueJob;
  3. 执行 queueJob 方法,参数就是 effect;

相信看到了这里,大家都有一种恍然大悟的感觉吧。哈哈哈!!!

😜 😜 😜

完!