基于源码分析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本质上不过是语 ...
随机推荐
- BERT 论文阅读笔记
BERT 论文阅读 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 由 @快刀切草莓君 ...
- iOS 自定义tabBarController(中间弧形)
效果图 1.在继承自UITabBarController的自定义controller中调用以下方法(LZCustomTabbar为自定义的tabbar) - (void)viewDidAppear:( ...
- [EF] - Entity Framework 6处理User Defined Function(UDF SQL Server)
随着EF5的发布,新增了对数据库(SQL Server) UDF的支持,具体可以看以下的连接:https://msdn.microsoft.com/en-us/data/hh859577.aspx,新 ...
- replaceAll
/** * 根据正则是,替换对应内容 * @return */ public static String replaceByRegex(String regex,String src,Strin ...
- 【mysql】- Expalin篇
简介 id:在一个大的查询语句中每个 SELECT 关键字都对应一个唯一的id 与查询优化器有关,假如被优化过,那么可能是上下两个的id都是一样的 select_type:SELECT 关键字对应的那 ...
- Qt学习笔记-Qtcreator的webkit和qt4.7.0的版本有关
之前下载了一个最新的是qtcreator,是通过ubuntu的是apt-get下载的.可是里面没有webkit控件.网上的网友说是最新的没有了.要用老版的,于是下载了一个2.5.2的就正常了. 用老版 ...
- Atcoder abc187 F Close Group(动态规划)
Atcoder abc187 F Close Group 题目 给出一张n个点,m条边的无向图,问删除任意数量的边后,留下来的最少数量的团的个数(\(n \le 18\) ) 题解 核心:枚举状态+动 ...
- Javascript函数闭包及案例详解
什么情况下会形成闭包,什么是闭包 闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员 下面来看一个 ...
- Face_to_object_design
二.实例 掷骰子游戏:三粒骰子,掷两次,比较两次的结果. 1.提炼 提炼对象:三粒骰子.游戏 提炼对象的属性和功能:掷骰子.比较点数 骰子: 属性:点数 功能:随机获取一个1~6之间的整数值. 游戏: ...
- Web Service 服务无法连接Oracle数据库
这个问题之前部署就遇到过,但是后来忘了,所以记录一下吧. 我部署Web Service服务的时候,服务没法正常运行,与Oracle数据库无法正常通信. 检查了数据库连接字没有任何问题,写了个测试接口, ...