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

相关推荐
腾讯TNTWeb前端团队2 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom7 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom7 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom7 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试