iis服务器助手广告广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >怎样理解从Vue.js源码看异步更新DOM策略及nextTick
  • 659
分享到

怎样理解从Vue.js源码看异步更新DOM策略及nextTick

2024-04-02 19:04:59 659人浏览 安东尼
摘要

怎样理解从vue.js源码看异步更新DOM策略及nextTick,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。操作DOM在使用Vue.js

怎样理解从vue.js源码看异步更新DOM策略及nextTick,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

操作DOM

在使用Vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样:

<template>
 <div>
 <div ref="test">{{test}}</div>
 <button @click="handleClick">tet</button>
 </div>
</template>
export default {
 data () {
  return {
   test: 'begin'
  };
 },
 methods () {
  handleClick () {
   this.test = 'end';
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}

打印的结果是begin,为什么我们明明已经将test设置成了“end”,获取真实DOM节点的innerText却没有得到我们预期中的“end”,而是得到之前的值“begin”呢?

Watcher队列

带着疑问,我们找到了Vue.js源码的Watch实现。当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。我们来看一下update的实现。

update () {
 
 if (this.lazy) {
  this.dirty = true
 } else if (this.sync) {
  
  this.run()
 } else {
  
  queueWatcher(this)
 }
}

我们发现Vue.js默认是使用异步执行DOM更新。

当异步执行update的时候,会调用queueWatcher函数。

 
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 >= 0 && queue[i].id > watcher.id) {
  i--
  }
  queue.splice(Math.max(i, index) + 1, 0, watcher)
 }
 // queue the flush
 if (!waiting) {
  waiting = true
  nextTick(flushSchedulerQueue)
 }
 }
}

查看queueWatcher的源码我们发现,Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等待下一个tick时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去,因为在最终渲染时,我们只需要关心数据的最终结果。

那么,什么是下一个tick?

nextTick

vue.js提供了一个nextTick函数,其实也就是上面调用的nextTick。

nextTick的实现比较简单,执行的目的是在microtask或者task中推入一个funtion,在当前栈执行完毕(也行还会有一些排在前面的需要执行的任务)以后执行nextTick传入的funtion,看一下源码:


 
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]()
 }
 }

 // the nextTick behavior leverages the microtask queue, which can be accessed
 // via either native Promise.then or MutationObserver.
 // MutationObserver has wider support, however it is seriously bugged in
 // UIWEBView in iOS >= 9.3.3 when triggered in touch event handlers. It
 // completely stops working after triggering a few times... so, if native
 // Promise is available, we will use it:
 

 
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
 
 var p = Promise.resolve()
 var logError = err => { console.error(err) }
 timerFunc = () => {
  p.then(nextTickHandler).catch(logError)
  // in problematic UIWebViews, Promise.then doesn't completely break, but
  // it can get stuck in a weird state where callbacks are pushed into the
  // microtask queue but the queue isn't being flushed, until the browser
  // needs to do some other work, e.g. handle a timer. Therefore we can
  // "force" the microtask queue to be flushed by adding an empty timer.
  if (isIOS) setTimeout(noop)
 }
 } else if (typeof MutationObserver !== 'undefined' && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === '[object MutationObserverConstructor]'
 )) {
 // use MutationObserver where native Promise is not available,
 // e.g. PhantomJS IE11, iOS7, Android 4.4
 
 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textnode = document.createTextNode(String(counter))
 observer.observe(textNode, {
  characterData: true
 })
 timerFunc = () => {
  counter = (counter + 1) % 2
  textNode.data = String(counter)
 }
 } 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()
 }
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})()

它是一个立即执行函数,返回一个queueNextTick接口。

传入的cb会被push进callbacks中存放起来,然后执行timerFunc(pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次)。

timerFunc是什么?

看了源码发现timerFunc会检测当前环境而不同实现,其实就是按照Promise,MutationObserver,setTimeout优先级,哪个存在使用哪个,最不济的环境下使用setTimeout。

这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。
如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。

为什么要优先使用microtask?我在顾轶灵在知乎的回答中学习到:

JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。

setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。

要创建一个新的 microtask,优先使用 Promise,如果浏览器不支持,再尝试 MutationObserver。

实在不行,只能用 setTimeout 创建 task 了。

为啥要用 microtask?

根据 html Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。

反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。

参考顾轶灵知乎的回答

首先是Promise,(Promise.resolve()).then()可以在microtask中加入它的回调,

MutationObserver新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入microtask,即textNode.data = String(counter)时便会加入该回调。

setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。

综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。

flushSchedulerQueue



 
function flushSchedulerQueue () {
 flushing = true
 let watcher, id

 // Sort queue before flush.
 // This ensures that:
 // 1. Components are updated from parent to child. (because parent is always
 // created before the child)
 // 2. A component's user watchers are run before its render watcher (because
 // user watchers are created before the render watcher)
 // 3. If a component is destroyed during a parent component's watcher run,
 // its watchers can be skipped.
 
 queue.sort((a, b) => a.id - b.id)

 // do not cache length because more watchers might be pushed
 // as we run existing watchers
 
 for (index = 0; index < queue.length; index++) {
 watcher = queue[index]
 id = watcher.id
 
 has[id] = null
 
 watcher.run()
 // in dev build, check and stop circular updates.
 
 if (process.env.NODE_ENV !== 'production' && has[id] != null) {
  circular[id] = (circular[id] || 0) + 1
  if (circular[id] > MAX_UPDATE_COUNT) {
  warn(
   'You may have an infinite update loop ' + (
   watcher.user
    ? `in watcher with expression "${watcher.expression}"`
    : `in a component render function.`
   ),
   watcher.vm
  )
  break
  }
 }
 }

 // keep copies of post queues before resetting state
 
 
 const activatedQueue = activatedChildren.slice()
 const updatedQueue = queue.slice()

 
 resetSchedulerState()

 // call component updated and activated hooks
 
 callActivatedHooks(activatedQueue)
 
 callUpdateHooks(updatedQueue)

 // devtool hook
 
 if (devtools && config.devtools) {
 devtools.emit('flush')
 }
}

flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图

为什么要异步更新视图

来看一下下面这一段代码

<template>
 <div>
 <div>{{test}}</div>
 </div>
</template>
export default {
 data () {
  return {
   test: 0
  };
 },
 created () {
  for(let i = 0; i < 1000; i++) {
  this.test++;
  }
 }
}

现在有这样的一种情况,created的时候test的值会被++循环执行1000次。

每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。

如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。

所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。

访问真实DOM节点更新后的数据

所以我们需要在修改data中的数据后访问真实的DOM节点更新后的数据,只需要这样,我们把文章第一个例子进行修改。

<template>
 <div>
 <div ref="test">{{test}}</div>
 <button @click="handleClick">tet</button>
 </div>
</template>
export default {
 data () {
  return {
   test: 'begin'
  };
 },
 methods () {
  handleClick () {
   this.test = 'end';
   this.$nextTick(() => {
    console.log(this.$refs.test.innerText);//打印"end"
   });
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}

使用Vue.js的global api的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。

关于怎样理解从Vue.js源码看异步更新DOM策略及nextTick问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注编程网JavaScript频道了解更多相关知识。

--结束END--

本文标题: 怎样理解从Vue.js源码看异步更新DOM策略及nextTick

本文链接: https://www.lsjlt.com/news/81268.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 怎样理解从Vue.js源码看异步更新DOM策略及nextTick
    怎样理解从Vue.js源码看异步更新DOM策略及nextTick,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。操作DOM在使用vue.js...
    99+
    2024-04-02
  • Vue异步更新DOM及$nextTick执行机制源码分析
    本篇内容介绍了“Vue异步更新DOM及$nextTick执行机制源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Vue异步更新DOM策...
    99+
    2023-07-05
  • vue源码之批量异步更新策略的深入解析
    vue异步更新源码中会有涉及事件循环、宏任务、微任务的概念,所以先了解一下这几个概念。 一、事件循环、宏任务、微任务 1.事件循环Event Loop:浏览器为了协调事件处理、脚本...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作