js异步等待完成后再进行下一步操作_Vue 之异步更新机制和nextTick原理

504408bfc298a6e1a0163a404bd9a369.png

211f6e0241df2a1db152ce32f50c526e.gif 阅读本文约需要8分钟

大家好,我是你们的导师,我每天都会在这里给大家分享一些干货内容(当然了,周末也要允许老师休息一下哈)。上次老师跟大家分享了JS 之函数式编程术语总结的知识,今天跟大家分享下Vue 之异步更新机制和nextTick原理的知识。

1 Vue 之异步更新机制和nextTick原理
参考文献:https ://www.cnblogs.com/chanwahfung/p/13296293.html

前言 最初更新是   vue核心   实现之一,在整体流程中预先着手观看者更新的调度者这一角色。大部分观察者更新都会通过它的处理,在适当时机让更新有序的执行。而nextTick作为替代更新的核心,也是需要学习的重点。 本文你能学习到:
  • 初步更新的作用

  • nextTick原理

  • 初步更新流程

js运行机制

在理解初步更新前,需要对 js 运行机制进行了解,如果你已经知道这些知识,可以选择跳过这部分内容。 js 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
  • 所有同步任务都在主线程上执行,形成一个执行栈(执行上下文堆栈)。

  • 主线程之外,还存在一个“任务队列”(task queue)。只要初始化任务有了运行结果,就在“任务变量”之中放置一个事件。

  • 一旦“执行栈”中的所有同步任务执行完毕,系统就会重新“任务类别”,看看里面有什么事件。那些对应的初始化任务,于是结束等待状态,进入执行栈,开始执行。

  • 主线程不断重复上面的第三步。

“任务类别”中的任务(任务)被分为两个类,分别是宏任务(宏任务)和微任务(micro task) 宏任务:在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务类别,但需要等到下一次事件循环才会执行。常见的宏任务有setTimeout,setImmediate,requestAnimationFrame 微任务:当前事件循环的任务队列为空时,微任务队列中的任务就会被依次执行在执行过程中,如果遇到微任务,微任务被加入到当前事件循环的微任务队列中。简单来说,只要有微任务就会继续执行,而不是放到下一个事件循环才执行。常见的微任务有MutationObserver,Promise.then 总的来说,在事件循环中,微任务会先于宏任务执行。而在微任务执行完后会进入 浏览器 更新渲染阶段,所以在更新渲染前使用微任务会比宏任务快一些。

为什么需要初步更新

既然异步更新是核心之一,首先要知道它的作用是什么,解决了什么问题。 先来看一个很常见的场景:
created(){    this.id = 10    this.list = []    this.info = {}}
总所周知, vue   基于 数据 驱动视图, 数据 更改会触发setter   函数 ,通知观察者进行更新。如果像上面的情况,是不是代表需要更新3次,而且在实际开发中的更新可不止那么少。 更新过程是需要经过繁杂的操作,例如模板编译,dom diff,不断进行更新的性能当然很差。 VUE   作为一个优秀的 框架 ,当然不会那么“直男”,来多少就照单全收。 VUE   内部实际是将观看者加入到一个队列数组中,最后再触发队列中所有观察家的运行方法来更新。 并且加入队列的过程中将会对watcher进行去重操作,因为在一个组件中数据内定义的 数据 都是存储同一个“渲染watcher”,所以以上场景中 数据 甚至更新了3次,最终也只会执行一次更新页面的逻辑。 为了达到这种效果, vue   使用异步更新,等待所有 数据 同步修改完成后,再去执行更新逻辑。

nextTick原理

异步更新内部是最重要的就是nextTick方法,它负责将异步任务加入队列和执行异步任务。 VUE   也将它暴露出来提供给用户使用。在 数据 修改完成后,立即获取相关DOM还没那么快更新,使用nextTick便可以解决这一问题。

认识nextTick

官方文档对它的描述: 在下一DOM更新循环结束之后执行连续的替代。在修改数据之后立即使用此方法,获取更新后的DOM。
// 修改数据vm.msg = 'Hello'// DOM 还没有更新vue.nextTick(function () {  // DOM 更新了})// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)vue.nextTick()  .then(function () {    // DOM 更新了  })
nextTick使用方法有一种和Promise两种,以上是通过构造 函数 调用的形式,更常见的是在实例调用this。$ nextTick。它们都是同一个方法。

内部实现

在   vue   源码2.5+后,nextTick的实现单独有一个 js 文件来维护它,它的内核并不复杂, 代码 实现不过100行,稍微花点时间可以啃下来。 比特位置在src / core / util /下一步  js ,接下来我们来看一下它的实现,先从入口 函数 开始:
export function nextTick (cb?: Function, ctx?: Object) {  let _resolve  // 1  callbacks.push(() => {    if (cb) {      try {        cb.call(ctx)      } catch (e) {        handleError(e, ctx, 'nextTick')      }    } else if (_resolve) {      _resolve(ctx)    }  })  // 2  if (!pending) {    pending = true    timerFunc()  }  // $flow-disable-line  // 3  if (!cb && typeof Promise !== 'undefined') {    return new Promise(resolve => {      _resolve = resolve    })  }}
  • cb即预期的最大值,它被push进一个回调回调,等待调用。

  • 等待的作用就是一个锁,防止后续的nextTick重复执行timerFunc。timerFunc内部创建会一个微任务或宏任务,等待所有的nextTick同步执行完成后,再去执行回调内部的替代。

  • 如果没有预先设定的,用户可能使用的是Promise形式,返回一个Promise,_resolve被调用时进入到。

继续往下走看看timerFunc的实现:
// Here we have async deferring wrappers using microtasks.// In 2.5 we used (macro) tasks (in combination with microtasks).// However, it has subtle problems when state is changed right before repaint// (e.g. #6813, out-in transitions).// Also, using (macro) tasks in event handler would cause some weird behaviors// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).// So we now use microtasks everywhere, again.// A major drawback of this tradeoff is that there are some scenarios// where microtasks have too high a priority and fire in between supposedly// sequential events (e.g. #4521, #6690, which have workarounds)// or even between bubbling of the same event (#6566).let timerFunc// 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:/* istanbul ignore next, $flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()  timerFunc = () => {    p.then(flushCallbacks)    // 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)  }  isUsingMicroTask = true} else if (!isIE && 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, iOS7, Android 4.4  // (#6466 MutationObserver is unreliable in IE11)  let counter = 1const 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} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {  // Fallback to setImmediate.  // Technically it leverages the (macro) task queue,  // but it is still a better choice than setTimeout.  timerFunc = () => {    setImmediate(flushCallbacks)  }} else {  // Fallback to setTimeout.  timerFunc = () => {    setTimeout(flushCallbacks, 0)  }}
顶层的 代码 并不复杂,主要通过一些兼容的判断来创建合适的timerFunc,最优先肯定是微任务,其次再到宏任务。504408bfc298a6e1a0163a404bd9a369.png 优先级为promise.then> MutationObserver> setImmediate> setTimeout。也很重要,它们能帮助我们理解设计的意义) 我们会发现在某种情况下创建的timerFunc,最终都会执行一个flushCallbacks的 函数 。
const callbacks = []let pending = falsefunction flushCallbacks () {  pending = falseconst copies = callbacks.slice(0)  callbacks.length = 0  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}
flushCallbacks里做的事情是如此简单,它负责执行回调里的事情。 好了,nextTick的原始码那么那么多,现在已经知道它的实现,下面再结合转化更新流程,让我们对它更充分的理解吧。

初步更新流程

数据 被改变时,触发watcher.update
// 源码位置:src/core/observer/watcher.jsupdate () {  /* istanbul ignore else */  if (this.lazy) {    this.dirty = true  } else if (this.sync) {    this.run()  } else {    queueWatcher(this) // this 为当前的实例 watcher  }}
调用queueWatcher,将watcher加入
// 源码位置:src/core/observer/scheduler.jsconst queue = []let has = {}let waiting = falselet flushing = falselet index = 0export function queueWatcher (watcher: Watcher) {const id = watcher.id  // 1  if (has[id] == null) {    has[id] = true    // 2    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    // 3    if (!waiting) {      waiting = true      nextTick(flushSchedulerQueue)    }  }}
  • 每个监视者都有他们自己的id,当没有记录到对应的监视者,即第一次进入逻辑,否则是重复的监视者,则不会进入。这一步就是实现监视者去重的点。

  • 将watcher加入到体重中,等待执行

  • 等待的作用是防止nextTick重复执行

flushSchedulerQueue作为替代预期nextTick初始化执行。
function flushSchedulerQueue () {  currentFlushTimestamp = getNow()  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]    if (watcher.before) {      watcher.before()    }    id = watcher.id    has[id] = null    watcher.run()  }  // keep copies of post queues before resetting stateconst activatedQueue = activatedChildren.slice()  const updatedQueue = queue.slice()  resetSchedulerState()  // call component updated and activated hooks  callActivatedHooks(activatedQueue)  callUpdatedHooks(updatedQueue)}
flushSchedulerQueue内将刚刚加入队列的观察者逐个运行更新。resetSchedulerState重置状态,等待下一轮的异步更新。
function resetSchedulerState () {  index = queue.length = activatedChildren.length = 0  has = {}  if (process.env.NODE_ENV !== 'production') {    circular = {}  }  waiting = flushing = false}
要注意此时flushSchedulerQueue仍未执行,它只是作为一个预期的插入而已。因为用户可能会调用nextTick方法。 这种情况下,回调里的内容为[“ flushSchedulerQueue”,“用户的nextTick选择”],当所有同步任务执行完成,才开始执行回调里面的一部分。 由此可见,最先执行的是页面更新的逻辑,其次再到用户的nextTick将会执行。这也是为什么我们能在nextTick中获取到更新后DOM的原因。

总结

初始更新机制使用微任务或宏任务,基于事件循环运行,在   vue   中对性能起着至关重要的作用,它对重复重复的watcher进行过滤。而nextTick根据不同的环境,使用优先级最高的初始任务。 此类的好处是等待所有的状态同步更新完成后,再一次性渲染页面。用户创建的nextTick运行页面更新之后,因此能够获取更新后的DOM。 今天就分享这么多, 于Vue 之异步更新机制和nextTick原理的 识点 会了多少 欢迎在留言区评论,对于有价值的留言,我们都会一一回复的。如果觉得文章对你有一丢丢帮助,请点右下角【 在看 】,让更多人看到该文章。

504408bfc298a6e1a0163a404bd9a369.png

     faf9c8c622bca6e841dbeee37e3fcfce.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值