深入理解Vue nextTick 机制


Posted in Javascript onApril 28, 2018

我们先来看一段Vue的执行代码:

export default {
 data () {
  return {
   msg: 0
  }
 },
 mounted () {
  this.msg = 1
  this.msg = 2
  this.msg = 3
 },
 watch: {
  msg () {
   console.log(this.msg)
  }
 }
}

这段脚本执行我们猜测1000m后会依次打印:1、2、3。但是实际效果中,只会输出一次:3。为什么会出现这样的情况?我们来一探究竟。

queueWatcher

我们定义 watch 监听 msg ,实际上会被Vue这样调用 vm.$watch(keyOrFn, handler, options) 。 $watch 是我们初始化的时候,为 vm 绑定的一个函数,用于创建 Watcher 对象。那么我们看看 Watcher 中是如何处理 handler 的:

this.deep = this.user = this.lazy = this.sync = false
...
 update () {
  if (this.lazy) {
   this.dirty = true
  } else if (this.sync) {
   this.run()
  } else {
   queueWatcher(this)
  }
 }
...

初始设定 this.deep = this.user = this.lazy = this.sync = false ,也就是当触发 update 更新的时候,会去执行 queueWatcher 方法:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
...
export function queueWatcher (watcher: Watcher) {
 const id = watcher.id
 if (has[id] == null) {
  has[id] = true
  if (!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 - 1
   while (i > index && queue[i].id > watcher.id) {
    i--
   }
   queue.splice(i + 1, 0, watcher)
  }
  // queue the flush
  if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
  }
 }
}

这里面的 nextTick(flushSchedulerQueue) 中的 flushSchedulerQueue 函数其实就是 watcher 的视图更新:

function flushSchedulerQueue () {
 flushing = true
 let watcher, id
 ...
 for (index = 0; index < queue.length; index++) {
  watcher = queue[index]
  id = watcher.id
  has[id] = null
  watcher.run()
  ...
 }
}

另外,关于 waiting 变量,这是很重要的一个标志位,它保证 flushSchedulerQueue 回调只允许被置入 callbacks 一次。 接下来我们来看看 nextTick 函数,在说 nexTick 之前,需要你对 Event Loop 、 microTask 、 macroTask 有一定的了解,Vue nextTick 也是主要用到了这些基础原理。如果你还不了解,可以参考我的这篇文章 Event Loop 简介 好了,下面我们来看一下他的实现:

export const nextTick = (function () {
 const callbacks = []
 let pending = false
 let timerFunc

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

 // An asynchronous deferring mechanism.
 // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
 // but microtasks actually has too high a priority and fires in between
 // supposedly sequential events (e.g. #4521, #6690) or even between
 // bubbling of the same event (#6566). Technically setImmediate should be
 // the ideal choice, but it's not available everywhere; and the only polyfill
 // that consistently queues the callback after all DOM events triggered in the
 // same loop is by using MessageChannel.
 /* istanbul ignore if */
 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
   setImmediate(nextTickHandler)
  }
 } else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = nextTickHandler
  timerFunc = () => {
   port.postMessage(1)
  }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // use microtask in non-DOM environments, e.g. Weex
  const p = Promise.resolve()
  timerFunc = () => {
   p.then(nextTickHandler)
  }
 } else {
  // fallback to setTimeout
  timerFunc = () => {
   setTimeout(nextTickHandler, 0)
  }
 }

 return function queueNextTick (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, reject) => {
    _resolve = resolve
   })
  }
 }
})()

首先Vue通过 callback 数组来模拟事件队列,事件队里的事件,通过 nextTickHandler 方法来执行调用,而何事进行执行,是由 timerFunc 来决定的。我们来看一下 timeFunc 的定义:

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
   setImmediate(nextTickHandler)
  }
 } else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = nextTickHandler
  timerFunc = () => {
   port.postMessage(1)
  }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // use microtask in non-DOM environments, e.g. Weex
  const p = Promise.resolve()
  timerFunc = () => {
   p.then(nextTickHandler)
  }
 } else {
  // fallback to setTimeout
  timerFunc = () => {
   setTimeout(nextTickHandler, 0)
  }
 }

可以看出 timerFunc 的定义优先顺序 macroTask --> microTask ,在没有 Dom 的环境中,使用 microTask ,比如weex

setImmediate、MessageChannel VS setTimeout

我们是优先定义 setImmediate 、 MessageChannel 为什么要优先用他们创建macroTask而不是setTimeout? HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel 和 setImmediate 的延迟明显是小于setTimeout的。

解决问题

有了这些基础,我们再看一遍上面提到的问题。因为 Vue 的事件机制是通过事件队列来调度执行,会等主进程执行空闲后进行调度,所以先回去等待所有的进程执行完成之后再去一次更新。这样的性能优势很明显,比如:

现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发 setter->Dep->Watcher->update->run 。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue实现了一个 queue 队列,在下一个Tick(或者是当前Tick的微任务阶段)的时候会统一执行 queue 中 Watcher 的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个Tick(或者是当前Tick的微任务阶段)的时候调用,大大优化了性能。

有趣的问题

var vm = new Vue({
  el: '#example',
  data: {
    msg: 'begin',
  },
  mounted () {
   this.msg = 'end'
   console.log('1')
   setTimeout(() => { // macroTask
     console.log('3')
   }, 0)
   Promise.resolve().then(function () { //microTask
    console.log('promise!')
   })
   this.$nextTick(function () {
    console.log('2')
   })
 }
})

这个的执行顺序想必大家都知道先后打印:1、promise、2、3。

  1. 因为首先触发了 this.msg = 'end' ,导致触发了 watcher 的 update ,从而将更新操作callback push进入vue的事件队列。
  2. this.$nextTick 也为事件队列push进入了新的一个callback函数,他们都是通过 setImmediate --> MessageChannel --> Promise --> setTimeout 来定义 timeFunc 。而 Promise.resolve().then 则是microTask,所以会先去打印promise。
  3. 在支持 MessageChannel 和 setImmediate 的情况下,他们的执行顺序是优先于 setTimeout 的(在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。其次因为事件队列里,优先收入callback数组)所以会打印2,接着打印3
  4. 但是在不支持 MessageChannel 和 setImmediate 的情况下,又会通过 Promise 定义 timeFunc ,也是老版本Vue 2.4 之前的版本会优先执行 promise 。这种情况会导致顺序成为了:1、2、promise、3。因为this.msg必定先会触发dom更新函数,dom更新函数会先被callback收纳进入异步时间队列,其次才定义 Promise.resolve().then(function () { console.log('promise!')}) 这样的microTask,接着定义 $nextTick 又会被callback收纳。我们知道队列满足先进先出的原则,所以优先去执行callback收纳的对象。

后记

如果你对Vue源码感兴趣,可以来这里:更多好玩的Vue约定源码解释

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
利用XMLHTTP传递参数在另一页面执行并刷新本页
Oct 26 Javascript
javascript 关于# 和 void的区别分析
Oct 26 Javascript
测试你的JS的掌握程度的代码
Dec 09 Javascript
jquery实现div阴影效果示例代码
Sep 16 Javascript
Bootstrap整体框架之CSS12栅格系统
Dec 15 Javascript
前端编码规范(3)JavaScript 开发规范
Jan 21 Javascript
bootstrap Validator 模态框、jsp、表单验证 Ajax提交功能
Feb 17 Javascript
JS设置手机验证码60s等待实现代码
Jun 14 Javascript
vue实现移动端轻量日期组件不依赖第三方库的方法
Apr 28 Javascript
javascript实现的字符串转换成数组操作示例
Jun 13 Javascript
原生JS实现贪吃蛇小游戏
Mar 09 Javascript
vue element el-transfer增加拖拽功能
Jan 15 Vue.js
jQuery实现的电子时钟效果完整示例
Apr 28 #jQuery
vue+jquery+lodash实现滑动时顶部悬浮固定效果
Apr 28 #jQuery
Vue实现PopupWindow组件详解
Apr 28 #Javascript
详解angular路由高亮之RouterLinkActive
Apr 28 #Javascript
vue弹窗组件使用方法
Apr 28 #Javascript
学习Vue组件实例
Apr 28 #Javascript
vue弹窗消息组件的使用方法
Sep 24 #Javascript
You might like
其他功能
2006/10/09 PHP
php开发文档 会员收费1期
2012/08/14 PHP
$_GET['goods_id']+0 的使用详解
2013/06/06 PHP
深入php处理整数函数的详解
2013/06/09 PHP
php判断手机访问还是电脑访问示例分享
2014/01/20 PHP
php中explode函数用法分析
2014/11/15 PHP
PHP检测字符串是否为UTF8编码的常用方法
2014/11/21 PHP
php强制用户转向www域名的方法
2015/06/19 PHP
thinkphp3.x连接mysql数据库的方法(具体操作步骤)
2016/05/19 PHP
JavaScript 输入框内容格式验证代码
2010/02/11 Javascript
js 代码优化点滴记录
2012/02/19 Javascript
JavaScript lastIndexOf方法入门实例(计算指定字符在字符串中最后一次出现的位置)
2014/10/17 Javascript
使用cluster 将自己的Node服务器扩展为多线程服务器
2014/11/10 Javascript
jQuery超简单选项卡完整实例
2015/09/26 Javascript
Bootstrap进度条实现代码解析
2017/03/07 Javascript
JavaScript正则表达式函数总结(常用)
2018/02/22 Javascript
jQuery中元素选择器(element)简单用法示例
2018/05/14 jQuery
CryptoJS中AES实现前后端通用加解密技术
2018/12/18 Javascript
使用Phantomjs和Node完成网页的截屏快照的方法
2019/07/16 Javascript
layui操作列按钮个数和文字颜色的判断实例
2019/09/11 Javascript
vue 实现用户登录方式的切换功能
2020/04/14 Javascript
JavaScript常用进制转换及位运算实例解析
2020/10/14 Javascript
python模拟新浪微博登陆功能(新浪微博爬虫)
2013/12/24 Python
python模拟Django框架实例
2016/05/17 Python
python的unittest测试类代码实例
2017/12/07 Python
Sanic框架应用部署方法详解
2018/07/18 Python
利用Python将数值型特征进行离散化操作的方法
2018/11/06 Python
python实现淘宝秒杀脚本
2020/06/23 Python
python防止随意修改类属性的实现方法
2019/08/21 Python
python 可视化库PyG2Plot的使用
2021/01/21 Python
迪奥美国官网:Dior美国
2019/12/07 全球购物
违章停车检讨书
2014/10/21 职场文书
2015年八一建军节演讲稿
2015/03/19 职场文书
党小组考察意见
2015/06/02 职场文书
《鸟的天堂》教学反思
2016/02/19 职场文书
Java Optional<Foo>转换成List<Bar>的实例方法
2021/06/20 Java/Android