likes
comments
collection
share

4-Vue源码之【响应式】

作者站长头像
站长
· 阅读数 26

前言

Vue3 中,我们使用 Proxy 代替 Vue2 中的 defineProperty 进行数据绑定。这里就涉及到了 2 个重要的 API ,reactiveref

在使用上,reactive 一般用来描述引用数据类型ref 用来描述基础数据类型偏多。

当然 ref 内部也做了处理,如果传递一个 引用数据类型 ,则会先进行一层 reactive 处理,在进行 ref 处理。

先简单了解下这 2 个 API 的实现原理。

Reactive

该方法接受一个对象,并返回一个 Proxy 对象Proxy 相比之前的 defineProperty 针对 数组 有了更好的处理,不再需要像 Vue2 那样去重写数组方法

export function reactive(target: object) {
  // target 必须为对象,否则无法被 new Proxy 绑定
  if (!isObject(target)) {
    return target
  }

  // target 必须在 targetType 定义的六个类型当中,否则直接返回 target
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 这里会根据不同的 TargetType 去选择合适的 handler,比如 如果是 Map,Set 这种,那么只需要去处理 get 选择器即可
  // 我们这里暂时只讨论 array 和 object 的情况
  // const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
  const proxy = new Proxy(target, baseHandlers)
  return proxy
}

/**
 * 根据原始类型 确定不同的 TargetType 用于在后面的 响应式中处理。
 *
 * 比如: 针对 Map,Set 类型,重写其 get 方法
 * 因为我们常规调用  const map = new Map()  都是调用 map.get('xx').yy
 *
 * @param rawType
 * @returns
 */
function targetTypeMap(rawType: string) {
  // 跟据 TargetType 使用不同的 proxyHandler
  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) {
  // toRawType 就是通过 Object.prototype.toString.call 去获取最原始的类型
  return targetTypeMap(toRawType(value))
}

源码里先去判断 target 的类型,然后再去 new Proxy,重点在于 baseHandlers 的处理方式。

1. baseHandlers

const get = createGetter()
const set = createSetter()

// 其他几个暂时不考虑,主要考虑 get追踪依赖 / set触发依赖 的情形
export const baseHandlers: ProxyHandler<object> = {
  get,
  set,
  // deleteProperty,
  // has,
  // ownKeys
}

get 方法

// 调用get,收集依赖
const createGetter = () => {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 这些 ReactiveFlags 的参数,用户可能不会调用,但是其他方法里会用到,比如 toRaw 里
    if (key === ReactiveFlags.RAW) {
      return target
    }

    const targetIsArray = isArray(target)

    // 针对数组进行特殊处理。
    // arrayInstrumentations 里保存了一些数组的方法: includes , indexOf , push, pop 等
    // Vue3 重写了这些方法并存放到了 arrayInstrumentations 里
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 这里就会去调用 arrayInstrumentations[key]
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 1. 返回 key 对应的数据 res (一般情况下,大部分都是返回这个)
    const res = Reflect.get(target, key, receiver)

    // 2. 建立跟踪(收集依赖,响应式系统里最重要的一步)
    // 后面会解释
    track(target, TrackOpTypes.GET, key)

    // 3. 如果返回的数据也是 object 那么再进行一次 reactive
    if (isObject(res)) {
      // 这么做避免了一开始就对所有数据进行深层次遍历。
      // 而是在调用该 key 时,如果发现其对应的数据是对象,那么就再次进行 reactive 代理绑定
      return reactive(res)
    }

    return res
  }
}

set 方法

// 调用set,触发更新
const createSetter = () => {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
    // 先获取旧值(更新之前, target[key] 里保存的还是旧值)
    let oldValue = (target as any)[key]

    // 如果 target 为数组,且 key 为正整数,就看 key 是否比 length 小,false 则说明是 新增
    // 如果 target 为对象,那么直接用 hasOwn 去判断,是否存在,false 则说明是 新增
    // 【注】 对于数组,有非常好的优化效果。由于 数组 变更长度,会导致 length 改变,所以会触发多次 set。
    // 【注】 当遇到 length 发生 set 时,会走到 hasOwn(target, key) 这里,导致 hadKey 为 true
    // 【注】 紧接着下方, hasChanged 判断 新旧value 是否发生变化,由于只是 length 改变,所以这一次虽然触发了 set,但是不会去触发 trigger ,避免了渲染
    const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)

    const result = Reflect.set(target, key, value, receiver)

    // 触发更新 trigger
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }

    // 之前想过针对数组,只考虑数组 length 发生 set 的时候去触发 trigger
    // 后来发现不行,因为 vue 可以使用 watch, computed, effect 等去单纯依赖数组中的某一个元素
    // 比如: const item = computed(()=> list[3]) 我这里只依赖了 list[3] ,那么 computed 的 effect函数就会被加入到 list[3] 的deps中
    // 这样,我去更新 list 的值,就会触发到 list[3] 的 deps ,继而触发到触发到 computed
    // if (key == 'length') {
    //   trigger(target, TriggerOpTypes.ADD, key, value)
    // }
    return result
  }
}

getset 中,响应式相关的最重要的就是 tracktrigger 了,当然还有些小细节,比如上面说到的:

  1. get 的时候再去判断是否需要进行深层次 reactive

  2. 利用 hadKey 避免数组的多次渲染(数组发生长度变化时,会调用多次 set

QA.1. 我们的数据是在何时 track 的?(即:何时调用的 get?)
主要有 2 种地方,JS部分 和 模板部分

Js 层的执行即可调用,模板层实际上在最后转换成 `Vnode` 时,也是属于了 JS 层,所以也是执行即可 track

【注】关于 track 和 trigger 需要结合 effect 啃,这里先单纯记住作用


Ref

由于我们的 Proxy 只接受对象,为了弥补这方面的不足,Vue3 创建了一个新的 API —— ref 用于对string, number 等基础数据类型进行处理

ref 无法使用 Proxy ,所以 Vue3 创建了一个 类, 类里面去设置了 get 和 set 的访问器

export const ref = <T>(tempValue: T) => {
  return new RefImpl<T>(tempValue)
}

class RefImpl<T = unknown> {
  private _value: T // 私有属性
  public dep?: Dep // 这里存储的依赖是在 get 时存进去的一些方法,set 时就会调用这些方法。
  constructor(defaultValue: T) {
    // 构造函数进行判断,如果是对象,那么先用 reactive 进行 Proxy 处理,在用 _value 包裹一层
    this._value = isObject(defaultValue) ? reactive(defaultValue as any) : defaultValue
  }

  get value() {
    // track
    trackRef(this)
    return this._value
  }

  set value(val) {
    // trigger
    this._value = val
    triggerRef(this)
  }
}

【Tips】不一定使用 类,也可以使用普通对象,但这种:统一类,再调用实例的方式显得更香

QA.2. 为什么 ref 返回的数据,需要加上 .value?
因为采用了上面的 类的访问器 进行拦截,只有调用 `.value` 才是真正对数据拦截并处理
转载自:https://juejin.cn/post/7237516248803688504
评论
请登录