第12期 - 学校小猫

封面图来源于大学路校园里一只可爱小猫,肥肥的像家里养的那只。

技术分享-vue 原理(响应式专项)

响应式原理

使用 Object.defineProperty 数据劫持(vue2.0) 或 ES6 的 Proxy 数据代理(vue3.0)

在 Vue2 中,基于 Object.defineProperty 实现数据劫持

Observer

定义一个 Observer 观察类(监听器),判断数据是 Array 类型,或是 Object 类型数据(data中定义的变量默认就具备响应式能力)

// 源码位置:src/core/observer/index.js 下面是简化后的代码

// Observer类 会通过递归的方式把一个对象的所有属性都转化成可观测对象
export class Observer {
  constructor (value) {
    // 响应式的数据
    this.value = value
    // 新增一个__ob__属性,标记此 value 已经变为响应式了,避免重复操作,值为该 value 的 Observers 实例
    def(value,'__ob__',this) 
    if (Array.isArray(value)) {
      // value 为 Array 型的侦测逻辑
      // 当前环境下的Object是否支持__proto__(隐式原型)属性
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // value 为 Object 型的侦测逻辑
      this.walk(value)
    }
  }

对象

对于 Object 类型的数据,Vue 在 defineReactive 方法中通过 Object.defineProperty 为其添加 getter/setter 追踪数据的变化,监测数据何时发生了变化。通过 Observer 递归添加响应式进行 Object 的深度侦测,遍历对象的所有属性执行 defineReactive 通过 Object.defineProperty 为其添加 getter/setter

  // 观测 Object:遍历对象的所有属性为其添加get/set
  // 定义在 Observer
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // for循环的特点: 不可遍历自定义或原型链上的自定义属性
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
/**
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj,key,val) {
  // 如果是不可配置属性,则搞不了响应式,直接结束程序
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }
  // 参数处理,只传了obj和key,那么val = obj[key]
  if (arguments.length === 2) {
    val = obj[key];
  }
  // obj对象里嵌套对象,递归添加响应式,Object的深度侦测 
  if (typeof val === "object") {
    new Observer(val);
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      console.log(`${key}属性被读取了`);
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      console.log(`${key}属性被修改了`);
      val = newVal;
    }
  })
}

为对象的属性定义响应式,使一个对象转化成可观测对象,通过 Object.defineProperty 方法实现了对 Object 数据的可观测,这个方法仅仅只能观测到 Object 数据的取值 getter及设置值setter(修改、更新已有属性),当我们向 Object 数据里添加一对新的 key/value 或删除一对已有的 key/value 时,它是无法观测到的,导致当我们对 Object 数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。Vue 增加了两个全局API: Vue.set(vm.$set) 和 Vue.delete(vm.$delete)

数组

通过重写数组原型实现响应式。因 Object.defineProperty 为监测到数组下标的变化,数组的索引相当于对象的 key,只能监测到通过下标获取某个元素和修改某个元素的值,而无法监测到数组长度的变化

在 Vue 中,通过拦截改变数组自身的 7 个方法(会改变数组长度的),实现监测数组的变化。(注:虽然数组的处理方式不一样,但最终都会使用 Object.defineProperty 完成整个响应式过程) (push、pop、shift、unshift、splice、sort、reverse)

// 观测 Array:遍历对象的所有属性为其添加get/set
// 定义在 Observer
observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
 }

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

/**
 * 如果支持__proto__访问prototype原型对象,则拦截
 * 将源对象的 __proto__ 指向目标对象 value.__proto__ = arrayMethods
 * @param target
 * @param src
 */
function protoAugment(target: Array<any>, src: Object): void {
  target.__proto__ = src;
}

/**
 * 如果不支持__proto__访问prototype原型对象,则把拦截器中重写的7个方法循环加入到value上
 * @param target
 * @param src
 * @param keys
 */
function copyAugment(target: Object, src: Object, keys: Array<string>): void {
  for (const key of keys) {
    def(target, key, src[key]);
  }
}

声明响应式 property

当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。getter/setter在内部让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更

由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。对于已经创建的实例,Vue 不允许直接动态添加根级别的响应式 property。

响应式修改对象/数组 使用 Vue.set(object, propertyName, value)

var vm = new Vue({
  data:{
    a:1
  }
})
// `vm.a` 是响应式的
vm.b = 2  // `vm.b` 是非响应式的

Vue.set(vm.someObject, 'b', 2) // 响应式
this.$set(this.someObject,'b',2) // 响应式 this.$set 等效 Vue.set

为已有对象赋值多个新 property:用原对象与要混合进去的对象的 property 一起创建一个新的对象

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` // 新属性非响应式
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) // 整体响应式

修改数组:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

// Vue.set 修改数组值
Vue.set(vm.items, indexOfItem, newValue)
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice 修改数组
vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

Dep(订阅者)

Vue 定义了一个订阅器 Dep,用来收集订阅者,主要作用是用来存放 Watcher 观察者对象。为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。它用来收集依赖、删除依赖和向依赖发送消息等

Dep 发布者(被观察者),可以订阅多个观察者,依赖收集之后 Deps 中会存在一个或多个 Watcher 对象,在数据变更的时候通知所有的 Watcher。

export default class Dep {
  static target: ?Watcher; // 取 Watcher 存放的依赖者
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = [] // 存放依赖(Watcher订阅) 添加 删除 通知
  }
  // 添加依赖
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 删除依赖
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // 添加一个依赖 调用 Watcher 中的 addDep ,addDep中调用上面的 addSub
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  //通知依赖更新
  notify () {
    // stabilize the subscriber list first 不改变原数组,浅拷贝
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历Watcher订阅者(subs依赖者实例数组)
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // Watcher 实例上的依赖 watcher.update()
    }
  }
}

观察者 Watcher

Watcher 收集依赖的四种场景

  • 观察模版中的数据
  • 观察创建 Vue 实例时 watch 选项中的数据
  • 观察创建 Vue 实例时 computed 选项里的数据所以来的数据
  • 调用 $watch API 观察的数据或表达式

在 Vue.js 中,每个组件都有自己的 Watcher 实例,用于监听数据变化。数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的 Watcher 实例,由 Watcher 实例去通知真正的视图。

Watcher 实例的存在为了使得 当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个 watch, 这时需要抽象出一个能集中处理这些情况的类。在实例化 Vue 构造函数时默认会将 Watcher 对象存在一个队列中,在下个 Tick 时更新异步更新视图

收集依赖:把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍

派发更新:当数据发生改变后,通知所有订阅了这个数据变化的 watcher 执行 update ,派发更新的过程中会把所有执行 update 的 watcher 推入到队列中,在 nextTick 后执行 flush 派发更新的核心流程是给对象赋值,触发 set 中派发更新函数。将所有 Watcher 都放入 nextTick 中进行更新,nextTick 回调中执行用户 watch 的回调函数并且渲染组件。

updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时重新执行 updateComponent 函数,这就完成了重新渲染

// 在组件挂载时,创建 Watcher 实例
mountComponent(vm: Component, el: ?Element, ...) {
    // 将组件实例的 $el 属性设置为挂载的 DOM 元素 el
    vm.$el = el

    // 定义更新组件的函数
    updateComponent = () => {
      // 调用组件的 _update 方法执行渲染
      vm._update(vm._render(), ...)
    }

    // 创建 Watcher 实例,传入组件实例 vm 和更新函数 updateComponent
    new Watcher(vm, updateComponent, ...)
    ...
}

// Watcher 类的简化实现
export default class Watcher {
  constructor (vm, expOrFn, cb) {
    // 保存组件实例、回调函数和表达式/函数
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn) // 解析表达式或函数
    this.value = this.get() // 获取初始值
  }

  // 获取值的方法
  get () {
    window.target = this; // 将 Dep.target 指向自身,从而使得收集到了对应的 Watcher
    const vm = this.vm
    // 调用表达式或函数的 getter 方法获取值
    let value = this.getter.call(vm, vm)
    window.target = undefined; // 重置 Dep.target
    return value
  }

  // 更新值的方法
  update () {
    const oldValue = this.value
    this.value = this.get() // 获取新值
    // 执行回调函数,传入新旧值
    this.cb.call(this.vm, this.value, oldValue)
  }
}

对于 computed 计算属性而言,实际上会在内部创建一个 computed watcher,每个 computed watcher 会持有一个 Dep 实例,当我们访问 computed 属性的时候,会调用 computed watcher 的 evaluate 方法,这时候会触发其持有的 depend 方法用于收集依赖,同时也会收集到正在计算的 watcher,然后把它计算的 watcher 作为 Dep 的 Subscriber 订阅者收集起来,收集起来的作用就是当计算属性所依赖的值发生变化以后,会触发 computed watcher 重新计算,如果重新计算过程中计算结果变了也会调用 dep 的 notify 方法,然后通知订阅 computed 的订阅者触发相关的更新。

对于 watch 而言,会创建一个 user watcher,可以理解为用户的 watcher,也就是用户自定义的一些 watch,它可以观察 data 的变化,也可以观察 computed 的变化。当这些数据发生变化以后,我们创建的这个 watcher 去观察某个数据或计算属性,让他们发生变化就会通知这个 Dep 然后调用这个 Dep 去遍历所有 user watchers,然后调用它们的 update 方法,然后求值发生新旧值变化就会触发 run 执行用户定义的回调函数(user callback)。

响应式系统设计思路

数据劫持-侦测数据的变化 依赖收集-收集视图依赖了哪些数据 发布订阅模式-数据变化时,自动通知更新视图

发布订阅(Publish-Subscribe)是一种设计模式,也称为观察者模式(Observer Pattern)。

监听器 Observer:用来劫持并监听所有属性,如果属性发生变化,就通知订阅者(Watcher); 订阅器 Dep:用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理; 订阅者 Watcher:可以收到属性的变化通知并执行相应的方法,从而更新视图; 解析器 Compile:可以解析每个节点的相关指令,对模板数据和订阅器进行初始化

当数据发生变化时,我们需要去通知视图更新,依赖收集的目的是为了当响应式数据发生变化时,触发它们的 setter 时通知订阅者去做相应的逻辑处理。在 getter 中收集依赖,在 setter 中通知依赖更新。

在 new Vue() 后, Vue 会调用 _init 函数进行初始化,在 init 过程 data 通过 Observer 转换成了 getter/setter 的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。 当render function 执行的时候,因为会读取所需对象的值,所以会触发 getter 函数从而将 Watcher 添加到依赖中进行依赖收集。 在修改对象的值的时候,会触发对应的 setter, setter 通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图。

在 Vue 初始化阶段,会对配置对象中定义的不同属性做相关的处理,对于 data 和 props 而言,Vue 会通过 observe 和 defineReactive 等一系列的操作把 data 和 props 的每个属性变成响应式属性,同时它们内部会持有一个 Dep 实例对象,当我们访问这些数据的时候,就会触发 dep 的 depend 方法来收集依赖,这些依赖是当前正在计算的 Watcher,当前在计算的依赖也就是 Dep.target,作为 Subscriber 订阅者用于订阅这些数据的变化。当修改数据的时候,会触发 dep 的 notify 方法通知这些订阅者执行 update 的逻辑。

在 Vue 的创建过程中,对于每个组件而言,它都会执行组件的 $mount 方法,$mount 执行过程中内部会创建唯一的 render watcher,该 render watcher 会在 render 也就是创建 VNode 过程中会访问到定义的 data、props 或者 computed 等等。render watcher 相当于订阅者,订阅了这些定义的数据的变化,一旦它们发生变化以后,就会触发例如 setter 里的 notify 或者 computed watcher 中的 dep.notify,从而触发 render watcher 的 update,然后执行其 run 方法,执行过程中最终会调用 updateComponent 的方法,该方法会重新进行视图渲染。