Vue源码阅读(四): Vue的异步更新队列

关于数据响应化,问一个常见的问题:html

下面示例代码中的两个输出console.log(p1.innerHTML),分别是什么?为何?html5

<!DOCTYPE html>
<html>
<body>
    <div id="demo">
        <h1>异步更新</h1>
        <p id="p1">{{foo}}</p>
    </div>
    <script>
        const app = new Vue({
            el: '#demo',
            data: { foo: '' },
            mounted() {
                setInterval(() => {                    
                    this.foo = 't1'
                    this.foo = 't2'
                    this.foo = 't3'
                    console.log(p1.innerHTML) //此时,页面的展现值?
                    this.$nextTick(() => {
                        console.log(p1.innerHTML) //此时,页面的展现值?
                    })
                }, 1000);
            }
        });
    </script>
</body>
</html>
复制代码

这个问题的第一问“是什么”,并不复杂。难的是"为何"。该问题的本质涉及到 Vue 的异步更新问题。算法

首先,须要明确的是:Vue 的更新 DOM 的操做是异步的,批量的。之因此这么作的原因也很简单:更新 DOM 的操做是昂贵的,消耗较大。如上面的展现例子所示,Vue 内部会连续更新三次 DOM 么?那显然是不合理的。批量、异步的操做才更优雅。express

咱们想要去源码看看,Vue 更新 DOM 的批量与异步操做,究竟是如何作的呢?bash

首先界定一个界限:咱们不会立马深刻到虚拟 DOM 的生成与页面更新的 patch 算法中去,只是想要看看这个批量与异步的过程,解决刚刚提到的问题。app

源码

从以前的笔记内容可知:数据响应的核心方法defineReactive()中,当数据发生变化的时候,会调用Dep.notify()方法,通知对应的Watcher执行updateComponent()操做,继而从新渲染执行更新页面。异步

让咱们从Dep的notify()方法提及。async

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  
  ...//省略

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

可知,其内部是执行的是相关联的 Watcher 的update()方法。函数

import { queueWatcher } from './scheduler'

export default class Watcher {

  ...//省略
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {//若是是同步
      this.run()
    } else {
      queueWatcher(this) //Watcher的入队操做
    }
  }

   //实际执行的更新方法,会被scheduler调用
  run () {
      if (this.active) {
      //this.get()是挂载时传入的updateComponent()方法
      const value = this.get()
      //若是是组件的Watcher,不会有返回值value,不会执行下一步
      //只有用户自定义Watcher才会进入if
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        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)
        }
      }
    }
  }
复制代码

看到这里,提问一哈:若是在同一时刻,组件实例中的 data 修改了屡次,其对应的 Watcher 也会执行queueWatcher(this)屡次,那么是否会在当前队列中存在多个一样的Watcher呢?oop

带着这个问题,查看同一文件夹下schedule.jsqueueWatcher()方法:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.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

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //异步刷新队列
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

代码中看到:每一个 Watcher 都会有一个 id 标识,只有全新的 Watcher 才会入队。批量的过程咱们看到了,将是将 Watcher 放入到队列里面去,而后批量操做更新。

看了这个批量更新的操做,有人会问:屡次数据响应化,只有第一次更新的 Watcher 才会进入队列,是否是意味着只有第一次的数据响应化才生效,然后几回的数据响应化无效了呢?

回答:并非这样的,数据响应化一直都在进行,变化的数据也一直在变。须要明确其和批量更新队列之间的关联,发生在 Watcher 的 run() 方法上。当执行 run() 方法的时候,其获取的 data 是最新的 data。

讲了批量,那么异步的过程是怎样的呢?让咱们来看看nextTick()函数内部,了解一些关于异步操做的知识点:

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

//关于timerFunc的选取过程
let timerFunc

//优先选择Promise,由于Promise是基于微任务的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  
//次优选择MutationObserver,MutationObserver也是基于微任务的
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  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
  
//若是以上二者都不行,那么选择setImmediate(),它是基于宏任务的
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最无奈的选择,选择setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

//nextTick: 按照特定异步策略timerFunc() 执行队列操做
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()
  }
}
复制代码

关于宏任务与微任务,能够查看更多有意思的页面:

juejin.im/post/5b498d…

jakearchibald.com/2015/tasks-…

相关文章
相关标签/搜索