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

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

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

kotlin 复制代码
// 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的值不等于original
        expect(observed).not.toBe(original);
        // 期待observed.foo 为 1
        expect(observed.foo).toBe(1);
    });
});

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

javascript 复制代码
// reactive.ts


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

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

javascript 复制代码
// reactive.ts


export 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即可**。**

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

javascript 复制代码
// effect.spec.ts


import { 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);
        // update
        user.age++;
        expect(nextAge).toBe(12);
    });
});

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

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

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

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

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

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

  5. 以此类推

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

下面我们来实现effect:

javascript 复制代码
// effect.ts


class ReactiveEffect {
    private _fn: any;
    constructor(fn) {
        this._fn=fn;
    }
    run(){
       activeEffect = this
       this._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) {
    // fn
    const _effect = new ReactiveEffect(fn)
    // 立即执行传入的函数
    _effect.run();
}

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

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

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


实现ref

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

cs 复制代码
// ref.spec.ts


describe("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,这里给出最终代码:

cs 复制代码
// ref.ts


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

叶阳辉

HFun 前端攻城狮

往期精彩:

相关推荐
F-2H25 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱056727 分钟前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
gqkmiss1 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247553 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255024 小时前
前端常用算法集合
前端·算法
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203984 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2344 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1235 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~6 小时前
npm error code ETIMEDOUT
前端·npm·node.js