【源码计划】vue3-reactive解读

前置

本文主要是对vue3中响应式reactive源码的解读,解读之前需要先一些前置知识

1、Proxy

在vue3的响应式中,用proxy取代了vue2的Object.defineProperty主要原因是

  • Object.defineProperty劫持对象需要递归对对象的每个属性,性能不佳;proxy可以劫持整个对象,只要对象的属性变了,都能劫持到。
  • 数组不采用Object.defineProperty来进行劫持,需要对数组单独进行处理; proxy可以直接监听数组的变化。
  • Object.defineProperty新增属性和删除属性时无法监控变化。需要通过$set$delete实现

用法:

ts 复制代码
// target 代理对象
// mutableHandlers 代理对象的操作逻辑
new Proxy(target, mutableHandlers)

2、Reflect

Reflect是JavaScript中的一个内置对象,它提供了一组静态方法,用于操作对象

Reflect主要用来配合proxy使用,用来映射代理对象

示例如下:

ts 复制代码
let person = {
    age: 18,    // 如果用户修改了,无法监控到
    get num(){  // 属性访问器
        return this.age;
    }
}
const people1 = new Proxy(person,{
    get(target, key, receiver){
        console.log('key:',key);
        return target[key]  // this=person
    }
})
console.log(people1.num);
// key:num
// 18
const people2 = new Proxy(person,{
    get(target, key, receiver){
        console.log('key:',key);
        return Reflect.get(target, key, receiver) //this=people2
    }
})
console.log(people2.num);
// key:num
// key:age
// 18

推导过程

1.在调用people1.num的时候,触发了代理对象get方法,返回的target[key]。这里的target原对象 ,也就是实例的person.num,直接打印出结果。

2.在调用people2.num的时候,返回的Reflect.get(target, key, receiver)。这里的receiver指向的是people2,也就是实例代理对象 people2.num。所以在num访问器里面访问this.age又可以触发代理对象get方法。

3.使用Reflect保证this指向永远指向代理对象 ,在对代理对象获取和设置值的时候都能走Proxy的操作逻辑。

简化代码

首先把reactive函数进行简化,为了解大概的内容,方便一下步的解读

ts 复制代码
function reactive(target: object) {
    // 要求传入的是对象,不是对象直接返回
    if (!isObject(target)) {
        return target
    }
    const proxy = new Proxy(target, baseHandlers)
    return proxy;
}
​
const baseHandlers: ProxyHandler<object> = {
    //  原始对象 属性 代理对象
    get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        return res;
    },
    set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver);
        return result;
    }
}

结合上面的方法来创建一个简单的reactive

ts 复制代码
const people = reactive({age: 18});
people.age;         // 触发baseHandlers的get方法
people.age = 20;    // 触发baseHandlers的set方法

源码解读

前置

源码提供的变量

ts 复制代码
// 标识
const enum ReactiveFlags {
  SKIP = '__v_skip',                // 不可代理的对象
  IS_REACTIVE = '__v_isReactive',   // 对象是响应式对象
  IS_READONLY = '__v_isReadonly',   // 对象是只读对象
  IS_SHALLOW = '__v_isShallow',     // 对象是浅代理对象
  RAW = '__v_raw'                   // 取原始对象
}
​
// 原对象与代理对象的映射表
const reactiveMap = new WeakMap<Target, any>()
const shallowReactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
const shallowReadonlyMap = new WeakMap<Target, any>()

映射表可以把原对象和代理对象关联起来,可以通过原对象找到代理对象,也可以通过代理对象找到原对象


ts 复制代码
import { mutableHandlers } from './baseHandlers'
import { mutableCollectionHandlers } from './collectionHandlers'
​
// reactive方法
export function reactive(target: object) {
    
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
    
  return createReactiveObject(
    target, // 原对象
    false,  // 该对象不是只读的
    mutableHandlers,    // 代理对象的操作逻辑
    mutableCollectionHandlers,  // 代理集合数据的操作逻辑
    reactiveMap // 原对象与代理对象的映射表
  )
}
​
// 是否只读
export function isReadonly(value: unknown): boolean {
  // ReactiveFlags.IS_READONLY = __v_isReadonly 对象是只读对象标志
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
​
// 创建一个reactive
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {}

解读过程

1.reactive接收一个对象 ,通过isReadonly方法来判断这个对象是否只读,只读就直接返回本身。

如果接收的对象是普通对象,就直接判断对象是否含有__v_isReadonly。如果接收的对象是proxy,则会调用传入proxy的第二个参数里面的get方法

2.接下来调用createReactiveObject方法,这个方法是个公共方法,reactiveshallowReactivereadonlyshallowReadonly都会调用这个方法,区别就在于传入的参数不同。


核心部分

源码位置:

  • reactive代码/packages/reactivity/src/reactive.ts
  • proxy处理逻辑代码packages/reactivity/src/baseHandlers.ts

这里将源码功能拆分出来方便理解,想看源码的可以从上面的路径找到源码参照阅读。

createReactiveObject

ts 复制代码
// isReadonly = false, 
// baseHandlers = mutableHandlers, 
// collectionHandlers = mutableCollectionHandlers,
// proxyMap = reactiveMap
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  // target不是对象,则直接返回,如果是在开发环境就报警。
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 1、对象已经是代理对象,返回本身
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 2、对象已经被代理过了,在映射表中找出代理对象并返回
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 3、只有在白名单中的类型能被观测
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

baseHandlers

ts 复制代码
export const baseHandlers: ProxyHandler<object> = {
  get(target, key, receiver){
      // ...
       const res = Reflect.get(target, key, receiver)
       return res
  },
  set(target, key, receiver){
      // ...
      const result = Reflect.set(target, key, value, receiver)
      return result
  },
}

解读过程:

1、对象已经是代理对象 ,返回本身。因为代理对象已经添加了get方法,里面的处理逻辑如下

kotlin 复制代码
function get(target, key, receiver){
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly    // reactive里面isReadonly变量的值为false 
    } // ...省略无关代码
    else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly     // reactive里面isReadonly变量的值为false
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap // reactiveMap就是上面的proxyMap.set(target, proxy)里面的proxyMap,是对象和代理对象的映射表
        ).get(target) // 所以这里拿到的就是代理对象,receiver指代的也是代理对象,结果是true
    ) {     
      return target
    }
    // ...省略无关代码
}

调用target[ReactiveFlags.RAW],在get里面key === ReactiveFlags.RAW

调用target[ReactiveFlags.IS_REACTIVE],在get里面key === ReactiveFlags.IS_REACTIVE,返回!isReadonly也就是true,结果这里return target返回代理对象本身

示例如下:

ts 复制代码
const obj = { age: 18 };
const proxy1 = reactive(obj); 
const proxy2 = reactive(proxy1);
console.log(proxy1 === proxy2); // true

2、对象已经被代理过了,在映射表中找出代理对象并返回

ts 复制代码
// 对象已经被代理,所以在映射表proxyMap里存在target->new Proxy的映射关系
// 这里就直接取出代理对象返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
    return existingProxy
}

示例如下:

ts 复制代码
const obj = { age: 18 };
const proxy1 = reactive(obj); 
const proxy2 = reactive(obj);
console.log(proxy1 === proxy2); // true

3、只有在白名单中的类型能被代理

ts 复制代码
function createReactiveObject(){
    // ...省略无关代码
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
}
​
// target
function getTargetType(value: Target) {
  // 对象中有跳过字段、或者不能被扩展
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))   // toRawType看原始类型,原理是[object xxx].slice(8, -1)得出类型
}
​
// Object、Array还有Map、Set、WeakMap、WeakSet可以代理
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

结论就是对象没有跳过SKIP标识、可以被扩展,并且类型是Object、Array还有Map、Set、WeakMap、WeakSet可以代理


拓展

  • 什么时候会有跳过SKIP标识?

markRaw方法可以把对象标记为不可代理

ts 复制代码
const proxy0 = markRaw({ a: 1}) // proxy0 = {a: 1,__v_skip: true}

markRaw原理

ts 复制代码
const enum ReactiveFlags {
  SKIP = '__v_skip',                // 不可代理的对象
  // ...省略无关代码
}
​
function markRaw(obj: object){
    Object.defineProperty(obj, ReactiveFlags.SKIP, {
        configurable: true,
        enumerable: false,
        value: true
    })
}
  • toRaw方法
ts 复制代码
const obj = { age: 18 };
const proxy1 = reactive(obj); // target -> proxy
const obj2 = toRaw(proxy);    // proxy -> target
console.log(obj === obj2);    // true

原理

ts 复制代码
function toRaw<T>(observed: T): T {
  // 递归将对象转换成原始类型
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}
​
// (observed as Target)[ReactiveFlags.RAW] 触发响应式get方法
function get(target: Target, key: string | symbol, receiver: object) {
// ... 
    else if (
        key === ReactiveFlags.RAW && // 实现toRaw方法
        receiver ===
        (isReadonly
         ? shallow
         ? shallowReadonlyMap
         : readonlyMap
         : shallow
         ? shallowReactiveMap
         : reactiveMap
        ).get(target)
    ) {
        return target   // 返回映射表reactiveMap里目标对象的原对象
    }
// ...
}

baseHandlers

解读完reactive部分的源码,发现baseHandlers里面的部分源码还未涉及,接下来继续解读baseHandlers关于getset部分的源码

get

ts 复制代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly // true 是响应式reactive
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly // false 不是readonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow // 浅代理
    } else if (
      key === ReactiveFlags.RAW && // 实现toRaw方法
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
​
    const targetIsArray = isArray(target)
    // 1、如果是数组,对数组的方法进行特殊处理
    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        // 访问hasOwnProperty,也会触发依赖收集
        return hasOwnProperty
      }
    }
    // 取值
    const res = Reflect.get(target, key, receiver)
    
    // 是内置symbol或者是访问的是__proto__属性直接返回,不用做依赖收集
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
​
    // 如果是仅读的不需要做依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
      
    // 如果是浅代理的直接返回取值
    if (shallow) {
      return res
    }
​
    // 2、取值是ref,判断是否脱ref
    if (isRef(res)) {
      // 如果通过数组索引访问的是ref类型则不进行拆包
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
​
    // 3、如果取的结果是对象,会进行递归代理
    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
  }
}

解读过程:

1、如果是数组,对数组的方法进行特殊处理

为什么要特殊处理?

响应式对象是数组的时候,比如调用includes方法,当数组里面的值变化了,这里也要能触发更新重新判断;当includes传入的是原数组某个对象类型的值,与响应式对象取出的响应式的值就一直不相等。所以这里需要重写数组的方法。

ts 复制代码
const targetIsArray = isArray(target)
​
if (!isReadonly) {
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
    }
    if (key === 'hasOwnProperty') {
        // 访问hasOwnProperty,也会触发依赖收集
        return hasOwnProperty
    }
}
​
const arrayInstrumentations = () => {
    const instrumentations: Record<string, Function> = {}
  // 数组对 'includes', 'indexOf', 'lastIndexOf' 方法进行处理
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    // 重写的方法
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      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 = arr[key](...args)
      // 如果参数找不到,会将参数转成原数据再找一次,也就是传入响应式的值也可以判断,这里会做处理
      // includes(原对象)、includes(代理对象)这两种都可以
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  // 数组对 'push', 'pop', 'shift', 'unshift', 'splice' 方法进行处理
  // 这些方法会导致数组长度发生变化,会导致无限更新问题
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      pauseTracking() // 调用此方法停止依赖收集
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking() // 恢复依赖收集
      return res
    }
  })
  return instrumentations
}

2、取值是ref,判断是否脱ref

ts 复制代码
if (isRef(res)) {
    // 如果通过数组索引访问的是ref类型则不进行拆包
    // ref unwrapping - skip unwrap for Array + integer key.
    return targetIsArray && isIntegerKey(key) ? res : res.value
}

实例如下:

ts 复制代码
const obj = reactive({ age: ref(18) });
const age = obj.age // 18   age取值的时候,会判断是否是ref,是ref自动调用.value,也就是脱ref
​
const proxyArr = reactive([ref(1), 2, 3])
console.log(proxyArr[0]); // key是数组索引,这种情况不支持脱ref

3、如果取的结果是对象,会进行递归代理

这里其实是proxy做的优化---懒代理。代理对象的对象属性,没有被取值,是不会转成代理的


set

ts 复制代码
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key] // 获取老值
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow) {
      // 如果深度代理,在赋值的时候先转换成非proxy
      if (!isShallow(value) && !isReadonly(value)) {
        // 把老值和新值都转换成原始值
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 1、不是数组,老的是ref,新的不是ref,则会给老的ref赋值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 更新原来ref的值
        oldValue.value = value
        return true
      }
    } else {
      // 浅层代理无所谓咋设置
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    // 2、设置值的时候,要判断是修改还是新增
    // 查看是否有过这个key
    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)) {
      // 减少原型链的触发
      if (!hadKey) {
        // 添加
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果前后置有变化触发修改逻辑
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

解读过程:

1、不是数组,老的是ref,新的不是ref,则会给老的ref赋值

ts 复制代码
 if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
     // 更新原来ref的值
     oldValue.value = value
     return true
 }

示例如下:

ts 复制代码
const obj = reactive({ age: ref(18) });
obj.age = 20; // 相当于obj.age.value = 20
​
obj.age = ref(20);  // 相当于重新赋一个ref

2、设置值的时候,要判断是修改还是新增

如果访问的是数组的索引,索引的长度在原数组长度之内的就是修改

如果访问的是对象,就判断对象里面有没有这个值,有就是修改

ts 复制代码
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)) {   // 如果是调用原型链,这里的receiver和target不相等,就会屏蔽
    if (!hadKey) {
        // 添加逻辑
        trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
        // 如果前后有变化触发修改逻辑
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
}

其他代理处理

ts 复制代码
// 使用delete删除某个属性触发
function deleteProperty(target: object, key: string | symbol): boolean {
  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
}
​
// 用in判断某个属性是否在对象中
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  // 内置属性不做依赖收集
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // 对key进行依赖收集,因为删除或增加这个key,也要触发更新
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
​
// 使用for in 数组收集长度,对象收集,也要依赖收集
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}
相关推荐
Jiaberrr1 分钟前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
LvManBa44 分钟前
Vue学习记录之六(组件实战及BEM框架了解)
vue.js·学习·rust
200不是二百1 小时前
Vuex详解
前端·javascript·vue.js
LvManBa1 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨1 小时前
前端vue-父传子
前端·javascript·vue.js
工业互联网专业2 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
J不A秃V头A2 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂3 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客3 小时前
pinia在vue3中的使用
前端·javascript·vue.js
天下无贼!5 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue