Vue2.0 异步更新解读

628 阅读1分钟

首先抛出个问题😁

下面的打印结果是什么呢?

<body>
  <div id="app">
    <h1>异步更新</h1>
    <p id="p1">{{foo}}</p>
  </div>
</body>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      foo: 'ready~~~',
    },
    mounted() {
      this.foo = 1;
      console.log('1', this.foo)

      this.foo = 2
      console.log('2', this.foo)

      this.foo = 3
      console.log('3', this.foo)

      this.$nextTick(() => {
          console.log('p1.innerHTML:' + p1.innerHTML)
      })

    },
  });
</script>

思考中🤔🤔🤔

几乎不用考虑,答案就可以脱口而出 那么我想问一下视图会更新几次呢?貌似只更新了一次,什么原因呢?😂下面我们带着问题来探讨一下。

数据劫持

我们都知道Object.defineProperty会对data中的数据劫持,当修改数据的时候会触发set方法,同时set会执行dep.notify(), dep.notify()会遍历subs(也就是我们常说的收集依赖的数组)的update方法,也就是触发watcher的update。(以上的一系列操作可以到源码中进行调试就很清晰了, 后续会对熟数据劫持做更加清晰的描述)。

上面描述了那么多,其实就是当数据发生变化了触发set方法让相关的依赖(Watcher)去执行update方法。

那update都干了什么呢?

// 路径: src\core\observer\watcher.js
update () {
  /* istanbul ignore else */
  // 计算属性
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    // watcher入队
    queueWatcher(this)
  }
}

Watcher进入队列发生的事情

lazy在计算属性中会用到,sync表示同步,我们这里不做讲解。 主要的一段代码在queueWatcher,让当前的watcher进入一个队列当中。

// 路径: src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
  // 获取watcher的id
  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
      // 如果没有在等待状态
      // 使用nextTick将flushSchedulerQueue入队
      // 尝试异步的方式将该函数放入微任务队列
      nextTick(flushSchedulerQueue)
    }
  }
}

首先判断Watcher是否已经加入了队列当中,如果入了队列则不做操作。所以刚开始我们的问题中,执行了三次修改了数据,最后只会有一个Watcher被加入队列😉(感觉有那么点意思了)。接着去执行nextTick将flushSchedulerQueue入队,那么flushSchedulerQueue又是干什么的呢?

// 路径:src\core\observer\scheduler.js
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行run函数
    watcher.run()
  }
}

这里我们把代码简化了,你可以理解为flushSchedulerQueue就是去让队列中的watcher执行run方法去更新视图的。当然这里我们只是把flushSchedulerQueue传给了nextTick,让其入队。

nextTick

接下来我们来看看nextTick方法:

// 路径: src\core\util\next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用户传递的回调函数会被放入callbacks里面
  // 前面的刷新函数就是执行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()
  }
}

只是把我们传入的flushSchedulerQueue加入了callbacks数组中,但是我们有可能使用this.$nextTick(cb),所以cb也会被将入callbacks,然后看下当时是否需要等待,如果不需要则执行timerFunc()

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 

异步更新

这里我们只讲解promise,我们可以看到上面的代码就是将flushCallbacks异步执行,flushCallbacks就是执行我们提到的callbacks数组中的所有的回调函数。按照我们刚开始写得代码,此时的callbacks中应该有两个回调,一个是执行watcher.run()的flushSchedulerQueue,还有一个是this.$nextTick中传入的回调。回忆刚刚的问题,为什么视图只更新了一次?当执行watcher.run()的时候,this.foo已经3了,所以只会更新一次。

不知道是否理解了呢?欢迎留言哦😂 最后来个流程图,希望能帮助大家理解。