Vue 进阶系列丨实现简易reactive和ref

a477a844fff6dfdf4a7830aa483224d9.png

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!


响应式原理

在前端开发中,"响应式"通常指的是用户界面对数据的变化做出相应的能力。换句话说,当数据发生变化时,界面能够自动更新以反映这些变化。这种机制可以让开发者专注于数据和业务逻辑,而不必手动管理界面的更新。

在Vue.js中,响应式是框架的核心特性之一。在Vue 3中,响应式的原理主要依赖于ES6中的Proxy对象。

具体来说,Vue 3的响应式原理包括以下几个步骤:

  1. 初始化阶段:当你创建一个Vue实例或者定义一个响应式对象时,Vue会对数据进行初始化。在初始化阶段,Vue会使用Proxy对象来监听数据的变化。

  2. Getter和Setter:对象被Proxy包裹后,每个属性都会有对应的Getter和Setter函数。当你访问响应式对象的属性时,会触发Getter函数,Vue会将这个属性与当前的组件实例关联起来,这样Vue就知道哪些组件依赖于这个属性。当属性被修改时,会触发Setter函数,Vue会通知所有依赖于该属性的组件进行更新。

  3. 依赖追踪:Vue使用依赖追踪来跟踪数据属性与组件之间的关联关系。每个组件都有一个依赖收集器,用于存储与该组件相关的所有数据属性。当属性被访问时,Vue会将当前组件与这个属性建立关联,并将属性的变化依赖于这个组件。

  4. 触发更新:当响应式对象的属性被修改时,会触发Setter函数。Setter函数会通知所有依赖于这个属性的组件进行更新,从而使界面能够反映数据的变化。

总的来说,Vue 3的响应式原理利用了ES6中的Proxy对象来实现数据的监听和依赖追踪,从而实现了高效的数据响应式更新。这种机制让Vue能够在数据发生变化时自动更新相关的界面组件,使开发者能够更加专注于业务逻辑的实现。


实现reactive

开发思想,从单元测试出发,先定义自己想要的最终结果,然后逐步实现相关的API

第一步:这里呢,我们定义第一个单元测试

// reactive.spec.ts (这里用的单元测试为 jest)// 这里引入的是我们即将实现的自己的reactive
import { reactive } from "../reactive";
// 定义单元测试的标题为reactive,此处定义为hello world都可以
describe("reactive",()→{it("first case",()→{// 定义一个原生对象const original = {foo:1};// 此处用reactive包裹后返回一个对象const observed = reactive(original);// 期待observed的值不等于originalexpect(observed).not.toBe(original);// 期待observed.foo 为 1expect(observed.foo).toBe(1);});
});

根据上面测试的内容,我们可以实现这样一个reactive

// reactive.tsexport function reactive(raw) {// reactive 实际上返回的就是一个proxy对象return new Proxy(raw, {// 拦截getget(target, key) {const res = Reflect.get(target, key);return res;}
}

此时我们已经实现了一个简易的reactive,只不过还不支持依赖收集和触发依赖的逻辑。通过上文我们知道,vue3中依赖收集和触发依赖是在getter和setter中触发的,所以我们的代码可以写成下面这样:

// reactive.tsexport function reactive(raw) {return new Proxy(raw, {get(target, key) {const res = Reflect.get(target, key);// TODO 依赖收集track(target, key);return res;},set(target,key,value) {const res = Reflect.set(target, key, value);// TODO 触发依赖trigger(target, key)return res;}
}

此时我们只需要实现 track和trigger即可

下面我们看我们的第二个单元测试:

// effect.spec.tsimport { reactive } from '../reactive'
// 这里的effect也是我们后面将要实现的
import { effect } from '../effect'// effect 就是我们的依赖,也叫做副作用
describe("effect",()→{it("second case",()→{const user = reactive({age: 10,});let nextAge;effect(()→{nextAge=user.age + 1;});expect(nextAge).toBe(11);// updateuser.age++;expect(nextAge).toBe(12);});
});

可以看到上面的单元测试中定义了一个函数effect,effect 是一个函数,用于创建副作用。它是 Vue 3 中响应式 API 的一部分,用于处理响应式数据的变化。effect 函数接受一个回调函数作为参数,并在这个回调函数中定义副作用。当回调函数中依赖的响应式数据发生变化时,副作用将被重新执行

这里先简单说一下这个依赖收集和触发依赖是个怎么回事,可以假设这么一个场景:

1. 在火车站都有寄存包裹的地方,每个旅游团就是一个对象,旅游团的每个人就是对象的键。

2. 当人员去存储包裹的时候,寄存处会看当前人员属于哪个旅游团,相同的旅游团集中放到一个包裹柜,后续方便查找。

3. 然后在这个包裹柜上面找一个箱子给人员,并且给他一把箱子钥匙(依赖收集

4. 当相同的人员第二次存储包裹的时候,他会继续在原有的箱子里放新的东西(依赖收集

5. 以此类推

6. 当人员回来拿包裹时,会把钥匙给寄存处,寄存处会将钥匙对应的箱子里的所有东西拿出来(触发依赖

下面我们来实现effect:

// effect.tsclass ReactiveEffect {private _fn: any;constructor(fn) {this._fn=fn;}run(){activeEffect = thisthis._fn();     }
}// 所有依赖收集到的地方,可以理解成一个寄存处
const targetMap = new Map();
// 收集依赖
export function track(target, key) {let depsMap = targetMap.get(target);// 先看寄存处里面是否已经由当前对象对应的包裹柜if(!depsMap){depsMap = new Map();targetMap.set(target,depsMap);}let dep = depsMap.get(key)// 再看当前对象对应的键值,是否有对应的箱子if(!dep){dep = new Set();depsMap.set(key, dep)}// 最后将用户传入的fn作为依赖,添加进入箱子中trackEffects(dep)
}export function trackEffects(dep){dep.add(activeEffect);
}// 实现trigger
export function trigger(target, key) {// 先根据旅游团找到对应的包裹柜let depsMap = targetMap.get(target);// 根据人员找到对应的箱子let dep = depsMap.get(key);// 把箱子里所有的内容拿出来执行triggerEffects(dep)
}export function triggerEffects(dep){for(const effect of dep){effect.run();}
}let activeEffect;
export function effect(fn) {// fnconst _effect = new ReactiveEffect(fn)// 立即执行传入的函数_effect.run();
}

此时我们的reactive就实现完成了,这里做个总结:

  1. 就是每个键在getter的时候,也就是effect函数传入的时候(这里会触发getter),将整个effect函数作为依赖,放入键值对应的箱子里

  2. 当数据更新的时候,也就是触发setter时,将箱子里的内容(fn函数)拿出来执行一遍。此时,相关的响应式数据也就更新了


实现ref

有了上面reactive的基础,ref会相当简单的学会。我们还是通过一个单元测试开始:

// ref.spec.tsdescribe("ref",()→{it("first case",()={const a = ref(1);expect(a.value).toBe(1);});it("second case",()=>{            const a = ref(1);let dummy;let calls = 0;effect(()=>{calls++;dummy = a.value;}};expect(calls).toBe(1);expect(dummy).toBe(1);a.value = 2;expect(calls).toBe(2);expect(dummy).toBe(2);})
})

ref都是通过.value来触发,我们可以使用一个类,然后拦截他的get和set,这里给出最终代码:

// ref.tsclass RefImpl {private _value: any;// 存放依赖的箱子public dep;constructor(value) {this._value = value;this.dep = new Set();}get value(){// 收集依赖trackEffects(this.dep)return this._value;}set value(newValue){this.value = newValue// 触发依赖triggerEffects(this.dep)}
}
export function ref(value) {return new RefImpl(value);
}

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

13fe43985a61ca71b4dca7984f403cd6.png

叶阳辉

HFun 前端攻城狮

往期精彩:

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

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

相关文章

docker (56) Recv failure: Connection reset by peer

docker 运行一个spring boot的api接口项目,在虚拟机上测试: curl 127.0.0.1:9997/doc.html 报错:(56) Recv failure: Connection reset by peer 在网上搜了很多包括: 检查防火墙是否关闭 systemctl status firewalld 检查防火…

PLC_博图系列☞基本指令“异或“运算

PLC_博图系列☞基本指令“异或“运算 文章目录 PLC_博图系列☞基本指令“异或“运算背景介绍X:“异或”运算说明参数示例真值表 关键字: PLC、 西门子、 博图、 Siemens 、 异或 背景介绍 这是一篇关于PLC编程的文章,特别是关于西门子的…

LeetCode 0106.从中序与后序遍历序列构造二叉树:分治(递归)——五彩斑斓的题解(若不是彩色的可以点击原文链接查看)

【LetMeFly】106.从中序与后序遍历序列构造二叉树:分治(递归)——五彩斑斓的题解(若不是彩色的可以点击原文链接查看) 力扣题目链接:https://leetcode.cn/problems/construct-binary-tree-from-inorder-an…

OpenTiny Vue 组件库适配微前端可能遇到的4个问题

本文由体验技术团队 TinyVue 项目成员岑灌铭同学创作。 前言 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略,每个应用可以选择不同的技术栈,独立开发、独立部署。 TinyVue组件库的跨技术栈能力与微前端十…

四、Burpsuite工具之proxy模块详解-intercept功能

前言: 过了一个年,感觉好久都没有更新了,今天就从burpsuite的更新开始吧。 前面已经说过了burpsuite的安装和proxy代理的配置,今天说一下proxy模块中非常有用的intercept功能。 intercept功能介绍: intercept是拦截…

element-ui 中的表格设置正确的排序以及怎么设置默认排序

本文主要解决两个问题,第一个,在element-ui中,直接设置参数排序,达不到预期效果,预期是按照数字的大小进行排序;第二个,想对表格中某个字段设置默认的排序方式 现象: 直接设置在 el-…

Unity NavMesh 清除不可行走区域

通常场景中物体设置为static或Navigation Static后,打开Navigation使用默认设置烘焙NavMesh,模型顶部和底部会出现蓝色网格,但其中有部分属于不可能到达区域,如下图 本文介绍两种可去掉NavMesh中不需要网格的方法: 方…

辽宁博学优晨教育科技有限公司视频剪辑培训打造技能新星

在数字时代,视频剪辑已成为一项炙手可热的技能。辽宁博学优晨教育科技有限公司,作为业内知名的教育培训机构,其视频剪辑培训项目备受关注。那么,辽宁博学优晨教育科技有限公司的视频剪辑培训究竟可靠吗?本文将为您深入…

从零学习Linux操作系统第二十七部分 shell脚本中的变量

一、什么是变量 变量的定义 定义本身 变量就是内存一片区域的地址 变量存在的意义 命令无法操作一直变化的目标 用一串固定的字符来表示不固定的目标可以解决此问题 二、变量的类型及命名规范 环境级别 export A1 在环境关闭后变量失效 退出后 关闭 用户级别&#xff…

vue大文件读取部分内容,避免重复加载大文件,造成流量浪费

使用场景:项目点云地图是pcd文件,但是文件可能上百兆,我需要获取到文件中的版本信息,跟本地的缓存文件做比较,如果不一致,才会加载整个文件。从而节省流量。 避免重复加载整个“.pcd文件,以最大…

LeetCode每日刷题:101. 对称二叉树

题目: 解题思路:可以新写一个函数,从root开始,root的left的头结点将记为lefttree(左子树),root的lright的头结点将记为righttree(右子树), 然后递归左子树的root.left与右…

qtday3作业

思维导图 完善对话框,点击登录对话框,如果账号和密码匹配,则弹出信息对话框,给出提示”登录成功“,提供一个Ok按钮,用户点击Ok后,关闭登录界面,跳转到其他界面 如果账号和密码不匹配…