Skip to content

Vue nextTick的实现 #27

@Jmingzi

Description

@Jmingzi

nextTick的实现本质是采用了js Event Loop的知识实现。

其使用场景在赋值state后使用,目的是等待watcher触发更新界面后调用回调。本文旨在理解Event Loop的基础上查看vue nextTick的实现,以便于在使用过程中更好的解决问题。

nextTick 伪代码

function nextTick(cb, context) {
  // 处理cb
  callbacks.push(() => {
     if (cb) {
       // 回调模式
       cb.call(context)
     } else {
       // 不传cb时,为promise形式
       _resolve()
     }
  })

  // 保证只有一次调用
  if (!pending) {
    pending = true
    // 当前事件队列结束后, 触发回调数组
    // 此处涉及事件队列知识,请戳https://github.com/Jmingzi/blog/issues/2
    // 至于为何要用microtasks和tasks,注释说明
    // 在vue2.4以下都只用microtasks,但是后来发现在连续的事件或同一事件的冒泡中,会有问题
    // 因为microtasks的优先级始终是最高的。默认使用microtasks
    // 何时用tasks?
    // 当组件内部的变化导致state变化时,就会使用tasks
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }

  // 如果没传cb,则返回一个promise对象
  if (!cb) {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

然后再就是定义macroTimerFunc和microTimerFunc时所涉及的点

  • 定义macroTimerFunc
    在ie中支持setImmediate,非ie中,都是使用MessageChanel发送消息来保持callback始终以队列的形式调用的。除非二者不支持,最后才用的setTimeout保底。
if (setImmediate) {
  setImmediate(flushCallbacks)
} else if (MessageChanel) {
  // 关于MessageChanel,请戳
  // http://www.zhangxinxu.com/study/201202/web-messing-channel-messaging-two-iframe.html
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  setTimeout(() => {
    flushCallbacks()
  }, 0)
}
  • 定义microTimerFunc
    用Promise模拟的,但是在iOS UIWebViews中有个bug,Promise.then并不会被触发,除非浏览器中有其他事件触发,例如处理setTimeout。所以手动加了个空的setTimeout

如果Promise都不支持,那microTimerFunc = macroTimerFunc

  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

疑问

  • 关于vue实例中使用microTask与task的区别(也就是注释中的问题描述)
  • 为何要使用MessageChanel来模拟task,而不是直接使用setTimeout
    w3c html规范中定义了setTimeout的默认最小时间为4ms,而嵌套的timeout表现为10ms,也就是说即使你赋值0,而实际却不是。再由于,setTimeout的时间,会受到任务队列的影响(或其它原因?)其实际时间远大于10ms,这或许就是vue不优先使用setTimeout的原因吧。
    此处参考
  • 何时用task,何时用microTask
    vue内置了一个函数withMacroTask,当组件的state变化时,就会使用task,其它默认都是microTask

推荐阅读

Tasks, microtasks, queues and schedules

最后,注释中的2个问题#4521, #6690,欢迎一起讨论。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions