1. 前言

nextTickVue 中的一个核心功能,在 Vue 内部实现中也经常用到 nextTick。在介绍 nextTick 实现原理之前,我们有必要先了解一下这个东西到底是什么,为什么要有它,它是干嘛用的。

2. nextTick到底是什么

官方文档对 nextTick 的功能如是说明:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
}) // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})

2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。

从上面的官方介绍中可以看到,nextTick 的主要功能就是更新数据后让回调函数作用于更新后的DOM 。看到这句话,你可能第一反应是:呸!说了等于没说,还是不理解。那么请看下面这个例子:

<template>
<div id="example">{{message}}</div>
</template>
<script>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
console.log(vm.$el.innerHTML) // '123'
Vue.nextTick(function () {
console.log(vm.$el.innerHTML) // 'new message'
})
</script>

在上面例子中,当我们更新了message的数据后,立即获取vm.$el.innerHTML,发现此时获取到的还是更新之前的数据:123。但是当我们使用nextTick来获取vm.$el.innerHTML时,此时就可以获取到更新后的数据了。这是为什么呢?

这里就涉及到Vue中对DOM的更新策略了,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个事件队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到事件队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新事件队列并执行实际 (已去重的) 工作。

在上面这个例子中,当我们通过 vm.message = 'new message'更新数据时,此时该组件不会立即重新渲染。当刷新事件队列时,组件会在下一个事件循环“tick”中重新渲染。所以当我们更新完数据后,此时又想基于更新后的 DOM 状态来做点什么,此时我们就需要使用Vue.nextTick(callback),把基于更新后的DOM 状态所需要的操作放入回调函数callback中,这样回调函数将在 DOM 更新完成后被调用。

OK,现在大家应该对nextTick是什么、为什么要有nextTick以及怎么使用nextTick有个大概的了解了。那么问题又来了,Vue为什么要这么设计?为什么要异步更新DOM?这就涉及到另外一个知识:JS的运行机制。

3. 前置知识:JS的运行机制

我们知道 JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是宏任务( macro task) 和微任务(micro task),并且每执行完一个个宏任务( macro task)后,都要去清空该宏任务所对应的微任务队列中所有的微任务(micro task),他们的执行顺序如下所示:

for (macroTask of macroTaskQueue) {
// 1. 处理当前的宏任务
handleMacroTask(); // 2. 处理对应的所有微任务
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}

在浏览器环境中,常见的

  • 宏任务( macro task) 有 setTimeoutMessageChannelpostMessagesetImmediate
  • 微任务(micro task)有 MutationObseverPromise.then

OK,有了这个概念之后,接下来我们就进入本篇文章的正菜:从Vue源码角度来分析nextTick的实现原理。

4. nextTick源码分析

nextTick 的源码位于 src/core/util/next-tick.js

nextTick源码主要分为两块:

  1. 能力检测
  2. 根据能力检测以不同方式执行回调队列

4.1 能力检测

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务。

这一部分的源码如下:

let timerFunc;
/* 优先检测微任务(micro task) */
// 检测浏览器是否原生支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
}
// 如果不支持Promise,则检测是否支持原生的 MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
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
}
/* 对于宏任务(macro task) */
// 检测是否支持原生 setImmediate(高版本 IE 和 Edge 支持)
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
// 以上都不支持的情况下,使用setTimeout
else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

以上代码中,Vue 会优先使用微任务,即会优先检测当前浏览器是否支持原生 Promise,如果不支持的话再去检测是否支持原生的 MutationObserver,如果还不支持,则表明当前浏览器不支持微任务,那么就使用宏任务。对于宏任务会优先检测是否支持原生 setImmediate,如果不支持的话就使用 setTimeout 0

4.2 执行回调队列

接下来就进入了核心函数nextTick 中,如下:

const callbacks = []   // 回调队列
let pending = false // 异步锁 // 执行队列中的每一个回调
function flushCallbacks () {
pending = false // 重置异步锁
// 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份并清空回调函数队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行回调函数队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
} 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
timerFunc()
}
// 如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

首先,先来看 nextTick函数,该函数的主要逻辑是:先把传入的回调函数 cb 推入 回调队列 callbacks 数组,同时在接收第一个回调函数时,执行能力检测中对应的异步方法 timerFunc,在该方法中会使用当前浏览器支持的异步方法去异步执行 flushCallbacks,而 flushCallbacks 函数中会对 callbacks 进行遍历,并执行每一个回调函数。

nextTick 函数最后还有一段逻辑:

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

这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:

nextTick().then(() => {})

_resolve 函数执行,就会跳到 then 的逻辑中。

这里有两个问题需要注意:

  1. 如何保证只在接收第一个回调函数时执行异步方法?

    nextTick 源码中使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的情况。

  2. 执行 flushCallbacks 函数时为什么需要备份回调函数队列?执行的也是备份的回调函数队列?

    因为,会出现这么一种情况:nextTick 的回调函数中还使用 nextTick。如果 flushCallbacks 不做特殊处理,直接循环执行回调函数,会导致里面 nextTick 中的回调函数会进入回调队列。

5. 总结

以上就是对 nextTick 的源码分析,我们了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。当我们在实际开发中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行。如下:

getData(res).then(()=>{
this.xxx = res.data
this.$nextTick(() => {
// 这里我们可以获取变化后的 DOM
})
})

(完)

通俗易懂了解Vue中nextTick的内部实现原理的更多相关文章

  1. Vue中$nextTick的理解

    Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...

  2. vue中nextTick

    vue中nextTick可以拿到更新后的DOM元素 如果在mounted下不能准确拿到DOM元素,可以使用nextTick 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue ...

  3. Vue中nextTick()解析

    最近,在开发的时候遇到一个问题,让我对vue中nextTick()的用法加深了了解- 下面是在组件中引用的一个拖拽的组件: <vue-draggable-resizable class=&quo ...

  4. vue中nextTick的理解

    A. vue 中的 nextTick 是什么? 1.首先需要清楚,nextTick是一个函数:这个函数的作用,简单理解就是下一次渲染后才执行 nextTick 函数中的操作: 2.在下一次 DOM 更 ...

  5. vue中$nextTick详细讲解保证你一看就明白

    1.功能描述 今天我们要实现这个一个小功能: 页面渲染完成后展示一个div元素: 当点击这个div元素后: div元素消失: 出现一个input元素:并且input元素聚焦 想必大家我觉得简单,我们一 ...

  6. vue中$nextTick的使用

    转载 https://www.jb51.net/article/154823.htm  ,写的通俗易懂 在这里我有一个疑问,因为在vue中mounted里面执行后,dom节点是挂载上去了的,所以视图上 ...

  7. vue中nextTick的使用(转载)

    转载自:https://www.cnblogs.com/chaoyuehedy/p/8985425.html 简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量 ...

  8. vue中$nextTick的用法

    简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量级的易上手的具有双向数据绑定特性的mvvm框架.本人比较喜欢用之.在我们用vue时,我们经常用到一个方法是th ...

  9. 对vue中nextTick()的理解及使用场景说明

    异步更新队列: 首先我们要对vue的数据更新有一定理解: vue是依靠数据驱动视图更新的,该更新的过程是异步的. 即:当侦听到你的数据发生变化时, Vue将开启一个队列(该队列被Vue官方称为异步更新 ...

随机推荐

  1. [ASP.NET Core 3框架揭秘] 依赖注入:控制反转

    ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...

  2. MyBatis拦截器自定义分页插件实现

    MyBaits是一个开源的优秀的持久层框架,SQL语句与代码分离,面向配置的编程,良好支持复杂数据映射,动态SQL;MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyB ...

  3. 写在Python学习前

    Python是一门非常有意思的语言,不需要太多的语言基础,就可以实现自己想要的操作.许多非计算机专业的学生都可以利用词云分析.分词.画图等实现自己想要的东西.学习Python和学习其他语言一样,需要耐 ...

  4. 基于 WebGL 的 3D 动态柱状图表

    发现现在工业SCADA上或者电信网管方面用图表的特别多,虽然绝大部分人在图表制作方面用的是echarts,他确实好用,但是有些时候我们不能调用别的插件,这个时候就得自己写这些美丽的图表了,然而图表轻易 ...

  5. 使用Line Pos Info 和 Modern C++ 改进打印日志记录

    使用Line Pos Info 和 Modern C++ 改进打印日志记录 使用跟踪值:不管自己是多么的精通,可能仍然使用调试的主要方法之一 printf , TRaCE, outputDebugSt ...

  6. 微信小程序开发环境安装以及相关设置配置

    微信小程序开发环境安装以及相关设置配置 一.安装 软件名称:wechat_devtools_1.02.1907232_x64 软件安装地址:https://developers.weixin.qq.c ...

  7. AutoCAD 2019 for mac 非常好用的CAD三维设计绘图软件

    macOS下用什么cad软件?mac在哪下载cad软件? AutoCAD 2019 for mac 是一款非常好用的CAD三维设计绘图软件,可应用三维建模.CAD.渲染.动画.视觉特效和数字图像. A ...

  8. Redis 哨兵机制以及灾难演练

    #### 哨兵都采用这个配置即可 ##### 1.修改sentinel.conf配置文件 ![image](https://img2018.cnblogs.com/blog/1334966/20191 ...

  9. [CF544] D. Destroying Roads

    D. Destroying Roads time limit per test 2 seconds memory limit per test 256 megabytes input standard ...

  10. Unity的学习笔记(射线检测)

    首先,射线检测的API是这样的,网上找了一下,这个图片看得很清楚: 接下来是自己使用这个进行测试 using System.Collections; using System.Collections. ...