created中两次数据修改,会触发几次页面更新

发布时间 2023-11-17 15:57:12作者: 柯基与佩奇

面试题:created 生命周期中两次修改数据,会触发几次页面更新?

一、同步的

先举个简单的同步的例子:

new Vue({
  el: "#app",
  template: `<div>
    <div>{{count}}</div>
  </div>`,
  data() {
    return {
      count: 1,
    };
  },
  created() {
    this.count = 2;
    this.count = 3;
  },
});

在 created 生命周期中,通过 this.count = 2 和 this.count = 3 的方式将 this.count 重新赋值。
这里直接抛出答案:渲染一次。

为什么?
这个与数据的响应式处理有关,先看响应式处理的逻辑:

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重点:创建一个发布者实例
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        // 重点:进行当前正在计算的渲染Watcher的收集
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      // 重点:当数据发生变化时,发布者实例dep会通知收集到的watcher进行更新
      dep.notify();
    },
  });
}

在数据响应式处理阶段,会实例化一个发布者 dep,并且通过 Object.defineProperty 的方式为当前数据定义 get 和 set 函数。在生成虚拟 vNode 的阶段,会触发 get 函数中会进行当前正在计算的渲染 Watcher 的收集,此时,发布者 dep 的 subs 中会多一个渲染 Watcher 实例。在数据发生变化的时候,会触发 set 函数,通知发布者 dep 中 subs 中的 watcher 进行更新。

至于数据修改会触发几次更新,就与当前发布者 dep 的 subs 中收集了几次渲染 watcher 有关了,再看 watcher 收集和 created 执行之间的顺序:

Vue.prototype._init = function (options) {
  // ...
  initState(vm);
  // ...
  callHook(vm, "created");
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

知道在 initState(vm)阶段对数据进行响应式处理,但是此时发布者 dep 的 subs 还是空数组。当执行 callHook(vm, 'created')的时候,会执行 this.count = 2 和 this.count = 3 的逻辑,也的确会触发 set 函数中的 dep.notify 通知收集到的 watcher 进行更新。但是,此时 dep 的 subs 是空数组,相当于啥也没做。

只有在 vm.$mount(vm.$options.el)执行过程中,生成虚拟 vNode 的时候才会进行渲染 Watcher 收集,此时,dep 的 subs 才不为空。最终,通过 vm.$mount(vm.$options.el)进行了页面的一次渲染,并未因为 this.count=2 或者 this.count=3 而触发多余的页面更新。

简言之,就是 created 钩子函数内的逻辑的执行是在渲染 watcher 收集之前执行的,所以未引起因为数据变化而导致的页面更新。

二、异步的

同步的场景说完了,再举个异步的例子:

new Vue({
  el: "#app",
  template: `<div>
    <div>{{count}}</div>
  </div>`,
  data() {
    return {
      count: 1,
    };
  },
  created() {
    setTimeout(() => {
      this.count = 2;
    }, 0);
    setTimeout(() => {
      this.count = 3;
    }, 0);
  },
});

在 created 生命周期中,通过异步的方式执行 this.count = 2 和 this.count = 3 的方式将 this.count 重新赋值。
这里直接抛出答案:首次渲染一次,因为数据变化导致的页面更新两次

为什么?
这个就与 eventLoop 事件循环机制有关了,知道 js 是一个单线程执行的语言,当通过 new Vue 实例化的过程中,会执行初始化方法 this._init 方法,开始了 Vue 底层的处理逻辑。当遇到 setTimeout 异步操作时,会将其推入到异步队列中去,等待当前同步任务执行完以后再去异步队列中取出队首元素进行执行。

当前例子中,在 initState(vm)阶段对数据进行响应式处理。当执行 callHook(vm, 'created')的时候,会将 this.count = 2 和 this.count = 3 的逻辑推入到异步队列等待执行。继续执行 vm.$mount(vm.$options.el)的过程中会去生成虚拟 vNode,进而触发 get 函数的渲染 Watcher 收集,此时,dep 的 subs 中就有了一个渲染 watcher。

等首次页面渲染完成以后,会去执行 this.count=2 的逻辑,数据的修改会触发 set 函数中的 dep.notify,此时发布者 dep 的 subs 不为空,会引起页面的更新。同理,this.count=3 会再次引起页面数据的更新。也就是说,首次渲染一次,因为 this.count=2 和 this.count=3 还会导致页面更新两次。

三、附加

如果改变的值和 data 中定义的值一致呢?

new Vue({
  el: "#app",
  template: `<div>
    <div>{{count}}</div>
  </div>`,
  data() {
    return {
      count: 1,
    };
  },
  created() {
    setTimeout(() => {
      this.count = 1;
    }, 0);
  },
});

这个时候,在触发 set 的逻辑中,会当执行到 if (newVal === value || (newVal !== newVal && value !== value)) { return }的逻辑,不会再执行到 dep.notify,这种场景下数据的数据也不会引起页面的再次更新。

总结

从生命周期 created 和页面渲染的先后顺序,Object.defineProperty 触发 get 和 set 函数的机理,以及 eventLoop 事件循环机制入手,去分析 created 中两次数据修改会触发几次页面更新的问题就会清晰很多。