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

相关推荐
程序媛-徐师姐5 分钟前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
yngsqq6 分钟前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing40 分钟前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风43 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave1 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟1 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾1 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm2 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
chusheng18402 小时前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站