【Vue源码解析】花你15分钟带你深入源码解析nextTick

前言

本篇文章是作者在学习Vue源码时做的笔记,如果有一些不正确的地方欢迎在评论区指正。假如您也和我一样,在准备春招。欢迎加我微信shunwuyu ,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!"

一、异步更新策略

Vue采用异步更新策略当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更。 这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作DOM的次数,如果不采用这种方法,假设数据改变10000次就要去更新10000次DOM,这样是很耗性能的。

二、nextTick的使用

nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行。

  • 下面是一个简单示例,展示了如何使用nextTick:
js 复制代码
import { ref } from "vue";

const str = ref("hello");
console.log(str.value); //hello

nextTick(() => {
  str.value = "world";
  console.log(str.value); // world
});

代码解析:

第一次 console.log 的时候,获取的到的是旧值,这是因为str数据发生变化的时候,Vue 没有立刻去更新DOM,而是将修改数据的操作放在了一个异步操作队列中,如果一直修改相同数据,异步操作队列还会进行去重,等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行 DOM 的更新。

第二次的 console.log 是放到 nextTick 回调函数中的,此时获取到的是新值,是因为 nextTick 的回调函数是在 DOM 更新之后触发的。

三、nextTick 原理

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;

源码解读

源码位置 core/util/next-tick

核心源码总共三个函数。

  • nextTick
  • timerFunc
  • flushCallbacks
ts 复制代码
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false
const callbacks: Array<Function> = []
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]()
  }
}


let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
 * @internal
 */
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        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 => {
      _resolve = resolve
    })
  }
}

顶部的三行导入

ts 复制代码
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
  1. noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错

  2. handleError 错误处理函数

  3. isIE, isIOS, isNative 环境判断函数,

  4. isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false

声明的三个变量

ts 复制代码
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
  • isUsingMicroTask 标记 nextTick 最终是否以微任务执行
  • callbacks存放调用 nextTick 时传入的回调函数
  • pending 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加,当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false。

nextTick()

ts 复制代码
// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数 
// 回调的 this 自动绑定到调用它的实例上 
export function nextTick(cb?: Function, ctx?: Object) { 
    let _resolve 
    // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调 
    callbacks.push(() => { 
        if (cb) { 
        // 对传入的回调进行 try catch 错误捕获 
            try { 
                cb.call(ctx)
            } catch (e) {
                // 进行统一的错误处理 
                handleError(e, ctx, 'nextTick') 
            } 
        } else if (_resolve) {
            _resolve(ctx) 
        }
    })
    
    // 如果当前没有在 pending 的回调, 
    // 就执行 timeFunc 函数选择当前环境优先支持的异步方法 
    if (!pending) { 
        pending = true timerFunc() 
    } 
    
    // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise 
    // 在返回的这个 promise.then 中 DOM 已经更新好了, 
    if (!cb && typeof Promise !== 'undefined') { 
        return new Promise(resolve => { _resolve = resolve }) 
    } 
}

timerFunc()

timerFunc()用于判断当前环境优先支持的异步方法,优先选择微任务。 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次。

优先级:Promise---> MutationObserver---> setImmediate---> setTimeout

setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行,setImmediate 在 IE10 和 node 中支持 。

ts 复制代码
let timerFunc // 判断当前环境是否原生支持 promise

if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promise
  const p = Promise.resolve()
  timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
    // 这里的 setTimeout 是用来强制刷新微任务队列的 
    // 因为在 ios 下 promise.then 后面没有宏任务的话,微任务队列不会刷新
  }
  // 标记当前 nextTick 使用的微任务
  isUsingMicroTask = true
} 
else if (
// 如果不支持 promise,就判断是否支持 MutationObserver 
// 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  let counter = 1
  // new 一个 MutationObserver 类
  const observer = new MutationObserver(flushCallbacks)
  // 创建一个文本节点
  const textNode = document.createTextNode(String(counter))
  // 监听这个文本节点,当数据发生变化就执行 flushCallbacks
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  // 标记当前 nextTick 使用的微任务
  isUsingMicroTask = true
} 
else if (
// 判断当前环境是否原生支持 setImmediate
typeof setImmediate !== 'undefined' && isNative(setImmediate)
) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} 
else {
  // 以上三种都不支持就选择 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks()

如果多次调用 nextTick,会依次执行上面的timerFunc方法,将 nextTick 的回调放在 callbacks 数组中。

通过 flushCallbacks函数遍历 callbacks 数组的拷贝并执行其中的回调。

ts 复制代码
function flushCallbacks() {
  pending = false
  // 拷贝一份 callbacks
  const copies = callbacks.slice(0)
  // 清空 callbacks
  callbacks.length = 0
  // 遍历执行传入的回调
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

为什么要拷贝callbacks

用 callbacks.slice(0) 将 callbacks 拷贝出来一份,是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况。

如果在nextTick回调中又调用了一次nextTick,则又会向callbacks中添加回调,而nextTick回调中的nextTick应该放在下一轮执行,否则就可能出现一直循环的情况,所以需要将callbacks复制一份出来然后清空,再遍历备份列表执行回调。

相关推荐
Amd79414 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You23 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生34 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
菜牙买菜1 小时前
让安卓也能玩出Element-Plus的表格效果
前端
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_1 小时前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun1 小时前
空间数据存储格式GeoJSON
前端
zhang-zan2 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium