从头实现 mini-vue(搭建环境&实现reactive、effect)

531 阅读3分钟

对于希望提升自己前端架构能力,写出更优雅代码的前端同学,
vue源码绝对是你成功路上的垫脚石,但由于尤大实现的vue源码库有许多对于一些边缘情况的判断逻辑,
所有这篇文章希望和大家一起从头实现一个简单版的vue源码库 mini-vue(感谢 阿崔cxr 的 mini-vue)
github源码地址 如果觉着还不错欢迎Star~

项目初始化

由于项目需要使用jest作为单元测试框架,使用typescript作为开发语言。所以需要先配置其开发环境

npm init

配置jest环境

yarn add --dev jest

使用 Babel

yarn add --dev babel-jest @babel/core @babel/preset-env

在项目的根目录下创建 babel.config.js ,通过配置 Babel 使其能够兼容当前的 Node 版本。

babel.config.js

module.exports = {
    presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        '@babel/preset-typescript'
    ],
};

通过 Babel,Jest 能够支持 Typescript。首先要确保你遵循了上述使用 Babel 指引。接下来使用 yarn 安装 @babel/preset-typescript :

yarn add --dev @babel/preset-typescript

Jest 在运行时并不会对你的测试用例做类型检查需要我们配置jest支持类型检测

yarn add --dev @types/jest

测试一下我们Jest的环境 下面我们开始给一个假定的函数写测试,这个函数的功能是两数相加。首先创建 sum.js 文件:

function sum(a, b) {
  return a + b;
}
module.exports = sum;

接下来,创建名为 sum.test.js 的文件。这个文件包含了实际测试内容:

const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

将如下代码添加到 package.json 中:

"scripts": {
"test": "jest"
}

最后,运行 yarn test 或者 npm run test ,Jest 将输出如下信息

$ jest
 PASS  ./sum.test.js (7.845 s)
  √ adds 1 + 2 to equal 3 (3 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.37 s
Ran all test suites.
Done in 10.58s.

配置typescript环境

 yarn add typescript --dev

初始化typescript配置文件

npx tsc --init

执行后他会自动生成tsconfig.json文件
我们要修改里面的
"lib": ["DOM", "ES6"]以消除Proxy的ts报错
"noImplicitAny": false以忽略类型关注主要逻辑

最终目录结构

屏幕截图 2022-04-06 115235.png

实现reactive

我们新建一些关于reactive的文件夹,并删除之前测试jest的sum.js与sum.test.js,调整一下目录结构

调整目录结构.png

1、 在src>reactive>test>reactive.test.ts中写不考虑异常的happy path测试用例

import { reactive } from '../reactive'

describe('reactive', () => {
    it('happy path', () => {
        const original = { 'bar': 1 }
        const observable = reactive(original)
        expect(original).not.toBe(observable)
        expect(observable.bar).toBe(1)
    })
})

2、在src>reactive>reactive.ts中实现reactive

export const reactive = (raw) => {
    //使用Proxy包裹传入的对象
    return new Proxy(raw, {
        get: (target, key) => {
            const ret = Reflect.get(target, key)
            //这里需要存储依赖
            return ret
        },
        set: (target, key, newValue) => {
            const ret = Reflect.set(target, key, newValue)
            //这里需要触发依赖
            return ret
        }
    })
}

执行一下我们的jest

yarn test reactive.test.ts

reactive测试.png

实现effect

1、在src>reactive>test>effect.test.ts中编写测试用例

import { reactive } from '../reactive'
import { effect } from '../effect'

describe('effect', () => {
    it('happy path', () => {
        const info = reactive({
            age: 18
        })
        let newAge
        effect(() => {
            newAge = info.age + 1
        })
        expect(newAge).toBe(19)
        info.age++
        expect(newAge).toBe(20)
    })
})

2、在src>reactive>effect.ts中实现effect


let activeEffect
class ReactiveEffect {
    private _fn
    constructor(fn) {
        this._fn = fn
    }
    run() {
        activeEffect = this
        this._fn()
    }
}

export const effect = (fn) => {
    let reactiveEffect = new ReactiveEffect(fn)
    reactiveEffect.run()
}

let targetMap = new Map()
//用于reactive中的get时存储依赖
export const track = (target, key) => {
    // {
    //     target, {
    //         key: []
    //     }
    // }
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }
    deps.add(activeEffect)
}
//用于reactive中的set时触发依赖
export const trigger = (target, key) => {
    const depsMap = targetMap.get(target)
    const deps = depsMap.get(key)
    for (const dep of deps) {
        dep.run()
    }
}

3、将track与trigger放到reactive中

import { track, trigger } from './effect'
export const reactive = (raw) => {
    //使用Proxy包裹传入的对象
    return new Proxy(raw, {
        get: (target, key) => {
            const ret = Reflect.get(target, key)
            
            //这里需要存储依赖
            track(target, key)
            
            return ret
        },
        set: (target, key, newValue) => {
            const ret = Reflect.set(target, key, newValue)
            
            //这里需要触发依赖
            trigger(target, key)
            
            return ret
        }
    })
}

4、执行一下effect测试用例

yarn test effect.test.ts

image.png