深入理解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 相关文章推荐
图片上传判断及预览脚本的效果实例
Aug 07 Javascript
自定义的一个简单时尚js下拉选择框
Nov 20 Javascript
flash遮住div问题的正确解决方法
Feb 27 Javascript
原生JS实现的放大镜效果实例代码
Oct 15 Javascript
JavaScript用JSONP跨域请求数据实例详解
Jan 06 Javascript
jQuery实现别踩白块儿网页版小游戏
Jan 18 Javascript
javascript实现的图片预览功能
Mar 25 Javascript
BootStrap的两种模态框方式
May 10 Javascript
JavaScript获取tr td 的三种方式全面总结(推荐)
Aug 15 Javascript
vue resource post请求时遇到的坑
Oct 19 Javascript
深入理解 TypeScript Reflect Metadata
Dec 12 Javascript
JSONObject与JSONArray使用方法解析
Sep 28 Javascript
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
合作指挥官:孟斯克
2020/03/16 星际争霸
Laravel 5框架学习之Blade 简介
2015/04/08 PHP
php简单实现多语言切换的方法
2016/05/09 PHP
javascript IE中的DOM ready应用技巧
2008/07/23 Javascript
对 lightbox JS 图片控件进行了一下改造, 使其他支持复杂的图片说明
2010/03/20 Javascript
js下用eval生成JSON对象
2010/09/17 Javascript
Javascript创建自定义对象 创建Object实例添加属性和方法
2012/06/04 Javascript
在JavaScript并非所有的一切都是对象
2013/04/11 Javascript
Jquery中ajax方法data参数的用法小结
2014/02/12 Javascript
JS去掉第一个字符和最后一个字符的实现代码
2014/02/20 Javascript
常见的javascript跨域通信方法
2015/12/31 Javascript
js如何准确获取当前页面url网址信息
2020/09/13 Javascript
jquery实现点击弹出可放大居中及关闭的对话框(附demo源码下载)
2016/05/10 Javascript
JS动态添加的div点击跳转到另一页面实现代码
2017/09/30 Javascript
浅谈Angular2 模块懒加载的方法
2017/10/04 Javascript
jQuery中复合选择器简单用法示例
2018/03/31 jQuery
npm 下载指定版本的组件方法
2018/05/17 Javascript
jQuery实现的监听导航滚动置顶状态功能示例
2018/07/23 jQuery
Bootstrap table表格初始化表格数据的方法
2018/07/25 Javascript
javascript数组去重方法总结(推荐)
2019/03/20 Javascript
vue+element+Java实现批量删除功能
2019/04/08 Javascript
Python多线程结合队列下载百度音乐的方法
2015/07/27 Python
python常用知识梳理(必看篇)
2017/03/23 Python
详解Python判定IP地址合法性的三种方法
2018/03/06 Python
django 解决自定义序列化返回处理数据为null的问题
2020/05/20 Python
Django 解决新建表删除后无法重新创建等问题
2020/05/21 Python
墨尔本最受欢迎的复古风格品牌:Princess Highway
2018/12/21 全球购物
美国亚洲时尚和美容产品的一站式网上商店:Stylevana
2019/09/05 全球购物
蔻驰英国官网:COACH英国
2020/07/19 全球购物
十佳班主任事迹材料
2014/01/18 职场文书
小学清明节活动总结
2014/07/04 职场文书
房屋转让协议书
2014/10/18 职场文书
离婚协议书范文2014(夫妻感情破裂)
2014/12/14 职场文书
法制教育主题班会
2015/08/13 职场文书
村官2015年度工作总结
2015/10/14 职场文书
matplotlib之pyplot模块实现添加子图subplot的使用
2021/04/25 Python