1. 前言

原文发布在语雀:

<Vue 源码笔记系列4>异步更新队列与$nextTick · 语雀​www.yuque.com

上一章我们讲到了修改数据是如何触发渲染函数的观察者,最终调用 Watcher 的 run 方法重新求值并渲染页面。
当时我们提到了页面更新是异步的,本章我们来看一下 Vue 是如何实现的异步更新队列。
当然,提起异步更新,我们自然会想到与之相关的 $nextTick 方法,这个方法接收一个回调,在页面更新完成后执行。

2. 流程图

老规矩,先上图:

3. renderWatcher 与 $nextTick

3.1 renderWatcher

Watcher 的 update 代码如下:

// src/core/observer/watcher.jsupdate () {// ...queueWatcher(this)
}

调用 queueWatcher 并将自己作为参数

queueWatcher 代码:

// src/core/observer/scheduler.jsexport function queueWatcher (watcher: Watcher) {const id = watcher.idif (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// queue the flushif (!waiting) {waiting = truenextTick(flushSchedulerQueue)}}
}

首先拿到 watcher 的 id。

if (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {// ...queue.splice(i + 1, 0, watcher)}
}

然后判断 has[id] 是否为空。has 是在 scheduler.js 文件声明的 Map。

let has: { [key: number]: ?true } = {}

由下一句 has[id] = true 可以知道,has 收集了所有 watcher 的 id,以 id 为键名,值为 true。这么做的目的是防止收集重复的 watcher。

下一个判断,if (!flushing) ,flushing 标志当前是否在执行更新,也就是是否在执行 watcher.run。如果为 false,说明当前的 queue 里的 watcher 还没有被触发,我们就将当前 watcher push 到 queue 中。如果 flushing 值为 true。说明 queue 队列中的 watcher 正在执行,我们不能简单地将 watcher 插入到 queue 末尾了,至于如何插入,我们将在另外的章节来讲。

再往下:

if (!waiting) {waiting = truenextTick(flushSchedulerQueue)
}

wating 定义在文件顶部,初始值为 false。目的是保证在 queue 中的 watcher 被执行前,只执行一次 nextTick。
为什么这么做呢,我们先来看一下 nextTick 传入的回调干了些什么。

flushSchedulerQueue:

// src/core/observer/scheduler.jsfunction flushSchedulerQueue () {flushing = truelet watcher, idqueue.sort((a, b) => a.id - b.id)for (index = 0; index < queue.length; index++) {watcher = queue[index]if (watcher.before) {watcher.before()}id = watcher.idhas[id] = nullwatcher.run()// ...}// ...resetSchedulerState()// ...
}

flushSchedulerQueue 的主要作用就是先将 flushing 置为 true,然后遍历 queue 队列,依次调用其 watcher 的 run 方法。最后调用 resetSchedulerState,看名字这个方法应该是重置状态用的,将 wating 和 flushing 重置为 初始值 false。

// src/core/observer/scheduler.js
function resetSchedulerState () {// ...waiting = flushing = false
}

我们来回顾一下 queueWatcher。它会排除重复的 watcher,存入 queue。然后执行一次 nextTick,由 nextTick 在合适的时机执行回调,该回调将遍历 queue,执行存储的 watche r的 run 方法。

由此看来,nextTick 是决定回调什么时候执行,也就是页面什么时候更新的。先不着急看 nextTick。我们来看一下我们经常使用的 $nextTick。

3.2 $nextTick

在 renderMixin 中我们找到了 $nextTick

// src/core/instance/render.jsexport function renderMixin (Vue: Class<Component>) {// ...Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)}// ...
}

代码比较简单,$nextTick 实际上也是调用的 nextTick 方法,唯一的不同是将当前组件实例作为第二个参数传入了。

既然大家最终都是调用了 nextTick 方法,我们就来看一下。

4. nextTick

理解 nextTick 需要对 js 异步队列的执行机制有一定的了解,至少需要知道宏任务(macroTask),微任务(microTask),事件循环(Event Loop),调用栈(Call Stack),回调事件队列(Callback Queue)这些概念。
我们这里只作大致的讲解,详细的解释大家可以自行查阅资料,或者我哪天会单独发一篇文章讲这个。

4.1 JS 异步

JS 通过回调的方式,异步处理耗时操作。
Event Loop 负责监听 Call Stack 与 Callback Queue。当 Call Stack 为空时,取出 Callback Queue 中的第一个事件(回调函数)放到 Call Stack 中执行。后续不断循环此操作。

如下代码:

console.log('Hi')
setTimeout(function cb1() { console.log('cb1')
}, 5000)
console.log('Bye')

执行过程如下图:

由此可以看出,setTimeout 并不是严格的延迟指定时间后执行回调。浏览器执行到 setTimeout 之后,会在 Callback Queue 中添加相应的回调,并且浏览器的 Web Api 会开启一个定时器,在 5s 后通知 Event Loop 将该回调放入 Call Stack 执行,但是此时 Call Stack 并不一定是空的,所以可能会等待 Call Stack 空了之后再将此回调放入 Call Stack 执行。

JS 除了有同步异步之分外,还有宏任务与微任务之分:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval;
  • micro-task(微任务):Promise,process.nextTick;

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

上边代码与图片来自 https://juejin.im/post/5a5d64fbf265da3e243b831f。

4.2 nextTick 实现

// src/core/util/next-tick.js
const callbacks = []
let pending = falseexport function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = trueif (useMacroTask) {macroTimerFunc()} else {microTimerFunc()}}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}

代码其实也不复杂,咱们慢慢看

export function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})// ...
}

callbacks 并不是直接存储 cb,而是存储箭头函数,在函数内部调用 cb,因为 cb 可能为空。

再往下:

if (!pending) {pending = trueif (useMacroTask) {macroTimerFunc()} else {microTimerFunc()}
}

pending 是一个标志位,与我们之前见到的 waiting 很像,保证了 if 内的语句只执行一次。
在 if 内部调用了 macroTimerFunc 或者 microTimerFunc,之所以出现这种情况,是因为有些浏览器不支持Promise 实现的 microTimerFunc。

先来看一下微任务 microTimerFunc 的实现:

// src/core/util/next-tick.jsif (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()microTimerFunc = () => {p.then(flushCallbacks)if (isIOS) setTimeout(noop)}
} else {microTimerFunc = macroTimerFunc
}

如果浏览器支持原生 Promise 的话,将 flushCallbacks 作为一个立即 resolve 的 Promise 的 then 回调。由于 promise 为 microTask,所以将在 call stack 空闲时,优先执行。所以我们优先使用 microTimerFunc。当浏览器不兼容时使用 macroTimerFunc。

// src/core/util/next-tick.jsif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {macroTimerFunc = () => {setImmediate(flushCallbacks)}
} else if (typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) ||// PhantomJSMessageChannel.toString() === '[object MessageChannelConstructor]'
)) {const channel = new MessageChannel()const port = channel.port2channel.port1.onmessage = flushCallbacksmacroTimerFunc = () => {port.postMessage(1)}
} else {/* istanbul ignore next */macroTimerFunc = () => {setTimeout(flushCallbacks, 0)}
}

我们知道 setTimeOut 为 macroTask,但在这里 setTimeOut 只作为优先级最低的方案。
首先使用 setImmediate,但是兼容性比较差,目前只有 IE10 实现了。其次使用 MessageChannel,这个是 webWorker 相关,可以了解一下。这两种方案都不需要超时检测,所以性能更好,最后我们使用 setTimeOut。

再来看一下被加入到 task 的方法 flushCallbacks:

// // src/core/util/next-tick.jsfunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}

复制 callbacks 并清空,遍历复制体,依次执行。这里就会执行最开始 nextTick 接收的 cb。

最后,如果没有传入 cb 是什么情况呢。
nextTick 还有最后几行代码:

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

将 _resolve 赋值为 Promise.resolve,所以在此时 nextTick 会返回立即 resolve 的 Promise。

5. 小结

异步更新与 $nextTick 是我们经常接触到的 Vue 特性。如果没有异步更新的特性,性能会相当差。
比如:

for (let i = 0; i < 1000; i++) {this.text = i
}

我们修改 1000 次 text,如果没有异步更新的话,浏览器将刷新 1000 次,使用异步更新后,只会执行最后一次也即 this.text = 999。
在前边几章中,我们都是讲解大致的代码流程,很多细节都放过了。下一章我们来补一下 watch 的实现。

vue如何让一句代码只执行一次_lt;Vue 源码笔记系列4gt;异步更新队列与$nextTick...相关推荐

  1. vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现

    1. 前言 原文发布在语雀: <Vue 源码笔记系列6>计算属性 computed 的实现 · 语雀​www.yuque.com 上一章我们已经学习过 watch,这一章就来看一下计算属性 ...

  2. vue源码分析系列三:render的执行过程和Virtual DOM的产生

    render 手写 render 函数,仔细观察下面这段代码,试想一下这里的 createElement 参数是什么 . new Vue({el: '#application',render(crea ...

  3. vue源码分析系列二:$mount()和new Watcher()的执行过程

    续vue源码分析系列一:new Vue的初始化过程 在initMixin()里面调用了$mount() if (vm.$options.el) {vm.$mount(vm.$options.el);/ ...

  4. Vue源码解析系列——数据驱动篇:patch的执行过程

    准备 vue版本号2.6.12,为方便分析,选择了runtime+compiler版本. 回顾 如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:<Vue源码分析系列:目录> ...

  5. vue修改节点class_Vue2.0 源码解读系列 来自 Vue 的神秘礼盒

    鄢栋,微医云服务团队前端工程师.有志成为一名全栈开发工程师甚至架构师,路漫漫,吾求索.生活中通过健身释放压力,思考问题. 目前 Vue3.0 打的很火热,都已经出了很多 Vue3.0 源码解析系列的博 ...

  6. 100个必会的python脚本-100行Python代码实现自动抢火车票(附源码)

    前言 又要过年了,今年你不妨自己写一段代码来抢回家的火车票,是不是很Cool.下面话不多说了,来一起看看详细的介绍吧. 先准备好: 12306网站用户名和密码 chrome浏览器及下载chromedr ...

  7. vue源码:Watcher系列(一)

    少年驰骋,仗剑天涯 愿你眼眸有星辰,心中有大海 从此,以梦为马,不负韶华 在分析之前我们先来看看,vue中都有哪些Watcher种类呢?以及分别在什么时候创建呢?从vue源码里面看,Watcher是一 ...

  8. 开源三级联动,Vue.js编写省份、城市、区、县三级联动源码

    开源三级联动,Vue.js编写省份.城市.区.县三级联动源码 1.三级联动框样式 上图: 请访问:这里!! 查看三级联动器效果. 2.如何在html里面引用 文件的目录路径为: data.js是存放我 ...

  9. java计算机毕业设计vue健康餐饮管理系统设计与实现MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计vue健康餐饮管理系统设计与实现MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计vue健康餐饮管理系统设计与实现MyBatis+系统+LW文档+源码+调试部 ...

最新文章

  1. 一文看尽深度学习中的20种卷积(附源码整理和论文解读)
  2. python设计模式-观察者
  3. Redis 常用操作命令
  4. [C#] 接收和发送UDP数据
  5. Camera360与全球1.8亿用户共同创造更美的照片
  6. cocos2dx xcode5 创建项目
  7. 服务器提交协议冲突 Section=ResponseStatusLine 的解决办法
  8. php 调用图,php 缩略图类(附调用示例)
  9. 计算任意两个圆的交点
  10. 2013暑假江西联合训练赛 -- by jxust_acm 解题报告
  11. php5.4 无法连接mongo,php连接MongoDB总是失败,为什么?
  12. 【Python - wxpython】- 卫星通信系统链路计算软件
  13. 股票和数据分析--加权平均数
  14. 一张帖搞定同学们入学黑马前所有难题
  15. 湖畔大学梁宁:比能力重要1000倍的,是你的底层操作系统,与CSDN伙伴们一起共勉!
  16. 未来软件开发的发展趋势
  17. 《土豆荣耀》重构笔记(五)创建角色以及怪物的动画
  18. 如何设置本电脑中的mysql让别人的电脑连接
  19. PDF转word怎么转换
  20. 携手Nutanix,AMD EPYC服务器打造全新超融合生态

热门文章

  1. 数据库——SQL-SERVER练习(4) 建表及数据完整性
  2. 算法工程师进化-基础理论
  3. Spring Security构建Rest服务-0600-SpringSecurity基本原理
  4. python--for循环
  5. 思考…求知(double和Double的区别)
  6. preprocessor预处理器
  7. Sql server2005中如何格式化时间日期
  8. MasterPage控件的用法
  9. 利用URL重写跟踪Session(多学一招)
  10. html未填写提示,文本框输入信息,未输入的文本框会提示输入,并且未输入的文本框会变红...