简谈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实现响应式,新增(删除)和修改属性操作都可以保持响应式同步。

相关推荐
程序员码歌2 小时前
短思考第261天,浪费时间的十个低效行为,看看你中了几个?
前端·ai编程
Swift社区3 小时前
React Navigation 生命周期完整心智模型
前端·react.js·前端框架
若梦plus3 小时前
从微信公众号&小程序的SDK剖析JSBridge
前端
用泥种荷花3 小时前
Python环境安装
前端
Light604 小时前
性能提升 60%:前端性能优化终极指南
前端·性能优化·图片压缩·渲染优化·按需拆包·边缘缓存·ai 自动化
Jimmy4 小时前
年终总结 - 2025 故事集
前端·后端·程序员
烛阴4 小时前
C# 正则表达式(2):Regex 基础语法与常用 API 全解析
前端·正则表达式·c#
roman_日积跬步-终至千里4 小时前
【人工智能导论】02-搜索-高级搜索策略探索篇:从约束满足到博弈搜索
java·前端·人工智能
GIS之路4 小时前
GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据
前端
多看书少吃饭4 小时前
从Vue到Nuxt.js
前端·javascript·vue.js