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 相关文章推荐
用javascript获取当页面上鼠标光标位置和触发事件的对象的代码
Dec 09 Javascript
Jquery 高亮显示文本中重要的关键字
Dec 24 Javascript
跨浏览器开发经验总结(三)   警惕“IE依赖综合症”
May 13 Javascript
juqery 学习之四 筛选过滤
Nov 30 Javascript
基于node.js的快速开发透明代理
Dec 25 Javascript
jquery中dom操作和事件的实例学习 下拉框应用
Dec 01 Javascript
Javascript绝句欣赏 一些经典的js代码
Feb 22 Javascript
JavaScript面试题大全(推荐)
Sep 22 Javascript
ionic中列表项增加和删除的实现方法
Jan 22 Javascript
JS打开摄像头并截图上传示例
Feb 18 Javascript
vue中实现图片和文件上传的示例代码
Mar 16 Javascript
JavaScript设计模式之责任链模式实例分析
Jan 16 Javascript
封装微信小程序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
利用Ffmpeg获得flv视频缩略图和视频时间的代码
2011/09/15 PHP
php在window iis的莫名问题的测试方法
2013/05/14 PHP
ThinkPHP3.1数据CURD操作快速入门
2014/06/19 PHP
浅析PHP中Session可能会引起并发问题
2015/07/23 PHP
深入浅析yii2-gii自定义模板的方法
2016/04/26 PHP
php+flash+jQuery多图片上传源码分享
2020/07/27 PHP
我也种棵OO树JXTree[js+css+xml]
2007/04/02 Javascript
JavaScript在IE中“意外地调用了方法或属性访问”
2008/11/19 Javascript
js null undefined 空区别说明
2010/06/13 Javascript
jQuery :first选择器使用介绍
2013/08/09 Javascript
jquery重复提交请求的原因浅析
2014/05/23 Javascript
JS实现的生成随机数的4个函数分享
2015/02/11 Javascript
基于jQuery实现左右图片轮播(原理通用)
2015/12/24 Javascript
使用开源工具制作网页验证码的方法
2016/10/17 Javascript
通过bootstrap全面学习less
2016/11/09 Javascript
Nodejs 发送Post请求功能(发短信验证码例子)
2017/02/09 NodeJs
js实现整体缩放页面适配移动端
2020/03/31 Javascript
[49:43]VG vs FNATIC 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/17 DOTA
Python封装shell命令实例分析
2015/05/05 Python
python集合比较(交集,并集,差集)方法详解
2018/09/13 Python
Python超越函数积分运算以及绘图实现代码
2019/11/20 Python
使用python操作lmdb对数据读取的实例
2020/12/11 Python
用纯css3和html制作泡沫对话框实现代码
2013/03/21 HTML / CSS
阿迪达斯德国官方网站:adidas德国
2017/07/12 全球购物
TUMI香港官网:国际领先的行李箱、背囊品牌
2021/03/01 全球购物
党员四风问题对照检查材料
2014/09/27 职场文书
2014年应急工作总结
2014/12/11 职场文书
五年级学生期末评语
2014/12/26 职场文书
2015年绩效考核工作总结
2015/05/23 职场文书
爱国主义教育主题班会
2015/08/13 职场文书
任命书格式范文
2015/09/22 职场文书
教你如何用python开发一款数字推盘小游戏
2021/04/14 Python
Java生成读取条形码和二维码的简单示例
2021/07/09 Java/Android
Winsows11性能如何? win11性能测评多核竟比Win10差了10%
2021/11/21 数码科技
sqlserver连接错误之SQL评估期已过的问题解决
2022/03/23 SQL Server
JS实现简单九宫格抽奖
2022/06/28 Javascript