简谈vue响应式原理

对面试Vue的小伙伴来说,Vue的响应式原理是备考项,而且相信大家都能脱口而出两个API:Object.defineProperty(Vue2)和Proxy(Vue3)。

当一个理论已经成为常识的阶段时,就不得不往下卷了。

那么问题来了:Vue3为什么要改用Proxy?

肯定是更好呗!那好在哪里呢?解决了什么问题呢?

我们先来看一段代码:

js 复制代码
// 原始对象
const initData = {
  value: 1
}

// 新的响应式对象
const data = {}

// 修改访问器属性
Object.keys(initData).forEach(key => {
  Object.defineProperty(data, key, {
    get() {
      console.log('访问key', key)
      return initData[key]
    },
    set(v) {
      console.log('修改key', key)
      initData[key] = v
    }
  })
})

给定一个原始对象initData,定义一个响应式对象data,默认值为空。通过遍历initData对象的键,修改data对象的访问器属性。data可以看成是对initData的拷贝,并且在每个属性上增加了get和set操作。当访问data已有属性的值时,会执行get的逻辑;当修改data已有属性的值时,会执行set的逻辑。在此我们可以通过console来查看get和set的执行时机。

控制台输入data.value,回车,可以看到"访问"信息输出。

控制台输入data.value = 3,回车,可以看到"修改"信息输出。

再次输入data.value,回车,可以看到data.value的值已变为3。并且initData的值也变为了3。

这就是vue2响应式核心原理的最简版(当然也忽略了很多细节,不过不影响说明)。

假如我们再给data增加一个属性,输入data.value1 = 1,回车,这次没有"修改"信息输出。

然后输入data.value1,回车,这次没有"访问"信息输出。

我们发现,在原有属性上访问和修改值都可以触发访问器属性操作,但属性的增加无法触发依赖收集,进而无法触发访问器属性操作。而这也是vue2中响应式存在的问题。

为此vue2中推出set用于数组或对象增删成员的响应式操作。以下是vue2中set的源码:

js 复制代码
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // if (process.env.NODE_ENV !== 'production' &&
  //   (isUndef(target) || isPrimitive(target))
  // ) {
  //   warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  // }

  // 数组操作
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  // 对象操作
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // if (target._isVue || (ob && ob.vmCount)) {
  //   process.env.NODE_ENV !== 'production' && warn(
  //     'Avoid adding reactive properties to a Vue instance or its root $data ' +
  //     'at runtime - declare it upfront in the data option.'
  //   )
  //   return val
  // }
  if (!ob) {
    target[key] = val
    return val
  }
  // 重新收集依赖
  defineReactive(ob.value, key, val)
  // 通知视图更新
  ob.dep.notify()
  return val
}

先将"process.env.NODE_ENV !== "字样的部分直接忽略,因为这部分代码不影响整体逻辑的梳理。

当目标对象为数组时,若操作的索引有效,则先将数组长度扩充为索引与原数组长度的最大值,然后利用splice方法直接修改或新增原数组对应索引值,进而再次触发了依赖的收集过程,响应式对象的值也就同步更新。

当目标对象为普通对象时,若操作的key已存在于目标对象中(仅修改操作)时,直接修改key对应的值。若操作的key不存在于目标对象中(仅新增操作)时,直接新增,并且若目标对象为响应式对象时,会重新将新{key,value}增加响应式,然后进行依赖收集,通知更新视图 。

总结一下:无论数组还是对象,用$set修改或新增属性会触发依赖收集过程,进而操作响应式对象时,会触发访问器属性。

接下来是Vue3响应式原理proxy,先看下面的代码:

js 复制代码
const initData = {
  value: 1
}

const proxy = new Proxy(initData, {
  get(target, key, receiver) {
    console.log('访问属性', target, key, receiver)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('修改属性', target, key, value, receiver)
    return Reflect.set(target, key, value, receiver)
  }
})

代码中通过new Proxy基于initData对象创建了代理对象proxy,其中第二个参数设置了get和set操作逻辑。

同样的操作再执行一下:

控制台输入proxy.value,回车,可以看到"访问"信息输出。

控制台输入proxy.value = 2,回车,可以看到"修改"信息输出。此时目标对象initData的value值也同步更新为2。

控制台输入initData.value1 = 1,回车,然后输入proxy.value1, 回车,可以看到"修改"信息输出。此时目标对象proxy也新增了value1属性,值也同步更新为1,同时有"访问"信息输出。

控制台输入proxy.value2 = 3,回车,然后查看initData对象中也有value2属性,值也为3。

总结一下,无论是操作目标对象,还是操作代理对象,Vue3中通过Proxy实现响应式,新增(删除)和修改属性操作都可以保持响应式同步。

相关推荐
saber_andlibert1 小时前
TCMalloc底层实现
java·前端·网络
逍遥德1 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
冻感糕人~1 小时前
【珍藏必备】ReAct框架实战指南:从零开始构建AI智能体,让大模型学会思考与行动
java·前端·人工智能·react.js·大模型·就业·大模型学习
程序员agions1 小时前
2026年,“配置工程师“终于死绝了
前端·程序人生
alice--小文子1 小时前
cursor-mcp工具使用
java·服务器·前端
晚霞的不甘2 小时前
揭秘 CANN 内存管理:如何让大模型在小设备上“轻装上阵”?
前端·数据库·经验分享·flutter·3d
小迷糊的学习记录2 小时前
0.1 + 0.2 不等于 0.3
前端·javascript·面试
空&白2 小时前
vue暗黑模式
javascript·vue.js
梦帮科技2 小时前
Node.js配置生成器CLI工具开发实战
前端·人工智能·windows·前端框架·node.js·json
VT.馒头3 小时前
【力扣】2695. 包装数组
前端·javascript·算法·leetcode·职场和发展·typescript