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 方法能监听到对象属性的取值和设置值的操作。

小结

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

仓库地址,点击访问

目录
相关文章
|
4天前
|
JavaScript 前端开发
Vue 2 和 Vue 3 之间响应式区别
10月更文挑战第7天
18 2
|
6天前
|
缓存 JavaScript API
Vue 3的全新Reactivity API:解锁响应式编程的力量
Vue 3引入了基于Proxy的全新响应式系统,提升了性能并带来了更强大的API。本文通过示例详细介绍了`reactive`、`ref`、`computed`、`watch`等核心API的使用方法,帮助开发者深入理解Vue 3的响应式编程。无论你是初学者还是资深开发者,都能从中受益,构建更高效的应用程序。
6 1
|
8天前
|
缓存 JavaScript API
Vue 3的全新Reactivity API:解锁响应式编程的力量
【10月更文挑战第9天】Vue 3的全新Reactivity API:解锁响应式编程的力量
11 3
|
6天前
|
缓存 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 的设计理念及其在构建现代前端应用中的优势。
16 0
深入理解 Vue 3 的 Composition API 与新特性
|
8天前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
8天前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
12天前
|
存储 数据可视化 JavaScript
可视化集成API接口请求+变量绑定+源码输出
可视化集成API接口请求+变量绑定+源码输出
27 4
|
12天前
|
存储 前端开发 JavaScript
深入理解Vue3的组合式API及其实践应用
【10月更文挑战第5天】深入理解Vue3的组合式API及其实践应用
38 0
|
2天前
|
编解码 监控 API
直播源怎么调用api接口
调用直播源的API接口涉及开通服务、添加域名、获取API密钥、调用API接口、生成推流和拉流地址、配置直播源、开始直播、监控管理及停止直播等步骤。不同云服务平台的具体操作略有差异,但整体流程简单易懂。
|
15天前
|
人工智能 自然语言处理 PyTorch
Text2Video Huggingface Pipeline 文生视频接口和文生视频论文API
文生视频是AI领域热点,很多文生视频的大模型都是基于 Huggingface的 diffusers的text to video的pipeline来开发。国内外也有非常多的优秀产品如Runway AI、Pika AI 、可灵King AI、通义千问、智谱的文生视频模型等等。为了方便调用,这篇博客也尝试了使用 PyPI的text2video的python库的Wrapper类进行调用,下面会给大家介绍一下Huggingface Text to Video Pipeline的调用方式以及使用通用的text2video的python库调用方式。