Vue3源码学习(2):响应式API reactive 的实现

简介: 本文介绍了 Vue2 和 Vue3 在响应式实现原理上的区别,并介绍了如何使用 Proxy 对象实现响应式 API reactive。

Vue2 和 Vue3 响应式的区别

Vue2 的响应式,利用了 ES5 的一个 API :Object.defineProperty。它的基本用法是这样的:

const obj = {name: 'kw'}
Object.defineProperty(obj, key, {
    get() {
        return obj[key]
    },
    set(val) {
        obj[key] = val
    }
})

这样,就能拦截到对象属性的基本操作,比如访问属性和给属性设置新值。当拦截到访问属性时,可以做依赖收集;当监听到属性更改时,可以做派发更新,从而实现响应式。

它存在几个缺点:

1、重写了对象的属性,性能较差;

2、只能拦截到对象属性的操作,不能处理数组。所以 Vue2 需要单独对数组数据进行处理。

3、对于属性的新增和删除,无法拦截到。所以额外提供了 $set$delete 方法,整体不和谐。

Vue3 采用了 ES6 的API Proxy 来实现响应式。由于该 API 不兼容 IE 浏览器,所以在使用 Vue3 开发时要考虑项目是否需要兼容 IE系列。

Proxy 和 Reflect

先来看下 Proxy 的基本语法:

const proxy = new Proxy(target, handler);

target:用 Proxy 包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler:是一个对象,其声明了代理 target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。

简单示例:

const target = {
  name: "kw",
  age: 18
};

const handler = {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
      target[key] = value
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // "kw"
proxy.name = 'zk'  
console.log(proxy.name); // "zk"

可以看到,Proxy 的使用其实和 Object.defineProperty是差不多的,也是能拦截到对象属性的一些操作。但它的特点是:

1、不仅可以代理普通的对象,还可以代理数组,函数

2、不仅能拦截到 getset 操作,还支持 applydelete 等一共13种操作

3、不需要重写 target,性能更高

再来看一下 Reflect 对象。

ProxyReflect 是一对好兄弟,形影不离。

按照 MDN 文档的说明:

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。

Reflect不是一个函数对象,因此它是不可构造的。

也就是说,我们在使用 Proxy 时传入的 handler 参数,它所有的属性,在 Reflect 中都有一一对应的。比如上面我们说了 Proxy 对象的 handler 可以支持 getgetapplydelete 操作,那么 Reflect 对象就提供了对应的静态方法:

const person = {
  name: 'kw',
  age: 18,
  sayHello: function() {
    console.log(`Hello! 我是${this.name}`);
  }
}

// 返回 name 属性的值
Reflect.get(person, 'name'); // 'kw'

// 执行 sayHello 方法
// param1:要执行的函数
// param2:指定 this
// param3:函数执行需要的参数
Reflect.apply(person.sayHello, person, []); // Hello! 我是kw

// 更新 age 属性。属性设置成功,返回 true
Reflect.set(person, 'age', 20); // true
Reflect.get(person, 'age'); // 20

初看起来,Reflect 的使用很繁琐,远不如传统的点语法来的方便简洁。确实如此,但是在这些 API 的设计上,它和 Proxy 拥有一致的属性和方法,所以搭配起来更加合适。再者,有些场景下,比如需要用到 receiver 参数时,此时就只有 Reflect 能堪大任了。

reactive 的基本使用

官网文档,点击访问

import { reactive } from 'vue'

const obj = {name: 'kw', age: 18, skill: ['JS', 'Vue']}

// 返回对象的响应式代理对象,并且是深层次的代理,所有嵌套的属性包括数组,都能被代理到
const state = reactive(obj)

// 修改响应式对象,组件可以自动更新
state.name = 'zk'
state.age = 20
state.skill.push('Node')

使用 reactive 时需要注意的地方:

  1. 只能实现对象数据的响应式
  2. 同一个对象,只会被代理一次
  3. 被代理过的对象,不会被再次代理
  4. 支持嵌套属性的响应式

实现 reactive

这是 Vue3 中一个最基础的响应式 API,它内部采用了 Proxy 来实现对象属性的拦截操作。

如下:

// reactivity/src/reactive.ts

import { isObject } from '@my-vue/shared'

export function reactive (target) {
   
  // 只能代理对象
  if(!isObject(target)) {
    return target
  }
  
  const handler = {
    // 监听属性访问操作
    get(target, key, receiver) {
      console.log(`${key}属性被访问,依赖收集`)
      return Reflect.get(target, key)
    },
    
    // 监听设置属性操作
    set(target, key, value, receiver) {
      console.log(`${key}属性变化了,派发更新`)
     
      // 当属性的新值和旧值不同时,再进行设置
      if(target[key] !== value) {
         const result = Reflect.set(target, key, value, receiver);;
         return result
      }
    }
  }
  
  // 实例化代理对象
  const proxy = new Proxy(target, handler)

  return proxy
}

无需多次代理

前面我们提到,如果一个对象被代理过了,就无需再被代理。实现的思路就是利用缓存,将代理过的对象进行缓存,每当调用 reactive 方法时,先判断缓存中是否存在 target ;每次 target 被代理后,都将 targetproxy 放到缓存中:

// 缓存响应式对象
const reactiveMap = new WeakMap

export function reactive (target) {
  // ...
    
  // 判断 target 是否被代理过。如果被代理过,则直接返回代理对象
  const existing = reactiveMap.get(target)
  if(existing) {
    return existing
  }
  
  // ...

  // 将代理对象进行缓存
  reactiveMap.set(target, proxy)

  return proxy
}

仅能够被代理一次

除此之外,对于一个已经被代理的对象 proxy,再次调用响应式 API 时,应该直接返回该 proxy 对象,而不会对 proxy 再做一次代理。

它的实现思路也很简单,是通过标识符的方式进行判断。

Vue2 中通过给 Observer 实例 增加一个__ob__属性作为标识,表示它已经被观测过了,无需再被观测。Vue3 是通过给 proxy 对象增加一个__v_isReactive 属性,表示该 proxy 对象已经是响应式数据了,从而无需再被代理:

const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
} 

export function reactive (target) {
  
  // 判断 target 是否是响应式对象
  // 每当一个 target 需要响应式时,先判断其有没有该属性。此时产生属性访问操作,如果 target 被代理过,则会进入下面的 get 方法中,做进一步的判断。
  if(target[ReactiveFlags.IS_REACTIVE]) {
    return target
  }
    
  // ...
  
  const handler = {
    // 监听属性访问操作
    get(target, key, receiver) {
      // 访问到 __v_isReactive 属性时,说明此时的 target 其实是一个 proxy 对象,无需再被代理
      if(key === ReactiveFlags.IS_REACTIVE) {
        return true
      }
      console.log(`${key}属性被访问,依赖收集`)
      return Reflect.get(target, key)
    },  
  }
  
   // ...
}

嵌套代理

Vue3 实现响应式采用的原则是懒代理,并不像 Vue2 那样在初始化时,就递归所有的属性进行属性重写。
只有在访问到某个属性,且该属性是对象类型时,才会再进行一层响应式包装:

到此,我们实现的 reactive 方法,就能监听到对象属性的访问和设置操作,从而在此时机做一些处理,从而实现响应式系统。同时也做了一些优化处理。

reactive 方法通过响应式模块的入口文件对外暴露出去:

// reactivity/src/index.ts

export { reactive } from './reactive' 

测试

执行打包命令:

pnpm dev

编写测试文件:

// test/1.reactive.html

<script src="../dist/reactivity.global.js"></script>

<script>
  // VueReactivity:在响应式模块的 package.json 中通过 buildOptions.name 起的名字
  const { reactive } = VueReactivity

  const obj = { name: 'kw', age: 18, grade: { math: 88 } }

  const state = reactive(obj)

  // 访问响应式对象的属性
  console.log(state.name)
  console.log(state.grade.math)
  // 修改响应式对象的属性
  state.age = 20
</script>

打开浏览器,查看控制台:

和我们预想的一样,reactive 方法能监听到对象属性的取值和设置值的操作。

小结

这只是实现响应式系统的第一步。我们还需要在访问属性时,做依赖收集,也就是记录下此刻谁用到了这个属性;当以后属性发生变化时,就可以告诉它,你用到的属性更新了,你也可以更新一下啦。这样,就能实现一个完整的响应式系统了。下一篇文章,我们就来实现这个功能。

仓库地址,点击访问

目录
相关文章
|
10天前
|
API 数据安全/隐私保护 UED
探索鸿蒙的蓝牙A2DP与访问API:从学习到实现的开发之旅
在掌握了鸿蒙系统的开发基础后,我挑战了蓝牙功能的开发。通过Bluetooth A2DP和Access API,实现了蓝牙音频流传输、设备连接和权限管理。具体步骤包括:理解API作用、配置环境与权限、扫描并连接设备、实现音频流控制及动态切换设备。最终,我构建了一个简单的蓝牙音频播放器,具备设备扫描、连接、音频播放与停止、切换输出设备等功能。这次开发让我对蓝牙技术有了更深的理解,也为未来的复杂项目打下了坚实的基础。
98 58
探索鸿蒙的蓝牙A2DP与访问API:从学习到实现的开发之旅
|
5天前
|
人工智能 数据可视化 API
自学记录鸿蒙API 13:Calendar Kit日历功能从学习到实践
本文介绍了使用HarmonyOS的Calendar Kit开发日程管理应用的过程。通过API 13版本,不仅实现了创建、查询、更新和删除日程等基础功能,还深入探索了权限请求、日历配置、事件添加及查询筛选等功能。实战项目中,开发了一个智能日程管理工具,具备可视化管理、模糊查询和智能提醒等特性。最终,作者总结了模块化开发的优势,并展望了未来加入语音助手和AI推荐功能的计划。
117 1
|
3月前
|
缓存 JavaScript 前端开发
深入理解 Vue 3 的 Composition API 与新特性
本文详细探讨了 Vue 3 中的 Composition API,包括 setup 函数的使用、响应式数据管理(ref、reactive、toRefs 和 toRef)、侦听器(watch 和 watchEffect)以及计算属性(computed)。我们还介绍了自定义 Hooks 的创建与使用,分析了 Vue 2 与 Vue 3 在响应式系统上的重要区别,并概述了组件生命周期钩子、Fragments、Teleport 和 Suspense 等新特性。通过这些内容,读者将能更深入地理解 Vue 3 的设计理念及其在构建现代前端应用中的优势。
53 1
深入理解 Vue 3 的 Composition API 与新特性
|
2月前
|
JavaScript 前端开发 API
Vue 3新特性详解:Composition API的威力
【10月更文挑战第25天】Vue 3 引入的 Composition API 是一组用于组织和复用组件逻辑的新 API。相比 Options API,它提供了更灵活的结构,便于逻辑复用和代码组织,特别适合复杂组件。本文将探讨 Composition API 的优势,并通过示例代码展示其基本用法,帮助开发者更好地理解和应用这一强大工具。
40 2
|
3月前
|
API
《vue3第四章》Composition API 的优势,包含Options API 存在的问题、Composition API 的优势
《vue3第四章》Composition API 的优势,包含Options API 存在的问题、Composition API 的优势
32 0
|
3月前
|
JavaScript 前端开发 API
《vue3第六章》其他,包含:全局API的转移、其他改变
《vue3第六章》其他,包含:全局API的转移、其他改变
26 0
|
23天前
|
人工智能 自然语言处理 API
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
谷歌推出的Multimodal Live API是一个支持多模态交互、低延迟实时互动的AI接口,能够处理文本、音频和视频输入,提供自然流畅的对话体验,适用于多种应用场景。
69 3
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
|
10天前
|
JSON 安全 API
淘宝商品详情API接口(item get pro接口概述)
淘宝商品详情API接口旨在帮助开发者获取淘宝商品的详细信息,包括商品标题、描述、价格、库存、销量、评价等。这些信息对于电商企业而言具有极高的价值,可用于商品信息展示、市场分析、价格比较等多种应用场景。
|
18天前
|
前端开发 API 数据库
Next 编写接口api
Next 编写接口api
|
24天前
|
XML JSON 缓存
阿里巴巴商品详情数据接口(alibaba.item_get) 丨阿里巴巴 API 实时接口指南
阿里巴巴商品详情数据接口(alibaba.item_get)允许商家通过API获取商品的详细信息,包括标题、描述、价格、销量、评价等。主要参数为商品ID(num_iid),支持多种返回数据格式,如json、xml等,便于开发者根据需求选择。使用前需注册并获得App Key与App Secret,注意遵守使用规范。