欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue源码解析03-异步更新队列

程序员文章站 2024-03-01 08:44:40
...

Vue 源码解析03-异步更新队列

前言

这篇文章分析了Vue更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习,加深对Vue更新机制的理解

什么是异步更新队列

先看看下面的例子:

    <div id="app">
        <div id="div" v-if="isShow">被隐藏的内容</div>
        <input @click="getDiv" value="按钮" type="button">
    </div>
  <script>

    let vm = new Vue({
        el: '#app',
        data: {
        //控制是否显示#div
        isShow: false
        },
        methods:{
        getDiv: function () {
            this.isShow=true
            var content = document.getElementById('div').innerHTML;
            console.log('content',content)
        }
        }
    })
</script>
  • 上面的例子是,点击按钮显示被隐藏的div,同时打印div内部html的内容。
  • 按照我们一般的认知,应该是点击按钮能够显示div并且在控制台看到div的内部html的内容。

但是实际执行的结果确是,div可以显示出来,但是打印结果的时候会报错,错误原因就是innerHTML为null,也就是div不存在。
只有当我们再次点击按钮的时候才会打印出div里面的内容。这就是Vue的异步更新队列的结果

异步更新队列的概念

Vue的dom更新是异步的,当数据发生变化时Vue不是立刻去更新dom,而是开启一个队列,并缓冲在同一个事件中循环发生的所有数据变化。
在缓冲时,会去除重复的数据,避免多余的计算和dom操作。在下一个事件循环tick中,刷新队列并执行已去重的工作。

  • 所以上面的代码报错是因为当执行this.isShow=true时,div还未被创建出来,知道下次Vue事件循环时才开始创建

  • 查重机制降低了Vue的开销

  • 异步更新队列实现的选择:由于浏览器的差异,Vue会根据当前环境选择Promise.then或者MuMutationObserver,如果两者都不支持,则会用setImmediate或者setTimeout代替

异步更新队列解析

异步队列源码入口

通过之前对Vue数据响应式的分析我们知道,当Vue数据发生变化时,会触发dep的notify()方法,该方法通知观察者watcher去更新dom,我们先看一下这的源码

  • from src/core/observer/dep.js
    //直接看核心代码
    notify () {
        //这是Dep的notify方法,Vue的会对data数据进行数据劫持,该方法被放到data数据的set方法中最后执行
        //也就是通知更新操作
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
        subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
        // !!!核心:通知watcher进行数据更新
        //这里的subs[i]其实是Dep维护的一个watcher数组,所以我们下面是执行的watcher中的update方法
        subs[i].update()
        }
  }
  • 上面的代码简单来说就是dep通知watcher尽心更新操作,我们看一下watcher相关的代码
    from :src/core/observer/watcher.js
    //这里只展示部分核心代码
    //watcher的update方法
    update () {
        /* istanbul ignore else */
        //判断是否存在lazy和sync属性
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
        this.run()
        } else {
        //核心:将当前的watcher放到一个队列中
        queueWatcher(this)
        }
    }
  • 上面watcher的update更新方法简单来说就是调用了一个queueWatcher方法,这个方法其实是将当前的watcher实例放入到一个队列中,以便完成后面的异步更新队列操作

异步队列入队

下面看看queueWatcher的逻辑 from src/core/observer/scheduler.js

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

        if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
        }
        // 启动异步任务(刷新当前的计划任务)
        nextTick(flushSchedulerQueue)
        }
    }
    }
  • 上面这段queueWatcher的代码的主要作用就是对任务去重,然后启动异步任务,进行跟新操作。接下来我们看一线nextTick里面的操作

from src/core/util/next-tick.js

//cb:
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //callbacks:这个方法维护了一个回调函数的数组,将回调函数添家进数组
  callbacks.push(() => {
      //添加错误处理
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    //启动异步函数
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
  • 这里的核心,其实就在timerFunc的函数上,该函数根据不同的运行时环境,调用不同的异步更新队列,下面看一下代码

from src/core/util/next-tick.js

    /**这部分逻辑就是根据环境来判断timerFunc到底是使用什么样的异步队列**/
    let timerFunc
    //首选微任务执行异步操作:Promise、MutationObserver
    //次选setImmediate最后选择setTimeout
    // 根据当前浏览器环境选择用什么方法来执行异步任务
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
        //如果当前环境支持Promise,则使用Promise执行异步任务
        const p = Promise.resolve()
        timerFunc = () => {
            //最终是执行的flushCallbacks方法
            p.then(flushCallbacks)
            //如果是IOS则回退,因为IOS不支持Promise
            if (isIOS) setTimeout(noop)
        }
        //当前使用微任务执行
        isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        //如果当前浏览器支持MutationObserver则使用MutationObserver
        isNative(MutationObserver) ||
        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)) {
        //如果支持setImmediate,则使用setImmediate
        timerFunc = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        //如果上面的条件都不满足,那么最后选择setTimeout方法来完成异步更新队列
        timerFunc = () => {
            setTimeout(flushCallbacks, 0)
        }
    }
  • 从上面代码可以看出,不论timerFunc使用的是什么样的异步更新队列,最终执行的函数还是落在了flushCallbacks上面,那么我们来看一看,这个方法到底是什么

from src/core/util/next-tick.js

    function flushCallbacks () {
        pending = false
        //拷贝callbacks数组内容
        const copies = callbacks.slice(0)
        //清空callbacks
        callbacks.length = 0
        //遍历执行
        for (let i = 0; i < copies.length; i++) {
            //执行回调方法
            copies[i]()
        }
    }
  • 上面的这个方法就是遍历执行了我们nextTick维护的那个回调函数数组,其实就是将数组的方法依次添加进异步队列进行执行。同时清空callbacks数组为下次更新作准备。

上面这几段代码其实都是watcher的异步队列更新中的入队操作,通过queueWatcher方法中调用的nextTick(flushSchedulerQueue),我们知道,其实是将flushSchedulerQueue这个方法入队

异步队列的具体更新方法

所以下面我们看一下flushSchedulerQueue这个方法到底执行了什么操作

from src/core/observer/scheduler.js

    /**我们这里只粘贴跟本次异步队列更新相关的核心代码**/
    //具体的更新操作
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow()
    flushing = true
    let watcher, id
    //重新排列queue数组,是为了确保:
    //更新顺序是从父组件到子组件
    //用户的watcher先于render 的watcher执行(因为用户watcher先于render watcher创建)
    //当子组件的watcher在父组件的watcher执行时被销毁,则跳过该子组件的watcher
    queue.sort((a, b) => a.id - b.id)
    //queue数组维护的一个watcher数组
    //遍历queue数组,在queueWatcher方法中我们将传入的watcher实例push到了该数组中
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        //清空has对象里面的"id"属性(这个id属性之前在queueWatcher方法里面查重的时候用到了)
        has[id] = null
        //核心:最终执行的其实是watcher的run方法
        watcher.run()
        //下面是一些警告提示,可以先忽略
        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
            }
        }
    }
    //调用组件updated生命周期钩子相关,先跳过
    const activatedQueue = activatedChildren.slice()
    const updatedQueue = queue.slice()

    resetSchedulerState()

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)
        if (devtools && config.devtools) {
            devtools.emit('flush')
        }
}
  • 上面的一堆 flushSchedulerQueue 代码,简单来说就是排列了queue数组,然后遍历该数组,执行watcher.run方法。所以,异步队列更新当我们入队完以后,真正执行的方法其实是watcher.run方法

下面我们来继续看一下watcher.run方法,到底执行了什么操作

from src/core/observer/watcher.js

    /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   * 上面这段英文注释 是官方注释,从这我们看出该方法最终会被scheduler调用
   */
  run () {
    if (this.active) {
        //这里调用了watcher的get方法
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  • 上述run方法最终要的操作就是调用了watcher的get方法,该方法我们在之前的源码分析有讲过,主要实现的功能是调用了data数据的get方法,获取最新数据。

至此,Vue异步更新队列的核心代码我们就分析完了,为了便于理清思路,我们来一张图总结一下

关于Vue.$nextTick

我们都知道.nextTick.nextTick方法,其实这个 **nextTick** 方法就是直接调用的上面的nextTick方法

from src/core/instance/render.js

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  • 由上面的代码我们可以看出,$nextTick是将我们传入的回调函数加入到了异步更新队列,所以它才能实现dom更新后回调

注意,$nextTick()是会将我们传入的函数加入到异步更新队列中的,但是这里有个问题,如果我们想获得dom更新后的数据,我们应该把该逻辑放到更新操作之后
因为加入异步队列先后的问题,如果我们在更新数据之前入队的话 ,是获取不到更新之后的数据的

总结

Vue源码解析03-异步更新队列

总结起来就是,当触发数据更新通知时,dep通知watcher进行数据更新,这时watcher会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。
这样就达到了多次更新同时完成,提升了用户体验,减少了浏览器的开销,增强了性能。