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

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更新后的数据,我们应该把该逻辑放到更新操作之后
因为加入异步队列先后的问题,如果我们在更新数据之前入队的话 ,是获取不到更新之后的数据的

总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CSh0tuHO-1573463374607)(https://static.123drm.com/front/drm/courses_wx/userside/ceshi/Vue异步队列更新.jpg)]

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

发布了18 篇原创文章 · 获赞 2 · 访问量 3503
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章