在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染。那么到底这是怎么做到的呢?
@vue/reactivity功能十分丰富,而petite-vue仅使用到reactive和effect两个最基本的api,作为入门本文将仅仅对这两个API进行源码解读。
我们知道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的特点:
// 定义响应式对象
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到底如何构造一个响应式对象。
@vue/reactivity的源码位于vue-next项目的packages/reactivity下,而reactive函数则位于其下的src/reactive.ts文件中。该文件中除了包含reactive函数外,还包含如shallowReactive、readonly、shallowReadonly和其它帮助函数。
而reactive核心工作则是通过Proxy将一个普通的JavaScript对象转换为监控对象,拦截对象属性的读写删操作,并收集依赖该对象(属性)的副作用函数。大致流程如下:
下面我们一起逐行理解源码吧!
// 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方法中会对被代理对象进行各种检查,从而减少不必要的操作提高性能。最后若被代理对象的类型为Object或Array则采用baseHandlers生成代理,否则使用collectionHandlers生成代理。
//文件 ./baseHandlers.ts
// /*#__PURE__*/用于告诉webpack等bundler工具后面紧跟的函数是纯函数,若没被调用过则可以采用tree-shaking移除掉该函数
const get = /*#__PURE__*/ createGetter()
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
}我们首先看看是如何拦截读操作吧
拦截读操作核心是收集依赖所读属性的辅作用函数的信息,具体流程逻辑是
//文件 ./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]) // 回显 hiTypeScript小课堂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
}既然拦截读操作是为了收集依赖,那么拦截写操作自然就是用于触发副作用函数了。流程逻辑如下:
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
}检查存在与否属于读操作,因此我们可以用于依赖收集。
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方法
流程逻辑是:对于数组则跟踪数组长度,否则跟踪由effect模块提供的ITERATE_KEY,这个是什么东东呢?继续往下看就知道了:)
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}在上述代码中我们发现会使用到Proxy拦截函数入参receiver,如:
另外,在开篇《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是什么呢?
对于数据属性(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对于访问器属性(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的作用,但对如下问题已经无法作出完整的解答:
转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/16037690.html肥仔John
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!
prtite-vue 保留了 Vue 的一些基础特性,这使得 Vue 开发者可以无成本使用,在以往,当我们在开发一些小而简单的页面想要引用 Vue 但又常常因为包体积带来的考虑而放弃,现在,petite-vue 的出现或许可以拯救这种情况了,毕竟它真的很小,大小只有 5.8kb,大约只是 Alpine.js 的一半
前端程序员想必对尤雨溪及其开发的 Vue 框架不陌生。Vue 是一套用于构建用户界面的渐进式 JavaScript 框架,在 2014 年发布后获得了大量开发者的青睐,目前已更新至 3.0 版本
我们看到在v-if和v-for的解析过程中都会生成块对象,而且是v-if的每个分支都对应一个块对象,而v-for则是每个子元素都对应一个块对象。
本篇我们会继续探索 reactive 函数中对 Map/WeakMap/Set/WeakSet 对象的代理实现。由于WeakMap和WeakSet分别是Map和Set的不影响GC执行垃圾回收的版本,这里我们只研究Map和Set即可。
当我们通过effect将副函数向响应上下文注册后,副作用函数内访问响应式对象时即会自动收集依赖,并在相应的响应式属性发生变化后,自动触发副作用函数的执行。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!