重学Vue【nextTick原理解析】

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 上篇派发更新的最后提到了 nextTick,在Vue中,nextTick 也是一个核心实现,本篇来详细说一下 nextTick 的实现原理。 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

上篇派发更新的最后提到了 nextTick,在Vue中,nextTick 也是一个核心实现,本篇来详细说一下 nextTick 的实现原理。 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


JS运行机制

JS执行是单线程的,它是基于事件循环,详细说明可以查看 一文讲解浏览器运行渲染机制、JS任务队列及事件循环,这里再简单过一下。事件循环大致分为以下几步:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 在主线程之外,存在一个 "任务队列"(task queue),只要异步任务有了运行结果,就在任务队列中放一个事件。
  3. 当执行栈中所有同步任务执行完毕,主线程就会读取任务队列,并把需要异步执行的逻辑放进来,此时异步队列的任务就会结束等待状态,开始执行。
  4. 主线程重复执行上面三步。

网络异常,图片无法展示
|

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

关于宏任务和微任务,也可以在 一文讲解浏览器运行渲染机制、JS任务队列及事件循环 里找到相应的解释,下面来模拟一下执行顺序:

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}


Vue的实现

在Vue里实现的异步操作就是 nextTick。在 Vue 源码 2.5+ 后,nextTick 的实现单独有一个 JS 文件来维护它,它的源码并不多,总共也就 100 多行。接下来我们来看一下它的实现,在 src/core/util/next-tick.js 中:

/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
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]()
  }
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// 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.
/* istanbul ignore if */
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 {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
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
}
/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}
export 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 (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

简单提一下,除了上面的 nextTick 的实现之外,还有两个地方用到了 nextTick

// 1. src/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this)
}
// 2. src/global-api/index.js
Vue.nextTick = nextTick

下面来分析一下 nextTick 的具体实现:

首先在全局定义了一个数组 callbacks,一个状态 pending,以及一个函数 flushCallbacks。接着后面定义了 microTimerFuncmacroTimerFunc,分别对应的是 micro task 的函数和 macro task 的函数,它们两个其实就是针对浏览器的支持程度,做不同的处理。

对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。

接着它对外暴露2个函数:withMacroTasknextTickwithMacroTask 其实就是做了一层封装,确保函数在执行过程中如果对数据进行了修改,触发变化执行 nextTick 的时候强制走一个 marcoTimeFunc,换句话说也就是,强制在DOM事件的回调函数期间,如果修改了数据,那这些数据更改推入的队列就会被当做 macroTasknextTick 后执行。

nextTick 函数,上篇派发更新的最后执行 nextTick(flushSchedulerQueue) 的时候用到了它,它不仅可以传入一个回调函数,还可以传入一个Promise,在这里使用 try/catch 的方式执行是因为JS是单线程的,如果不使用 try/catch 并且执行期间有一个报错了,整个逻辑就会崩掉,后面的逻辑就不会执行了。在执行 nextTick 的时候传入的是匿名函数,通过 push 的方式,把匿名函数全部压到 callbacks 中,接着判断 pending,目的就是确保这块的逻辑只执行一次。然后根据 useMaroTask 来判断走 macroTimerFunc 还是 microTimerFunc,注意,无论执行那个,都会在下一个tick的时候才执行 flushCallbacks

也就是说在当前 tick 内,无论进行多少次 nextTick,都会把 cb 收集起来,放到 callbacks 数组中,然后在下一个 tick 的时候遍历并执行这些匿名函数,整个就是一个异步过程。

除了传入匿名函数的方式之外,也可以不传入匿名函数,通过 nextTick.then(() => {}) 的方式调用:

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

_resolve 执行的时候,就会跳到 then 里面了。

所以:

if (cb) {
  try {
    cb.call(ctx)
  } catch (e) {
    handleError(e, ctx, 'nextTick')
  }
} else if (_resolve) {
  _resolve(ctx)
}

当检测不到 cb 的时候,就判断是不是传入了一个 Promise。

最后注意一点,当 flushCallbacks 执行的时候, cb 是同步执行的,promise 是异步执行。


有趣的问题

下面执行结果是什么?

<template>
  <div>
    <p ref="msg">{{ msg }}</p>
    <button @click="change">change</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "123"
    };
  },
  methods: {
    change() {
      this.$nextTick(() => {
        console.log("nextTick:", this.$refs.msg.innerText);
      });
      this.msg = "456";
      console.log("sync:", this.$refs.msg.innerText);
      this.$nextTick().then(() => {
        console.log("promise nextTick:", this.$refs.msg.innerText);
      });
    }
  }
};
</script>


输出:

sync: 123
nextTick: 123
promise nextTick: 456

过程大概是这样的:首先打印的是 sync 的值,接着打印 nextTick 的,最后打印 promise nextTick 的值。第一步调用 setter 的时候会调用内部的 nextTick 函数,第二步手动调用 nextTick,第三步也是手动调用,那上面分析过每一次进行 nextTick 的时候,都是在 callbacks 里进行 push 操作,这样也就意味着先 push 的先执行,遍历也是从前往后的,所以先添加的就会先执行,而对于上面的例子来说,先添加的是一个 nextTick 匿名函数,然后再添加修改msg为 456 时的watcher,也就是 flushSchedulerQueue,也就是说整体在执行 flushCallbacks 的时候,会先执行 nextTick 的匿名函数,然后在执行 flushSchedulerQueue 的时候才会重新渲染,所以重新渲染是在匿名函数之后,所以匿名函数打印的是原来的值,而第三个 promise nextTick 就是打印的渲染之后的了,

如果说打印的不是 this.$refs.msg.innerText,而是 this.msg,那么所有的就都是 456 了,因为 this.msg 修改了值,会立刻发生那个变化,而视图的更新(DOM的变化)是在下一个 tick 才会进行的,所以打印的就都一样了。

目录
相关文章
|
30天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
20天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
17天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
31 1
|
23天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
62 3
|
5天前
|
存储 供应链 物联网
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
|
5天前
|
存储 供应链 安全
深度解析区块链技术的核心原理与应用前景
深度解析区块链技术的核心原理与应用前景
12 0
|
20天前
|
供应链 安全 分布式数据库
探索区块链技术:从原理到应用的全面解析
【10月更文挑战第22天】 本文旨在深入浅出地探讨区块链技术,一种近年来引起广泛关注的分布式账本技术。我们将从区块链的基本概念入手,逐步深入到其工作原理、关键技术特点以及在金融、供应链管理等多个领域的实际应用案例。通过这篇文章,读者不仅能够理解区块链技术的核心价值和潜力,还能获得关于如何评估和选择适合自己需求的区块链解决方案的实用建议。
37 0
|
22天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
1月前
|
前端开发 JavaScript UED
axios取消请求CancelToken的原理解析及用法示例
axios取消请求CancelToken的原理解析及用法示例
89 0
|
1月前
|
JavaScript
深入解析:JS与Vue中事件委托(事件代理)的高效实现方法
深入解析:JS与Vue中事件委托(事件代理)的高效实现方法
41 0

推荐镜像

更多