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

相关推荐
Redstone Monstrosity14 分钟前
字节二面
前端·面试
东方翱翔21 分钟前
CSS的三种基本选择器
前端·css
Fan_web44 分钟前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
冯宝宝^1 小时前
基于mongodb+flask(Python)+vue的实验室器材管理系统
vue.js·python·flask
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js