Vue3 响应式原理(四)

简介: Vue3 响应式原理(四)

collectionHandlers.ts 文件

collectionHandlers.ts 文件包含了 MapWeakMapSetWeakSet 的处理器对象,分别对应完全响应式的 proxy 实例、浅层响应的 proxy 实例、只读 proxy 实例。这里只讲解对应完全响应式的 proxy 实例的处理器对象:

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

为什么只监听 get 操作,set has 等操作呢?不着急,先看一个示例:

const p = new Proxy(new Map(), {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})
p.set('ab', 100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]

运行上面的代码会报错。其实这和 Map Set 的内部实现有关,必须通过 this 才能访问它们的数据。但是通过 Reflect 反射的时候,target 内部的 this 其实是指向 proxy 实例的,所以就不难理解为什么会报错了。

那怎么解决这个问题?通过源码可以发现,在 Vue3.0 中是通过代理的方式来实现对 Map Set 等数据结构监听的:

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations
  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    // 这三个 if 判断和 baseHandlers 的处理方式一样
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (key === ReactiveFlags.raw) {
      return target
    }
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

把最后一行代码简化一下:

target = hasOwn(instrumentations, key) && key in target? instrumentations : target
return Reflect.get(target, key, receiver);

其中 instrumentations 的内容是:

const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size() {
    return size((this as unknown) as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false, false)
}

从代码可以看到,原来真正的处理器对象是 mutableInstrumentations。现在再看一个示例:

const proxy = reactive(new Map())
proxy.set('key', 100)

生成 proxy 实例后,执行 proxy.set('key', 100)proxy.set 这个操作会触发 proxy 的属性读取拦截操作。

打断点可以看到,此时的 key 为 set。拦截了 set 操作后,调用 Reflect.get(target, key, receiver),这个时候的 target 已经不是原来的 target 了,而是 mutableInstrumentations 对象。也就是说,最终执行的是 mutableInstrumentations.set()

接下来再看看 mutableInstrumentations 的各个处理器逻辑。

get

// 如果 value 是对象,则返回一个响应式对象(`reactive(value)`),否则直接返回 value。
const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
get(this: MapTypes, key: unknown) {
    // this 指向 proxy
    return get(this, key, toReactive)
}
function get(
  target: MapTypes,
  key: unknown,
  wrap: typeof toReactive | typeof toReadonly | typeof toShallow
) {
  target = toRaw(target)
  const rawKey = toRaw(key)
  // 如果 key 是响应式的,额外收集一次依赖
  if (key !== rawKey) {
    track(target, TrackOpTypes.GET, key)
  }
  track(target, TrackOpTypes.GET, rawKey)
  // 使用 target 原型上的方法
  const { has, get } = getProto(target)
  // 原始 key 和响应式的 key 都试一遍
  if (has.call(target, key)) {
    // 读取的值要使用包装函数处理一下
    return wrap(get.call(target, key))
  } else if (has.call(target, rawKey)) {
    return wrap(get.call(target, rawKey))
  }
}

get 的处理逻辑很简单,拦截 get 之后,调用 get(this, key, toReactive)

set

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  // 取得原始数据
  const target = toRaw(this)
  // 使用 target 原型上的方法
  const { has, get, set } = getProto(target)
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }
  const oldValue = get.call(target, key)
  const result = set.call(target, key, value)
  // 防止重复触发依赖,如果 key 已存在就不触发依赖
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 如果新旧值相等,也不会触发依赖
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return result
}

set 的处理逻辑也较为简单,配合注释一目了然。

还有剩下的 hasadddelete 等方法就不讲解了,代码行数比较少,逻辑也很简单,建议自行阅读。

ref.ts 文件

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
export function ref(value?: unknown) {
  return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
  // 如果已经是 ref 对象了,直接返回原值
  if (isRef(rawValue)) {
    return rawValue
  }
  // 如果不是浅层响应并且 rawValue 是个对象,调用 reactive(rawValue)
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    __v_isRef: true, // 用于标识这是一个 ref 对象,防止重复监听 ref 对象
    get value() {
      // 读取值时收集依赖
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        // 设置值时触发依赖
        trigger(
          r,
          TriggerOpTypes.SET,
          'value',
          __DEV__ ? { newValue: newVal } : void 0
        )
      }
    }
  }
  return r
}

在 Vue2.x 中,基本数值类型是不能监听的。但在 Vue3.0 中通过 ref() 可以实现这一效果。

const r = ref(0)
effect(() => console.log(r.value)) // 打印 0
r.value++ // 打印 1

ref() 会把 0 转成一个 ref 对象。如果给 ref(value) 传的值是个对象,在函数内部会调用 reactive(value) 将其转为 proxy 实例。

computed.ts 文件

export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  // 如果 getterOrOptions 是个函数,则是不可被配置的,setter 设为空函数
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 如果是个对象,则可读可写
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // dirty 用于判断计算属性依赖的响应式属性有没有被改变
  let dirty = true
  let value: T
  let computed: ComputedRef<T>
  const runner = effect(getter, {
    lazy: true, // lazy 为 true,生成的 effect 不会马上执行
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => { // 调度器
      // trigger 时,计算属性执行的是 effect.options.scheduler(effect) 而不是 effect()
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    __v_isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

下面通过一个示例,来讲解一下 computed 是怎么运作的:

const value = reactive({})
const cValue = computed(() => value.foo)
console.log(cValue.value === undefined)
value.foo = 1
console.log(cValue.value === 1)
  1. 生成一个 proxy 实例 value。
  2. computed() 生成计算属性对象,当对 cValue 进行取值时(cValue.value),根据 dirty 判断是否需要运行 effect 函数进行取值,如果 dirty 为 false,直接把值返回。
  3. 在 effect 函数里将 effect 设为 activeEffect,并运行 getter(() => value.foo) 取值。在取值过程中,读取 foo 的值(value.foo)。
  4. 这会触发 get 属性读取拦截操作,进而触发 track 收集依赖,而收集的依赖函数就是第 3 步产生的 activeEffect。
  5. 当响应式属性进行重新赋值时(value.foo = 1),就会 trigger 这个 activeEffect 函数。
  6. 然后调用 scheduler() 将 dirty 设为 true,这样 computed 下次求值时会重新执行 effect 函数进行取值。

index.ts 文件

index.ts 文件向外导出 reactivity 模块的 API。

Vue3 系列文章

参考资料

目录
打赏
0
相关文章
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
创建vue3项目步骤以及安装第三方插件步骤【保姆级教程】
这是一篇关于创建Vue项目的详细指南,涵盖从环境搭建到项目部署的全过程。
286 1
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
176 3
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
136 56
|
4月前
|
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
159 58
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
151 64
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
197 64
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
190 60
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
109 17

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等