• 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

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

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

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

    1. // reactive.ts
    2. export function reactive(raw) {
    3. // reactive 实际上返回的就是一个proxy对象
    4. return new Proxy(raw, {
    5.         // 拦截get
    6. get(target, key) {
    7.             const res = Reflect.get(target, key);
    8. return res;
    9.         }
    10. }

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

    1. // reactive.ts
    2. export function reactive(raw) {
    3. return new Proxy(raw, {
    4. get(target, key) {
    5. const res = Reflect.get(target, key);
    6. // TODO 依赖收集
    7. track(target, key);
    8. return res;
    9. },
    10. set(target,key,value) {
    11. const res = Reflect.set(target, key, value);
    12. // TODO 触发依赖
    13. trigger(target, key)
    14. return res;
    15. }
    16. }

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

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

    1. // effect.spec.ts
    2. import { reactive } from '../reactive'
    3. // 这里的effect也是我们后面将要实现的
    4. import { effect } from '../effect'
    5. // effect 就是我们的依赖,也叫做副作用
    6. describe("effect",()→{
    7.     it("second case",()→{
    8. const user = reactive({
    9. age: 10,
    10. });
    11. let nextAge;
    12. effect(()→{
    13. nextAge=user.age + 1;
    14. });
    15. expect(nextAge).toBe(11);
    16. // update
    17. user.age++;
    18. expect(nextAge).toBe(12);
    19. });
    20. });

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

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

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

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

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

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

    5. 以此类推

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

    下面我们来实现effect:

    1. // effect.ts
    2. class ReactiveEffect {
    3. private _fn: any;
    4. constructor(fn) {
    5. this._fn=fn;
    6. }
    7. run(){
    8. activeEffect = this
    9. this._fn();
    10. }
    11. }
    12. // 所有依赖收集到的地方,可以理解成一个寄存处
    13. const targetMap = new Map();
    14. // 收集依赖
    15. export function track(target, key) {
    16. let depsMap = targetMap.get(target);
    17.     // 先看寄存处里面是否已经由当前对象对应的包裹柜
    18. if(!depsMap){
    19. depsMap = new Map();
    20. targetMap.set(target,depsMap);
    21. }
    22. let dep = depsMap.get(key)
    23.     // 再看当前对象对应的键值,是否有对应的箱子
    24. if(!dep){
    25. dep = new Set();
    26. depsMap.set(key, dep)
    27. }
    28.     // 最后将用户传入的fn作为依赖,添加进入箱子中
    29.     trackEffects(dep)
    30. }
    31. export function trackEffects(dep){
    32. dep.add(activeEffect);
    33. }
    34. // 实现trigger
    35. export function trigger(target, key) {
    36.     // 先根据旅游团找到对应的包裹柜
    37. let depsMap = targetMap.get(target);
    38.     // 根据人员找到对应的箱子
    39. let dep = depsMap.get(key);
    40.     // 把箱子里所有的内容拿出来执行
    41. triggerEffects(dep)
    42. }
    43. export function triggerEffects(dep){
    44. for(const effect of dep){
    45. effect.run();
    46. }
    47. }
    48. let activeEffect;
    49. export function effect(fn) {
    50. // fn
    51. const _effect = new ReactiveEffect(fn)
    52.     // 立即执行传入的函数
    53. _effect.run();
    54. }

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

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

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


    实现ref

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

    1. // ref.spec.ts
    2. describe("ref",()→{
    3.     it("first case",()={
    4. const a = ref(1);
    5. expect(a.value).toBe(1);
    6. });
    7. it("second case",()=>{
    8. const a = ref(1);
    9. let dummy;
    10. let calls = 0;
    11. effect(()=>{
    12. calls++;
    13. dummy = a.value;
    14. }};
    15. expect(calls).toBe(1);
    16. expect(dummy).toBe(1);
    17. a.value = 2;
    18. expect(calls).toBe(2);
    19. expect(dummy).toBe(2);
    20. })
    21. })

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

    1. // ref.ts
    2. class RefImpl {
    3. private _value: any;
    4.     // 存放依赖的箱子
    5. public dep;
    6. constructor(value) {
    7. this._value = value;
    8. this.dep = new Set();
    9. }
    10. get value(){
    11.         // 收集依赖
    12.         trackEffects(this.dep)
    13. return this._value;
    14. }
    15. set value(newValue){
    16. this.value = newValue
    17. // 触发依赖
    18. triggerEffects(this.dep)
    19. }
    20. }
    21. export function ref(value) {
    22. return new RefImpl(value);
    23. }

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

    13fe43985a61ca71b4dca7984f403cd6.png

    叶阳辉

    HFun 前端攻城狮

    往期精彩:

  • 相关阅读:
    BIOS 中断服务 设置颜色
    (八)Vue3-huohuo-admin src构建-上
    Python采集外网美女照片,又是养眼的一天
    读Densely Connected Pyramid Dehazing Network
    基于遗传算法的二进制图像重建(Matlab代码实现)
    基础MySQL的语法练习
    【基于C的排序算法】插入排序之希尔排序
    Vue框架插槽(第八课)
    lvs(Linux virual server)
    MTK MFNR
  • 原文地址:https://blog.csdn.net/HFunTeam/article/details/136201710