当前位置: 代码网 > it编程>前端脚本>Vue.js > Vue之关于异步更新细节

Vue之关于异步更新细节

2024年06月10日 Vue.js 我要评论
前言vue官网对于异步更新的介绍如下:vue 在更新 dom 时是异步执行的。只要侦听到数据变化,vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触

前言

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方法来实现视图更新和相关逻辑更新操作

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com