重学Vue【nextTick原理解析】

简介: 上篇派发更新的最后提到了 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 才会进行的,所以打印的就都一样了。

目录
相关文章
|
8天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
12 0
|
8天前
|
JavaScript 前端开发
vue中nextTick使用以及原理
vue中nextTick使用以及原理
9 0
|
6天前
|
负载均衡 算法
Dubbo-负载均衡原理解析(1),一个本科渣渣是怎么逆袭从咸鱼到Offer收割机的
Dubbo-负载均衡原理解析(1),一个本科渣渣是怎么逆袭从咸鱼到Offer收割机的
|
6天前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
6天前
|
JavaScript 前端开发
深入理解Vue.js中的nextTick:实现异步更新的奥秘
深入理解Vue.js中的nextTick:实现异步更新的奥秘
|
7天前
|
Web App开发 开发框架 前端开发
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
13 0
|
7天前
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
18 2
|
8天前
|
机器学习/深度学习 存储 算法
卷积神经网络(CNN)的数学原理解析
卷积神经网络(CNN)的数学原理解析
37 1
卷积神经网络(CNN)的数学原理解析
|
8天前
|
传感器 数据采集 存储
岩土工程监测仪器之一:振弦采集仪的工作原理解析
岩土工程监测仪器之一:振弦采集仪的工作原理解析
岩土工程监测仪器之一:振弦采集仪的工作原理解析
|
8天前
|
XML JavaScript 数据格式
Beautiful Soup 库的工作原理基于解析器和 DOM(文档对象模型)树的概念
【5月更文挑战第10天】Beautiful Soup 使用解析器(如 html.parser, lxml, html5lib)解析HTML/XML文档,构建DOM树。它提供方法查询和操作DOM,如find(), find_all()查找元素,get_text(), get()提取信息。还能修改DOM,添加、修改或删除元素,并通过prettify()输出格式化字符串。它是处理网页数据的利器,尤其在处理不规则结构时。
39 2

推荐镜像

更多