likes
comments
collection
share

Vue 的响应式实现

作者站长头像
站长
· 阅读数 15

前言

数据双向绑定的原理即 通过监听数据的变化,将变化反映到绑定的视图上,同时也监听视图的变化,将变化反映到数据上。常用的实现数据双向绑定的框架有 Vue 和 React。 那今天的主题自然就是 vue 中的响应式啦 ! 在实现的过程中主要用到了三个构造器:Observe、Watcher、Dep 由他们构成了响应式的核心机制。

响应式原理

在实现响应式之前先来看看 Vue 中的双向绑定流程

  1. 在实例化 Vue 对象时,会对 data 属性进行响应化处理,将 data 属性中的每个属性都转换为 getter 和 setter 函数,同时为每个属性创建一个 Dep 对象。这个过程发生在 Observe 中。
  2. 组件 created 之后对模板执行编译,会对模板中的动态绑定数据进行解析,找到其中动态绑定的数据,并从 data 中获取并初始化视图,这个过程发生在 Complie 的过程中。
  3. 在组件挂载的过程中会定义⼀个更新函数并实例化一个Render Watcher对象,对象会监听所有在模板中使用的 data 属性,在数据发生变化时会触发更新函数。
  4. 当data属性发生变化时,对应的setter函数会被触发,setter函数会通知该属性对应的所有Dep对象进行更新操作。
  5. 由于 data 的某个属性在⼀个视图中可能出现多次,所以每个属性都需要⼀个 Dep 管家来依赖于该属性的所有 Watcher 对象。
  6. 当 data 中的数据⼀旦发生变化,就会立即先找到对应的 Dep,Dep对象会通知所有依赖于该属性的Watcher对象进行更新操作,更新视图。

Vue 的响应式实现 在了解响应式的实现原理之后来看一下源码中的具体实现 😜

Vue 初始化

先从最简单的一段 hello world 开始分析:

<template>
  <div>
    {{ message }}
  </div>
</template>
<script>
new Vue({
  data() {
    return {
      message: "hello world",
    };
  },
});
</script>

毫无意外的是这段代码,最终会在页面上打印一个 hello world,那么它究竟是如何实现的呢? 我们从源头:new Vue 的地方开始分析。 在new Vue()时,会执行Vue构造函数,初始化Vue实例,并将用户传入的配置项合并到实例对象上, 在初始化的过程中 initState 会对数据进行响应化处理。

1. Vue.prototype._init(option)
2. initState(vm)
3. observe(vm._data)
4. new Observer(data)

Observe

Observe 用于监听数据对象的变化,当数据对象发生变化时,Observe会通知所有依赖于该数据对象的Watcher对象。


// 5. 调用 walk 方法,遍历 data 中的每一个属性,监听数据的变化。
function walk(obj) {
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
}

// 6. 执行 defineProperty 监听数据读取和设置。
function defineReactive(obj, key, val) {
  // 为每个属性创建 Dep(依赖搜集的容器)
  const dep = new Dep();
  // 绑定 get、set
  Object.defineProperty(obj, key, {
    get() {
      const value = val;
      // 如果当前存在 watcher 实例,则开始收集依赖
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      val = newVal;
      // 修改数据时,通知页面重新渲染
      dep.notify();
    },
  });
}

Observe 会遍历 data 对象中的所有属性,将每个属性设置 getter 和 setter 函数。

  • getter 会将当前 Watcher 添加到该属性的 Dep 订阅者列表中。
  • 由 setter 通知该属性对应的 Dep,通知所有依赖于该属性的 Watcher 对象进行更新。

注意:Observe 只能监听已经存在于 data 对象中的属性,对于新增的属性,需要使用 Vue.set() 或者vm.$set() 方法进行添加,并手动触发更新。

Dep

Dep 对象用于依赖收集,管理 Watcher 和 Observe 之间的依赖关系,它实现了发布了订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅。

class Dep {
  // 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型,
  // Dep.target 是全局对象,为当前的 watcher 。
  static target: ?Watcher;
  // subs 存放搜集到的 Watcher 对象集合
  subs: Array<Watcher>;
  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    // 搜集所有使用到这个 data 的 Watcher 对象。
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 收集依赖,最终会调用 watcher 实例上的 addDep方法,将自身添加到 dep 数组中
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用对应的 Watcher,更新视图
      subs[i].update();
    }
  }
}

Dep对象有两个主要的作用:

  1. 收集依赖:在 getter 中将当前 Watcher 添加到 Dep 的订阅者列表中
  2. 派发更新:在 setter 中通知 Dep,通知所有依赖于该属性的 Watcher 进行更新。

一个属性可能会有多个 Watcher 依赖于它,因此一个 Dep 可能会被多个 Watcher 订阅它。为了避免重复通知,Dep维护了一个订阅者列表,该列表中存储了所有依赖于该属性的 Watcher 。当属性发生变化时,Dep对象会遍历订阅者列表,依次通知每个 Watcher 进行更新。

watcher

Watcher 的作用主要是监听视图的变化,当视图发生变化时会通知所有依赖于该视图的数据对象,并更新相应的视图。

class Watcher {
  constructor(vm: Component, expOrFn: string | Function) {
    // 将 vm._render 方法赋值给 getter。
    // 这里的 expOrFn 其实就是 vm._render,后文会讲到。
    this.getter = expOrFn;
    this.value = this.get();
  }
  get() {
    // 给 Dep.target 赋值为当前 Watcher 对象
    Dep.target = this;
    // this.getter 其实就是 vm._render
    // vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
    const value = this.getter.call(this.vm, this.vm);
    return value;
  }
  addDep(dep: Dep) {
    // 将当前的 Watcher 添加到 Dep 收集池中
    dep.addSub(this);
  }
  update() {
    // 开启异步队列,批量更新 Watcher
    queueWatcher(this);
  }
  run() {
    // 和初始化一样,会调用 get 方法,更新视图
    const value = this.get();
  }
}

源码中我们能看到 Watcher 实现了渲染方法 _render 和 Dep 的关联, 在组件挂载的过程中 render watcher 会被创建,实例创建时立即调用 get 方法收集依赖进行页面的渲染。

watcher类型

Vue 中有三种类型的 Watcher 对象:Render Watcher、Computed Watcher 和 User Watcher:

  • Render Watcher: 是 Vue 的核心组件,负责将模板编译成渲染函数,并在数据发生变化时重新渲染视图。
    • this.get() 的本质是运行了 vm._update 方法,在调用的过程中进行首次依赖收集。
  • users watcher (initWatch):是用户自定义的 Watcher 对象,用于监听用户自定义的数据变化。
    • this.get() 监听数据变更时执行的操作。
  • Computed Watcher:用于计算属性的依赖收集和更新,在计算属性的 getter 函数中,自动创建一个Computed Watcher ,并将该对象作为当前的 Watcher 对象。当计算属性依赖的数据发生变化时,触发 Computed Watcher 的更新函数,重新计算并缓存计算结果。
    • this.get() 就是执行计算属性的方法,当计算属性所依赖的数据发生变化时,触发计算属性的 getter 函数会判断计算属性是否已经被缓存。

注意: 这里的 this.get() 指的是 Watcher 对象中的 get 方法

模板渲染

到这里我们分析了 Vue 初始化过程中的数据是如何进行绑定的: Vue 通过 defineProperty 完成了 Data 中所有数据的代理, 当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中, 当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。

那么接下来就进入到了数据渲染的环节: new Vue 执行到最后,会调用 mount 方法,将编译好的 render 函数渲染成 DOM 挂载到页面上 。

// new Vue 执行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
// 5. mountComponent
// 6. 创建 Watcher 实例
const updateComponent = () => {
  vm._update(vm._render());
};
/**
*    结合上文 Watcher 实例化时的传参分析
*		 在页面进行第一次渲染时会实例化一个 Watcher
*    在实例化过程中 updateComponent 就是传入 Watcher 内部的 getter 方法。
**/
// 
new Watcher(vm, updateComponent);

// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._render())

// 10. 调用 vm._render 生成虚拟 dom
Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const { render } = vm.$options;
  let vnode = render.call(vm._renderProxy, vm.$createElement);
  return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode: VNode) {
  const vm: Component = this;
  if (!prevVnode) {
    // 初次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom,这里就不展开了。

模板渲染的过程中能得出 watcher 创建的时机 结合之前生命周期原理的介绍,我们能发现在 beforeMount 阶段会实例化一个 Render Watcher , 在创建 Watcher 的过程中会执行 render 方法,最终将 Vue 代码渲染成真实的 DOM。

数据变化时vue的更新

当数据变化时,Vue 又是怎么进行更新的?

  1. 当 data 属性发生变化时,会调用Dep.notify方法,通知所有依赖该数据的 Watche r对象进行更新。
  2. 当 Watcher 对象收到更新通知后,会调用 watcher 中的 update 方法。在更新过程中,在更新过程中,Vue会将所有需要更新的Watcher对象加入一个队列,并在下一个事件循环中执行flushSchedulerQueue 方法,该方法会按照一定的顺序依次执行队列中的 Watcher 对象的更新操作。
  3. Watcher对象会调用渲染函数,将最新的数据渲染成虚拟 DOM 元素。在 render 过程中执行 patch 算法将最新的虚拟 DOM 元素与之前的虚拟 DOM 元素进行比较,找出差异。
  4. Vue 根据差异对视图进行更新。在更新过程中,尽量复用已有的 DO元素,减少 DOM 操作的次数,提高性能。

在 Data 变化时,会调用 Dep.notify 方法,随即调用 Watcher 内部的 update 方法,此方法会将所有使用到这个 Data 的 Watcher 加入一个队列,并开启一个异步队列进行更新,最终执行 _render 方法完成页面更新。 整体的流程如下: Vue 的响应式实现

组件渲染

当组件在转化为 VNode 过程中遇到了子组件时,Vue 会调用 createComponent 函数来创建子组件实例并将其挂载到指定的上下文中。

// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement
// 6. createComponent
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
  const baseCtor = context.$options._base;
  // Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
  if (isObject(Ctor)) {
    // 将组件中 export 出的对象,继承自 Vue,得到一个构造函数
    // 相当于 Vue.extend(YourComponent)
    Ctor = baseCtor.extend(Ctor);
  }
  const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
  return vnode;
}

// 7. 实现组件继承 Vue,并调用 Vue._init 方法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
  const Super = this;
  const Sub = function VueComponent(options) {
    // 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
    this._init(options);
  };
  // 原型继承,相当于:Component extends Vue
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  return Sub;
};

createComponent 函数在 Vue 中扮演着非常重要的角色,它负责创建和管理所有的组件实例。 结合上文,流程图中的蓝色部分就是渲染组件的过程。

Vue 的响应式实现

总结

Vue的响应式原理是其核心功能之一,它使得Vue能够在数据发生变化时自动更新视图: 1.Vue 通过 Object.defineProperty() 实现响应式。该方法可以对对象的属性进行劫持,当属性被访问或者修改时,可以触发相应的操作。 2.Vue的响应式系统基于依赖追踪。当一个组件渲染时,它会对所有用到的属性进行依赖收集,建立一个依赖关系图。当属性发生变化时,会触发依赖关系图中相应的更新操作。

Vue 的响应式实现

参考

  1. 图解 Vue 响应式原理