关于javascript:petitevue源码剖析逐行解读vuereactivity之reactive

61次阅读

共计 11988 个字符,预计需要花费 30 分钟才能阅读完成。

在 petite-vue 中咱们通过 reactive 构建上下文对象,并将依据状态渲染 UI 的逻辑作为入参传递给 effect,而后神奇的事件产生了,当状态发生变化时将主动触发 UI 从新渲染。那么到底这是怎么做到的呢?
@vue/reactivity 性能非常丰盛,而 petite-vue 仅应用到reactiveeffect两个最根本的 API,作为入门本文将仅仅对这两个 API 进行源码解读。

所有源于 Proxy

咱们晓得 Vue2 是基于 Object.defineProperty 拦挡对象属性的读写操作,从而实现依赖收集和响应式 UI 渲染。而 @vue/reactivity 作为 Vue3 的子项目,采纳的是 ES6 的 Proxy 接口实现这一性能。

const state = {count: 1}

const proxyState = new Proxy(state, {get(target: T, property: string, receiver?: T | Proxy): any {
    // 拦挡读操作
    console.log('get')
    return Reflect.get(target, property, receiver)
  },
  set(target: T, property: string, value: any, receiver?: T | Proxy): boolean {
    // 拦挡写操作
    console.log('set')
    return Reflect.set(target, property, value, receiver)
  },
  deleteProperty(target, prop) {
    // 拦挡属性删除操作
    console.log('delete')
    delete target[prop]
    return true
  }
})

绝对Object.defineProperty,Proxy 的特点:

  1. 通过 new Proxy 构建的对象进行操作能力拦挡对象属性的读写操作,而被代理的对象则没有任何变动;
  2. 能够监听数组元素的变动和增减;
  3. 能够监听对象属性的增减;
  4. Proxy 能够逐层代理对象属性,而 Object.defineProperty 则须要一次性代理对象所有层级的属性。

响应式编程

// 定义响应式对象
const state = reactive({
  num1: 1,
  num2: 2
})

// 在副作用函数中拜访响应式对象属性,当这些属性发生变化时副作用函数将被主动调用
effect(() => {console.log('outer', state.num1)
  effect(() => {console.log('inner', state.num2)
  })
})
// 回显 outer 1
// 回显 inner 2

state.num2 += 1
// 回显 inner 3

state.num1 += 1
// 回显 outer 2
// 回显 inner 3

state.num2 += 1
// 回显 inner 4
// 回显 inner 4

本篇咱们将从 reactive 动手,解读 Vue3 到底如何结构一个响应式对象。

深刻 reactive 的工作原理

@vue/reactivity 的源码位于 vue-next 我的项目的 packages/reactivity 下,而 reactive 函数则位于其下的 src/reactive.ts 文件中。该文件中除了蕴含 reactive 函数外,还蕴含如 shallowReactivereadonlyshallowReadonly 和其它帮忙函数。
reactive 外围工作则是通过 Proxy 将一个一般的 JavaScript 对象转换为监控对象,拦挡对象属性的读写删操作,并收集依赖该对象(属性)的副作用函数。大抵流程如下:

  1. 通过 reactive 结构的响应式对象都会将被代理对象和响应式对象的映射关系保留在reactiveMap,避免反复生成响应式对象,优化性能;
  2. 当调用 reactive 后会对被代理对象进行查看,若不是只读对象、响应式对象、primitive value 和 reactiveMap 中不存在则依据被代理对象的类型结构响应式对象
  3. 拦挡读操作 (get,hasownKeys)时调用 effect.ts 中的 track 收集依赖
  4. 拦挡写操作 (set, deleteProperty) 时调用 effect.ts 中的 trigger 触发副作用函数执行

上面咱们一起逐行了解源码吧!

源码解读——reactive入口

// Vue3 外部定义的对象个性标识
export const enum ReactiveFlags {
  SKIP = '__v_skip', // 标识该对象不被代理
  IS_REACTIVE = '__v_isReactive', // 标识该对象是响应式对象
  IS_READONLY = '__v_isReadonly', // 标识该对象为只读对象
  RAW = '__v_raw' // 指向被代理的 JavaScript 对象
}

// 响应式对象的接口
export interface Target {[ReactiveFlags.SKIP]?: boolean
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.RAW]?: any // 用于指向被代理的 JavaScript 对象
}

// 用于缓存被代理对象和代理对象的关系,避免反复代理
export const reactiveMap = new WeakMap<Target, any>()

// 将被代理对象的解决形式分为不代理 (INVALID)、一般对象和数组(COMMON) 和 Map、Set(COLLECTION)
const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  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
  }
}

function getTargetType(value: Target) {
  // 若对象标记为跳过,或不可扩大则不代理该对象
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    // 依据类型决定解决形式
    : targetTypeMap(toRawType(value))
}

export function reative(target: object) {
  // 不拦挡只读对象的读写删操作
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {return target}
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

function createReactiveObject (
  target: Target,
  isReadonly: boolean,
  beaseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // reactive 函数入参必须是 JavaScript 对象或数组,若是 primitive value 则会间接返回 primitive value
  if (!isObject(target)) {return target}
  /**
   * 1. 仅能对非响应式和非只读对象结构响应式对象
   * 2. 能够对非只读对象结构响应式对象
   */
  if (target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {return target}
  // 若对象已被代理过,则间接返回对应的代理对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {return existingProxy}
  // 依据被代理的对象类型决定采纳哪种代理形式
  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
}

能够看到 reactive 办法中会对被代理对象进行各种查看,从而缩小不必要的操作进步性能。最初若被代理对象的类型为 ObjectArray则采纳 baseHandlers 生成代理,否则应用 collectionHandlers 生成代理。

源码解读 - 代理 ObjectArraybaseHandlers

// 文件 ./baseHandlers.ts

// /*#__PURE__*/ 用于通知 webpack 等 bundler 工具前面紧跟的函数是纯函数,若没被调用过则能够采纳 tree-shaking 移除掉该函数
const get = /*#__PURE__*/ createGetter()

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
}

咱们首先看看是如何拦挡读操作吧

拦挡读操作

拦挡读操作外围是收集依赖所读属性的辅作用函数的信息,具体流程逻辑是

  1. 对于 Vue3 外部属性的读操作,即返回对应的值而不必收集依赖
  2. 对于数组内置办法的读操作,须要改写这些内置办法用于在调用该办法前对数组元素进行依赖收集,或解决一些边界问题
  3. 对于内置 Symbol 属性和其它 Vue3 外部属性的读操作,间接返回原始值且不必收集依赖
  4. 对于非只读对象的除上述外的其余属性的读操作,执行依赖收集 ( 外围逻辑)
  5. 若浅层响应式对象则间接返回属性值,否则若属性值为对象,则将其结构为响应式对象 (reactive) 或只读对象(readonly)
// 文件 ./baseHandlers.ts

/**
 * isNonTrackableKeys = {'__proto__': true, '__v_isRef': true, '__isVue': true}
 */
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)

// 内置的 Symbol 实例蕴含:hasInstance, isConcatSpreadable, iterator, asyncIterator, match, matchAll, replace, search, split, toPrimitive, toStringTag, species, unscopables
const builtInSymbols = new Set(Object.getOwnPropertyNames(Symbol)
    .map(key => (Symbol as any)[key])
    .filter(isSymbol)
)

function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {// 解决 Vue3 外部属性名 (`__v_isReactive`, `__v_isReadonly`, `__v_raw`) 的值
    if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly}
    else if (key === ReactiveFlags.IS_READONLY) {return isReadonly}
    // TODO
    else if (
      key === ReactiveFlags.RAW &&
      receiver === reactiveMap
    ) {return target}

    // 如果 key 是 includes,indexOf,lastIndexOf,push,pop,shift,unshift,splice 时则返回能跟踪依赖变动的版本
    const targetIsArray = isArray(target)
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }

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

    // 不拦挡内置 Symbol 属性和__proto__,__v_isRef 和__isVue 属性
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}

    // 收集依赖该属性的副作用函数
    if (!isReadonly) {track(target, TrackOpTypes.GET, key)
    }

    // 如果是构建 ShallowReactive 则不会基于属性值结构响应式式对象
    if (shallow) {return res}

    /* 对于属性值为 @vue/reactivity 的 Ref 实例时,如果不是执行 [1,2,3][0] 的操作则返回 Ref 实例蕴含的 primitive value,否则返回 Ref 实例
     * 因而咱们在 effect updator 中能够通过如下形式间接获取 Ref 实例属性的 primitive value
     * const age = ref(0), state = reactive({age})
     * console.log(age.value) // 回显 0
     * effect(() => { console.log(state.age) }) // 回显 0
     */
    if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // 若属性值不是 primitive value 或 BOM,则基于属性值结构响应式对象
    if (isObject(res)) {return isReadonly ? readonly(res) :  reactive(res)
    }
  }
}

这里能够看到当读取属性时才依据属性值类型来为属性值结构响应式对象,而不是当咱们调用 reactive 时就一股脑的遍历对象所有属性,并为各个属性构建响应式对象。

另外,针对 includes 等数组操作会返回对应的能跟踪依赖变动的版本,到底什么是能跟踪依赖变动的版本呢?

// 文件 ./baseHandlers.ts

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {const instrumentations: Record<string, Function> = {}
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {instrumentations[key] = function(this: unknown[], ...args: unknown[]) {const arr = toRaw(this) as any
      /* 提前遍历数组所有元素,跟踪每个元素的变动。若其中一个元素发生变化都会触发调用 includes,indexOf 或 lastIndexOf 副作用函数的执行。* 如果执行 `[2,1,2].includes(1)`,那么当匹配到第二个元素 1 时就会返回匹配后果,后续的元素不会被读取到,因而也就不会被跟踪收集到,那么当咱们执行 `[2,1,2][2] = 1` 时就不会触发副作用执行。*/
      // 
      for (let i = 0, l = this.length; i < l; i++) {track(arr, TrackOpTypes.GET, i + '')
      }

      // 调用数组原生的 includes,indexOf 和 lastIndexOf 办法
      const res = arr[key](...args)
      if (res === -1 // indexOf 和 lastIndexOf
          || res === false // includes
      ) {
        // 因为数组元素有可能为响应式对象而入参也有可能是响应式对象,因而当匹配失败,则将尝试获取数组元素的被代理对象从新匹配
        return arr[key](...args.map(toRaw))
      } else {return res}
    }
  })
  // 上面的操作会批改数组的长度,这里防止触发依赖长度的副作用函数执行
  ;(['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
}

// 文件 ./reactive.ts
export function toRaw<T>(observed: T): T {const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

TypeScript 小课堂 1:['includes', 'indexOf', 'lastIndexOf'] as const在 TypeScript 中用于标识对象或数组为不可批改对象。即

let a = ['includes', 'indexOf', 'lastIndexOf'] as const
a[0] = 'hi' // 编译时报错

const b = ['includes', 'indexOf', 'lastIndexOf']
b[0] = 'hi' // 批改胜利
console.log(b[0]) // 回显 hi

TypeScript 小课堂 2:instrumentations[key] = function(this: unknown[], ...args: unknown[]) {...}采纳的是 TypeScript 的 this 参数,用于限度调用函数时的 this 类型。
转换为 JavaScript 就是

instrumentations[key] = function(...args){pauseTracking()
  const res = (toRaw(this) as any)[key].apply(this, args)
  resetTracking()
  return res
}

拦挡写操作

既然拦挡读操作是为了收集依赖,那么拦挡写操作天然就是用于触发副作用函数了。流程逻辑如下:

  1. 若属性值为 Ref 对象,而新值取原始值后不是 Ref 对象,则更新 Ref 对象的 value,由 Ref 外部触发副作用函数
  2. 判断是否为新增属性,还是更新属性值,并触发副作用函数
const set = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
  return function set(
    target: Object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // Proxy 的 set 拦截器返回 true 示意赋值胜利,false 示意赋值失败

    let oldValue = (target as any)[key]
    if (!shallow) {
      /* 若旧属性值为 Ref,而新值不是 Ref,则间接将新值赋值给旧属性的 value 属性
       * 一眼看上去貌似没有触发依赖该属性的副作用函数执行工作压入调度器,但 Ref 对象也是响应式对象,赋值给它的 value 属性,会触发依赖该 Ref 对象的辅助用函数压入调度器
       */  
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(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) {
        // 触发依赖该属性的副作用函数执行工作压入调度器
        trigger(target, TriggerOpTypes.ADD, key, value)
      }
      else if (hasChange(value, oldValue)) {
        // 触发依赖该属性的副作用函数执行工作压入调度器
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

// 文件 @vue/shared
export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)

为什么不必 === 而要应用 Object.is 来比拟两个值是否相等呢?
对于 -0===0 返回 true,NaN === NaN 返回 false,而Object.is(-0, 0) 返回 false,Object.is(NaN, NaN) 返回 true
更多信息请查看《Source Code Reading for Vue 3: How does hasChanged work?》

拦挡删除操作

删除操作会批改属性天然也会触发依赖该属性的副作用函数啦

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
}

拦挡查看存在与否操作('name' in state)

查看存在与否属于读操作,因而咱们能够用于依赖收集。

function has(target: object, key: string | symbol): boolean {const result = Reflect.has(target, key)
  // Symbol 内置属性不收集
  if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, TrackOpTypes.HAS, key)
  }
  return result
}

拦挡键遍历操作

以下操作都会执行ownKeysProxy trap 办法

  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Object.keys
  • Object.names
  • for..in

流程逻辑是:对于数组则跟踪数组长度,否则跟踪由 effect 模块提供的ITERATE_KEY,这个是什么东东呢?持续往下看就晓得了:)

function ownKeys(target: object): (string | symbol)[] {track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

Proxy 中的 receiver 到底是什么?

在上述代码中咱们发现会应用到 Proxy 拦挡函数入参receiver,如:

  1. 在写入拦挡时,如果 target === toRaw(receiver) 成立则触发副作用函数执行
  2. 在读取拦挡时,若 key === ReactiveFlags.RAW && receiver === reactiveMap 则不以入参会根底构建响应式对象
  3. 另外,在开篇《petite-vue 源码分析 - 从动态视图开始》中创立作用域链 createScopedContext 有如下代码

     const reactiveProxy = reactive(
       new Proxy(mergeScope, {set(target, key, val, receiver) {
           // 若当设置的属性不存在于以后作用域则将值设置到父作用域上,因为父作用域以同样形式创立,因而递归找到领有该属性的先人作用域并赋值
           if (receiver === reactiveProxy && !target.hasOwnProperty(key)) {return Reflect.set(parentScope, key, val)
           }
           return Reflect.set(target, key, val, receiver)
         }
       })
     )

那么到底 receiver 是什么呢?

  1. 对于数据属性 (data properties) 的拦挡,receiver指向以后构建的 Proxy 实例自身

    // `receiver` 指向以后构建的 `Proxy` 实例自身
    const state = {name: 'john'}
    let pState = new Proxy(state, {get(target, key, receiver) {console.log("receiver === pState:", receiver === pState)
        return Reflect.get(target, key, receiver)
      }
    })
    
    pState.name
    // 回显 receiver === pState: true
  2. 对于拜访器属性 (accessor properties) 的拦挡,receiver指向 this 或者继承 Proxy 实例的对象

    const state = {
      _name: 'john',
      name() {return this._name}
    }
    
    let pState = new Proxy(state, {get(target, key, receiver) {console.log("target[key]():", target[key])
        console.log("receiver !== pState:", receiver !== pState)
        return Reflect.get(target, key, receiver)
      }
    })
    
    const son = {
      __proto__: pState,
      _name: 'son'
    }
    
    console.log(son.name)
    // 回显 target[key](): john
    // 回显 receiver !== pState: true
    // 回显 son

尽管理解了 receiver 的作用,但对如下问题曾经无奈作出残缺的解答:

  1. 在写入拦挡时,如果 target === toRaw(receiver) 成立则触发副作用函数执行
    首先 receiver 是 Proxy 实例肯定不会等于 target,而toRaw(receiver) 则是获取其代理的对象,仅当被代理的对象和以后 target 雷同时才触发副作用函数执行。(至于什么场景会呈现,求高人领导?)
  2. 在读取拦挡时,若 key === ReactiveFlags.RAW && receiver === reactiveMap 则不以入参会根底构建响应式对象
    为何 reactiveMap 会进行 Proxy 呢?
  3. 另外,在开篇《petite-vue 源码分析 - 从动态视图开始》中创立作用域链 createScopedContext 如下代码
    receiver === reactiveProxy && !target.hasOwnProperty(key) 即对以后作用域 (receiver === reactiveProxy) 进行写操作时,若属性不存在于该作用域对象,则往父作用域上递归执行写操作。

总结

下一篇咱们来看看代理 Map/WeakMap/Set/WeakSetmutableCollectionHandlers的实现吧!
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjoh… 肥仔 John

正文完
 0