还不会响应式原理?Object变化侦测从入门到吊打面试官,卷起来!

458 阅读8分钟

参考资料:

  • 《深入浅出vue.js》
  • 一位不愿意透露姓名的阿里大佬的语雀笔记,暂且叫他药药
  • 一位不愿意透露姓名的面试官大佬(经常巴拉他问问题),暂且叫他坤坤
  • 网上的vue.js源码解读视频教程

image.png

什么是变化侦测

变化侦测分为两种

    • Angular和React中的变化侦测都属于“拉”
    • 只知道状态有可能变了,框架内部收到信号后,会进行一个暴力比对来找出哪些DOM节点需要重新渲染
    • 在Angular中是脏检查的流程,在React中使用的是虚拟DOM
    • Vue.js的变化侦测属于“推”
    • 变化时立刻就知道了,而且在一定程度上知道哪些状态变了
    • 可以进行更细粒度的更新
  • 细粒度的影响
    • 粒度越细,每个状态所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。
    • vue2.0引入虚拟DOM,将粒度调整为中等粒度,状态所绑定的依赖从DOM节点改为一个组件

如何追踪变化

vue2用Object.defineProperty 来侦测变化,因为不能很好的侦测对象类型变化,vue3用Proxy重写了这部分代码

function defineReactive (data, key, val) {
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        return val
      },
      set: function (newVal) {
        if(val === newVal){
          return
        }
        val = newVal
      }
    })
  }

defineReactive 用来对Object.defineProperty 进行重写封装,每当从data 的key 中读取数据时,get 函数被触发;每当往data 的key 中设置数据时,set 函数被触发。

get是一个给属性提供的getter方法,当访问该属性的时候会触发getter方法;set是一个给属性提供的setter方法,当对属性做修改的时候会触发setter方法;

  • 响应式对象核心是利用了Object.defineProperty给对象的属性加getter和setter
  • Vue会把props、data等变成响应式对象,在创建过程中,发现子属性也为对象则递归把该对象变成响应式

如何收集依赖

  • vue1.0假如有一个状态绑定着好多个依赖,每个依赖表示一个具体的DOM节点,那么当这个状态发生变化时,向这个状态的所有依赖发送通知,让它们进行DOM更新操作。
  • vue2.0,模板使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到所依赖组件,然后组件内部再通过虚拟DOM重新渲染。 不管是vue1.0还是2.0都要收集依赖,如何收集呢
<template>
    <h1>{{ name }}</h1>
</template>

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

即:在getter中收集依赖,在setter中触发依赖 。

依赖收集在哪里

  • 先封装一个Dep类,用于管理依赖,可以收集依赖、删除依赖或向依赖发送通知
  • 再在defineReactive中操作依赖 新增Dep类:
01.  export default class Dep {
02.    constructor () {
03.      this.subs = [] // 数据的watcher
04.    }
05.  
06.    addSub (sub) {
07.      this.subs.push(sub)
08.    }
09.  
10.    removeSub (sub) {
11.      remove(this.subs, sub)
12.    }
13.  
14.    depend () {
15.      if (window.target) {
16.        this.addSub(window.target)
17.      }
18.    }
19.  
20.    notify () {
21.      const subs = this.subs.slice()
22.      for (let i = 0, l = subs.length; i < l; i++) {
23.        subs[i].update()
24.      }
25.    }
26.  }
27.  
28.  function remove (arr, item) {
29.    if (arr.length) {
30.      const index = arr.indexOf(item)
31.      if (index > -1) {
32.        return arr.splice(index, 1)
33.      }
34.    }
35.  }

defineReactive 方法改造:

01.  function defineReactive (data, key, val) {
02.    let dep = new Dep() // 新增 创建依赖实例dep
03.    Object.defineProperty(data, key, {
04.      enumerable: true,
05.      configurable: true,
06.      get: function () {
07.        dep.depend() // 新增 收集并添加依赖
08.        return val
09.      },
10.      set: function (newVal) {
11.        if(val === newVal){
12.          return
13.        }
14.        val = newVal
15.        dep.notify() // 新增 更新依赖
16.      }
17.    })
18.  }

依赖是谁

我们收集的依赖是window.target,到底是什么window.target?也就是当属性发生变化后,通知谁?

我们要通知用到数据的地方,而使用这个数据的地方很多,而且类型不一样,有可能是模板或是用户写的watcher等,因此需要抽象出一个能集中处理“通知数据用到地方”的一个类,我们称之为Watcher

在vue1.0时,一个Dep对应着多个Watcher(因为用到某数据的地方很多),数据过大时,会出现性能问题,因此在vue2.0的时候改为在每个组件绑定一个Watcher,当该组件数据发生变化时,会通知watch触发update方法,生成虚拟DOM,和旧的DOM进行比对,只更新需要改变的部分,极大的减少了Watcher的数量,优化了性能

总的来说:

  • 属性发生变化后通知Watcher,因此收集的是Watcher

什么是Watcher

Watcher 是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方

Dep是建立数据和Watcher的桥梁

01.  export default class Watcher {
02.    constructor (vm, expOrFn, cb) {
03.      this.vm = vm
04.      // 执行this.getter(),就可以读取data.a.b.c的内容
05.      this.getter = parsePath(expOrFn)
06.      this.cb = cb
07.      this.value = this.get()
08.    }
09.  
10.    get() {
11.      window.target = this
12.      let value = this.getter.call(this.vm, this.vm)
13.      window.target = undefined
14.      return value
15.    }
16.  
17.    update () {
18.      const oldValue = this.value
19.      this.value = this.get()
20.      this.cb.call(this.vm, this.value, oldValue)
21.    }
22.  }

这段代码可以把自己主动添加到data.a.b.c 的Dep 中去, Watcher 的原理是先把自己设置到全局唯一的指定位置(例如window.target ,在源码中实际是存放在Dep的静态变量target中,作者可能想简写,因此直接放在window只要其他类可以引用即可),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher ,并把这个Watcher 收集到Dep 中去。通过这样的方式,Watcher 可以主动去订阅任意一个数据的变化。

image.png

render function 进行渲染data的时候,会触发getter,就会触发依赖收集的逻辑,上面介绍了,依赖收集就是从window.target 中读取一个依赖并添加到Dep 中。

只要先在window.target 赋一个this ,然后再读一下值,去触发getter,就可以把this 主动添加到keypath 的Dep 中

递归侦测所有key

以上已经实现了变化检测的功能,但是无法检测数据中的所有属性(包括子属性)都侦测到,所以要封装一个Observer 类,即观察者。

01.  /**
02.   * Observer类会附加到每一个被侦测的object上。
03.   * 一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式
04.   * 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
05.   */
06.  export class Observer {
07.    constructor (value) {
08.      this.value = value
09.  
10.      if (!Array.isArray(value)) { //只有Object类型才调用walk()
11.        this.walk(value)
12.      }
13.    }
14.  
15.    /**
16.     * walk会将每一个属性都转换成getter/setter的形式来侦测变化
17.     * 这个方法只有在数据类型为Object时被调用
18.     */
19.    walk (obj) {
20.      const keys = Object.keys(obj)
21.      for (let i = 0; i < keys.length; i++) {
22.        defineReactive(obj, keys[i], obj[keys[i]])
23.      }
24.    }
25.  }
26.  
27.  function defineReactive (data, key, val) {
28.    // 新增,递归子属性
29.    if (typeof val === 'object') {
30.      new Observer(val)
31.    }
32.    let dep = new Dep()
33.    Object.defineProperty(data, key, {
34.      enumerable: true,
35.      configurable: true,
36.      get: function () {
37.        dep.depend()
38.        return val
39.      },
40.      set: function (newVal) {
41.        if(val === newVal){
42.          return
43.        }
44.  
45.        val = newVal
46.        dep.notify()
47.      }
48.    })
49.  }

在defineReactive中添加使用Observer递归子属性后,传入的val也就变成了响应式的Object

关于Object的问题

由于是在getter中对Object的子属性进行依赖收集,因此,假设我们在obj上新增name属性,或删除某原有属性,将无法侦测到,所以不会向依赖发送通知,但修改obj原有属性的值可以侦测到

  • Vue.js通过Object.defineProperty 来将对象的key 转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会导致上上述问题。

为了解决这个问题,Vue.js提供了两个API——vm.$set 与vm.$delete 

vue3为了解决defineProperty的缺陷,使用了Proxy替代依赖收集的逻辑,但因此也导致了尽管是数值类型的数据也实际被封装成了Object,如age = ref(18), 实际上是age = { value : 18 }, 因为Proxy只试用于对象。

总结

  • Object 可以通过Object.defineProperty 将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter。
  • 在getter中收集依赖,在setter中触发依赖
  • Dep ,用来收集依赖、删除依赖和向依赖发送消息等
  • 所谓的依赖,其实就是Watcher 。只有Watcher 触发的getter才会收集依赖,哪个Watcher 触发了getter,就把哪个Watcher 收集到Dep 中。当数据发生变化时,会循环依赖列表,把所有的Watcher 都通知一遍。
  • Observer ,它的作用是把一个object 中的所有数据(包括子数据)都转换成响应式的,也就是它会侦测object 中所有数据(包括子数据)的变化。 配上个图图,方便食用

image.png Data 通过Observer 转换成了getter/setter的形式来追踪变化。

当外界通过Watcher 读取数据时,会触发getter从而将Watcher 添加到依赖中。

当数据发生了变化时,会触发setter,从而向Dep 中的依赖(Watcher )发送通知。

Watcher 接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

超5个赞,写下一篇——Array的变化侦测 image.png