vue中nextTick源码解析

375 阅读5分钟

1.js的事件循环

在事件循环中有两个比较重要的概念,分别叫做宏任务和微任务。宏任务和微任务都是指代异步任务。 我们都知道JavaScript是自上而下执行的,在执行过程中涉及到执行栈和任务队列两个东西。执行中的代码会放在执行栈中执行,宏任务和微任务会放在任务队列中等待执行。

js首先自上而下执行,当遇到异步任务会将任务加入到异步任务栈中,当异步任务完毕(达到触发条件,例如定时时间到了),推入任务队列等待执行。当js栈执行完毕,去检查微任务队列中是否存在可以被执行的任务,如果存在就把任务从队列中取出来放入到执行栈中执行,微任务队列被清空之后再开始检查宏任务队列,存在宏任务取出执行。从任务队列取出任务(清空任务队列,即有很多等待执行的任务的时候会将他们全部取出,可能不止一个),执行栈执行任务,取出任务,执行任务,就是一次又一次的事件循环。

总结一句话就是: 先执行同步代码,再执行微任务,再检查宏任务是否到达时间,到达时间再执行。

图片.png

2.nextTick有什么用?

Vue.nextTick( [callback, context] )

参数:
    {Function} [callback]
    {Object} [context]

用法:

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

什么是nextTick,next是下一个,tick是任务,这里解释为事件循环,也就是下一个事件循环。简单来说,nexttick作用就是拿一个队列存储所有要执行的任务,在下一个tick(异步)执行这些任务。(放在一个回调函数中,当进入下一个事件循环时触发这个回调,而不是立即触发这个函数)

3.nextTick源码实现

nextTickHandler

export const nextTick = (function () {
 /* 存放异步执行的回调*/
 const callbacks = []
 /* 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
 let pending = false
 /* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
 let timerFunc

  /* 下一个tick时的回调*/
 function nextTickHandler () {
   /* 一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
   pending = false
   // 拷贝存储所有回调的数组
   const copies = callbacks.slice(0)
   // 清空数组
   callbacks.length = 0
   /* 执行所有callback*/
   for (let i = 0; i < copies.length; i++) {
     copies[i]()
   }
 }

这里的nextTick是一个立即执行函数,所以当加载vue.js的时候就已经执行了,声明了一些全局变量,定义了一个函数nextTickHandler,他的作用就是去执行存储在callbacks这个数组中的所有回调函数。


timerFunc

下面是对当前执行vue.js的环境做一个判断,根据支持程度决定当前环境下nexttick推迟一个任务用何种异步方式执行它。根据代码可知,若支持setImmediate,则采用setImmediate,若支持MessageChannel,采用MessageChannel,若支持Promise,采用Promise,若以上都不支持,采用setTimeout。所以优先级为setImmediate > MessageChannel >Promise >setTimeout

// An asynchronous deferring mechanism.
 // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
 // but microtasks actually has too high a priority and fires in between
 // supposedly sequential events (e.g. #4521, #6690) or even between
 // bubbling of the same event (#6566). Technically setImmediate should be
 // the ideal choice, but it's not available everywhere; and the only polyfill
 // that consistently queues the callback after all DOM events triggered in the
 // same loop is by using MessageChannel.
 /* istanbul ignore if */
 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
   timerFunc = () => {
     setImmediate(nextTickHandler)
   }
 } else if (typeof MessageChannel !== 'undefined' && (
   isNative(MessageChannel) ||
   // PhantomJS
   MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) {
   const channel = new MessageChannel()
   const port = channel.port2
   channel.port1.onmessage = nextTickHandler
   timerFunc = () => {
     port.postMessage(1)
   }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
   // use microtask in non-DOM environments, e.g. Weex
   const p = Promise.resolve()
   timerFunc = () => {
     p.then(nextTickHandler)
   }
 } else {
   // fallback to setTimeout
   timerFunc = () => {
     setTimeout(nextTickHandler, 0)
   }
 }

注意注意,这里最开头有一段注释,这是尤大大写的注释,大概的意思是
// 一个异步的延迟机制。 // 在2.4之前,我们曾经使用微任务(Promise/MutationObserver)。但是微任务实际上具有太高的优先级,并且会在所谓的连续事件之间(例如#4521、#6690)甚至在同一事件的冒泡之间(#6566)触发。 从技术上讲,setImmediate 应该是理想的选择,但它并非随处可用;在同一循环中触发的所有 DOM 事件之后,唯一一致地将回调排队的 polyfill 是使用 MessageChannel。

所以说现在的nexttick使用的异步任务类型优先级和之前是不一样的,因此可以看到网上有很多人对nexttick的源码做解析,但是不同年份时间点,大家对这个优先级的说法不一样,不是当时那个作者写错了,而是这部分内容更新调整过了!!!


queueNextTick

关键的queueNextTick来了,还记得我上面说的源码中nexttick是一个立即执行函数嘛,这个立即执行函数的返回值就是这个queueNextTick函数,所以说我们平常使用的this.$nexttick 其实调用的就是这个queueNextTick函数。他接收2个参数,一个cb 回调函数,一个ctx 上下文。

是不是和vue官方给出的$nexttick参数一模一样 ^_^

ok我们来看这个函数做了什么,首先在callbacks这个回调集合数组中push回调函数,若cb存在,将回调的this指向指向vue实例,ctx默认vue实例。
pending为最开始定义的全局变量,是一个标记位,表示现在是否函数被推送到任务队列等待执行栈去执行。若pending为false ,则先将标志改为true,然后执行timerFunc函数(注意此时nextTickHandler函数就被推进异步任务中去了,之前callbacks这个数组中所有的回调将被等待执行,但是注意此时新的回调仍然可以被push到callbacks这个变量中。开始执行时callbacks变量会被清空),timerFunc就是之前上面定义的,想不起了可以再回过头上去看下。若pending为true,表示上一个事件循环在执行中,不会执行timerFunc(),这样做是为了避免timerFunc多次推入任务队列。当任务队列中的事件被执行栈开始执行,pending将变回false。

    //推送到队列中下一个tick时执行
  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    /* cb存到callbacks中*/
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }

4.完整nexttick函数代码

github.com/vuejs/vue
源码位置在src/core/util/env.js

/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
/* 存放异步执行的回调*/
const callbacks = []
/* 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc

 /* 下一个tick时的回调*/
function nextTickHandler () {
  /* 一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
  pending = false
  // 拷贝存储所有回调的数组
  const copies = callbacks.slice(0)
  callbacks.length = 0
  /* 执行所有callback*/
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and the only polyfill
// that consistently queues the callback after all DOM events triggered in the
// same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(nextTickHandler)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = nextTickHandler
  timerFunc = () => {
    port.postMessage(1)
  }
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // use microtask in non-DOM environments, e.g. Weex
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(nextTickHandler)
  }
} else {
  // fallback to setTimeout
  timerFunc = () => {
    setTimeout(nextTickHandler, 0)
  }
}
/*
  推送到队列中下一个tick时执行
  cb 回调函数
  ctx 上下文
*/
return function queueNextTick (cb?: Function, ctx?: Object) {
  let _resolve
  /* cb存到callbacks中*/
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => {
      _resolve = resolve
    })
  }
}
})()

此文仅为个人学习记录,若有错误欢迎随时指出。