简单理解Vue2的响应式原理

使用Vue作为前端开发技术栈的同学,在使用Vue时都会有一些好奇:为啥我们的响应式变量要在data中定义?Vue是如何监听到变化,实现响应式的?这次我们就来探究一下,Vue2的响应式原理。

对象的响应式

修改属性描述实现响应式

首先我们来实现基础的响应式,即监听data数据的变化。我的代码中提供了较详细的注释。

// 判断是否属于object
function isObject(obj) {// 注意 null的typeof 也是 'object'return typeof obj === "object" && obj != null;
}// 为入参提供响应式逻辑
export function observer(obj) {// 不是object 原样返回if (!isObject(obj)) return obj;// 遍历对象,为每个属性增加响应性for (const key in obj) {defineReactive(obj, key, obj[key]);}
}// 存储真正的属性值
const _obj = {};
// 为每个属性增加响应性
function defineReactive(obj, key, value) {_obj[key] = value;Object.defineProperty(obj, key, {// 取值get() {console.log(`${key}取值: ${_obj[key]}`);return _obj[key];},// 存值set(newValue) {if (newValue === _obj[key]) return;console.log(`${key}存值: 由 ${_obj[key]} 改为 ${newValue}`);_obj[key] = newValue;},});
}

在observer函数中,我们仅对object进行处理。我们对object的每个属性都增加独立的响应性。方法是使用Object.defineProperty。它可以修改对象现有属性的描述。我们把一个普通属性改为一个由getter,setter描述的属性。在get和set函数中就能够监听到属性值的存取和变化,进行处理。而真正的值却存储在另一个对象中。我们来看一下使用效果。

import { observer } from './index.js'
const data = {a: 1,b: 2,
};
observer(data);data.a = '你好'
data.b = 'js'
console.log(data.a + data.b);

我们在对象中定义了几个属性,把对象通过响应式处理后再进行存取。

# 输出效果
a存值: 由 1 改为 你好
b存值: 由 2 改为 js
a取值: 你好
b取值: js
你好js

使用闭包优化数据存储

上面代码中定义一个内部对象来存储真正的数据。内部对象的key即是响应式对象的key。这时如果有多个对象都通过这个函数获得响应性,且定义了相同key的属性,此时这个内部存储就会产生覆盖的情况。那么我们要设多个存储变量来分别为不同的对象存储数据么?不需要,使用闭包,我们可以简单的做到这点,

闭包指的是当一个函数执行结束后,其内部的资源并没有被完全释放,还可以被继续使用。利用这个特性,我们可以在函数闭包中存储部分变量,而不用定义一个内部对象来存储。

// 为每个属性增加响应性
function defineReactive(obj, key, value) {Object.defineProperty(obj, key, {// 取值get() {console.log(`${key}取值: ${value}`);return value},// 存值set(newValue) {if (newValue === value) return;console.log(`${key}存值: 由 ${value} 改为 ${newValue}`);value = newValue;},});
}

我们使用value作为闭包的关键变量。这时即使defineReactive函数结束后,由于内部的get和set函数依然在使用value变量,因此它并不会被销毁。当触发set函数修改属性值时,我们直接更改value为新的值。后续get函数取值时,也能拿到新值。

深度监听嵌套对象

我们的已经实现的observer函数,只能对于对象的一层属性进行处理,对于多层嵌套对象我们的响应式是失效的,例如这样:

const data = {a: 1,b: { c: 2 },
};
observer(data);
data.b.c = "js";
// 输出结果:
// b取值: [object Object]

目前我们的代码只在data.b取值的时候有响应性,而对data.b.c赋值的时候,响应性就没有了。我们处理下嵌套对象的监听:

// 为每个属性增加响应性
function defineReactive(obj, key, value) {// 如果是嵌套对象,继续监听其属性observer(value);Object.defineProperty(obj, key, {// 取值get() {console.log(`${key}取值: ${value}`);return value;},// 存值set(newValue) {if (newValue === value) return;console.log(`${key}存值: 由 ${value} 改为 ${newValue}`);value = newValue;},});
}

实现也非常简单,就是在defineReactive中对value进行递归监听,即如果value是object,则继续监听其属性。

新增对象深度监听

试想这样一种情况:某个深度监听的对象属性,一开始并不是个对象,但是后来被改成了对象。又或者嵌套对象本身被替换成了另一个对象。这时候我们更改的这个嵌套对象,就是没有响应性的。举个例子:

import { observer } from "./index.js";
const data = {a: 1,b: 2
};
observer(data);data.a = "你好";
data.b = { c: 2 };
data.b.c = 'js';
data.b.d = 3;
console.log(data.a + data.b.c);

此时的输出为:

# 输出效果
a存值: 由 1 改为 你好
b存值: 由 2 改为 [object Object]
b取值: [object Object]
a取值: 你好
b取值: [object Object]
你好js

可以看到,对于修改b属性为对象后,新增的c属性没有被监听到。这里和Vue2的机制是一样的。此时在Vue中可以使用Vue.set来为新增的对象增加响应性。对于我们的代码来说,实现可以更简单:直接适配新增嵌套对象时的响应性处理(但是新增属性依然是不行的)。改动也很简单,对于进入set函数的对象新值增加响应性即可。

set(newValue) {if (newValue === value) return;// 如果存储的是一个对象,那么继续增加响应性observer(newValue);console.log(`${key}存值: 由 ${value} 改为 ${newValue}`);value = newValue;
}

这时的输出效果如下:(注意data.b.d为新增属性,这里依然没有加入响应性)。

# 输出效果
a存值: 由 1 改为 你好
b存值: 由 2 改为 [object Object]
b取值: [object Object]
c存值: 由 2 改为 js
a取值: 你好
b取值: [object Object]
c取值: js
你好js

为新增属性增加响应性

上一节我们的新增属性data.b.d没有增加响应性,也就是说,一开始没有在data中定义的属性,是没有响应性的。Vue提供了Vue.set方法为这类新增属性增加响应性。正好,我们的defineReactive函数也能达到这个目的。来看一下例子。

import { observer, defineReactive } from "./index.js";
const data = {a: 1,b: 2
};
observer(data);data.a = "你好";
data.b = {};
defineReactive(data.b, 'c', 2);
data.b.c = 'js';
defineReactive(data, 'd', 3);
data.d = 4;
console.log(data.a + data.b.c);

不管对于嵌套的新增属性,还是在data上绑定的新增属性,使用defineReactive函数都可以为其新增响应性。看下输出效果:

# 输出效果
a存值: 由 1 改为 你好
b存值: 由 2 改为 [object Object]
b取值: [object Object]
b取值: [object Object]
c存值: 由 2 改为 js
d存值: 由 3 改为 4
a取值: 你好
b取值: [object Object]
c取值: js
你好js

数组的响应式

使用Object.defineProperty是无法监听到数组的push等方法引发的变化,因此对于数组形式我们还要单独进行处理。我们重新包装数组的原型,重写原型方法。使数组在使用方法修改数组对象前,我们也能监听到。

// 继承数组原型对象
const arrProperty = Object.create(Array.prototype)
// 重写部分数组原型方法
const methods = ['push','pop','shift','unshift','splice']
methods.forEach(method => {arrProperty[method] = function () {console.log(`数组${method}方法`);// 重新调用对应的数组方法Array.prototype[method].call(this, ...arguments);}
})

可以看到,我们对常用的数组方法进行了重写,在我们重写的函数内容,首先监听到改动,然后再重新调用真正的方法执行改动逻辑。

然后在observer函数中,对数组进行特殊处理:

export function observer(obj) {// 不是object 原样返回if (!isObject(obj)) return obj;// 如果是数组则修改原型if (Array.isArray(obj)) {obj.__proto__ = arrProperty}// 遍历对象,为每个属性增加响应性for (const key in obj) {defineReactive(obj, key, obj[key]);}
}

然后我们就能监听到数组方法的响应性了。看一下例子:

import { observer } from "./index.js";
const data = [1, 2]
observer(data);data[1] = 3;
data.push(4);
console.log(data[2])

这时的输出效果如下:(后续都省略了get方法的输出)

# 输出效果
1存值: 由 2 改为 3
数组push方法: 4
4

总结

这里仅仅是简单的理解了一些Vue2的响应式原理,并且简化了场景。实际上Vue2对于响应式的处理要复杂的多,还涉及到一些对象通信和和设计模式的方法。后面有时间的时候,我们会更详细的探讨,Vue2中是如何实现响应式的。

参考

  • Object.defineProperty() MDN
    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  • 深入响应式原理 Vue 2.x 文档
    https://v2.cn.vuejs.org/v2/guide/reactivity.html
  • Vue响应式原理 文章中用到的源码
    https://github.com/jzplp/VueJz
  • Vue2 & Vue3 响应式实现原理
    https://juejin.cn/post/7253148953600262203
  • 面试官: 能不能手写 Vue 响应式?(Vue2 响应式原理【完整版】)
    https://juejin.cn/post/7079807948830015502

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

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

相关文章

Ubuntu 20.04 DNS解析原理, 解决resolv.conf被覆盖问题

------------------------------------------------------------------ author: hjjdebug date: 2023年 11月 09日 星期四 14:01:11 CST description: Ubuntu 20.04 DNS解析原理, 解决resolv.conf被覆盖问题 ----------------------------------------------------------------…

(三)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB

一、七种算法(DBO、LO、SWO、COA、LSO、KOA、GRO)简介 1、蜣螂优化算法DBO 蜣螂优化算法(Dung beetle optimizer,DBO)由Jiankai Xue和Bo Shen于2022年提出,该算法主要受蜣螂的滚球、跳舞、觅食、偷窃和繁…

【Copilot】登录报错 Extension activation failed: “No auth flow succeeded.“(VSCode)

问题描述 当尝试在 Visual Studio Code 中登录 GitHub Copilot 插件时,会出现报错的情况,如下图所示: 尽管在浏览器中成功授权了 GitHub 账户,但在返回 VSCode 后仍然报错,如下图所示: 同时,在…

PHP中传值与引用的区别

在PHP中,变量的传递方式主要分为传值和传引用两种。这两种方式在操作中有一些重要的区别,影响着变量在函数调用或赋值操作中的表现。下面详细解释一下这两种传递方式的区别。 传值(By Value) 传值是指将变量的值复制一份传递给函…

OpenAtom OpenHarmony三方库创建发布及安全隐私检测

OpenAtom OpenHarmony三方库(以下简称“三方库”或“包”),是经过验证可在OpenHarmony系统上可重复使用的软件组件,可帮助开发者快速开发OpenHarmony应用。三方库根据其开发语言分为2种,一种是使用JavaScript和TypeScr…

分成互质组——DFS

给定 n 个正整数,将它们分组,使得每组中任意两个数互质。至少要分成多少个组? 输入格式 第一行是一个正整数 n。 第二行是 n 个不大于10000的正整数。 输出格式 一个正整数,即最少需要的组数。 数据范围 1≤n≤10 输入样例&am…

React【异步逻辑createAsyncThunk(一)、createAsyncThunk(二)、性能优化、createSelector】(十二)

文章目录 异步逻辑 createAsyncThunk(一) createAsyncThunk(二) 性能优化 createSelector 异步逻辑 //Product.js const onAdd () > {const name nameRef.current.value// 触发添加商品的事件dispatch(addProduct({name…

3D Gaussian Splatting:用于实时的辐射场渲染

Kerbl B, Kopanas G, Leimkhler T, et al. 3d gaussian splatting for real-time radiance field rendering[J]. ACM Transactions on Graphics (ToG), 2023, 42(4): 1-14. 3D Gaussian Splatting 是 Siggraph 2023 的 Best Paper,法国团队在会议上展示了其实现的最…

线性代数(二)| 行列式性质 求值 特殊行列式 加边法 归纳法等多种方法

文章目录 1. 性质1.1 重要性质梳理1.1.1 转置和初等变换1.1.2加法行列式可拆分1.1.3 乘积行列式可拆分 1.2 行列式性质的应用1.2.1 简化运算1.2.2 将行列式转换为(二)中的特殊行列式 2 特殊行列式2.1 上三角或下三角行列式2.2 三叉行列式2.3 行列式行和&…

VueCli 自定义创建项目及配置

一、VueCli 自定义创建项目 1.安装脚手架 (已安装) npm i vue/cli -g2.创建项目 vue create hm-exp-mobile选项 Vue CLI v5.0.8 ? Please pick a preset:Default ([Vue 3] babel, eslint)Default ([Vue 2] babel, eslint) > Manually select features 选自定义手动…

生活污水处理一体化处理设备有哪些

生活污水处理一体化处理设备有多种类型,包括但不限于以下几种: 鼓风机:提供曝气系统所需的气流。潜水污水提升泵:将污水从低处提升到高处。旋转式滚筒筛分机:对污水中的悬浮物进行分离和筛选。回旋式格栅:…

100天精通风控建模(原理+Python实现)——第6天:风控建模中离散化是什么?

风控模型已在各大银行和公司都实际运用于业务,用于营销和风险控制等。    之前已经阐述了100天精通风控建模(原理+Python实现)——第1天:什么是风控建模?    100天精通风控建模(原理+Python实现)——第2天:风控建模有什么目的?    100天精通风控建模(原理+Python实现…

ARM Linux 基础学习 / 配置交叉编译工具链 / 编译 Linux 应用和驱动 / 编译内核

编辑整理 by Staok。 本文部分内容摘自 “100ask imx6ull” 开发板的配套资料(如 百问网的《嵌入式Linux应用开发完全手册》,在 百问网 imx6ull pro 开发板 页面 中的《2.1 100ASK_IMX6ULL_PRO:开发板资料》或《2.2 全系列Linux教程&#xf…

AE-水晶/玻璃相框的制作

目录 一、新建合成“蓝色相框” 二、新建蓝色水晶/玻璃相框 三、为蓝色相框添加效果

【计算机网络笔记】网络层服务模型——虚电路网络

系列文章目录 什么是计算机网络? 什么是网络协议? 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能(1)——速率、带宽、延迟 计算机网络性能(2)…

Arduino驱动A01NYUB防水超声波传感器(超声波传感器)

目录 1、传感器特性 2、控制器和传感器连线图 3、通信协议 4、驱动程序 A01NYUB超声波测距传感器是一款通过发射和接收机械波来感应物体距离的电子传感器。该款产品具有监测距离远、范围广、防水等优点,且具有一定的穿透能力(烟雾、粉尘等)。该产品带有可拆卸式喇叭口,安…

ubuntu(18.04)中安装open babel docker镜像并在php项目中调用容器中的obabel命令解析结果使用

使用软件: obabel镜像:informaticsmatters/obabel docker:http:// https://www.docker.com/ 安装docker #卸载旧版本sudo apt-get remove docker docker-engine docker-ce docker.io#更新索引包sudo apt-get update#安装 apt 依赖包&…

图的表示与基础--Java

1.图的基础知识 该图片来自于&#xff1a; https://b23.tv/KHCF2m6 2.稀疏图与稠密图 G(V,E)&#xff1a;V顶点个数&#xff0c;E边的个数 稀疏图&#xff1a;E<<V 一般用邻接表表示(数组链表) 稠密图&#xff1a;E接近V 一般用邻接矩阵表示&#xf…

为什么要有__init__.py这个文件?(一篇文章带你详细了解)

__init__.py 文件在Python中有着特殊的意义&#xff0c;尤其是在定义包&#xff08;package&#xff09;时。这个文件的存在有几个主要目的&#xff1a; 将目录标记为Python包&#xff1a;最重要的作用就是它告诉Python解释器&#xff0c;包含这个文件的目录应该被视作一个Pyth…

中断处理机制解析

要处理中断&#xff0c;需要有一个中断处理函数。定义如下&#xff1a; irqreturn_t (*irq_handler_t)(int irq, void * dev_id);/*** enum irqreturn* IRQ_NONE interrupt was not from this device or was not handled* IRQ_HANDLED interrupt was handled by this de…