Vue3 响应式原理(二)

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

baseHandlers.ts 文件

baseHandlers.ts 文件中针对 4 种 proxy 实例定义了不对的处理器。

由于它们之间差别不大,所以在这只讲解完全响应式的处理器对象:

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

处理器对五种操作进行了拦截,分别是:

  1. get 属性读取
  2. set 属性设置
  3. deleteProperty 删除属性
  4. has 是否拥有某个属性
  5. ownKeys

其中 ownKeys 可拦截以下操作:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. Reflect.ownKeys()

其中 get、has、ownKeys 操作会收集依赖,set、deleteProperty 操作会触发依赖。

get

get 属性的处理器是用 createGetter() 函数创建的:

// /*#__PURE__*/ 标识此为纯函数 不会有副作用 方便做 tree-shaking
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // target 是否是响应式对象
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
      // target 是否是只读对象
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (
      // 如果访问的 key 是 __v_raw,并且 receiver == target.__v_readonly || receiver == target.__v_reactive
      // 则直接返回 target
      key === ReactiveFlags.raw &&
      receiver ===
        (isReadonly
          ? (target as any).__v_readonly
          : (target as any).__v_reactive)
    ) {
      return target
    }
    const targetIsArray = isArray(target)
    // 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
    // 如果不用 Reflect 来获取,在监听数组时可以会有某些地方会出错
    // 具体请看文章《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
    const res = Reflect.get(target, key, receiver)
    // 如果 key 是 symbol 并且属于 symbol 的内置方法之一,或者访问的是原型对象,直接返回结果,不收集依赖。
    if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
      return res
    }
    // 只读对象不收集依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 浅层响应立即返回,不递归调用 reactive()
    if (shallow) {
      return res
    }
    // 如果是 ref 对象,则返回真正的值,即 ref.value,数组除外。
    if (isRef(res)) {
      // ref unwrapping, only for Objects, not for Arrays.
      return targetIsArray ? res : res.value
    }
    if (isObject(res)) {
      // 由于 proxy 只能代理一层,所以 target[key] 的值如果是对象,就继续对其进行代理
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

这个函数的处理逻辑看代码注释应该就能明白,其中有几个点需要单独说一下:

  1. Reflect.get()
  2. 数组的处理
  3. builtInSymbols.has(key) 为 true 或原型对象不收集依赖
Reflect.get()

Reflect.get() 方法与从对象 (target[key]) 中读取属性类似,但它是通过一个函数执行来操作的。

为什么直接用 target[key] 就能得到值,却还要用 Reflect.get(target, key, receiver) 来多倒一手呢?

先来看个简单的示例:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
    }
})
p.push(100)

运行这段代码会报错:

Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'

但做一些小改动就能够正常运行:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
        return true // 新增一行 return true
    }
})
p.push(100)

这段代码可以正常运行。为什么呢?

区别在于新的这段代码在 set() 方法上多了一个 return true。我在 MDN 上查找到的解释是这样的:

set() 方法应当返回一个布尔值。

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。

这时我又试了一下直接执行 p[3] = 100,发现能正常运行,只有执行 push 方法才报错。到这一步,我心中已经有答案了。为了验证我的猜想,我在代码上加了 console.log(),把代码执行过程的一些属性打印出来。

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        console.log('get: ', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        target[key] = value
        return true
    }
})
p.push(100)
// get:  push
// get:  length
// set:  3 100
// set:  length 4

从上面的代码可以发现执行 push 操作时,还会访问 length 属性。推测执行过程如下:根据 length 的值,得出最后的索引,再设置新的置,最后再改变 length

结合 MDN 的解释,我的推测是数组的原生方法应该是运行在严格模式下的(如果有网友知道真相,请在评论区留言)。因为在 JS 中很多代码在非严格模式和严格模式下都能正常运行,只是严格模式会给你报个错。就跟这次情况一样,最后设置 length 属性的时候报错,但结果还是正常的。如果不想报错,就得每次都返回 true

然后再看一下 Reflect.set() 的返回值说明:

返回一个 Boolean 值表明是否成功设置属性。

所以上面代码可以改成这样:

const p = new Proxy([1, 2, 3], {
    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.push(100)

另外,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

通过上面的示例,不难理解为什么要通过 Reflect.set() 来代替 Proxy 完成默认操作了。同理,Reflect.get() 也一样。

数组的处理
// 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

在执行数组的 includes, indexOf, lastIndexOf 方法时,会把目标对象转为 arrayInstrumentations 再执行。

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    // 如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。
    // 所以这里的 this 指向 receiver,即 proxy 实例,toRaw 为了取得原始数据
    const arr = toRaw(this) as any
    // 对数组的每个值进行 track 操作,收集依赖
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    // 参数有可能是响应式的,函数执行后返回值为 -1 或 false,那就用参数的原始值再试一遍
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

从上述代码可以看出,Vue3.0 对 includes, indexOf, lastIndexOf 进行了封装,除了返回原有方法的结果外,还会对数组的每个值进行依赖收集。

builtInSymbols.has(key) 为 true 或原型对象不收集依赖
const p = new Proxy({}, {
    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.toString() // get:  toString
             // get:  Symbol(Symbol.toStringTag)
p.__proto__  // get:  __proto__

p.toString() 的执行结果来看,它会触发两次 get,一次是我们想要的,一次是我们不想要的(我还没搞明白为什么会有 Symbol(Symbol.toStringTag),如果有网友知道,请在评论区留言)。所以就有了这个判断: builtInSymbols.has(key)true 就直接返回,防止重复收集依赖。

再看 p.__proto__ 的执行结果,也触发了一次 get 操作。一般来说,没有场景需要单独访问原型,访问原型都是为了访问原型上的方法,例如 p.__proto__.toString() 这样使用,所以 key 为 __proto__ 的时候也要跳过,不收集依赖。

set

const set = /*#__PURE__*/ createSetter()
// 参考文档《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 如果原来的值是 ref,但新的值不是,将新的值赋给 ref.value 即可。
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 如果 target 没有 key,就代表是新增操作,需要触发依赖
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果新旧值不相等,才触发依赖
        // 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter
        // 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变
        // 但由于 length 也是自身属性,所以 value === oldValue
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set() 的函数处理逻辑反而没那么难,看注释即可。track()trigger() 将放在下面和 effect.ts 文件一起讲解。

deleteProperty、has、ownKeys

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)
  // 如果删除结果为 true 并且 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)
  track(target, TrackOpTypes.HAS, key)
  return result
}
function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

这三个函数比较简单,看代码即可。

目录
相关文章
|
2月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
577 139
|
2月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
273 1
|
7月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
1023 5
|
3月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
438 11
|
2月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
306 0
|
4月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
493 1
|
4月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
293 0
|
5月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
146 0
|
7月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
650 17
|
8月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点