vue源码nextTick使用及原理解析


Posted in Javascript onAugust 13, 2019

1 nextTick的使用

vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码。

<template>
 <div class="box">{{msg}}</div>
</template>
export default {
 name: 'index',
 data () {
  return {
   msg: 'hello'
  }
 },
 mounted () {
  this.msg = 'world'
  let box = document.getElementsByClassName('box')[0]
  console.log(box.innerHTML) // hello
 }
}

可以看到,修改数据后并不会立即更新dom ,dom的更新是异步的,无法通过同步代码获取,需要使用nextTick,在下一次事件循环中获取。

this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
this.$nextTick(() => {
 console.log(box.innerHTML) // world
})

如果我们需要获取数据更新后的dom信息,比如动态获取宽高、位置信息等,需要使用nextTick。

2 数据变化dom更新与nextTick的原理分析

2.1 数据变化

vue双向数据绑定依赖于ES5的Object.defineProperty,在数据初始化的时候,通过Object.defineProperty为每一个属性创建getter与setter,把数据变成响应式数据。对属性值进行修改操作时,如this.msg = world,实际上会触发setter。下面看源码,为方便越读,源码有删减。

双向数据绑定

vue源码nextTick使用及原理解析

数据改变触发set函数

Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 // 数据修改后触发set函数 经过一系列操作 完成dom更新
 set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  if (getter && !setter) return
  if (setter) {
   setter.call(obj, newVal)
  } else {
   val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify() // 执行dep notify方法
 }
})

执行dep.notify方法

export default class Dep {
 constructor () {
  this.id = uid++
  this.subs = []
 }
 notify () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
   // 实际上遍历执行了subs数组中元素的update方法
   subs[i].update()
  }
 }
}

当数据被引用时,如<div>{{msg}}</div> ,会执行get方法,并向subs数组中添加渲染Watcher,当数据被改变时执行Watcher的update方法执行数据更新。

update () {
 /* istanbul ignore else */
 if (this.lazy) {
  this.dirty = true
 } else if (this.sync) {
  this.run()
 } else {
  queueWatcher(this) //执行queueWatcher
 }
}

update 方法最终执行queueWatcher

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 保证nextTick只执行一次
   waiting = true
   // 最终queueWatcher 方法会把flushSchedulerQueue 传入到nextTick中执行
   nextTick(flushSchedulerQueue)
  }
 }
}

执行flushSchedulerQueue方法

function flushSchedulerQueue () {
 currentFlushTimestamp = getNow()
 flushing = true
 let watcher, id
 ...
 for (index = 0; index < queue.length; index++) {
  watcher = queue[index]
  if (watcher.before) {
   watcher.before()
  }
  id = watcher.id
  has[id] = null
  // 遍历执行渲染watcher的run方法 完成视图更新
  watcher.run()
 }
 // 重置waiting变量 
 resetSchedulerState()
 ...
}

也就是说当数据变化最终会把flushSchedulerQueue传入到nextTick中执行flushSchedulerQueue函数会遍历执行watcher.run()方法,watcher.run()方法最终会完成视图更新,接下来我们看关键的nextTick方法到底是啥

2.2 nextTick

nextTick方法会被传进来的回调push进callbacks数组,然后执行timerFunc方法

export function nextTick (cb?: Function, ctx?: Object) {
 let _resolve
 // push进callbacks数组
 callbacks.push(() => {
   cb.call(ctx)
 })
 if (!pending) {
  pending = true
  // 执行timerFunc方法
  timerFunc()
 }
}

timerFunc

let timerFunc
// 判断是否原生支持Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
 const p = Promise.resolve()
 timerFunc = () => {
  // 如果原生支持Promise 用Promise执行flushCallbacks
  p.then(flushCallbacks)
  if (isIOS) setTimeout(noop)
 }
 isUsingMicroTask = true
// 判断是否原生支持MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
 let counter = 1
 // 如果原生支持MutationObserver 用MutationObserver执行flushCallbacks
 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
// 判断是否原生支持setImmediate 
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 timerFunc = () => {
 // 如果原生支持setImmediate 用setImmediate执行flushCallbacks
  setImmediate(flushCallbacks)
 }
// 都不支持的情况下使用setTimeout 0
} else {
 timerFunc = () => {
  // 使用setTimeout执行flushCallbacks
  setTimeout(flushCallbacks, 0)
 }
}

// flushCallbacks 最终执行nextTick 方法传进来的回调函数
function flushCallbacks () {
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
}

nextTick会优先使用microTask, 其次是macroTask 。

也就是说nextTick中的任务,实际上会异步执行,nextTick(callback)类似于
Promise.resolve().then(callback),或者setTimeout(callback, 0)。

也就是说vue的视图更新 nextTick(flushSchedulerQueue)等同于setTimeout(flushSchedulerQueue, 0),会异步执行flushSchedulerQueue函数,所以我们在this.msg = hello 并不会立即更新dom。

要想在dom更新后读取dom信息,我们需要在本次异步任务创建之后创建一个异步任务。

异步队列

vue源码nextTick使用及原理解析

为了验证这个想法我们不用nextTick,直接用setTimeout实验一下。如下面代码,验证了我们的想法。

<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 ,那么我们添加的异步任务会在渲染的异步任务之前执行,拿不到更新后的dom。

<template>
 <div class="box">{{msg}}</div>
</template>

<script>
export default {
 name: 'index',
 data () {
  return {
   msg: 'hello'
  }
 },
 mounted () {
  this.$nextTick(() => {
   console.log(box.innerHTML) // hello
  })
  this.msg = 'world'
  let box = document.getElementsByClassName('box')[0]
 }
}

3 总结

vue为了保证性能,会把dom修改添加到异步任务,所有同步代码执行完成后再统一修改dom,一次事件循环中的多次数据修改只会触发一次watcher.run()。也就是通过nextTick,nextTick会优先使用microTask创建异步任务。

vue项目中如果需要获取修改后的dom信息,需要通过nextTick在dom更新任务之后创建一个异步任务。如官网所说,nextTick会在下次 DOM 更新循环结束之后执行延迟回调。

参考文章

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

Javascript 相关文章推荐
WordPress 照片lightbox效果的运用几点
Jun 22 Javascript
jQuery隔行变色与普通JS写法的对比
Apr 21 Javascript
jquery选择器之内容过滤选择器详解
Jan 27 Javascript
5个JavaScript经典面试题
Oct 13 Javascript
简单谈谈jQuery(function(){})与(function(){})(jQuery)
Dec 19 Javascript
深入理解JavaScript系列(45):代码复用模式(避免篇)详解
Mar 04 Javascript
javascript获取当前的时间戳的方法汇总
Jul 26 Javascript
Vue.js实现在下拉列表区域外点击即可关闭下拉列表的功能(自定义下拉列表)
May 30 Javascript
ES6 Proxy实现Vue的变化检测问题
Jun 11 Javascript
微信小程序实现一张或多张图片上传(云开发)
Sep 25 Javascript
浅析Vue 防抖与节流的使用
Nov 14 Javascript
Vue3实现简易音乐播放器组件
Aug 14 Vue.js
封装微信小程序http拦截器过程解析
Aug 13 #Javascript
Vue中通过Vue.extend动态创建实例的方法
Aug 13 #Javascript
微信小程序封装分享与分销功能过程解析
Aug 13 #Javascript
node删除、复制文件或文件夹示例代码
Aug 13 #Javascript
vue实现下拉加载其实没那么复杂
Aug 13 #Javascript
vue中created和mounted的区别浅析
Aug 13 #Javascript
微信小程序实现点击空白隐藏的方法示例
Aug 13 #Javascript
You might like
PHP 如何向 MySQL 发送数据
2006/10/09 PHP
解决GD中文乱码问题
2007/02/14 PHP
Yii中CGridView关联表搜索排序方法实例详解
2014/12/03 PHP
PHP HTTP 认证实例详解
2016/11/03 PHP
thinkphp实现把数据库中的列的值存到下拉框中的方法
2017/01/20 PHP
PHP使用POP3读取邮箱接收邮件的示例代码
2020/07/08 PHP
Js 获取当前日期时间及其它操作实现代码
2021/03/04 Javascript
jQuery与其它库冲突的解决方法
2010/06/25 Javascript
javascript判断ie浏览器6/7版本加载不同样式表的实现代码
2011/12/26 Javascript
jQuery仿淘宝网产品品牌隐藏与显示效果
2015/09/01 Javascript
JavaScript函数的一些注意要点小结及js匿名函数
2015/11/10 Javascript
Webwork 实现文件上传下载代码详解
2016/02/02 Javascript
分享一个插件实现水珠自动下落效果
2016/06/01 Javascript
js实现鼠标拖动功能
2017/03/20 Javascript
Vue实现路由跳转和嵌套
2017/06/20 Javascript
Bootstrap Tooltip显示换行和左对齐的解决方案
2017/10/11 Javascript
JS如何获取地址栏的参数实例讲解
2018/10/06 Javascript
微信小程序云开发之使用云数据库
2019/05/17 Javascript
JavaScript事件委托实现原理及优点进行
2020/08/29 Javascript
[00:36]DOTA2勇士令状莱恩声望物品——冥晶之厄展示
2018/05/25 DOTA
python中的装饰器详解
2015/04/13 Python
Python实现单词拼写检查
2015/04/25 Python
Python中super的用法实例
2015/05/28 Python
Python正则表达式使用范例分享
2016/12/04 Python
Kears+Opencv实现简单人脸识别
2019/08/28 Python
websocket+sockjs+stompjs详解及实例代码
2018/11/30 HTML / CSS
美国香薰蜡烛品牌:PADDYWAX
2018/10/06 全球购物
职业生涯规划怎么写
2013/12/29 职场文书
班会关于环保演讲稿
2013/12/29 职场文书
青年文明号事迹材料
2014/01/18 职场文书
项目负责人任命书
2014/06/04 职场文书
股东合作协议书
2014/09/12 职场文书
单方离婚协议书范本(2014版)
2014/09/30 职场文书
2019年“我为祖国点赞”演讲稿(3篇)
2019/09/26 职场文书
使用PDF.js渲染canvas实现预览pdf的效果示例
2021/04/17 Javascript
彻底卸载VMware虚拟机的超详细步骤记录
2022/07/15 Servers