精华内容
下载资源
问答
  • } 这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。 JS运行机制 众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是: 当代码...

    关注公众号 前端开发博客,回复“加群”

    加入我们一起学习,天天进步

    作者:Liqiuyue

    链接:https://juejin.cn/post/6908264284032073736

    最近面试总是会被问到这么一个问题:在使用vue的时候,将for循环中声明的变量i从1增加到100,然后将i展示到页面上,页面上的i是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程。

    怎么可以让页面上有从1到100显示的过程呢,就是用setTimeout或者Promise.then等方法去模拟。

    讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢?

    for(let i=1; i<=100; i++){
     console.log(i);
    }
    

    这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。

    JS运行机制

    众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是:

    1. 当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈

    2. 在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;

    3. 一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。

    4. 主线程不断重复以上的步骤。

    我们把主线程执行一次的过程叫一个tick,所以nextTick就是下一个tick的意思,也就是说用nextTick的场景就是我们想在下一个tick做一些事的时候。

    所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate,微任务有MutationObsever/Promise.then

    想要透彻学习事件循环,推荐Jake在JavaScript全球开发者大会的演讲,保证讲懂!

    nextTick原理

    派发更新

    大家都知道vue的响应式的靠依赖收集和派发更新来实现的。在修改数组之后的派发更新过程,会触发setter的逻辑,执行dep.notify()

    // src/core/observer/watcher.js
    class Dep {
     notify() {
         //subs是Watcher的实例数组
         const subs = this.subs.slice()
            for(let i=0, l=subs.length; i<l; i++){
             subs[i].update()
            }
        }
    }
    

    遍历subs里每一个Watcher实例,然后调用实例的update方法,下面我们来看看update是怎么去更新的:

    class Watcher {
     update() {
         ...
         //各种情况判断之后
            else{
             queueWatcher(this)
            }
        }
    }
    

    update执行后又走到了queueWatcher,那就继续去看看queueWatcher干啥了(希望不要继续套娃了:

    //queueWatcher 定义在 src/core/observer/scheduler.js
    const queue: Array<Watcher> = []
    let has: { [key: number]: ?true } = {}
    let waiting = false
    let flushing = false
    let index = 0
    
    export function queueWatcher(watcher: Watcher) {
     const id = watcher.id
        //根据id是否重复做优化
        if(has[id] == null){
         has[id] = true
            if(!flushing){
             queue.push(watcher)
            }else{
             let i=queue.length - 1
                while(i > index && queue[i].id > watcher.id){
                 i--
                }
                queue.splice(i + 1, 0, watcher)
            }
           
         if(!waiting){
          waiting = true
             //flushSchedulerQueue函数: Flush both queues and run the watchers
             nextTick(flushSchedulerQueue)
         }
        }
    }
    

    这里queue在pushwatcher时是根据idflushing做了一些优化的,并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到⼀个队列⾥,然后在nextTick后执⾏flushSchedulerQueue

    flushSchedulerQueue函数是保存更新事件的queue的一些加工,让更新可以满足Vue更新的生命周期。

    这里也解释了为什么for循环不能导致页面更新,因为for是主线程的代码,在一开始执行数据改变就会将它push到queue里,等到for里的代码执行完毕后i的值已经变化为100时,这时vue才走到nextTick(flushSchedulerQueue)这一步。

    nextTick源码

    接着打开vue2.x的源码,目录core/util/next-tick.js,代码量很小,加上注释才110行,是比较好理解的。

    const callbacks = []
    let pending = false
    
    export function nextTick (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()
      }
    

    首先将传入的回调函数cb(上节的flushSchedulerQueue)压入callbacks数组,最后通过timerFunc函数一次性解决。

    let timerFunc
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
        }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      let counter = 1
      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
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    

    timerFunc下面一大片if else是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise,不⽀持的话再去检测是否⽀持MutationObserver,如果都不行就只能尝试宏任务实现,首先是setImmediate,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。

    这⾥使⽤callbacks⽽不是直接在nextTick中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。

    nextTick使用

    nextTick不仅是vue的源码文件,更是vue的一个全局API。下面来看看怎么使用吧。

    当设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

    官网用例:

    <div id="example">{{message}}</div>
    
    var vm = new Vue({
      el: '#example',
      data: {
        message: '123'
      }
    })
    vm.message = 'new message' // 更改数据
    
    vm.$el.textContent === 'new message' // false
    Vue.nextTick(function () {
      vm.$el.textContent === 'new message' // true
    })
    

    并且因为$nextTick() 返回一个 Promise 对象,所以也可以使用async/await 语法去处理事件,非常方便。

    相关文章

    1. 分享8个非常实用的Vue自定义指令

    2. Vue这些修饰符帮我节省20%的开发时间

    3. Vue路由权限控制分析

    最后

    转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料

    1. 回复「电子书」领取27本精选电子书

    2. 回复「加群」加入前端大神交流群,一起学习进步

    3. 回复「Vue」获取 Vue 精选文章

    分享和在看就是最大的支持❤️

    展开全文
  • Vue异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。 当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update方法,watcher.update ...

    Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。

    当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。

    然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的callbacks 数组中。

    • 如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。 flushCallbacks 函数负责执行 callbacks数组中的所有 flushSchedulerQueue 函数。
      flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。
    展开全文
  • 作者 | WahFung来源 | https ://www.cnblogs.com/chanwahfung/p/13296293.html前言最初更新vue核心实现之一,在整体...

    作者 | WahFung
    来源 | 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,最优先肯定是微任务,其次再到宏任务。

    优先级为promise.then> MutationObserver> setImmediate> setTimeout。也很重要,它们能帮助我们理解设计的意义)

    我们会发现在某种情况下创建的timerFunc,最终都会执行一个flushCallbacks的函数。

    const callbacks = []let pending = false
    function 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 = 0
    export 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 响应式原理》中,我们通过 9 张流程图,理解了 Vue 的渲染流程,相信大家对整个 Vue 的渲染流程有了一定的了解,这一篇我们来重点关注一下 Vue 异步更新原理模块。 本文主要分析 Vue 从 Data 更新...

    上一篇《图解 Vue 响应式原理》中,我们通过 9 张流程图,理解了 Vue 的渲染流程,相信大家对整个 Vue 的渲染流程有了一定的了解,这一篇我们来重点关注一下 Vue 异步更新原理模块。

    本文主要分析 Vue 从 Data 更新,到通知 Watcher 异步更新视图的流程,也就是下图中的橙色部分。

    我们先来回顾一下图中的几个对象:

    • Data 对象:Vue 中的 data 方法中返回的对象。
    • Dep 对象:每一个 Data 属性都会创建一个 Dep,用来搜集所有使用到这个 Data 的 Watcher 对象。
    • Watcher 对象:主要用于渲染 DOM。

    接下来,我们就开始分析这个流程。

    Vue 异步更新 DOM 原理

    很多同学都知道,Vue 中的数据更新是异步的,意味着我们在修改完 Data 之后,并不能立刻获取修改后的 DOM 元素。

    例如:

    <template>
      <div>
        <span id="text">{{ message }}</span>
        <button @click="changeData">
          changeData
        </button>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          message: "hello",
        };
      },
      methods: {
        changeData() {
          this.message = "hello world";
          const textContent = document.getElementById("text").textContent;
          // 直接获取,不是最新的
          console.log(textContent === "hello world"); // false
    			// $nextTick 回调中,是最新的
          this.$nextTick(() => {
            const textContent = document.getElementById("text").textContent;
            console.warn(textContent === "hello world"); // true
          });
        },
      },
    };
    </script>
    

    什么时候我们才能获取到真正的 DOM 元素?

    答:在 Vue 的 nextTick 回调中。

    这一点在 Vue 官网有详细的介绍,但你是否有想过,为什么 Vue 需要通过 nextTick 方法才能获取最新的 DOM?

    带着这个疑问,我们直接看一下源码。

    // 当一个 Data 更新时,会依次执行以下代码
    // 1. 触发 Data.set
    // 2. 调用 dep.notify
    // 3. Dep 会遍历所有相关的 Watcher 执行 update 方法
    class Watcher {
      // 4. 执行更新操作
      update() {
        queueWatcher(this);
      }
    }
    
    const queue = [];
    
    function queueWatcher(watcher: Watcher) {
      // 5. 将当前 Watcher 添加到异步队列
      queue.push(watcher);
      // 6. 执行异步队列,并传入回调
      nextTick(flushSchedulerQueue);
    }
    
    // 更新视图的具体方法
    function flushSchedulerQueue() {
      let watcher, id;
      // 排序,先渲染父节点,再渲染子节点
      // 这样可以避免不必要的子节点渲染,如:父节点中 v-if 为 false 的子节点,就不用渲染了
      queue.sort((a, b) => a.id - b.id);
      // 遍历所有 Watcher 进行批量更新。
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        // 更新 DOM
        watcher.run();
      }
    }
    

    根据上面的代码,我们可以得出这样一个流程图:

    图中可以看到,Vue 在调用 Watcher 更新视图时,并不会直接进行更新,而是把需要更新的 Watcher 加入到 Queue 队列里,然后把具体的更新方法 flushSchedulerQueue 传给 nextTick 进行调用。

    接下来,我们分析一下 nextTick。

    const callbacks = [];
    let timerFunc;
    
    function nextTick(cb?: Function, ctx?: Object) {
      let _resolve;
      // 1.将传入的 flushSchedulerQueue 方法添加到回调数组
      callbacks.push(() => {
        cb.call(ctx);
      });
      // 2.执行异步任务
      // 此方法会根据浏览器兼容性,选用不同的异步策略
      timerFunc();
    }
    

    可以看到,nextTick 函数非常简单,它只是将传入的 flushSchedulerQueue 添加到 callbacks 数组中,然后执行了 timerFunc 方法。

    接下来,我们分析一下 timerFunc 方法。

    let timerFunc;
    // 判断是否兼容 Promise
    if (typeof Promise !== "undefined") {
      timerFunc = () => {
        Promise.resolve().then(flushCallbacks);
      };
      // 判断是否兼容 MutationObserver
      // https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
    } else if (typeof MutationObserver !== "undefined") {
      let counter = 1;
      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);
      };
      // 判断是否兼容 setImmediate
      // 该方法存在一些 IE 浏览器中
    } else if (typeof setImmediate !== "undefined") {
      // 这是一个宏任务,但相比 setTimeout 要更好
      timerFunc = () => {
        setImmediate(flushCallbacks);
      };
    } else {
      // 如果以上方法都不知道,使用 setTimeout 0
      timerFunc = () => {
        setTimeout(flushCallbacks, 0);
      };
    }
    
    // 异步执行完后,执行所有的回调方法,也就是执行 flushSchedulerQueue
    function flushCallbacks() {
      for (let i = 0; i < copies.length; i++) {
        callbacks[i]();
      }
    }
    

    可以看到,timerFunc 是根据浏览器兼容性创建的一个异步方法,它执行完成之后,会调用 flushSchedulerQueue 方法进行具体的 DOM 更新。

    分析到这里,我们就可以得到一张整体的流程图了。

    接下来,我们来完善一些判断逻辑。

    • 判断 has 标识,避免在一个 Queue 中添加相同的 Watcher。
    • 判断 waiting 标识,让所有的 Watcher 都在一个 tick 内进行更新。
    • 判断 flushing 标识,处理 Watcher 渲染时,可能产生的新 Watcher。
      • 如:触发了 v-if 的条件,新增的 Watcher 渲染。

    结合以上判断,最终的流程图如下:

    最后,我们分析一下,为什么 this.$nextTick 能够获取更新后的 DOM?

    // 我们使用 this.$nextTick 其实就是调用 nextTick 方法
    Vue.prototype.$nextTick = function (fn: Function) {
      return nextTick(fn, this);
    };
    

    可以看到,调用 this.$nextTick 其实就是调用了图中的 nextTick 方法,在异步队列中执行回调函数。根据先来后到原则,修改 Data 触发的更新异步队列会先得到执行,执行完成后就生成了新的 DOM ,接下来执行 this.$nextTick 的回调函数时,能获取到更新后的 DOM 元素了。

    由于 nextTick 只是单纯通过 PromiseSetTimeout 等方法模拟的异步任务,所以也可以手动执行一个异步任务,来实现和 this.$nextTick 相同的效果。

    this.message = "hello world";
    // 手动执行一个异步任务,也能获取最新的 DOM
    Promise.resolve().then(() => {
      const textContent = document.getElementById("text").textContent;
      console.log(textContent === "hello world"); // true
    });
    setTimeout(() => {
      const textContent = document.getElementById("text").textContent;
      console.log(textContent === "hello world"); // true
    });
    

    思考与总结

    本文从源码的角度,介绍了 Vue 异步更新的原理,来简单回顾一下吧。

    1. 修改 Vue 中的 Data 时,就会触发所有和这个 Data 相关的 Watcher 进行更新。
    2. 首先,会将所有的 Watcher 加入队列 Queue。
    3. 然后,调用 nextTick 方法,执行异步任务。
    4. 在异步任务的回调中,对 Queue 中的 Watcher 进行排序,然后执行对应的 DOM 更新。

    最后,如果你对此有任何想法,欢迎留言评论!

    展开全文
  • 这篇文章将介绍关于Vue异步更新队列的知识,学习异步更新队列将帮助我们更好的理解Vue的更新机制。 先来看一个例子:一个div,有v-if控制是否渲染,点击按钮对div的显示/隐藏做改变,并在显示时,获取里边的文本 。 ...
  • 这段代码不难理解,但是运行时却在后台抛出一个错误:Cannot read property ‘innerHTML’ of null,意思是获取不到div元素,这句是“异步更新队列” Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并...
  • 作者 | WahFung来源 | https ://www.cnblogs.com/chanwahfung/p/13296293.html前言最初更新vue核心实现之一,在整体...
  • vue中状态更新异步的,这一点和react中的setstate类似。 解决方案 非组件解决方案: &lt;div id="example"&gt;{{message}}&lt;/div&gt; var vm = new Vue({ el: '#example', data:...
  • vue批量异步更新策略

    2020-02-12 13:32:56
    vue高效的秘诀是一套批量,异步更新策略 在程序中有好多个组件,每个组件对应一个watch实例,在一次事件循环更新周期内,可能有好多数据发生变化,可能每个组件都会变,这时候最好的方式就是将组件批量的一次更新...
  • vue已是目前国内前端web端三分天下之一,同时也作为本人主要技术栈之一,在日常使用中知其然也好奇着所以然,另外最近的社区涌现了一大票vue源码阅读类的文章,在下借这个机会从大家的文章和...
  • Vue异步更新队列

    2021-09-15 11:33:05
    Vue更新 DOM 时是异步执行的 看下官网介绍: 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除...
  • 前言 上一篇的 Vue 源码解读(3)—— 响应式原理 说到通过 Object.defineProperty 为对象的每个 key 设置 getter、setter,从而拦截对...所以,异步更新 的入口点就是 setter 中最后调用的 dep.notify() 方法。 目的
  • Vue之DOM异步更新问题

    2020-01-17 15:59:13
    vue采用的虚拟dom,在改变dom属性的时候采用的是异步策略 document.getElementById("app").innerHTML(); this.num+=1; this.$nextTick(()=>{ console.log(num) }...
  • 通过对异步更新队列的研究和学习,加深对Vue更新机制的理解 什么是异步更新队列 先看看下面的例子: <div id="app"> <div id="div" v-if="isShow">被隐藏的内容</div> <i...
  • 之前我们学到了 Vue 更新数据是如何更新视图的。...在更新视图这一步,使用异步更新策略 为什么呢?引用小册中的例子,下面有一个这样的 Vue 组件 &lt;template&gt; &lt;div&gt; ...
  • 接下来在本文里一起看看当数据变化时,从源码层面逐步分析一下触发页面的响应动作之后,如何做渲染到页面上,展示到用户层面的。...从一个例子体验一下异步渲染机制。 import Vue from 'Vue' new Vue({ el: '#app'
  • 如果不采取异步更新,那么每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue 会在本轮数据更新后,再去异步更新数据。
  • Vue源码解析——异步更新队列+虚拟dom 异步更新队列 Vue高效的秘诀是一套批量、异步更新策略。 每一个组件对应一个watcher实例,在一个队列中插入watcher,再进行批量操作。 概念 事件循环:浏览器的一套工作...
  • 【探究Vue原理】watcher的异步更新

    千次阅读 2019-09-02 20:57:59
    目录开篇同步更新异步更新异步更新的源码分析flushSchedulerQueuequeueWatcher     开篇   之前的文章提到过,当用户修改了组件状态之后,dep会通知它所关联的watcher进行更新更新过程分两种,一种是...
  • 浅析Vue.nextTick()原理 1、为什么用Vue.nextTick() 2、什么是Vue.nextTick() 3、怎么用 4、小结 为什么用Vue.nextTick() 首先来了解一下JS的运行机制 JS运行机制(Event Loop) JS执行是单线程的,它是基于事件...
  • Vue.js异步更新及nextTick

    千次阅读 2018-08-29 15:04:22
    在查阅各种资料之后,在这里总结一下Vue.js异步更新的策略以及nextTick的用途和原理。如有总结错误的地方,欢迎指出! 本文将从以下3点进行总结: 为什么Vue.js要异步更新视图? JavaScript异步运行的机制是...
  • vue的双向绑定基于Object.defineProperty以及对于数组原型改变数组自身的7种方法进行增强来实现。 在其值发生改变时,通过拦截数据时设置的dep实例调用dep.notify()即循环dep实例的subs数组(一个由wacher所组成的...
  • Vue2的异步更新策略与$nextTick源码解析前言一、事件循环机制1.概念解释1.1 事件循环Event Loop1.2 宏任务Task1.3 微任务MicroTask2. 案例解析二、Vue2的批量异步更新策略1. 概念解释1.1 异步1.2 批量1.3 异步策略2....
  • react是通过setState() 驱动数据 当前节点更新整个state 运用的技术是immutable 技术这immutable 翻译过来就是不可变的 ,就是说state的数据是不可变,如果要改变,就要...vue更新原来是通过数据劫持来驱动更新,在
  • 深究Vue异步更新策略原理 我们先看一个示例: <template> <div> <div ref="test">{{test}}div> <button @click="handleClick">tetbutton> div> template> <script> export default { data () { return { test: '...
  • 因为Vue异步更新队列,$nextTick是用来知道什么时候DOM更新完成的。 详细解读: 我们先来看这样一个场景:有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,347
精华内容 6,138
关键字:

vue的异步更新机制

vue 订阅