从零到一打造 Vue3 响应式系统 Day 28 - shallowRef、shallowReactive

refreactive 都属于深层响应 (deep) API。它们会递归地将内部所有嵌套对象都转换为响应式代理。在多数情况下这非常方便,但当处理大型数据结构时,这种深度监听的性能开销可能会造成瓶颈。这时候我们就会使用到 shallowRefshallowReactive

ShallowRef

shallowRef 会创建一个 ref,但只对 .value 属性本身的赋值操作具有响应性,它并不会将 .value 的内容(如果是对象)转换为响应式对象。

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import { shallowRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'

    const c = shallowRef({
        count: 1
    });

    effect(() => {
      console.log(c.value.count)
    })

    setTimeout(() => {
      c.value.count++ // 这一行不会触发 effect
    }, 1000)
  </script>
</body>

查看上述代码,你会观察到 effect 只输出一次。一秒钟后 setTimeout 执行了 c.value.count++,但这并没有触发 effect 再次执行。

如果希望 effect 再次执行,必须修改 c.value 本身,例如改为 c.value = { count: 2 }

依照惯例,我们 console.log(c),可以看到它是一个 RefImpl 类的实例,但差异在于 __v_isShallow 标记为 true。由此我们可以知道 refshallowRef 复用了相同的类,只是通过这个标记来告知 RefImpl 是否需要进行深层响应。

TypeScript 复制代码
// ref.ts

// 如果 value 是对象,则使用 reactive 转换为响应式对象
const convert = (value) => isObject(value) ? reactive(value) : value

class RefImpl implements Dependency {
  _value;
  [ReactiveFlags.IS_REF] = true
  private _isShallow: boolean // 增加一个私有标记

  subs: Link
  subsTail: Link
  constructor(value, isShallow: boolean) {
    this._isShallow = isShallow // 保存标记
    // 如果是 shallow,直接使用原始值;否则,进行 convert 转换
    this._value = isShallow ? value : convert(value)
  }

  get value() {
    if (activeSub) {
      trackRef(this)
    }
    return this._value
  }

  set value(newValue) {
    // 如果新值和旧值发生过变化,则更新
    if (hasChanged(newValue, this._value)) {
      // 如果是 shallow,直接使用新值;否则,进行 convert 转换
      this._value = this._isShallow ? newValue : convert(newValue)
      triggerRef(this)
    }
  }
}

所以我们的实现思路是:

  • RefImpl 类增加一个私有的 _isShallow 属性。
  • 将"检查值是否为对象,若是则用 reactive 转换"的逻辑封装成 convert 函数。
  • constructorsetter 中,根据 _isShallow 标记来决定是直接赋值,还是要经过 convert 函数处理。

ShallowReactive

shallowReactive 只对对象的第一层属性进行代理,任何对嵌套对象的修改都不会触发响应。

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import {  shallowReactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'

    const state = shallowReactive({
      a: 1,
      b: {
        c: 1
      }
    })

    effect(() => {
      console.log(state.b.c)
    })

    setTimeout(() => {
      state.b.c++ // 这一行不会触发 effect
    }, 1000)
  </script>
</body>

shallowReactive 也是一样的效果,state.b.c++ 没有反应。需要将修改变为 state.b = { c: 2 }effect 才会触发输出。

reactive 是通过 mutableHandlers 来处理的,因此我们需要先重构 mutableHandlers

TypeScript 复制代码
// baseHandlers.ts

// 提取 set
const set = (target, key, newValue, receiver) => {
  // ...
}

// 提取 get
const get = (target, key, receiver) => {
  track(target, key)
  const res = Reflect.get(target, key, receiver)
  if (isRef(res)) {
    return res.value
  }
  if (isObject(res)) {
    // 默认是深层响应,递归调用 reactive
    return reactive(res)
  }
  return res
}

export const mutableHandlers = {
  get,
  set
}

我们将 get 的逻辑抽离成一个工厂函数 createGetter,并用 isShallow 参数来控制是否要递归地将嵌套对象转换为 reactive

TypeScript 复制代码
// baseHandlers.ts

function createGetter(isShallow = false) {
  return function get(target, key, receiver) {
    track(target, key)
    const res = Reflect.get(target, key, receiver)
    if (isRef(res)) {
      return res.value
    }
    if (isObject(res)) {
      // 关键:如果是 shallow,直接返回原始对象,不再递归调用 reactive
      return isShallow ? res : reactive(res)
    }
    return res
  }
}

// 创建深层 getter
const get = createGetter()
// 创建浅层 getter
const shallowGet = createGetter(true)

// ... (set 逻辑不变)

export const mutableHandlers = {
  get,
  set
}

export const shallowReactiveHandlers = {
  get: shallowGet, // 使用浅层 getter
  set
}

创建了不同的 handler 并导出后,我们在 reactive.ts 中引入它们。

TypeScript 复制代码
// reactive.ts
import { mutableHandlers, shallowReactiveHandlers } from './baseHandlers'

// 缓存 reactive 创建的代理
const reactiveMap = new WeakMap()
// 新增 shallowReactive 的缓存
const shallowReactiveMap = new WeakMap()

// 重构 createReactiveObject,使其可以接收 handlers 和 map
function createReactiveObject(target, handlers, proxyMap) {
  if (!isObject(target)) return target

  // ... (检查是否已存在代理的逻辑)
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 根据传入的 handlers 创建代理
  const proxy = new Proxy(target, handlers)

  // 存储 target 和响应式对象的关联关系
  proxyMap.set(target, proxy)
  
  // reactiveSet.add(proxy) // (注:原文有 reactiveSet,这里简化为只用 map)

  return proxy
}

export function reactive(target) {
  return createReactiveObject(target, mutableHandlers, reactiveMap)
}

// 新增 shallowReactive 函数
export function shallowReactive(target) {
  return createReactiveObject(target, shallowReactiveHandlers, shallowReactiveMap)
}

这样我们就完成了 shallowReactive

"深层响应"为我们带来了开发上的便利,而"浅层响应"则让我们有进一步优化性能的机会。在一般开发过程中,我们应该优先使用 refreactive,当遇到明确的性能瓶颈时,可以观察我们自己在使用数据的情况,适度地替换为 shallow 版本。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
Coffeeee2 小时前
Labubu很难买?那是因为还没有用Compose来画一个
前端·kotlin·android jetpack
用户3421674905522 小时前
Java高手速成--吃透源码+手写组件+定制开发教程
前端·深度学习
却尘2 小时前
当你敲下 `pnpm run dev`,这台机器到底在背后干了什么?
前端·javascript·面试
歪歪1002 小时前
React Native开发有哪些优势和劣势?
服务器·前端·javascript·react native·react.js·前端框架
却尘2 小时前
Vite 炸裂快,Webpack 稳如山,Turbopack 想两头要:谁才是下一个王?
前端·面试·vite
北辰alk2 小时前
React 多组件状态管理:从组件状态到全局状态管理全面指南
前端
一个很帅的帅哥2 小时前
伪类选择器和伪元素选择器
javascript
葡萄城技术团队3 小时前
SpreadJS ReportSheet 与 DataManager 实现 Token 鉴权:全流程详解与代码解析
前端
勤劳打代码3 小时前
触类旁通 —— Flutter 与 React 对比解析
前端·flutter·react native