源码角度深度解析reactive、baseHandlers、ref

60 阅读5分钟

前言

相信大家都对数据响应式这个概念有深刻的理解,所谓的数据响应式不过是使数据的变化可以被检测到,从而对这种变化呢做出响应机制

我们知道mvvm框架中的核心就是连接视图层和数据层,通过数据驱动应用,数据变化,视图更新,vue参照了mvvm框架,但又没有完全遵循(严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。)那么要做到数据驱动试图呢,这个时候就需要对数据做响应式,当数据发生变化就可以立即做出更新处理。

vue中,通过数据响应式,加上虚拟DOMpatch算法,使得我们开发人员只用关心业务逻辑,我们只需要操作数据,完全不用接触繁琐的dom操作,从而大大的提升了我们的开发效率并且降低了我们的开发难度。(如果是jquery代码,好多代码都是操作dom,这就是reactvue为什么能够干掉它的原因)

vue2中的数据响应式会根据数据类型来做不同处理,本质上用了Observe这个类(为什么使用这个类包裹,后面会做解答,这里很重要!)包裹了datadata就是我们在vue2中写数据的那个函数)这个选项中返回的对象,然后对这个对象做递归遍历每一个属性调用Object.defineProperty(),传入数据存储描述符,在getter中收集依赖,在setter中触发依赖。(Object.defineProperty这个API做一次调用只能是对象中的一个属性变成响应式数据,所以对对象的每个属性做响应式就必须递归的去遍历这个对象的每一个属性。)而这个过程中会生成很多watcherDep,非常消耗内存,这样会浪费很多性能。

简单说一下vue2数据初始化过程initState(vm) => initData(vm) => observe(data) => new Observer(value)observe函数内部会判断是否是一个对象/数组并进行Observe类创建Observer类的作用就是先把传入的数据变成一个响应式对象,并产生一个自己的dep关联自己(为什么要这么做呢,后续会解答),再进行判断如果传入是数组(进行7个原型方法覆盖达到数组响应式,后面详细讲),如果传入是对象就遍历每个属性调用defineReactivedefineReactive本质就是调用Object.defineProperty,而defineReactive内部又会对每个属性值进行observe函数调用,所以以我们说Object.defineProperty很笨重,只能递归遍历去初始化一个响应式对象。
简单看看Observe类

image.png

由于 Object.defineProperty只会对属性进行监测,无法监听对象的变化(就比如我们给foo添加name属性,foo.name = 'zdy'),所以Vue2中设置了一个Observer类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。如果我们要给对象添加属性就必须用$set这个api;删除对象的某个属性,使用$delete这个api(但这个监测是手动,不是自动的,也就意味我们给对象加/删除属性必须手动调用这两个属性)

function set(target, key, val) {
//这个ob就是在New一个Observe保存上去的
    const ob = target.__ob__
    //再把添加的属性变成响应式
    defineReactive(ob.value, key, val)
    //把Observe上保存的对象响应式依赖执行一下
    ob.dep.notify()
    return val
}
function del(target, key) {
    const ob = target.__ob__
    delete target[key]
    //把Observe上保存的对象响应式依赖执行一下
    ob.dep.notify()
}

我们都说vue2监听数组变化是通过对数组原型上的 7 个方法进行重写进行监听的,而使用Object.defineProperty监听不到数组的变化,其实不然

const arr = [1, 2, 3]
arr.forEach((val, index) => {
  Object.defineProperty(arr, index, {
    get() {
      console.log('监听到了')
      return val
    },
    set(newVal) {
      console.log('变化了:', val, newVal)
      val = newVal
    }
  })
})

image.png
Object.defineProperty只是监听不到数组原型方法的调用而已。所以Object.defineProperty 也能监听数组变化,那么为什么Vue2弃用了这个方案呢? 首先这种直接通过下标获取数组元素的场景就比较少,其次即便通过了Object.defineProperty对数组进行监听,但也监听不了push、pop、shift等对数组进行操作的方法,所以还是需要通过对数组原型上的那 7 个方法进行重写监听。所以为了性能考虑Vue2直接弃用了使用 Object.defineProperty 对数组进行监听的方案。看看是怎么重写的呢

image.png 我们知道在数组进行响应式初始化的时候会在Observer类里面给这个数组对象的添加一个 __ob__ 的属性,这个属性的值就是Observer这个类的实例对象,而这个Observer类里面有存在一个收集依赖的属性 dep,所以在对数组里的内容通过那 7 个方法进行操作的时候,会触发数组的拦截器,那么在拦截器里面就可以访问到这个数组的 Observer 类的实例对象,从而可以向这些数组的依赖发送变更通知。 由于 Vue2 放弃了 Object.defineProperty 对数组进行监听的方案,所以通过下标操作数组是无法实现响应式操作的。比如this.list[0] = xxx

那么vue2是什么时候对数组收集依赖呢,这时候我们就好好想一下,首次ObServe类的创建肯定是传递的data对象 => defineReactive(data),对data的每个属性做defineProperty,而且如果data对象的属性值是一个对象/数组,递归对这些属性值observe(创建Observe类,Observe类为自己生产一个dep),那么在每一个属性值被触发getter时,不仅收集本身的依赖,还会收集这个属性值作为对象/数组的依赖。看看源码具体实现细节

image.png

说了这么多,Vue2实现响应式真是不太满意,又耗内存,初始化速度又慢,对象的属性添加删除还得手动操作,而且数组还不能通过索引改变值(无法监听),对于es6中新产⽣的Map、Set这些数据结构不⽀持等(Object.defineProperty本身监听不到),真是一大堆不满意的地方(我的同学总是说vue2Bug多,可能他没好好看源码,哈哈哈哈哈)!

为了解决这些问题,vue3重新编写了这⼀部分的实现:利⽤ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是⼀致的,不需要使⽤特殊api,初始化性能和内存消耗都得到了⼤幅改善(一次proxy代理,就对整个对象进行了响应式化,无需遍历对象),当然其中也对数组中一些方法做特殊处理,下面就来看看吧。

reactive

偷偷夸一下尤大,vue3代码结构如此清晰,响应式模块是被独立封装的一个包,这样做其实有很多好处,比如说更好的树摇优化,让我们写compositionApi(hooks比如useUtil)可以更好的提供响应式数据,真是太棒了!

image.png 在 Vue3 中我们可以使用 reactive() 创建一个响应式对象或数组,先说一下用法:

import { reactive } from 'vue'

const state = reactive({ count: 0 })

响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。而创建出的代理对象为什么说是响应式的呢,因为返回的代理对象在他的属性被访问时收集依赖,在他的属性被设置时便会触发依赖。

那么我们先看看源码里是怎么写的,我基于自己的理解做了注释

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 判断是否是一个readonly proxy, 是的话直接返回这个readonly proxy
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }

  // 本质是调用createReactiveObject函数
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}


function createReactiveObject(
  // Target 目标对象
  // isReadonly 是否只读
  // baseHandlers 基本类型的 handlers
  // collectionHandlers 主要针对(set、map、weakSet、weakMap)的 handlers
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any> //map中保存已经时响应式的数据
) {
  // 判断是否是对象或者数组
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    // 如果target已经是proxy对象, 直接返回
    return target
  }
  // target already has corresponding Proxy
  // 如果之前有在proxyMap中缓存Proxy, 直接从map中取出并且返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  // 只有在白名单中的类型才可以是响应式的
  // 比如带标记 __v_skip的对象, 或者不在白名单('Object,Array,Map,Set,WeakMap,WeakSet')
  // 上面的这些是不能实现响应式的
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 创建Proxy对象, 实现响应式
  // targetType值为1, 所以会使用baseHandlers, 也就是传入的mutableHandlers
  const proxy = new Proxy(
    target,
    //TargetType.COLLECTION:
    // 'Map'
   //'Set'
   //'WeakMap'
   //'WeakSet'
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 放到map中缓存起来
  proxyMap.set(target, proxy)
  // 返回proxy对象
  return proxy
}

这段代码如此简单,不过是做了一些edgeCase,创建一个Proxy代理,存入map,最后返回。那么我们知道proxy是要传入Handler,而这个baseHandlers才是我们值得研究的。

baseHandlers

image.png
可以看到 basehandlers 中包含了四种 handler

  • mutableHandlers 可变处理
  • readonlyHandlers 只读处理
  • shallowReactiveHandlers 浅观察处理(只观察目标对象的第一层属性)
  • shallowReadonlyHandlers 浅观察 && 只读处理

其中 readonlyHandlers shallowReactiveHandlers shallowReadonlyHandlers 都是 mutableHandlers 的变形版本,这里我们主要针对 mutableHandlers 展开


export const mutableHandlers: ProxyHandler<object> = {
  get, // 用于拦截对象的读取属性操作
  set, // 用于拦截对象的设置属性操作
  deleteProperty, // 用于拦截对象的删除属性操作
  has, // 检查一个对象是否拥有某个属性
  ownKeys // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法
}

下面的get方法,我做了超详细的解释


//get就是CreateGetter返回的
function createGetter(isReadonly = false, shallow = false) {

  return function get(target: Target, key: string | symbol, receiver: object) {
    //ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值

    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      //如果key是raw 则直接返回目标对象
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    // 数组对象
    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // arrayInstrumentations包含数组修改的函数
      // 如果目标对象是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
      //这里就是对数组方法的特殊处理
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 求值
    const res = Reflect.get(target, key, receiver)

    if (
      //如果 key 是 symbol 内置方法,或者访问的是原型对象,直接返回结果,不收集依赖
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : isNonTrackableKeys(key)
    ) {
      return res
    }

    if (!isReadonly) {
      // 目标对象不为只读则调用 track Get
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      //shallowReactive直接返回
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      //是否解包装
      //如果是arr[1]这样返回的ref就不解包装
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    //由于 proxy 只能代理一层,res 的值如果是对象,就继续对其进行代理
    // 如果res的本身是一个数组或者对象, 那么递归的让其变成响应式的
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

const arrayInstrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
// 当数组响应式对象使用 includes、indexOf、lastIndexOf 这方法的时候,它们内部的 this 指向的是代理对象,并且在获取数组元素时得到的值要也是代理对象,所以当使用原始值去数组响应式对象中查找的时候,如果不进行特别的处理,是查找不到的,所以我们需要对这些数组方法进行重写才能解决这个问题。
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {

  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})

还有一点值得注意,在一些数组的方法中除了修改数组的内容之外也会隐式地修改数组的长度。例如下面的例子:

const arr = new Proxy([1], {
  get(target, key) {
    console.log('获取值')
    return target[key]
  },
  set(target, key, newValue, reciver) {
    console.log('设置值')
    const result = Reflect.set(target, key, newValue, reciver)
    return result
  }
})
arr.push(2)


image.png
arr.push 的操作却也触发了 getter 拦截器,并且触发了两次,其中一次就是数组 push 属性的读取,还有一次是什么呢?还有一次就是调用 push 方法会间接读取 length 属性,那么问题来了,进行了 length 属性的读取,也就会建立 length 的响应依赖,可 arr.push 本意只是修改操作,并不需要建立 length 属性的响应依赖。所以我们需要 “屏蔽” 对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系。

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
 const method = Array.prototype[key] as any
 arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
   //这些数组的函数调用时会隐式触发数组的getter(有可能还是多次),比如push操作即读取数组的push属性,还读取数组length属性
   //所以这里要停止依赖收集
   pauseTracking()
   const res = method.apply(this, args)
   //函数执行完开启依赖收集
   resetTracking()
   return res
 }
})

下面是set

function createSetter(shallow = false) {
  return function set(
    target: object, //目标对象
    key: string | symbol, // 设置的属性的名称
    value: unknown, //要改变的属性值
    // 如果遇到 setter,receiver则为setter调用时的this值
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      //目标对象不是数组,旧值是ref,新值不是ref,则直接赋值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
      //在浅模式下,不管是否响应,对象都被设置为原样
    }
    // 检查对象是否有这个属性
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    //设置值
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      // trigger函数派发通知
      if (!hadKey) {
        // 新增属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 修改属性
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

下面几个就很简单,就是触发依赖,没什么难点

function deleteProperty(target: object, key: string | symbol): boolean {
  //检查一个对象是否包含当前key
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
// 返回一个由目标对象自身的属性键组成的数组并且触发依赖
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

ref 跟 reactive 都是响应系统的核心方法,作为整个系统的入口

可以将 ref 看成 reactive 的一个变形版本,这是由于 reactive 内部采用 Proxy 来实现,而 Proxy 只接受对象作为入参,这才有了 ref 来解决值类型的数据响应,如果传入 ref 的是一个对象,内部也会调用 reactive 方法进行深层响应转换, 如果传入的是基本数据类型,直接做一下数据劫持就可以简单的实现响应式(基本数据类型没必要做proxy,太重了)。

export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
//如果已经是ref了返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
  //判断是不是调用了shallowRef,如果是就不转花了
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
    //下面就是进行的数据劫持,如此简单
  get value() {
  //收集依赖
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
  //先判断是否改变
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      //不是shallowRef要进行转换
      this._value = this._shallow ? newVal : convert(newVal)
      触发依赖
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

//转换,如果是对象,直接包裹reactive
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

总结

静下心来看,响应式原理比较简单,难的是那些edgecase的判断以及优秀的代码封装,希望对你有帮助。