04 | 【阅读Vue2源码】$nextTick实现原理

前言

$nextTick是个很常用的API,简单来讲其作用是让函数延后执行。

来看下官方的描述

4

深入响应式原理的文章中也有介绍到

4

上面官方的描述,其实也解答了,nextTick就是使用Promise、setTimeout等异步函数实现的。

分析

基本用法

<section id="app">
  <div id="count">{{ count }}</div>
  <button @click="plus">+1</button>
</section>

new Vue({
  name: 'SimpleDemoAPI',
  data() {
    return {
      count: 0
    }
  },
  methods: {
    plus() {
      this.count += 1;

      // 未更新前的值
      console.log('alan->count sync', document.getElementById('count').innerText) 
      
      this.$nextTick(() => {
        // 更新后的值
        console.log('alan->count $nextTick', document.getElementById('count').innerText) 
      })
    }
  }
})

4

实现原理

源码分析

$nextTick实际上是nextTick函数,在初始化Vue的时候,把nextTick赋值给了$nextTick

// src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this)
}

源码位置:src/core/util/next-tick.js

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 收集回调函数的队列
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]() // 把回调队列中的函数取出来执行
}
}
// 定义定时器函数,后面赋值
let timerFunc
// 如果运行环境支持Promise,则使用Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => { // 定义行数,赋值给timerFunc
p.then(flushCallbacks) // 将flushCallbacks作为resolve的回调函数,执行完回调队列中的函数
// ios中有特殊情况
// 在有问题的UIWebViews中,Promise.then不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。
// 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true // 标记使用微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 当运行环境不支持Promise时,判断下是否支持MutationObserver,如支持则使用MutationObserver
let counter = 1
// 用flushCallbacks作为MutationObserver的回调函数
const observer = new MutationObserver(flushCallbacks)
// 创建临时的文本节点,用MutationObserver观测它的变化,以触发new MutationObserver(flushCallbacks)执行
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => { // 将更改DOM的函数赋值给timerFunc
// 当执行nextTick时,会执行timerFunc,这里改变textNode的值,每次+1
// 触发new MutationObserver(flushCallbacks)执行
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 当运行环境不支持Promise、MutationObserver时,判断下是否支持setImmediate,如支持则使用setImmediate
// setImmediate这个API只在node环境下可用
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 如果上面的方式都不行,则使用setTimeout
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 调用$nextTick时,执行该函数
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve // 缓存Promise的resolve
// 收集回调函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 执行Promise的resolve回调
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 执行定时器函数,核心逻辑
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

实现逻辑:

  1. 初始化模块文件

  2. 定义callbacks

  3. 定义状态pending = false

  4. 定义冲刷队列函数flushCallbacks()

  5. 定义定时器函数timerFunc()

    • 如果有Promise,直接执行Promise.resolve(flushCallbacks)
    • 如果有MutationObserver,就new MutationObserver(flushCallbacks),然后创建一个文本节点,用observer观测它。在timerFunc()里改变文本节点的值textNode.data = String((counter + 1) % 2),当执行timerFunc()时,改变文本节点,变化一次就触发一次MutationObserver,就会执行flushCallbacks()
    • 如果有setImmediate,就执行setImmediate(flushCallbacks)
    • 如果上面都不行,就使用setTimeout,执行setTimeout(flushCallbacks, 0)
  6. 把回调函数cb,放入一个callbacks队列里

  7. 标记执行状态pending = true

  8. 执行定时器函数timerFunc(),走timerFunc里面的逻辑

调用链路

4

一些疑问

ios下为什么要执行一下setTimeout?

在有问题的UIWebViews中,Promise.then不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。因此,我们可以通过添加空计时器来“强制”刷新微任务队列。

为什么要用MutationObserver呢?

在不支持Promise的地方使用,例如:PhantomJS, iOS7, Android 4.4

降级到定时器,为什么优先选择setImmediate?

从技术上讲,它利用了(宏)任务队列,但它仍然是比setTimeout更好的选择。

总结

nextTick其实挺简单的,底层就是使用了微任务/宏任务来实现,Promise -> MutationObserver -> setImmediate -> setTimeout,将回调函数存放到队列中,然后利用事件循环的特点,每次循环结束前,先将微任务、宏任务清空。

原文链接:https://juejin.cn/post/7256176612197367866 作者:AlanLee

(0)
上一篇 2023年7月17日 上午10:22
下一篇 2023年7月17日 上午10:32

相关推荐

发表回复

登录后才能评论