【vue源码】Vue的批量、异步更新策略

402 阅读3分钟

vue高效的一个很重要的原因是vue的批量、异步更新策略。

要理解批量、异步更新需要先了解一个概念:事件循环。

事件循环

事件循环:因为js是一个单线程的语言,特定的时间内只能特定的代码执行。要等待上一段代码执行完成后再执行一下段代码,如果上一段代码需要很长时间才能执行完成,那么就必须等待吗?答案显然是否定的,因为js除了单线程外还有一个叫任务队列的东西。它会把一些需要等待一定时间的操作放在任务队列中执行。 任务队列有两种 :macro-task(宏任务) 、micro-task(微任务) macro-task(宏任务)

  • setTimeout/setInterval
  • setImmediate
  • I/O操作
  • 主文档对象、解析HTML
  • 事件
  • 页面加载、输入

micro-task(微任务)

  • process.nextTick
  • Promise
  • MutationObserver

注意:以上方法的回调函数会被放到任务队列中,他们自身会直接执行。例:Promise会直接执行,但是then()会被加入到执行队列中。 javascript的执行机制是:首先执行调用栈中的函数,当调用栈中的执行上下文全部被弹出,只剩全局上下文的时候,就开始执行micro-task(微任务)的执行队列,微任务执行完成就开始执行宏任务(macro-task)中的。先进入的先执行,后进入的后执行。宏任务执行完成之后,js代码会检查是否有微任务需要执行,这样就形成了宏任务-微任务-宏任务-微任务的循环。这就形成了event loop。

vue就是借用了事件循环机制,在一次页面加载或事件循环中一次性把要更新的全部更新,而不是变一个属性进行一次更新,因此会非常高效。

vue中的具体实现

  • 异步:只要监听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
  • 批量:如果一个Watcher被多次触发,只会被推入到队列中一次。去重对于不必要的计算和DOM操作是非常重要的,然后,在下一个事件循环‘tick’中,Vue刷新队列执行更新
  • 异步策略,vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver、setImmediate,如果执行环境不支持(IE),则使用setTimeout(fn,0)代替。

update() 执行入队操作

src/core/observer/watcher.js

dep.notify()之后Watcher执行更新,执行入队操作。

//更新函数
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      //加入watcher队列  执行入队操作******
      queueWatcher(this)
    }
  }

queueWatcher() 执行Watcher入队操作

src/core/observer/scheduler.js

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  //去重,不存在Watcher才加入,非常高效
  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
      }
      //从这里进入next-tick.js
      nextTick(flushSchedulerQueue)
    }
  }
}

nextTick(flushSchedulerQueue) 异步策略

src/core/util/next-tick.js

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()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

为了验证vue的异步更新,下面的小例子拿去调试可以深入去理解一下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src='../../dist/vue.js'></script>
</head>

<body>
  <div id="app">
    <h2>异步更新</h2>
    <p id="p1">{{obj.bar}}</p>
  </div>
  <script>
    //创建实例
    /*
    会有几个Observer ?  几个Dep  ?   几个watcher
          2个:一个对象会有一个Observer,obj是一个对象,bar是一个对象。    2个Dep :有几个key就会有几个dep,obj是一个key,bar是一个key     1个Watcher,因为一个组件只有一个watcher
    */
    new Vue({
      el: '#app',
      data: {
        foo: 'ready'
      },
      mounted() {
        setInterval(() => {
          this.foo = Math.random()
          this.foo = Math.random()
          this.foo = Math.random()
          //异步行为,此时foo的内容没变
          console.log(p1.innerHTML)
          this.$nextTick(() => {
            //这里才是最新的值,值 是第三个,为什么?  第一次入队了,第二次进入队列的时候,发现已经进了不再进队列了,但是值 是会变的
            console.log(p1.innerHTML);

          })
        }, 2000);
      },
    })
  </script>

</body>

</html>