Vue3 reactive 源码分析

2年前 (2022) 程序员胖胖胖虎阿
203 0 0

概要

本文通过抽取Vue3 reactive源码中的主要部分,来理解其响应式object的实现方式。本文源码基于这个入口文件:github reactive.ts

reactive()

源码位置:github reactive.ts

export function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

reactive内部调用了createReactiveObject函数,并传入mutableHandlers作为Proxy的handler

createReactiveObject()

源码位置:github reactive.ts

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
    const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
    return proxy
}

可见reactive的本质,是将普通的object转换成了Proxy。而Proxy只支持非空的object,所以reactive函数也只能用于object

const handler = {}
new Proxy({}, handler) // 正常运行
new Proxy(1, handler) // Cannot create proxy with a non-object as target or handler

如果用于原始类型的响应式,应该使用Vue3提供的ref函数。

mutableHandlers

源码位置:github baseHandlers.ts

mutableHandlersmutableCollectionHandlers用于new Proxy的第二个参数,是reactive函数最核心的逻辑,重点关注其getset属性。

export const mutableHandlers = {
  get: createGetter(),
  set: createSetter(),
  deleteProperty,
  has,
  ownKeys,
}

createGetter()

源码位置:github baseHandlers.ts

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 里面代码比较长,下面拆成几部分理解
    // part 1 - 处理内部变量
    // part 2 - 处理数组
    // part 3 - 处理其他情况
  }
}

part 1 - 处理内部变量

// ts语法
const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

if (key === ReactiveFlags.IS_REACTIVE) {
  return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
  return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
  return shallow
} else if (
  key === ReactiveFlags.RAW &&
  receiver ===
  (isReadonly
   ? shallow
   ? shallowReadonlyMap
   : readonlyMap
   : shallow
   ? shallowReactiveMap
   : reactiveMap
  ).get(target)
) {
  return target
}

使用reactive()生成的object,都包含ReactiveFlags对应的属性,可以输出看下效果:RunJS Demo。

使用shallowReactive()生成的object,只有根结点的属性是响应式的,这和Vue2中的响应式变量的特性是一样的。参见这个示例:Vue3 shallowReactive

part 2 - 处理数组

const targetIsArray = isArray(target)

if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

Proxy为数组,且调用数组原型方法时,其实是先get到方法名,再用这个方法去set相应的值,比如下面的代码:

const arr = [{count: 1}, {count: 2}]
const proxyArray = new Proxy(arr, {
  get(target, prop, receiver) {
    console.log(`get ${prop}`)
    return Reflect.get(target, prop, receiver)
  },
  set(target, prop, receiver) {
    console.log(`set ${prop}`)
    return Reflect.set(target, prop, receiver)
  }
})
proxyArray.push({count: 3})

// 依次输出为

get push
get length
set 2
set length

实际运行效果见:Javascript Proxy an array

part 3 - 处理其他情况

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

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

if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

if (shallow) {
  return res
}

if (isRef(res)) {
  // ref unwrapping - skip unwrap for Array + integer key.
  return targetIsArray && isIntegerKey(key) ? res : res.value
}

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

reactive初始化的变量中,如果包含ref类型的值,则取值时不需要带上.value。例如:

const count = ref(0)
const state = reactive({count: count})
// 以下两行,都能取到count值
console.log(count.value)
console.log(state.count)

实际运行效果见:Vue3 reactive object with ref

createSetter()

源码位置:github baseHandlers.ts

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // part 1 - 赋值
    // part 2 - 触发事件
  }
}

part 1 - 赋值

let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
  return false
}
if (!shallow) {
  if (!isShallow(value) && !isReadonly(value)) {
    oldValue = toRaw(oldValue)
    value = toRaw(value)
  }
  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)

part 2 - 解发事件

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

只有新增(TriggerOpTypes.ADD)和设置新值(TriggerOpTypes.SET)的时候,才会触发(trigger)订阅过的事件

结语

原文链接:https://runjs.work/projects/a082a7c4e4b748a8

原文示例保持更新。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月2日 上午4:16。
转载请注明:Vue3 reactive 源码分析 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...