vue源码分析之nextTick源码分析-逐行逐析-有所困惑

204 阅读7分钟

nextTick的使用背景

  • 在vue项目中,经常会使用到nextTick这个api,一直在猜想其是怎么实现的,今天有幸研读了下,虽然源码又些许问题,但仍值得借鉴

核心源码解析

判断当前环境使用最合适的API并保存函数

promise

  • 判断是否支持promise,如果支持就使用Promise对象的then方法包裹要执行的 flushCallbacks函数

MutationObserver

  • 判断是否支持MutationObserver,如果支持就创建一个MutationObserver 用于监听dom改动之后执行 flushCallbacks 函数 并赋值给 observer

setImmediate

  • 判断是否支持setImmediate,如果支持就使用setImmediate包裹 flushCallbacks函数

setTimeout

  • 如果以上三种都不支持使用setTimeout包裹 flushCallbacks函数
export let isUsingMicroTask = false//是否使用微任务标志

const callbacks = []//任务对列 调用nextTick时传入的回调函数组成的数组
let pending = false//初始化 是否在进行中状态  默认是false  

function flushCallbacks() {
  //循环执行 callbacks  任务队列中的任务
  pending = false
  const copies = callbacks.slice(0)//
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    // 依次执行callbacks数组中的函数
    copies[i]()
  }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 向下兼容操作 如果支持Promise 则使用Promise
  const p = Promise.resolve()//直接返回一个resolved状态的Promise对象
  timerFunc = () => {
    p.then(flushCallbacks)
    // 在Promise的then方法中执行 flushCallbacks 函数
    if (isIOS) setTimeout(noop)//ios中在一些异常的webview中,promise结束后任务队列并没有刷新,所以强制执行setTimeout(noop)来刷新任务队列
  }
  isUsingMicroTask = true//重置使用微任务标示为true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||

  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// MutationObserver 属性支持,则使用MutationObserver 
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  // 创建一个MutationObserver 用于监听dom改动之后执行 flushCallbacks 函数 并赋值给 observer
  const textNode = document.createTextNode(String(counter))
  // 创建一个文本节点
  observer.observe(textNode, {
    characterData: true
  })
  // 每次执行timeFunc都会让文本节点的内容在0/1之间切换
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  // 切换之后将新值赋值到那个我们MutationObserver观测的文本节点上去
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// setImmediate 属性支持,则使用setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 以上都不支持,则使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

调用异步函数执行回调对列

入参分析

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

}
  • cb是 传入的回调函数
  • ctx是函数执行的上下文
  • 而两者又都是可选参数, 有所困惑 下文有解析

函数执行逻辑

  • 将调用nextTick是传入的执行函数添加到 callbacks中
  • 可使用call和传入的ctx修改传入的执行函数的this指向
export function nextTick(cb?: Function, ctx?: Object) {
  /*  nexttick的参数中cb不能为可选参数,如果cb参数不传将没有回调函数,
    nextTick将没有意义,并且ctx将成为第一个参数,由于是形参,ctx将顶替cb,此时ctx相当于没有了,
    resolve出去的将是一个undefined */
  // cb 是 nextTick 包裹的执行函数
  // ctx 是函数执行的上下文 
  // console.log("nextTick000",cb,ctx)
  let _resolve//伪代码
  // 向 callbacks 数组中添加一个函数
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)//修改回调函数的this指向 
      } catch (e) {
        //  异常捕获 如果cb不是函数 捕获异常
        handleError(e, ctx, 'nextTick')
      }
      console.log("00000")
    } else if (_resolve) {
      // 伪代码
      console.log('ctx')
      _resolve(ctx)
    }
  })
  console.log("9999")
  // console.log("pendingcallbacks",callbacks,pending)
  if (!pending) {
    // console.log("pendingcallback0999",callbacks,pending)
    pending = true
    timerFunc()
  }
    if (!cb && typeof Promise !== 'undefined') {//判断浏览器是否支持 Promise
//  cb不可能没有,由于传递的是形参,第一个回调函数如果没有传递,则第二个参数ctx顶上,所以cb不可能没有
// 并且如果cb没有了,则表示没有传递任何参数
    console.log("111")
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  • 这个代码有删减,因为其余代码执行 有所困惑 下文有解析

代码分析

nextTick错误分析2.png

  • nexttick的参数中cb不能为可选参数,如果cb参数不传将没有回调函数,nextTick将没有意义,并且ctx将成为第一个参数,由于是形参,ctx将顶替cb,此时ctx相当于没有了,resolve出去的将是一个undefined

代码实测

  • 结果很意外,得到的是当前组件的实例
  • 实在没看明白,这个ctx 组件的实例 是怎么出现在这个代码中的

依次执行nextTick

  • 循环执行 callbacks 任务队列中的任务
function flushCallbacks() {
  //循环执行 callbacks  任务队列中的任务
  pending = false
  const copies = callbacks.slice(0)//
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    // 依次执行callbacks数组中的函数
    copies[i]()
  }
}

源码

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// isIE 判段运行环境是否在ie浏览器
// isIOS 判断是否是ios
//isNative  判断函数是否是由JavaScript引擎原生实现的
export let isUsingMicroTask = false//是否使用微任务标志

const callbacks = []//任务对列 调用nextTick时传入的回调函数组成的数组
let pending = false//初始化 是否在进行中状态  默认是false  

function flushCallbacks() {
  //循环执行 callbacks  任务队列中的任务
  pending = false
  const copies = callbacks.slice(0)//
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    // 依次执行callbacks数组中的函数
    copies[i]()
  }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 向下兼容操作 如果支持Promise 则使用Promise
  const p = Promise.resolve()//直接返回一个resolved状态的Promise对象
  timerFunc = () => {
    p.then(flushCallbacks)
    // 在Promise的then方法中执行 flushCallbacks 函数
    if (isIOS) setTimeout(noop)//ios中在一些异常的webview中,promise结束后任务队列并没有刷新,所以强制执行setTimeout(noop)来刷新任务队列
  }
  isUsingMicroTask = true//重置使用微任务标示为true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||

  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// MutationObserver 属性支持,则使用MutationObserver 
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  // 创建一个MutationObserver 用于监听dom改动之后执行 flushCallbacks 函数 并赋值给 observer
  const textNode = document.createTextNode(String(counter))
  // 创建一个文本节点
  observer.observe(textNode, {
    characterData: true
  })
  // 每次执行timeFunc都会让文本节点的内容在0/1之间切换
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  // 切换之后将新值赋值到那个我们MutationObserver观测的文本节点上去
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// setImmediate 属性支持,则使用setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 以上都不支持,则使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(cb?: Function, ctx?: Object) {
   /*  nexttick的参数中cb不能为可选参数,如果cb参数不传将没有回调函数,
    nextTick将没有意义,并且ctx将成为第一个参数,由于是形参,ctx将顶替cb,此时ctx相当于没有了,
    resolve出去的将是一个undefined */
  // cb 是 nextTick 包裹的执行函数
  // ctx 是函数执行的上下文 
  // console.log("nextTick000",cb,ctx)
  let _resolve//伪代码
  // 向 callbacks 数组中添加一个函数
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)//修改回调函数的this指向 
      } catch (e) {
        //  异常捕获 如果cb不是函数 捕获异常
        handleError(e, ctx, 'nextTick')
      }
      console.log("00000")
    } else if (_resolve) {
      console.log('ctx')
      _resolve(ctx)
    }
  })
  console.log("9999")
  // console.log("pendingcallbacks",callbacks,pending)
  if (!pending) {
    // console.log("pendingcallback0999",callbacks,pending)
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {//判断浏览器是否支持 Promise
//  cb不可能没有,由于传递的是形参,第一个回调函数如果没有传递,则第二个参数ctx顶上,所以cb不可能没有
// 并且如果cb没有了,则表示没有传递任何参数
    console.log("111")
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

个人困惑

  • 本人实在是不太理解在没有传入参数时,ctx是怎么拿到的,并且返回resolve有何作用
  _resolve(ctx)

致谢

  • 感谢您百忙之中抽时间阅读我写的博客,谢谢你的肯定,也希望对您能有所帮助
  • 如果您有更好的见解请在评论区留言或者私聊我,期待与您的交流