2

Vue3响应式源码分析 - reactive篇

 1 year ago
source link: https://www.fly63.com/article/detial/11786
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更新日期: 2022-06-23阅读量: 6标签: 源码作者: 周小羊分享

扫一扫分享

最近一阶段在学习vue3,Vue3中用 reactive、ref 等方法将数据转化为响应式数据,在获取时使用 track 往 effect 中收集依赖,在值改变时,使用 trigger 触发依赖,执行对应的监听函数,这次就先来看一下 reactive 的源码。

reactive的源码在官方源码的packages/reactivity/src/reactive.ts文件中,源码中提供了四个api来创建reactive类对象:

reactive:创建可深入响应的可读写对象
readonly:创建可深入响应的只读对象
shallowReactive:创建只有第一层响应的浅可读写对象(其他层,值改变视图不更新)
shallowReadonly:创建只有一层响应的浅只读对象

它们都是调用createReactiveObject方法来创建响应式对象,区别在于传入不同的参数,本文只讲reactive,其他几个大同小异:

export function reactive(target: object) {
  // 如果是只读的话直接返回
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    // 目标对象
    target,
    // 标识是否是只读
    false,
    // 常用类型拦截器
    mutableHandlers,
    // 集合类型拦截器
    mutableCollectionHandlers,
    // 储了每个对象与代理的map关系
    reactiveMap
  )
}

export const reactiveMap = new WeakMap<Target, any>()

createReactiveObject代码如下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果代理的数据不是对象,则直接返回原对象
  if (!isObject(target)) {
    return target
  }

  // 如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 查看当前代理对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
  // proxyMap 是一个全局的缓存WeakMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 如果当前对象无法创建代理,则直接返回源对象
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  //  根据targetType 选择集合拦截器还是基础拦截器
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )

  // 向全局缓存Map里存储
  proxyMap.set(target, proxy)
  return proxy
}

其中有个方法是 getTargetType,用来获取传入target的类型:

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

export const enum ReactiveFlags {
  SKIP = '__v_skip',              // 标记阻止成为代理对象
  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
  IS_READONLY = '__v_isReadonly', // 标记一个只读对象
  IS_SHALLOW = '__v_isShallow',   // 标记只有一层响应的浅可读写对象
  RAW = '__v_raw'                 // 标记获取原始值
}

const enum TargetType {
  // 无效的 比如基础数据类型
  INVALID = 0,
  // 常见的 比如object Array
  COMMON = 1,
  // 集合类型比如 map set
  COLLECTION = 2
}

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
  }
}

当target被标记为 ReactiveFlags.SKIP 或是 不可拓展的,则会返回 TargetType.INVALID,无法创建代理,因为Vue需要对Target代理附加很多东西,如果是不可拓展的则会附加失败;或是用户主动调用 markRaw 等方法将数据标记为非响应式数据,那么也无法创建代理。

export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

看完了入口函数,接下来就是创建Proxy对象的过程了,Vue3会根据getTargetType返回的数据类型来选择是使用collectionHandlers集合拦截器还是baseHandlers常用拦截器,原因下面讲到集合拦截器的时候再说。

常用拦截器baseHandlers:

get 拦截器:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
 if (key === ReactiveFlags.IS_REACTIVE) { // 获取当前是否是reactive
   return !isReadonly
 } else if (key === ReactiveFlags.IS_READONLY) { // 获取当前是否是readonly
   return isReadonly
 } else if (key === ReactiveFlags.IS_SHALLOW) { // 获取当前是否是shallow
   return shallow
 } else if (
   // 如果获取源对象,在全局缓存WeakMap中获取是否有被创建过,如果创建过直接返回被代理对象
   key === ReactiveFlags.RAW &&
   receiver ===
     (isReadonly
       ? shallow
         ? shallowReadonlyMap
         : readonlyMap
       : shallow
       ? shallowReactiveMap
       : reactiveMap
     ).get(target)
 ) {
   return target
 }

 // 是否是数组
 const targetIsArray = isArray(target)

 // arrayInstrumentations相当于一个改造器,里面定义了数组需要改造的方法,进行一些依赖收集等操作
 // 如果是数组,并且访问的方法在改造器中,则使用改造器获取
 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
   return Reflect.get(arrayInstrumentations, key, receiver)
 }

 // 获取结果
 const res = Reflect.get(target, key, receiver)

 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
   return res
 }

 // 如果不是只读则收集依赖,Vue3中用track收集依赖
 if (!isReadonly) {
   track(target, TrackOpTypes.GET, key)
 }

 // shallow只有表层响应式,不需要下面去深度创建响应了
 if (shallow) {
   return res
 }

 // 如果获取的值是ref类型
 if (isRef(res)) {
   // 如果是数组 并且 是int类型的 key,则返回,否则返回.value属性
   return targetIsArray && isIntegerKey(key) ? res : res.value
 }

 if (isObject(res)) {
   // *获取时才创建相对应类型的代理,将访问值也转化为reactive,不是一开始就将所有子数据转换
   return isReadonly ? readonly(res) : reactive(res)
 }

 return res
  }
}

注意点是当代理类型是 readonly 时,不会收集依赖。
Vue3对于深层次的对象是使用时才创建的,还有如果结果是ref类型,则需要判断是否要获取它的.value类型,举个例子:

const Name = ref('张三')
const Array = ref([1])

const data = reactive({
  name: Name,
  array: Array
})

console.log(Name)          // RefImpl类型
console.log(data.name)     // 张三
console.log(data.array[0]) // 1

Vue3中使用 arrayInstrumentations对数组的部分方法做了处理,为什么要这么做呢? 对于 push、pop、 shift、 unshift、 splice 这些方法,写入和删除时底层会获取当前数组的length属性,如果我们在effect中使用的话,会收集length属性的依赖,当使用这些api是也会更改length,就会造成死循环:

 let arr = []
 let proxy = new Proxy(arr, {
get: function(target, key, receiver) {
  console.log(key)
  return Reflect.get(target, key, receiver)
}
 })
 proxy.push(1)
 /* 打印 */
 // push
 // length
// 当把这个代码注释掉时
// if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
//     return Reflect.get(arrayInstrumentations, key, receiver);
// }

const arr = reactive([])

watchEffect(() => {
 arr.push(1)
})

watchEffect(() => {
 arr.push(2)    
 // 上面的effect里收集了对length的依赖,push又改变了length,所以上面的又会触发,以此类推,死循环
})

// [1,2,1,2 ...] 死循环
console.log(arr)

对于 includes、 indexOf、 lastIndexOf,内部会去获取每一个的值,上面讲到如果获取出来的结果是Obejct,会自动转换为reactive对象:

let target = {name: '张三'}

const arr = reactive([target])

console.log(arr.indexOf(target)) // -1

因为实际上是 reactive(target) 和 target 在对比,当然查不到。

set 拦截器

function createSetter(shallow = false) {
 return function set(target, key, value, receiver) {
     // 获取旧数据
     let oldValue = target[key];
     if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
         return false;
     }
     // 如果当前不是shallow并且不是只读的
     if (!shallow && !isReadonly(value)) {
         if (!isShallow(value)) {
             // 如果新value本身是响应对象,就把他变成普通对象
             // 在get中讲到过如果取到的值是对象,才转换为响应式
             // vue3在代理的时候,只代理第一层,在使用到的时候才会代理第二层
             value = toRaw(value);
             oldValue = toRaw(oldValue);
         }
         // 如果旧的值是ref对象,新值不是,则直接赋值给ref对象的value属性
         if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
             // 这里不触发trigger是因为,ref对象在value被赋值的时候会触发写操作,也会触发依赖更新
             oldValue.value = value;
             return true;
         }
     }
     const hadKey = isArray(target) && isIntegerKey(key)
         ? Number(key) < target.length
         : hasOwn(target, key);
     const result = Reflect.set(target, key, value, receiver);
     // 这个判断主要是为了处理代理对象的原型也是代理对象的情况
     if (target === toRaw(receiver)) {
         if (!hadKey) {
             // key不存在就 触发add类型的依赖更新
             trigger(target, "add" /* ADD */, key, value);
         }
         else if (hasChanged(value, oldValue)) {
             // key存在就触发set类型依赖更新
             trigger(target, "set" /* SET */, key, value, oldValue);
         }
     }
     return result;
 };
}

set中还有一个要注意的地方就是 target === toRaw(receiver),这主要是为了处理代理对象的原型也是代理对象的情况:

const child = reactive({})

let parentName = ''
const parent = reactive({
  set name(value) {
     parentName = value
  },
  get name() {
     return parentName
  }
})

Object.setPrototypeOf(child, parent)

child.name = '张三'

console.log(toRaw(child)) // {name: 张三}
console.log(parentName) // 张三

当这种时候,如果不加上这个判断,由于子代理没有name这个属性,会触发原型父代理的set,加上这个判断避免父代理也触发更新。

集合拦截器collectionHandlers:

集合类型的数据比较特殊,其相关实例方法Proxy没有提供相关的捕获器,但是因为方法调用属于属性获取操作,所以都可以通过捕获get操作来实现,所以Vue3也只定义了get拦截:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 注意这里
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

之前的文章《代理具有内部插槽的内建对象》中说过Proxy代理具有内部插槽的内建对象,访问Proxy上的属性会发生错误。Vue3中是如何解决的呢?

Vue3中新创建了一个和集合对象具有相同属性和方法的普通对象,在集合对象 get 操作时将 target 对象换成新创建的普通对象。这样,当调用 get 操作时 Reflect 反射到这个新对象上,当调用 set 方法时就直接调用新对象上可以触发响应的方法,这样访问的就不是Proxy上的方法,是这个新对象上的方法:

function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {
    get(key: unknown) {
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }
  
  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
  })

  return [
    mutableInstrumentations
  ]
}

接下来看一看几个具体的拦截器:

get 拦截器:

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // 如果出现readonly(reactive())这种嵌套的情况,在readonly代理中获取到reactive()
  // 确保get时也要经过reactive代理
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 确保 包装后的key 和 没包装的key 都能访问得到
  if (!isReadonly) {
     if (key !== rawKey) {
       track(rawTarget, TrackOpTypes.GET, key)
     }
     track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  const { has } = getProto(rawTarget)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
     return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
     return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
     target.get(key)
  }
}

集合拦截器里把 key 和 rawKey 都做了处理,保证都能取到数据:

let child = {
  name: 'child'
}

const childProxy = reactive(child)

const map = reactive(new Map())

map.set(childProxy, 1234)

console.log(map.get(child)) // 1234
console.log(map.get(childProxy)) // 1234

set 拦截器:

// Map set拦截器
function set(this: MapTypes, key: unknown, value: unknown) {
  // 存origin value
  value = toRaw(value);
  // 获取origin target
  const target = toRaw(this);
  const { has, get } = getProto(target);

  // 查看当前key是否存在
  let hadKey = has.call(target, key);
  // 如果不存在则获取 origin
  if (!hadKey) {
     key = toRaw(key);
     hadKey = has.call(target, key);
  } else if (__DEV__) {
     // 检查当前是否包含原始版本 和响应版本在target中,有的话发出警告
     checkIdentityKeys(target, has, key);
  }

  // 获取旧的value
  const oldValue = get.call(target, key);
  // 设置新值
  target.set(key, value);
  if (!hadKey) {
     trigger(target, TriggerOpTypes.ADD, key, value);
  } else if (hasChanged(value, oldValue)) {
     trigger(target, TriggerOpTypes.SET, key, value, oldValue);
  }
  return this;
}

has 拦截器:

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  // 获取代理前数据
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果key是响应式的都收集一遍
  if (key !== rawKey) {
     !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  // 如果key是Proxy 那么先访问 proxyKey 在访问 原始key 获取结果
  return key === rawKey
 ? target.has(key)
 : target.has(key) || target.has(rawKey)
}

forEach 拦截器:

function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
     this: IterableCollections,
     callback: Function,
     thisArg?: unknown
  ) {
 const observed = this as any
 const target = observed[ReactiveFlags.RAW]
 const rawTarget = toRaw(target)
 const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
 !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
 // 劫持传递进来的callback,让传入callback的数据转换成响应式数据
 return target.forEach((value: unknown, key: unknown) => {
   // 确保拿到的值是响应式的
   return callback.call(thisArg, wrap(value), wrap(key), observed)
 })
  }
}

来自:https://segmentfault.com/a/1190000042021408

链接: https://www.fly63.com/article/detial/11786


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK