基于源码分析Vue的nextTick
摘要:本文通过结合官方文档、源码和其他文章整理后,对Vue的nextTick做深入解析。理解本文最好有浏览器事件循环的基础,建议先阅读上文《事件循环Event loop到底是什么》。
一、官方定义
实际上在弄清楚浏览器的事件循环后,Vue的nextTick就非常好理解了,它就是利用了事件循环的机制。我们首先来看看nextTick在Vue官方文档中是如何描述的:
Vue在更新DOM时是异步执行的,只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个事件循环“tick”中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用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方法。接下来的步骤总结如下:
- update又调用了queueWatcher方法;
- queueWatcher方法中使用静态全局Watcher数组queue来保存当前的watcher,并且如果Watcher重复,只会保留最新的Watcher;
- 然后是flushSchedulerQueue方法,简单来说,flushSchedulerQueue方法中主要就是遍历queue数组,依次执行了所有的Watcher.run,操作DOM更新;
- 但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代码流程总结如下:
- 结合前面代码分析来看,遍历Watcher执行DOM更新的方法传入了nextTick,在nextTick中被添加到了callbacks数组,随后执行了timerFunc方法;
- timerFunc方法使用了flushCallbacks方法,flushCallbacks执行了flushSchedulerQueue方法,即执行Watcher关联的DOM更新。
- 而timerFunc是根据浏览器支持情况,将flushCallbacks(DOM更新操作)作为参数传递给Promise.then、MutationObserver、setImmediate或setTimeout(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的更多相关文章
- 从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分发的过程(二)
缘起 哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angu ...
- vue源码分析—Vue.js 源码构建
Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...
- Java容器 | 基于源码分析List集合体系
一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...
- vue源码分析—Vue.js 源码目录设计
Vue.js 的源码都在 src 目录下,其目录结构如下 src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── ser ...
- Java容器 | 基于源码分析Map集合体系
一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...
- Vue系列---理解Vue.nextTick使用及源码分析(五)
_ 阅读目录 一. 什么是Vue.nextTick()? 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作. 2.2 在created生命周期中进行DO ...
- Vue.js 源码分析(六) 基础篇 计算属性 computed 属性详解
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的.在模板中放入太多的逻辑会让模板过重且难以维护,比如: <div id="example">{{ messag ...
- Vue.js 源码分析(二十五) 高级应用 插槽 详解
我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过slot标签(插槽)规定对应的内容放置在哪里,比如: ...
- Vue.js 源码分析(二十二) 指令篇 v-model指令详解
Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...
随机推荐
- python爬虫爬取安居客并进行简单数据分析
本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 爬取过程一.指定爬取数据二.设置请求头防止反爬三.分析页面并且与网页源码进行比对四.分析页面整理数据 ...
- 服务器安装ESXI6.7
1 从官网下载ESXI镜像文件到本地 https://my.vmware.com/web/vmware/details?downloadGroup=ESXI670&productId=7 ...
- PHP功能代码片段
1.连接MYSQL数据库代码 <?php $connec=mysql_connect("localhost","root","root&qu ...
- Java与C#
Java和C#都是编程的语言,它们是两个不同方向的两种语言 相同点: 他们都是面向对象的语言,也就是说,它们都能实现面向对象的思想(封装,继承,多态) 区别: 1.c#中的命名空间是namespace ...
- 篇章一:SVN服务搭建【基于Windows server 2008R2 + Windows7】
1.软件下载 1.1 软件介绍 Subversion是优秀的版本控制工具,其具体的的优点和详细介绍,这里就不再多说. 首先来下载和搭建SVN服务器. 现在Subversion已经迁移到apache网站 ...
- 持续提升程序员幸福指数——使用abp vnext设计一款面向微服务的单体架构
可能你会面临这样一种情况,在架构设计之前,你对业务不甚了解,需求给到的也模棱两可,这个时候你既无法明确到底是要使用单体架构还是使用微服务架构,如果使用单体,后续业务扩展可能带来大量修改,如果使用微服务 ...
- 超级电容(Supercapacitor) 和电池的比较
之前看到同事在电路设计里使用了超级电容来进行供电,好奇为什么没有用到普通的电池,于是就是找了找两个的区别.有篇文章讲得挺好,所以就直接翻译一下. 超级电容有点像普通电池和一般电容的结合体,能比一般的电 ...
- Scriptable Render Pipeline
Scriptable Render Pipeline SRP的核心是一堆API集合,使得整个渲染过程及相关配置暴露给用户,使得用户可以精确地控制项目的渲染流程. SRP API为原有的Unity构件提 ...
- 看图知义,Winform开发的技术特点分析
整理一下自己之前的Winform开发要点,以图文的方式展示一些关键性的技术特点,总结一下. 1.主体界面布局 2.权限管理系统 3.工作流模块 4.字典管理 5.通用的附件管理模块 6.系统模块化开发 ...
- 在.NET Core中使用Channel(一)
我最近一直在熟悉.net Core中引入的新Channel<T>类型.我想在它第一次发布的时候我了解过它,但是有关文章非常非常少,我不能理解它们与其他队列有什么不同. 在使用了一段时间后, ...