摘要:本文通过结合官方文档、源码和其他文章整理后,对Vue的nextTick做深入解析。理解本文最好有浏览器事件循环的基础,建议先阅读上文《事件循环Event loop到底是什么》。

一、官方定义


实际上在弄清楚浏览器的事件循环后,Vue的nextTick就非常好理解了,它就是利用了事件循环的机制。我们首先来看看nextTick在Vue官方文档中是如何描述的:

Vue在更新DOM时是异步执行的,只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个事件循环“tick”中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserversetImmediate,如果执行环境不支持,则会采用setTimeout(fn,0)代替。
当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的
DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触
DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。

简单来说,Vue为了保证数据多次变化操作DOM更新的性能,采用了异步更新DOM的机制,且同一事件循环中同一个数据多次修改只会取最后一次修改结果。而这种方式产生一个问题,开发人员无法通过同步代码获取数据更新后的DOM状态,所以Vue就提供了Vue.nextTick方法,通过这个方法的回调就能获取当前DOM更新后的状态。

但只看官方解释可能还是会有些疑问,比如描述中说到的下一个事件循环“tick”是什么意思?为什么会是下一个事件循环?接下来我们看源码到底是怎么实现的。

二、源码解析


Vue.nextTick的源码部分主要分为Watcher部分和NextTick部分,由于Watcher部分的源码在前文《深入解析vue响应式原理》中,已经详细分析过了,所以这里关于Watcher的源码就直接分析触发update之后的部分。

update

update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

queueWatcher

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

flushSchedulerQueue

function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
} // keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue) // devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}

根据前文《深入解析vue响应式原理》可以知道,数据变化后会首先触发关联Dep的notify方法,然后会调用所有依赖该数据的Watcher.update方法。接下来的步骤总结如下:

  1. update又调用了queueWatcher方法;
  2. queueWatcher方法中使用静态全局Watcher数组queue来保存当前的watcher,并且如果Watcher重复,只会保留最新的Watcher;
  3. 然后是flushSchedulerQueue方法,简单来说,flushSchedulerQueue方法中主要就是遍历queue数组,依次执行了所有的Watcher.run,操作DOM更新;
  4. 但flushSchedulerQueue并不会立即执行,而是作为nextTick参数进入下一层。

重点来到了nextTick这一层。

nextTick

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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

timerFunc

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)
}
}

flushCallbacks

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

nextTick代码流程总结如下:

  1. 结合前面代码分析来看,遍历Watcher执行DOM更新的方法传入了nextTick,在nextTick中被添加到了callbacks数组,随后执行了timerFunc方法;
  2. timerFunc方法使用了flushCallbacks方法,flushCallbacks执行了flushSchedulerQueue方法,即执行Watcher关联的DOM更新。
  3. 而timerFunc是根据浏览器支持情况,将flushCallbacks(DOM更新操作)作为参数传递给Promise.then、MutationObserver、setImmediatesetTimeout(fn,0)

到这里我们明白了,原来在Vue中数据变更触发DOM更新操作也是使用了nextTick来实现异步执行的,而Vue提供给开发者使用的nextTick是同一个nextTick。所以官方文档强调了要在数据变化之后立即使用
Vue.nextTick(callback),这样就能保证callback是插入队列里DOM更新操作的后面,并在同一个事件循环中按顺序完成,因为开发者插入的callback在队尾,那么始终是在DOM操作后立即执行。

而针对官方文档“在下一个事件循环"tick"中,Vue刷新队列并执行实际(已去重的)工作”的描述我觉得是不够严谨的,原因在于,根据浏览器的支持情况,结合浏览器事件循环宏任务和微任务的概念,nextTick使用的是Promise.then或MutationObserver,那就应该是和script(整体代码)是同一个事件循环;当使用的是setImmediate或setTimeout(fn,0)),那才在下一个事件循环。
同时,聪明的你或许已经想到了,那按这个原理实际我不需要使用nextTick好像也可以达到同样的效果,比如使用setTimeout(fn,0),那我们直接用一个例子来看一下吧。

<template>
<div class="box">{{msg}}</div>
</template> <script>
export default {
name: 'index',
data () {
return {
msg: 'hello'
}
},
mounted () {
this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
setTimeout(() => {
console.log(box.innerHTML) // world
})
}
}

结果确实符合我们的想象,不过仔细分析一下,虽然能达到同样的效果,但跟nextTick有点细微差异的。这个差异就在于,如果使用nextTick是能保证DOM更新操作和callback是放到同一种任务(宏/微任务)队列来执行的,但使用setTimeout(fn,0)就很可能跟DOM更新操作没有在同一个任务队列,而不在同一事件循环执行,不过这细微差异目前还没发现有什么问题,反正是可以正确获取DOM更新后状态的。

基于源码分析Vue的nextTick的更多相关文章

  1. 从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分发的过程(二)

    缘起 哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angu ...

  2. vue源码分析—Vue.js 源码构建

    Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...

  3. Java容器 | 基于源码分析List集合体系

    一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...

  4. vue源码分析—Vue.js 源码目录设计

    Vue.js 的源码都在 src 目录下,其目录结构如下 src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── ser ...

  5. Java容器 | 基于源码分析Map集合体系

    一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...

  6. Vue系列---理解Vue.nextTick使用及源码分析(五)

    _ 阅读目录 一. 什么是Vue.nextTick()? 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作. 2.2 在created生命周期中进行DO ...

  7. Vue.js 源码分析(六) 基础篇 计算属性 computed 属性详解

    模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的.在模板中放入太多的逻辑会让模板过重且难以维护,比如: <div id="example">{{ messag ...

  8. Vue.js 源码分析(二十五) 高级应用 插槽 详解

    我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过slot标签(插槽)规定对应的内容放置在哪里,比如: ...

  9. Vue.js 源码分析(二十二) 指令篇 v-model指令详解

    Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...

随机推荐

  1. 【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 

    问题描述 Service Bus如何确保消息发送成功,发送端是否有Ack机制(是否有回调API告诉发送端,服务端已经收到消息)?根据对.NET发送Service Bus消息代码的分析,发送方法queu ...

  2. linux下使用vsftp搭建FTP服务器:匿名登录,账号登录,SSL加密传输

    目录 一.关于FTP和VSFTP 二.ftp.sftp.vsftp.vsftpd的区别 三.项目一:搭建一台所有人都可以访问的通用FTP服务器 3.1 项目要求 3.2 项目思路分析 3.3 使用vs ...

  3. Python 中更优雅的日志记录方案

    在 Python 中,一般情况下我们可能直接用自带的 logging 模块来记录日志,包括我之前的时候也是一样.在使用时我们需要配置一些 Handler.Formatter 来进行一些处理,比如把日志 ...

  4. (七)、touch--创建文件或者更新时间戳

    一.命令说明与格式 创建文件并更新时间戳,若要创建的文件名已经存在,则仅仅更新时间戳,而不改变其他任何信息 格式:touch     [选项]   目录名/文件名 选项: -a            ...

  5. springboot日志输出到文件

    今天来谈一谈日志,主要是说一说springboot的日志,因为最近在学习springboot.首先在写代码的时候,要养成记日志的习惯,这点真的很重要,因为之前吃了很多亏.过去我对日志很不在意,该有的日 ...

  6. 【Go】四舍五入在go语言中为何如此困难

    四舍五入是一个非常常见的功能,在流行语言标准库中往往存在 Round 的功能,它最少支持常用的 Round half up 算法. 而在 Go 语言中这似乎成为了难题,在 stackoverflow ...

  7. Logstash学习之路(四)使用Logstash将mysql数据导入elasticsearch(单表同步、多表同步、全量同步、增量同步)

    一.使用Logstash将mysql数据导入elasticsearch 1.在mysql中准备数据: mysql> show tables; +----------------+ | Table ...

  8. Elasticsearch节点下线(退役)and unassigned shards

    一.节点退役当集群中个别节点出现故障预警等情况,需要进行退役工作,即让所有位于该退役节点上的分片的数据分配到其他节点上后,再将此节点关闭并从集群中移除. 1.ES提供了让某个节点上所有数据都移走的功能 ...

  9. 树莓派(4B)新手入门教程

    前期准备 必要物料 树莓派4B 主机 Type-C 电源 内存卡(8G+) 一般建议一步到位64G 系统镜像 镜像写入工具 下载地址 镜像下载 官方下载地址: https://www.raspberr ...

  10. oracle11g数据库安装采坑记录

    第一处坑: 解决方案: 原文:https://blog.csdn.net/yhj198927/article/details/49178279 1.打开oracle的"Net Manager ...