热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

vue源码nextTick使用及原理解析

这篇文章主要介绍了vue源码nextTick使用及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

1 nextTick的使用

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


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。下面看源码,为方便越读,源码有删减。

双向数据绑定

数据改变触发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 

当数据被引用时,如

{{msg}}
,会执行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 

也就是说当数据变化最终会把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 

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信息,我们需要在本次异步任务创建之后创建一个异步任务。

异步队列

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




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。




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 更新循环结束之后执行延迟回调。

参考文章

Vue nextTick 机制

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


推荐阅读
  • 本文内容为asp.net微信公众平台开发的目录汇总,包括数据库设计、多层架构框架搭建和入口实现、微信消息封装及反射赋值、关注事件、用户记录、回复文本消息、图文消息、服务搭建(接入)、自定义菜单等。同时提供了示例代码和相关的后台管理功能。内容涵盖了多个方面,适合综合运用。 ... [详细]
  • Monkey《大话移动——Android与iOS应用测试指南》的预购信息发布啦!
    Monkey《大话移动——Android与iOS应用测试指南》的预购信息已经发布,可以在京东和当当网进行预购。感谢几位大牛给出的书评,并呼吁大家的支持。明天京东的链接也将发布。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 本文介绍了求解gcdexgcd斐蜀定理的迭代法和递归法,并解释了exgcd的概念和应用。exgcd是指对于不完全为0的非负整数a和b,gcd(a,b)表示a和b的最大公约数,必然存在整数对x和y,使得gcd(a,b)=ax+by。此外,本文还给出了相应的代码示例。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
author-avatar
手机用户2502897397
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有