Skip to content

第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

核心是利用 ES5Object.defineProperty,这也是 Vue.js 为什么不能兼容 IE8 及以下浏览器的原因。

实现一个数据监听器 Observer

  • Observe的功能就是用来监测数据的变化。实现方式是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。Observer 是一个类,它的作用是给对象属性添加 gettersetter,用于 依赖收集派发更新

依赖收集 getter

  • const dep = new Dep() // 实例化一个 Dep 实例
  • 在 get 函数中通过 dep.depend 做依赖收集

Dep 是一个 Class,它定义了一些属性和方法,它有一个静态属性 target,这是一个全局唯一 Watcher【同一时间内只能有一个全局的 Watcher 被计算】。Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的。WatcherDep 就是典型的观察者设计模式。

Watcher 是一个 Class,在它的构造函数中定义了一些和 Dep 相关的属性:

js
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();

收集过程

当我们实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后执行他的 this.get() 方法,进入 get 函数把 Dep.target 赋值为当前渲染 watcher 并压栈(为了恢复用)。接着执行vm._render() 方法,生成渲染 VNode ,并且在这个过程对 vm 上的数据访问,这个时候就触发数据对象的 getter(在此期间执行 Dep.target.addDep(this) 方法,将 watcher 订阅到这个数据持有的 depsubs 中,为后续数据变化时通知到哪些 subs 做准备)。然后递归遍历添加所有子项的 getter

信息

Watcher 在构造函数中初始化两个 Dep 实例数组。newDeps 代表新添加的 Dep 实例数组,deps 代表上一次添加的 Dep 实例数组。 依赖清空:在执行清空依赖(cleanupDeps)函数时,会首先遍历 deps,移除对 dep 的订阅,然后把 newDepsIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。考虑场景,在条件渲染时,及时对不需要渲染数据的订阅移除,减少性能浪费。

考虑到 Vue 是数据驱动的,所以每次数据变化都会重写 Render,那么 vm._render() 方法会再次执行,并再次触发数据

收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理【派发更新】

派发过程

当我们组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法,它是 Dep 的一个实例方法。具体做法是遍历依赖收集中建立的 subs,也就是 Watcher 的实例数组【subs 数组在依赖收集 getter 中被添加,期间通过一些逻辑处理判断保证同一数据不会被添加多次】,然后调用每一个 watcherupdate 方法。

update 函数中有个 queueWatcher(this) 方法引入了队列的概念,是 vue 在做派发更新时优化的一个点,它并不会每次数据改变都会触发 watcher 回调,而是把这些 watcher 先添加到一个队列中,然后在nextTick 后执行 watcherrun 函数

队列排序保证

  1. 组件的更新由父到子。父组件创建早于子组件,watcher 的创建也是
  2. 用户自定义 watcher 要早于渲染 watcher 执行,因为用户自定义 watcher 是在渲染 watcher 前创建的
  3. 如果一个组件在父组件 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

run 函数解析

先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值 value 和旧值 oldValue,这就是当我们自己添加 watcher 时候可以在参数中取到新旧值的来源。对应渲染 watcher 而言,在执行 this.get() 方法求值的时候,会执行 getter 方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行 patch 的过程

简单手写 Vue 的响应式

js
<input id="input" type="text" />
<div id="text"></div>

let input = document.getElementById("input");
let text = document.getElementById("text");
let data = { value: "" };
Object.defineProperty(data, "value", {
  set: function(val) {
    text.innerHTML = val;
    input.value = val;
  },
  get: function() {
    return input.value;
  }
});
input.onkeyup = function(e) {
  data.value = e.target.value;
};