Vue3响应式原理解析

news2024/11/23 17:25:49

前言

今年上半年开始,自己开始在新项目中使用 Vue3 进行开发,相比较于 Vue2 来说,最大的变化就是 composition Api 代替了之前的 options Api,更像是 React Hooks 函数式组件的编程方式。

Vue3相对于Vue2响应式原理也发生了变化,由原先的 Object.defineproperty 改成了使用 Proxy 替代。Proxy 相对于 Object.defineproperty 有以下几个优化点:

  • 对象新增属性不再需要手动 $set 添加响应式,Proxy 默认会监听动态添加属性和属性的删除等操作。
  • 消除无法监听数组索引,length 属性等等,不再需要在数组原型对象上重写数组的方法。
  • Object.defineproperty 是劫持所有对象属性的 get/set 方法,需要遍历递归去实现,Proxy 是代理整个对象。
  • Vue2 只能拦截对象属性的 getset 操作,而 Proxy 拥有 13 种拦截方法。

所有这些优化,都指向了同一个点:Vue3 将拥有更快的响应速度。下面,将结合代码揭秘 Vue3 实现响应式的原理。

Proxy

Proxy 能够为另一个对象创建代理,该代理可以拦截和重新定义该对象的基本操作,例如获取、设置和定义属性。

Proxy 接受两个参数:

  • 要代理的原始对象。
  • 一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。
const target = {name: "ts",age: 18
};
const handler = {};
const proxy = new Proxy(target, handler); 

我们可以在 handler 对象上定义函数做自定义代理:

const target = {name: "ts",age: "18"
};
const handler = {get(target, key, receiver) {console.log(`访问属性${key}值`)return Reflect.get(target, key, receiver)},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)},
};
const proxy = new Proxy(target, handler);
console.log(proxy.name)
proxy.name = 'jkl';
proxy.sex = 'male'; 

打印:

注意

  • set 方法,要求返回一个布尔值,而 Reflect.set 方法刚好就是一个返回一个布尔值,直接 return 就好了。
  • sex 属性是我们后面新增的,但是也能在 getset 中拦截到,说明 Proxy 是自动给新增属性添加响应式,而不需要手动 $set 添加响应式。

通过对 Proxy 用法的基本介绍,我们发现 ProxyObject.defineproperty 用法有一个相似之处,它们内部都有 getset 方法,我们可以在 getset 方法中拦截和重新定义一些逻辑处理,和 Object.defineproperty 一样,我们可以在 Proxyget 方法中进行依赖收集即 track 操作,在 set 方法中进行触发更新即 trigger 操作。

Reflect

Reflect定义

Reflect 是一个内置的对象,与 Math 类似,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy handlers 提供的的方法是一一对应的,且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。

Reflect.get/set 参数说明

  • target 指的是原始数据对象。
  • key 指的是操作的属性名。
  • newVal 指的是操作接收到的最新值。
  • receiver 指向的是当前操作正确的上下文,代理对象。

receiver 作用

receiver 是为了在执行对应的拦截操作的方法时能传递正确的 this 上下文。

reactive

基于上面对 Proxy 的基本使用,我们可以试着实现 reactive,在 Vue3 中 reactive 是返回一个 Proxy 的方法,接受一个对象作为参数:

基本实现

export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {return Reflect.get(target, key, receiver)},set(target, key, value, receiver) {return Reflect.set(target, key, value, receiver)}})
} 

如果 target 对象存在深层次结构,我们就需要递归实现:

递归完整实现

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)}})
} 

测试

<!DOCTYPE html>
<html lang="en">

<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>

<body><script type="module"> import { reactive } from './reactive.js'const proxy = reactive({name: 'ts', age: 18})console.log(proxy.name);console.log(proxy.age);proxy.name = 'jkl';proxy.age = 20; </script>
</body>

</html> 

收集依赖和触发更新

收集依赖和触发更新是 Vue3 响应式最核心的部分。这里涉及到三个核心概念:effecttracktrigger依赖收集依赖触发更新

访问代理对象 target 属性,会触发 get 方法,在这里会进行依赖收集即执行 track 方法。收集的依赖存储在 deps 里。修改 target 对象属性时,触发 set 方法,在这里会进行触发更新的操作即依次执行 deps 里面的依赖。

存储容器说明:

  • 选择 weakMap 类型作为容器是因为 weakMap 对键的引用是弱类型,当外部没有对键引用时,weakMap会自动删除,保证对象能被垃圾回收。
  • Map 类型对键的引用是强引用,即便外部没有对该对象保持引用,但至少还存在 Map 本身对该对象的引用关系,因此会导致该对象不能及时的被垃圾回收。
  • 对应的响应式数据对象作为 targetMap 的键,存储和当前响应式数据对象相关的依赖关系 depsMap,即 depsMap 存储的就是和当前响应式对象的每一个 key 对应的具体依赖。
  • deps 作为 depsMap 每个 key 对应的依赖集合,因为每个响应式数据可能在多个副作用函数中被使用,并且 Set 类型用于自动去重的能力。

effect

effect 依赖里面放着数据更新的逻辑,通常我们放在一个函数里面。

// activeEffect 表示当前正在走的 effect
let activeEffect = null;
export const effect = (fn:Function) => {activeEffect = fnfn()activeEffect = null
} 

这里使用一个全局变量 activeEffect 来收集当前正在走的副作用函数,并且初始化的时候调用一下。

let age = 18;
let result;
const effect = () => result = age * 2
age = 20;
effect();
console.log(result) // 40 

为了让大家理解 effect,上面这段代码是一个比较形象的例子:age 是一个变量,effect 是副作用函数,当 age 发生了变化 age = 20,这时候我们调用 effect(),更新了 result 值。在这里我们是手动写的调用 effect(),在真实响应式流程中,我们如何进行依赖收集以及自动触发更新 effect 呢?

track

track 函数用来进行依赖收集,即把依赖于变量的 effect 函数收集起来,放在 deps 里面,deps 是一个 Set 数据结构。

const targetMap = new WeakMap()
export const track = (target, key) => {// 没有activeEffect就不进行追踪if (!activeEffect) return// 获取target的依赖图let depsMap = targetMap.get(target)// 没有就新建if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 获取key所对应依赖的集合let deps = depsMap.get(key)// 没有就新建if (!deps) {deps = new Set()depsMap.set(key, deps)}// 判断activeEffect是否存在,不存在才添加,防止重复添加if (!deps.has(activeEffect)) {deps.add(activeEffect)}
} 

在介绍 Proxy 的时候,我们提到“我们会在 Proxy 的 get 方法中进行依赖收集即 track 操作”,现在我们可以把 track 添加到 get 方法中了:

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 收集依赖track(target,key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)}})
} 

trigger

实现 trigger

const targetMap = new WeakMap()
export const trigger = (target, key) => {// 获取target的依赖图const depsMap = targetMap.get(target)// 没有说明没有被追踪,就returnif (!depsMap) return// 获取key所对应依赖的集合const deps = depsMap.get(key)// 遍历依赖的集合,依次执行副作用函数if (deps) {deps.forEach(effect => effect())}
} 

在介绍 Proxy 的时候,我们提到“在 set 方法中进行触发更新即 trigger 操作”,现在我们可以把 trigger 添加到 set 方法中了:

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log('访问属性"+key+"值')const result = Reflect.get(target, key, receiver)// 收集依赖track(target,key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)// 触发更新trigger(target, key)return Reflect.set(target, key, value, receiver)}})
} 

完整代码

core.js

// activeEffect 表示当前正在走的 effect
let activeEffect = null
export const effect = fn => {activeEffect = fnfn()activeEffect = null
}

const targetMap = new WeakMap()
export const track = (target, key) => {// 没有activeEffect就不进行追踪if (!activeEffect) return// 获取target的依赖图let depsMap = targetMap.get(target)// 没有就新建if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 获取key所对应依赖的集合let deps = depsMap.get(key)// 没有就新建if (!deps) {deps = new Set()depsMap.set(key, deps)}// 判断activeEffect是否存在,不存在才添加,防止重复添加if (!deps.has(activeEffect)) {deps.add(activeEffect)}
}

export const trigger = (target, key) => {// 获取target的依赖图const depsMap = targetMap.get(target)// 没有说明没有被追踪,就returnif (!depsMap) return// 获取key所对应依赖的集合const deps = depsMap.get(key)console.log(deps, 'deps=====')// 遍历依赖的集合,依次执行副作用函数if (deps) {deps.forEach(effect => effect())}
} 

reactive.js

import { track, trigger } from './core.js'
const isObject = target => target !== null && typeof target == 'object'
export const reactive = target => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 收集依赖track(target, key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)// 触发更新trigger(target, key)return Reflect.set(target, key, value, receiver)}})
} 

index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div class="box"></div><button>修改</button><script type="module"> import { reactive } from './reactive.js'import { effect } from './core.js'const user = reactive({name: 'ts'})effect(() => { document.querySelector('.box').innerText = `${user.name}` })document.querySelector('button').onclick = function () {user.name = 'jkl'} </script>
</body>
</html> 

效果

总结

Vue2 和 Vue3 实现响应式的思路或者核心都是相同的,即数据劫持/对象代理(自定义get / set)、依赖收集、触发更新。Vue3 使用 Proxy 实现响应式是对 Object.defineproperty 实现方案存在缺陷的一种优化。

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue日期组件el-date-picker中更改默认日期格式并且实时显示的方法

在项目中有一个需求是这样的,要求实时显示他的当前默认时间,并且不能修改 使用了默认:default-value"currentTime"属性之后,新增的时候会报错,前端与后端传递的数据不匹配 因为默认时间被new date() 解析之后返回的数据是默认时间形式的,格式不符 方法如下: 第一步&a…

Elasticsearch入门 - Mac上Elasticsearch和Kibana的安装运行与简单使用

文章目录一&#xff0c;Mac上Elasticsearch和Kibana的安装1.1 环境与下载1.2 安装与运行1.3 问题1.3.1 elasticsearch安装后其他机器不能访问1.3.2 kibana安装后其他机器不能访问二&#xff0c;Elasticsearch在Kibana的常见命令2.1 查看集群的健康状态2.2 索引2.2.1 查看所有索…

Scrum 敏捷开发

什么是敏捷开发 敏捷 开发是一个术语&#xff0c;用于描述迭代软件开发。 迭代软件开发通过在短增量完成工作&#xff08;通常称为 冲刺&#xff0c; Sprint&#xff09;来缩短 DevOps 生命周期。 冲刺通常长达一到四周。 敏捷开发通常与传统或瀑布式开发形成鲜明对比&#xff…

Vue基础9之脚手架的使用、ref属性、props配置项和mixin混入

Vue基础9使用Vue脚手架初始化脚手架说明具体步骤项目文件介绍将前面写好的单文件组件放入这里运行脚手架文件结构render的作用修改默认配置配置项ref属性props配置项简单的传值方法默认的字符串传值使用v-bind对数字类型进行传值限制数据类型接收数据时候只对数据类型进行限制接…

Java 搜索二维矩阵 II

搜索二维矩阵 II中等编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a;每行的元素从左到右升序排列。每列的元素从上到下升序排列。示例 1&#xff1a;输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22]…

v-model表单

1、v-model的基本使用 表单提交是开发中非常常见的功能&#xff0c;也是和用户交互的重要手段&#xff1a; 比如用户在登录、注册时需要提交账号密码&#xff1b;比如用户在检索、创建、更新信息时&#xff0c;需要提交一些数据&#xff1b; 这些都要求我们可以在代码逻辑中获…

【GD32F427开发板试用】+软件IIC(OLED显示)

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;四季的温度 gitee开源地址https://gitee.com/sijiwendu/gd32-f427-v/tree/master/module/oled 上一次完成DHT11开发文章https://aijishu.com/a…

什么是EDA?常见的EDA工具有哪些?

大家都知道&#xff0c;芯片设计难度非常大&#xff0c;要把上千亿颗晶体管集成在面积不到指甲盖大小的芯片上。其实能实现这样目标所以靠的是电子设计自动化&#xff08;Electronics Design Automation&#xff09;工具&#xff0c;也就是我们所称的EDA工具。 什么是EDA&…

从零搭建SpringCloud服务

一.微服务基础1.什么是SpringCloud&#xff1f;SpringCloud官网&#xff1a;https://spring.io/projects/spring-cloud&#xff08;个人建议是用谷歌浏览器访问官网打开中文翻译粗略把官网读一遍&#xff09;个人理解&#xff1a;以前的服务器就好像&#xff0c;一个会语数外全…

Elasticsearch:从实例中学习 nested 数据类型的 CRUD 及搜索

nested 数据类型是一个比较高级的话题。在本文中&#xff0c;将介绍 Elasticsearch 中针对嵌套对象的一些高级 CRUD 和搜索查询。 如果你想了解有关 Elasticsearch 基础知识的更多信息&#xff0c;可以查看这些文章以快速入门或复习&#xff1a; Elasticsearch&#xff1a;关于…

koa-router 正解

Koa-Router 之前分析过 Koa/ Koa-Bodyparser 的源码&#xff0c;今天让我们来分析下koa-router的源码&#xff0c;这个插件其实还是挺重要的。毕竟作为路由&#xff0c;我们还是要知道他的工作原理 这里会重申下 其实我是分析了 koa-router 主干流程。一些小众类的方法并没有看…

多步骤复杂 SQL 优化实例

问题先看数据&#xff1a;deliver 表是主表&#xff0c;一个客户会发生多次投递行为&#xff1a;deliverItem 表是从表&#xff0c;一个投递行为有多个投递项&#xff0c;delivered 是投递状态&#xff08;1 表示未完成&#xff0c;2 表示投递完成&#xff09;&#xff1a;需求…

如何了解一个软件的设计?

刚入职&#xff0c;接手新项目&#xff0c;面对一个全新项目&#xff0c;怎么快速研究它&#xff1f; 很多人直接看源码&#xff0c;一头扎入代码&#xff0c;很快就迷失其中&#xff0c;最初那股子探索精神&#xff0c;也会逐渐被迷茫所替。有多少次你满怀激情打开一个开源项…

极光推送REST API与Java后台对接

极光推送官网的web推送页面 因为是对接它的api&#xff0c;所以我参照这这个样式实现了一个&#xff0c;效果如下&#xff1a; 定时任务推送界面&#xff0c;可定制。实现了推送一次和每日定时推送&#xff0c;如果再扩展的话有每周、每月的功能&#xff0c;只是没有这个业务…

银行数字化转型导师坚鹏:银行数字化转型的五大痛点

首先从汇丰银行业绩持续下滑谈起&#xff0c;汇丰银行作为一家国际知名的全球性银行&#xff0c;最近10年左右的时间里&#xff0c;营业收入持续下降&#xff0c;已经从2008年的1400多亿美元到2021年的804.29亿美元; 净利润徘徊不前,2021年比2020年下降29.2%&#xff0c;仅为52…

kafka心得记录

1.为何引入kafka? 削峰填谷,主要还是为了应对上游瞬时大流量的冲击&#xff0c;避免出现流量毛刺现象&#xff0c;保护下游应用和数据库不被大流量打垮。 2.kafka备份机制&#xff0c;主从机制&#xff0c;Leader-Follower&#xff1a; Kafka 定义了两类副本&#xff1a;领导…

C语言文件操作函数详解——将你的代码永久化 ( •̀ ω •́ )✧

&#x1f384;博客主页&#xff1a;&#x1f390;大明超听话 &#x1f38b;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;关注✍评论 &#x1f38d;系列专栏&#xff1a;&#x1f391;从零开始C语言 &#x1f38a;从0开始数据结构与算法详解 &#x1f386;计算机考研——…

JavaScript中的原型链

本文作者为奇舞团前端开发工程师概述JavaScript 是 Web 的编程语言&#xff0c;简单易学&#xff0c;功能强大&#xff0c;但由于过于灵活设计理念&#xff0c;导致初学者经常一脸懵&#xff0c;本文要谈的是JavaScript中难点之一原型链。原型链的前世JavaScript的诞生要理解Ja…

Nessus介绍与安装

Nessus介绍与安装 1.Nessus简介 Nessus号称是世界上最流行的漏洞扫描程序&#xff0c;全世界有超过75000个组织在使用它。该工具提供完整的电脑漏洞扫描服务&#xff0c;并随时更新其漏洞数据库。Nessus不同于传统的漏洞扫描软件&#xff0c;Nessus可同时在本机或远端上遥控&…

测试开发 | Dubbo 接口测试原理及多种方法实践总结

image1080478 86.9 KB 1、什么是 Dubbo&#xff1f; Dubbo 最开始是应用于淘宝网&#xff0c;由阿里巴巴开源的一款优秀的高性能服务框架&#xff0c;由 Java 开发&#xff0c;后来贡献给了 Apache 开源基金会组织。 下面以官网的一个说明来了解一下架构的演变过程&#xff0…