[vue] 源码阅读 next-tick

本文深入探讨了next-tick模块的工作原理,包括flushCallback、nextTick等核心方法,以及macroTask和microTask的区别与实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从今天开始每天阅读一些源码吧
next-tick官方文档

简介

next-tick的位置在src/core/utils/next-tick.js

next-tick文件

  1. 最核心的几个方法是flushCallback,nextTick及macroTask与microTask,其中前两者是负责消息的处理与建立,后两者是构建不同的消息执行体,此外还有一个withMacroTask

flushCallback

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

就是一个队列的消费者

nextTick

nextTick的作用是将传入的callback分发到不同的TimerFunc中

function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })

  ... // 分发部分先跳过

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

闭包,当没有cb传入的时候,返回的就是一个简单的promise(microtask),并且在回调队列中会直接触发then事件;否则就是执行回调,并不会有返回值。

function nextTick (cb?: Function, ctx?: Object) {

  ...

  if (!pending) {
    pending = true
    if (useMacroTask) { // 手动指定使用macroTask方式
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }

  ...
}

分发部分,这部分是next-tick函数的核心
触发的timerFunc分为macroTask和microTask
pending的状态只有在flushCallback中才会修改,因此分发部分可以保证一次nextTick处理流程中只触发一次timerFunc,亦即只有一次flushCallback事件
由于整个模块中只有一个callback,所以一次nextTick或者是由macro或者是由micro来处理;而若这次nextTick中有macro事件,那么在下一个eventLoop前(或者本次eventloop的最后处理setImmediate前),所有的microTask都无法触发,必须等到microTimerFunc中定义的macroTask来处理

有一个问题,如果先调用microTask然后接着在其还没有被执行的时候再nextTick一个macroTask(在vue中是绑定的v-dom事件),那么macroTask会在下次执行microTask的时候被执行,这明显是不对的
好吧,这个问题和eventloop的触发有关,Vue在所有DOM元素的addEventListener上定义了使用macroTask,所以如果是用户触发,最外层的回掉事件就会首先采用macroTask;而如果使用的是programmatical trigger,则在当前执行栈jsstack内就会触发bubble和capture,不会存在Vue注释里面说的那种问题
参考1
参考2
根据这个特性,写了一个,用来区分用户触发事件和程序触发事件

timerFunc

macroTask

let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

可以看到,分别使用了setImmediateMessageChannelsetTimeout三种方法。保证flushCallback会在下一次eventloop中执行
早期的是使用mutationObserve实现的

microTask

// Determine microtask defer implementation.
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  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)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

采用promise实现,有意思的一点是针对ios的UIWebViews进行了优化,强行触发了一次microtask的事件处理

withMacroTask

function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

装饰器,返回了一个通过macro处理的函数,看下使用
platforms\web\runtime\modules\event.js

function add (
  event: string,
  handler: Function,
  once: boolean,
  capture: boolean,
  passive: boolean
) {
  handler = withMacroTask(handler)
  if (once) handler = createOnceHandler(handler, event, capture)
  target.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

增加事件监听方法,将DOM事件放到了下一个macroTask队列中

总结

  1. next-ticker.js 只是一个底层的封装,将函数放到函数队列中,并通过microTask和macroTask将处理函数推迟处理;
  2. 感觉next-ticker需要在上层对macroTask的使用进行判断处理,当然也可能是我还不明确eventloop的具体机制和顺序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值