Vue之关于异步更新细节

来自:网络
时间:2024-06-10
阅读:

前言

Vue官网对于异步更新的介绍如下:

  • Vue 在更新 DOM 时是异步执行的。
  • 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
  • 如果同一个 watcher 被多次触发,只会被推入到队列中一次。
  • 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的

Vue使用Object.defineProperty对数据劫持后,当对对象进行set操作,就会触发视图更新。

更新逻辑

以下面实例来分析视图更新处理逻辑:

<div>{{ message }}</div>
<button @click="handleClick">更新</button>

new Vue({
	data: {
		message: ''
	},
	methods: {
		handleClick() {
			this.message = Date.now();
		}
	}
})

当点击更新按钮后会对已劫持的属性message做赋值操作,此时会触发Object.defineProperty的set操作。

Object.defineProperty set操作

Object.defineProperty的set函数的设置,实际上最核心的逻辑就是触发视图更新,具体代码逻辑如下:

set: function reactiveSetter (newVal) {
	// 其他逻辑
	
    // 触发视图更新
    dep.notify();
}

每个属性都会对应一个Dep对象,当对属性进行赋值时就会调用Dep的notify实例方法,该实例方法的功能就是是通知视图需要更新。

Dep notify实例方法

notify实例方法的代码逻辑如下:

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

subs中存储是watcher对象,每个Vue实例都存在一个与视图更新关联的watcher对象,该对象的创建是在$mount阶段,具体看查看之前的文章Vue实例创建整体流程

代表属性的Dep对象与watcher对象的关联是在render函数调用阶段具体属性获取时建立的即依赖收集

notify方法会执行与当前属性关联的所有watcher对象的update方法,必然会存在一个视图更新相关的watcher。

watcher对象的按照分类实际上分为两类:

  • 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
  • 逻辑计算相关的,计算属性和watch监听所创建的watcher对象

Watcher update实例方法

update实例方法的代码逻辑具体如下:

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

lazy、sync都是Watcher的属性,分别表示:

  • lazy:表示懒处理,即延迟相关处理,用于处理计算属性
  • computedsync:表示同步执行,即触发属性更新就立即更新视图

从上面逻辑中可知,默认是queueWatcher处理即开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,即视图是异步更新的。

这里需要注意的一点是:

queueWatcher中必然存在视图更新的watcher对象,不会存在计算属性computed对应的watcher(computed对应的watcher对象lazy属性默认为true),可能存在watch API对应的用户性质的watcher对象

queueWatcher执行逻辑

function queueWatcher (watcher) {
  var 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.
      var 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);
    }
  }
}

实际上面逻辑主要分成3点:

  • 对于同一个watcher对象,使用has对象结构+id为key来判断队列中是否已存在对应watcher对象,如果存在就不会将其添加到queue中
  • 通过flushing标识区分当在清空队列过程中和正常情况下,如何向queue中添加watcher
  • 通过waiting标识区分是否要执行nextTick即清空queue的动作

因为queue是全局变量,在此步骤之前就将watcher对象添加到queue,如果waiting为true就标识已经调用nextTick实现异步处理queue了,就不要再次调用nextTick

从上面整体逻辑可知,queueWacther的逻辑主要就两点:

  • 判断是否重复watcher,对于不重复的watcher将其添加到queue中
  • 调用nextTick开启异步处理queue操作即flushSchedulerQueue函数执行

nextTick + flushSchedulerQueue

nextTick函数实际上跟$nextTick是相同的逻辑,主要的区别就是上下文的不同,即函数的this绑定值的不同。

使用macroTask API还是microTask API来执行flushSchedulerQueue

而flushSchedulerQueue函数就是queue的具体处理逻辑,主要逻辑如下:

function flushSchedulerQueue () {
  flushing = true;
  var watcher, id;

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort(function (a, b) { return a.id - b.id; });

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    id = watcher.id;
    has[id] = null;
    watcher.run();
  }

  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);
}

flushSchedulerQueue函数的主要逻辑可以总结成如下几点:

  • 对队列queue中watcher对象进行排序
  • 遍历queue执行每个watcher对象的run方法
  • 重置控制queue的相关状态,用于下一轮更新
  • 执行组件的updated和activated生命周期

这里就不展开了,需要注意的是activated是针对于keep-alive下组件的特殊处理,updated生命周期是先子组件再父组件的,队列queue的watcher对象是按照父组件子组件顺序排列的,所以在源码中updated生命周期的触发是倒序遍历queue触发的。

首先说说watcher对象的run实例方法,该方法的主要逻辑就是执行watcher对象的getter属性和cb属性对应的函数。

上面说过watcher对象的按照分类实际上分为两类:

  • 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
  • 逻辑计算相关的,计算属性和watch监听所创建的watcher对象

watcher对象的getter属性和cb属性就是对应着上面各类watcher的实际处理逻辑,例如watch API对应的getter属性就是监听项,cb属性才是具体的处理逻辑。

为什么需要对queue中watcher对象进行排序?

实际上Vue源码中有相关说明,这主要涉及到嵌套组件Vue实例创建、render watch和用户watch创建的时机。

每个组件都是一个Vue实例,嵌套组件创建总是从父组件Vue实例开始创建的,在父组件patch阶段才创建子组件的Vue实例。

而这个顺序决定了watcher对象的id值大小问题:

父组件的所有watcher对象id < 子组件的所有watcher对象id

render watch实际上就是与视图更新相关的watcher对象,该对象是其对应的Vue实例创建的末期即挂载阶段才创建的,是晚于用户watch即计算属性computed和watch API创建的watcher对象,所以:

render watch的id < 所有用户watch的id的

子组件可能是更新触发源,如果父组件也需要更新视图,这样queue队列中子组件的watcher对象位置会在父组件的watcher对象之前,对queue中watcher对象进行排序就保证了:

视图更新时 父组件 总是先于 子组件开始更新操作,而每个组件对应的视图渲染的watcher最后再执行(即用户watcher对象对应的逻辑先执行)

总结

Vue异步更新的过程还是非常清晰的:

  • 对属性赋值触发Dep对象notify方法执行
  • 继而执行Watcher对象的update方法将对象保存到队列queue中
  • 继而调用mircoTask API或macroTask API执行queue中任务
  • 对队列中watcher进行排序,保证顺序执行的正确性,调用其对应run方法来实现视图更新和相关逻辑更新操作

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

返回顶部
顶部