Vue3 响应式原理(一)

简介: Vue3 响应式原理

前言

学习 Vue3.0 源码必须对以下知识有所了解:

  1. proxy reflect iterator
  2. map weakmap set weakset symbol

这些知识可以看一下阮一峰老师的《ES6 入门教程》

如果不会 ts,我觉得影响不大,了解一下泛型就可以了。因为我就没用过 TS,但是不影响看代码。

阅读源码,建议先过一遍该模块下的 API,了解一下有哪些功能。然后再看一遍相关的单元测试,单元测试一般会把所有的功能细节都测一边。对源码的功能有所了解后,再去阅读源码的细节,效果更好。

proxy 术语

const p = new Proxy(target, handler)
  • handler,包含捕捉器(trap)的占位符对象,可译为处理器对象。
  • target,被 Proxy 代理的对象。

友情提醒

在阅读源码的过程中,要时刻问自己三个问题:

  1. 这是什么?
  2. 为什么要这样?为什么不那样?
  3. 有没有更好的实现方式?

正所谓知其然,知其所以然。

阅读源码除了要了解一个库具有什么特性,还要了解它为什么要这样设计,并且要问自己能不能用更好的方式去实现它。

如果只是单纯的停留在“是什么”这个阶段,对你可能没有什么帮助。就像看流水账似的,看完就忘,你得去思考,才能理解得更加深刻。

正文

reactivity 模块是 Vue3.0 的响应式系统,它有以下几个文件:

baseHandlers.ts
collectionHandlers.ts
computed.ts
effect.ts
index.ts
operations.ts
reactive.ts
ref.ts

接下来按重要程度顺序来讲解一下各个文件的 API 用法和实现。

reactive.ts 文件

在 Vue.2x 中,使用 Object.defineProperty() 对对象进行监听。而在 Vue3.0 中,改用 Proxy 进行监听。Proxy 比起 Object.defineProperty() 有如下优势:

  1. 可以监听属性的增删操作。
  2. 可以监听数组某个索引值的变化以及数组长度的变化。

reactive()

reactive() 的作用主要是将目标转化为响应式的 proxy 实例。例如:

const obj = {
    count: 0
}
const proxy = reactive(obj)

如果是嵌套的对象,会继续递归将子对象转为响应式对象。

reactive() 是向用户暴露的 API,它真正执行的是 createReactiveObject() 函数:

// 根据 target 生成 proxy 实例
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.raw] &&
    !(isReadonly && target[ReactiveFlags.isReactive])
  ) {
    return target
  }
  // target already has corresponding Proxy
  if (
    hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
  ) {
    return isReadonly
      ? target[ReactiveFlags.readonly]
      : target[ReactiveFlags.reactive]
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
  const observed = new Proxy(
    target,
    // 根据是否 Set, Map, WeakMap, WeakSet 来决定 proxy 的 handler 参数
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  // 在原始对象上定义一个属性(只读则为 "__v_readonly",否则为 "__v_reactive"),这个属性的值就是根据原始对象生成的 proxy 实例。
  def(
    target,
    isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
    observed
  )
  return observed
}

这个函数的处理逻辑如下:

  1. 如果 target 不是一个对象,返回 target。
  2. 如果 target 已经是 proxy 实例,返回 target。
  3. 如果 target 不是一个可观察的对象,返回 target。
  4. 生成 proxy 实例,并在原始对象 target 上添加一个属性(只读则为 __v_readonly,否则为 __v_reactive),指向这个 proxy 实例,最后返回这个实例。添加这个属性就是为了在第 2 步做判断用的,防止对同一对象重复监听。

其中第 3、4 点需要单独拎出来讲一讲。

什么是可观察的对象
const canObserve = (value: Target): boolean => {
  return (
    !value[ReactiveFlags.skip] &&
    isObservableType(toRawType(value)) &&
    !Object.isFrozen(value)
  )
}

canObserve() 函数就是用来判断 value 是否是可观察的对象,满足以下条件才是可观察的对象:

  1. ReactiveFlags.skip 的值不能为 __v_skip__v_skip 是用来定义这个对象是否可跳过,即不监听。
  2. target 的类型必须为下列值之一 Object,Array,Map,Set,WeakMap,WeakSet 才可被监听。
  3. 不能是冻结的对象。
传递给 proxy 的处理器对象是什么

根据上面的代码可以看出来,在生成 proxy 实例时,处理器对象是根据一个三元表达式产生的:

// collectionTypes 的值为 Set, Map, WeakMap, WeakSet

collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers

这个三元表达式非常简单,如果是普通的对象 ObjectArray,处理器对象就使用 baseHandlers;如果是 Set, Map, WeakMap, WeakSet 中的一个,就使用 collectionHandlers。

collectionHandlers 和 baseHandlers 是从 collectionHandlers.tsbaseHandlers.ts 处引入的,这里先放一放,接下来再讲。

有多少种 proxy 实例

createReactiveObject() 根据不同的参数,可以创建多种不同的 proxy 实例:

  1. 完全响应式的 proxy 实例,如果有嵌套对象,会递归调用 reactive()
  2. 只读的 proxy 实例。
  3. 浅层响应的 proxy 实例,即一个对象只有第一层的属性是响应式的。
  4. 只读的浅层响应的 proxy 实例。

浅层响应的 proxy 实例是什么?

之所以有浅层响应的 proxy 实例,是因为 proxy 只代理对象的第一层属性,更深层的属性是不会代理的。如果确实需要生成完全响应式的 proxy 实例,就得递归调用 reactive()。不过这个过程是内部自动执行的,用户感知不到。

其他一些函数介绍
// 判断 value 是否是响应式的
export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.raw])
  }
  return !!(value && (value as Target)[ReactiveFlags.isReactive])
}
// 判断 value 是否是只读的
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.isReadonly])
}
// 判断 value 是否是 proxy 实例
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}
// 将响应式数据转为原始数据,如果不是响应数据,则返回源数据
export function toRaw<T>(observed: T): T {
  return (
    (observed && toRaw((observed as Target)[ReactiveFlags.raw])) || observed
  )
}
// 给 value 设置 skip 属性,跳过代理,让数据不可被代理
export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.skip, true)
  return value
}


目录
相关文章
|
1天前
|
JavaScript 定位技术 API
在 vue3 中使用高德地图
在 vue3 中使用高德地图
8 0
|
1天前
vue3 键盘事件 回车发送消息,ctrl+回车 内容换行
const textarea = textInput.value.textarea; //获取输入框元素
9 3
|
4天前
|
JavaScript 前端开发 CDN
vue3速览
vue3速览
14 0
|
4天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
|
4天前
|
JavaScript 前端开发 安全
Vue3官方文档速通(下)
Vue3官方文档速通(下)
15 0
|
4天前
|
JavaScript API
Vue3 官方文档速通(中)
Vue3 官方文档速通(中)
20 0
|
4天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(上)
Vue3 官方文档速通(上)
26 0
|
4天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
8 1
|
4天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
10 0
|
4天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
8 0