vue的异步更新及 nexttick,vue的nexttick实现原理

  vue的异步更新及 nexttick,vue的nexttick实现原理

  我最近在学习一些基础知识,所以我想做一系列的尝试来谈谈这些复杂而重要的知识点。下面这篇文章主要介绍了关于Vue异步更新机制的信息和$ TERM $nextTick的原理,供大家参考

  

目录

  前言:Vue的异步更新DOM更新就是异步DOM更新或者批处理事件循环执行过程。源代码深入异步更新队列nextTick$nextTick摘要一般来说,更新DOM是同步的。既然更新DOM是同步过程,为什么Vue需要借用$nextTick来处理?为什么优先考虑微任务?摘要

  

前言

  相信很多人会好奇Vue的内部更新机制,或者日常工作中遇到的一些奇怪的问题需要用$nextTick来解决。今天我们来说说Vue中的异步更新机制以及$nextTick的原理。

  

Vue的异步更新

  如果您没有注意到,Vue异步执行DOM更新。每当观察到数据变化时,Vue将打开一个队列并缓冲在同一事件周期中发生的所有数据变化。如果同一个观察器被多次触发,它只会被推入队列一次。这种在缓冲期间删除重复数据的方法对于避免不必要的计算和DOM操作非常重要。然后,在下一个事件循环“滴答”中,Vue刷新队列并执行实际的(已消除重复的)工作。

  

DOM更新是异步的

  当我们在更新完数据后立即获取DOM中的内容时,会发现获取的内容还是旧的。

  模板

  div class=next_tick

  div ref= title class= title { name } }/div

  /div

  /模板

  脚本

  导出默认值{

  data() {

  返回{

  姓名:“钱端九难”

  }

  },

  已安装(){

  this.name=前端

  console.log(sync ,this。$refs.title.innerText)

  这个。$nextTick(()={

  console.log(nextTick ,this。$refs.title.innerText)

  })

  }

  }

  /脚本

  从图中可以发现,当数据发生变化时,dom元素中的内容是旧数据,而更新的数据是在nextTick中获取的。为什么?

  实际上,这里你使用微任务或宏任务来获取dom元素中的内容,这也是更新后的数据。我们可以试试:

  已安装(){

  this.name=前端

  console.log(sync ,this。$refs.title.innerText)

  Promise.resolve()。然后(()={

  Console.log(微任务,这个。$refs.title.innerText)

  })

  setTimeout(()={

  Console.log(宏任务,this。$refs.title.innerText)

  }, 0)

  这个。$nextTick(()={

  console.log(nextTick ,this。$refs.title.innerText)

  })

  }

  你不觉得有点奇怪吗?其实也没什么奇怪的。在vue源代码中,其实现原理是微任务和宏任务。慢慢往下看,后面会解释。

  

DOM更新还是批量的

  没错,vue中的DOM更新仍然是批量处理的。这样做的好处是可以最大程度的优化性能。好的,这里也有可看的。别担心。

  Vue同时更新了多个数据。你认为dom更新了多次还是一次?让我们试试。

  模板

  div class=next_tick

  div ref= title class= title { name } }/div

  div class= verse { verse } }/div

  /div

  /模板

  脚本

  导出默认值{

  名称: nextTick ,

  data() {

  返回{

  名称:南九前端,

  诗句:‘东山若能东山再起,大鹏展翅冲天’,

  计数:0

  }

  },

  已安装(){

  this.name=前端

  这个. verse=世间万物皆空,名利如风

  //console.log(sync ,this。$refs.title.innerText)

  //Promise.resolve()。然后(()={

  //console.log(微任务,这个。$refs.title.innerText)

  //})

  //setTimeout(()={

  //console.log(宏任务,这个。$refs.title.innerText)

  //}, 0)

  //这个。$nextTick(()={

  //console.log(nextTick ,这个。$refs.title.innerText)

  //})

  },

  已更新(){

  这个.计数

  console.log(update:,this.count)

  }

  }

  /脚本

  style lang=less 。诗句{

  font-size:(20/@ rem);

  }

  /风格

  我们可以看到更新的钩子只执行了一次,表明我们是同时更新了多个数据,DOM只会更新一次

  我们再来看另一种情况,同步和异步混合。DOM会更新多少次?

  已安装(){

  this.name=前端

  这个. verse=世间万物皆空,名利如风

  Promise.resolve()。然后(()={

  this.name=研究…

  })

  setTimeout(()={

  “半冷半热,流年一杯浊酒”

  })

  //console.log(sync ,this。$refs.title.innerText)

  //Promise.resolve()。然后(()={

  //console.log(微任务,这个。$refs.title.innerText)

  //})

  //setTimeout(()={

  //console.log(宏任务,这个。$refs.title.innerText)

  //}, 0)

  //这个。$nextTick(()={

  //console.log(nextTick ,这个。$refs.title.innerText)

  //})

  },

  已更新(){

  这个.计数

  console.log(update:,this.count)

  }

  从图中可以发现,DOM会渲染三次,分别是一次同步(两次同步一起更新),一次微任务,一次宏任务。而当用setTimeout更新数据时,页面数据变化的过程会很明显。(这句话是重点,记得小本子。)这也是为什么nextTick源代码中的setTimeout作为最后手段,优先考虑微任务的原因。

  

事件循环

  是的,和事件循环有很大关系。这里稍微提一下。更多细节,我们可以看看探索JavaScript执行机制。

  因为JavaScript是单线程的,所以它的任务不可能只有同步任务。那些耗时较长的任务,如果也按照同步任务执行,会造成页面阻塞。所以JavaScript任务一般分为同步任务和异步任务两类,异步任务又分为宏观任务和微观任务。

  宏任务:脚本(整体代码),setTimeout,setInterval,setImmediate,I/O,UI渲染

  微任务:承诺

  

执行过程

  同步任务直接放入主线程执行,而异步任务(点击事件、定时器、ajax等。)挂在后台执行,等待I/O事件完成或者行为事件触发。系统在后台执行异步任务。如果异步任务事件(或行为事件)被触发,该任务将被添加到任务队列中,每个任务将由一个回调函数处理。这里,异步任务分为宏观任务和微观任务。宏任务进入宏任务队列,微任务进入微任务队列。执行队列中的任务在执行堆栈中完成。当主线程中的所有任务完成后,读取微任务队列。如果有什么微任务,就全部执行,然后读取宏任务队列。以上过程会不断重复,也就是我们常说的「事件循环(Event-Loop)」总的来说,在事件循环中,微任务会先于宏任务执行。而在微任务执行完后会进入浏览器更新渲染阶段,所以在更新渲染前使用微任务会比宏任务快一些,一次循环就是一次tick 。

  在一个事件循环中,微任务在这个循环中保持提取,直到微任务队列被清空,而宏任务在一个循环中保持提取一次。

  如果在事件周期的执行过程中添加了一个异步任务,如果是一个宏任务,它将被放在宏任务的末尾,等待下一个周期的执行。如果是微任务,就放在这个事件循环中微任务任务的末尾继续执行。直到微任务队列为空。

  

源码深入

  

异步更新队列

  在Vue中,DOM更新一定是数据变化引起的,所以我们可以快速找到更新DOM的入口,也就是在set期间通过dep.notify通知观察者更新的时候。

  //观察者. js

  //当依赖关系改变时,触发更新

  update() {

  如果(this.lazy) {

  //惰性执行将在这里进行,如computed

  this.dirty=true

  }else if(this.sync) {

  //同步执行会到这里,比如这个。$watch()或watch选项,并传递一个同步配置{sync: true}

  this.run()

  }否则{

  //将当前观察器放入观察器队列,通常是这样。

  队列观察器(this)

  }

  }

  从这里我们可以发现vue默认是异步更新机制,会实现一个队列来缓存当前需要更新的watcher。

  //scheduler.js

  /*将一个观察者对象推入观察者队列。如果队列中已经存在相同的id,将会跳过观察者对象,除非在刷新队列时推送该对象*/

  导出函数queueWatcher (watcher: Watcher) {

  /*获取观察者的id*/

  const id=watcher.id

  /*检查id是否存在。如果已经存在,直接跳过。如果不存在,将在has中进行标记,以便下次检查*/

  if(有[id]==null) {

  has[id]=true

  //如果flushing为false,则表示当前watcher队列没有被刷新,那么watcher直接进入队列。

  如果(!冲洗){

  queue.push(观察器)

  }否则{

  //如果观察器队列已经被刷新,则需要特殊处理来插入新的观察器。

  //确保新的观察器刷新仍然正常。

  设i=queue.length - 1

  while (i=0 queue[i]。id watcher.id) {

  异

  }

  queue.splice(Math.max(i,index) 1,0,watcher)

  }

  //将刷新排队

  如果(!等待){

  //watching为false,表示当前浏览器的异步任务队列中没有flushSchedulerQueue函数。

  等待=真

  //这是我们共同的这个。$nextTick

  nextTick(flushSchedulerQueue)

  }

  }

  }

  好了,从这里我们可以发现,vue并没有在数据发生变化后立即更新视图,而是维护了一个watcher队列,id重复的watcher只会推送队列一次,因为我们只关心最终的数据,而不是更新了多少次。等到下一个滴答,这些观察者将被从队列中取出,视图将被更新。

  

nextTick

  nextTick的目的是生成一个回调函数添加到task或microtask中。在当前栈执行完之后(之前可能还有其他函数),回调函数会被调用,这个回调函数会被异步触发(也就是在下一个tick)。

  //next-tick.js

  常量回调=[]

  让待定=假

  //批处理

  函数flushCallbacks () {

  待定=假

  const copies=callbacks.slice(0)

  回调长度=0

  //依次执行nextTick的方法

  for(设I=0;一份,长度;i ) {

  份数[i]()

  }

  }

  导出函数nextTick (cb,ctx) {

  让_解决

  callbacks.push(()={

  if (cb) {

  尝试{

  cb .呼叫(ctx)

  } catch (e) {

  handleError(e,ctx, nextTick )

  }

  } else if (_resolve) {

  _解析(ctx)

  }

  })

  //因为nextTick会在内部调整,所以用户也会调整nextTick,但是异步只需要一次。

  如果(!待定){

  待定=真

  定时器函数()

  }

  //执行后会返回一个promise实例,这也是$nextTick可以调用then方法的原因。

  如果(!cb类型的承诺!==未定义){

  返回新承诺(resolve={

  _resolve=解决

  })

  }

  }

  兼容性处理,优先使用promise.then优雅降级(兼容性处理是一个不断试错的过程,谁用谁会。

  E Vu内部尝试使用原生Promise.then,对异步队列使用MutationObserver和setImmediate,如果执行环境不支持,它将改为使用setTimeout(fn,0)。

  //定时器函数

  //promise . then-MutationObserver-set immediate-setTimeout

  Vue3中不再做兼容性处理,而是直接使用promise.then。

  如果(类型的承诺!==undefined 是Native(Promise)) {

  const p=Promise.resolve()

  timerFunc=()={

  p.then(刷新回调)

  if (isIOS)设置超时(noop)

  }

  isUsingMicroTask=true

  } else if(!这是一种变异观察者!==未定义 (

  isNative(变异观察器)

  //PhantomJS和iOS 7.x

  mutationobserver . tostring()===[object mutationobserver constructor]

  )) {

  让计数器=1

  const observer=new mutation observer(flush callbacks)//可以监控DOM变化,监控后会异步更新。

  //但是我这里不是想用它来做DOM监控,而是想利用它的微任务。

  const textNode=document . create textNode(String(counter))

  observer.observe(textNode,{

  characterData: true

  })

  timerFunc=()={

  计数器=(计数器1) % 2

  textNode.data=String(计数器)

  }

  isUsingMicroTask=true

  } else if (typeof setImmediate!==undefined 是Native(setImmediate)) {

  timerFunc=()={

  setImmediate(flushCallbacks)

  }

  }否则{

  //回退到setTimeout。

  timerFunc=()={

  setTimeout(flushCallbacks,0)

  }

  }

  

$nextTick

  我们平时调用的$nextTick其实就是上面的方法,只是为了方便我们在源代码renderMixin中附加了vue的原型。

  导出函数renderMixin (Vue) {

  //安装运行时便利助手

  installRenderHelpers(vue . prototype)

  vue . prototype . $ next tick=function(fn){

  返回nextTick(fn,this)

  }

  vue . prototype . _ render=function(){

  //.

  }

  //.

  }

  

总结

  

一般更新DOM是同步的

  说了这么多,相信你已经初步了解了Vue的异步更新机制和$nextTick的原理。在每个事件周期结束时,页面会被渲染一次,从上面我们知道渲染过程也是一个宏任务。这里可能有一个误区,就是DOM树的修改是同步的,只有渲染过程是异步的,也就是说我们修改完DOM就可以马上得到更新后的DOM。如果你不相信我,我们可以试试:

  !声明文档类型

  html lang=en

  头

  meta charset=UTF-8

  meta http-equiv= X-UA-Compatible content= IE=edge

  meta name= viewport content= width=device-width,initial-scale=1.0

  标题文档/标题

  /头

  身体

  Div id=title 欲试人间烟火,怎能奢求人间沧桑/div

  脚本

  Title.innerText=万卷诗书无用,半旧志留疏狂

  console.log(已更新,标题)

  /脚本

  /body

  /html

  

既然更新DOM是个同步的过程,那为什么Vue却需要借用$nextTick来处理呢?

  答案很明显,因为Vue考虑的是性能,Vue会缓存用户多次同步修改的数据。当同步代码完成后,就意味着这一次的数据修改结束了,然后它会更新相应的DOM。一方面可以省去不必要的DOM操作,比如同时多次修改一个数据,只需要关心最后一次。另一方面可以聚合DOM操作,提高渲染性能。

  看下图应该更容易理解。

  

为什么优先使用微任务?

  这个应该不用多说,因为微任务必须在宏任务之前执行。如果nexttick是一个微任务,那么它会在当前同步任务执行完之后立即执行所有的微任务,即修改DOM的操作也会在当前tick内执行。当执行完本轮所有tick任务后,将开始UI渲染。如果nexttick是一个宏任务,则被推入宏任务队列,在当前一轮tick执行后的某一轮执行。注意不一定是下一轮,因为你不确定在它之前的宏任务队列里有几个宏任务在等着。所以为了尽快更新DOM,在Vue中首选微任务,在Vue3中,不具备兼容性判断。promise.then直接用微任务,不再考虑宏任务。

  

总结

  关于Vue异步更新机制和$nextTick原理的这篇文章到此为止。关于Vue异步更新和$nextTick原理的更多信息,请搜索我们之前的文章或者继续浏览下面的相关文章。希望大家以后能多多支持我们!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: