├── .gitignore ├── README.md ├── babel.config.js ├── docs ├── 05-setup环境-集成jest做单元测试-集成 ts.md ├── 06-实现effect.md ├── 07-实现 effect 返回 runner.md ├── 08-实现 effect 的scheduler 功能.md ├── 09-实现 effect 的 stop 功能.md ├── 10-实现readonly 功能.md ├── 11-实现IsReadonly和IsReactive.md ├── 12-优化stop功能.md ├── 13-实现reactive和readonly嵌套对象转换功能.md ├── 14-实现shallowReadonly功能.md ├── 15-实现isProxy功能.md ├── 16-实现ref功能.md ├── 17-实现isRef和unRef功能.md ├── 18-实现proxyRefs 功能.md ├── 19-实现computed 计算属性.md ├── 20.实现component主流程.md ├── 21-rollup 打包.md ├── 22-element 渲染逻辑.md ├── 23-实现组件代理对象.md ├── 24-实现shapeFlags 功能.md ├── 25-实现注册组件事件功能.md ├── 26-实现组件props功能.md ├── 27-实现emit 功能.md ├── 28-实现slots 功能.md ├── 29-实现Fragment和Text类型组件渲染.md ├── 30-实现getCurrentInstance功能.md ├── 31-实现provide和inject 功能.md ├── 32-实现自定义渲染器 custom render.md ├── 33-更新Element流程搭建.md ├── 34-更新element 的props.md ├── 35-更新element 的children.md ├── 36-双端对比算法更新children.md ├── 37-双端对比算法更新children-2.md ├── 38-双端对比算法更新element 的children 3.md ├── 39-学习犹大处理问题的思路.md ├── 40-更新component.md ├── 41-实现nextTick.md ├── 42-编译模块概述.md ├── 43-实现解析插值功能.md ├── 44- 实现解析element类型.md ├── 45-实现解析text类型.md ├── 46-实现三种联合类型的解析.md ├── 47-浅析parse 原理.md ├── 48-transform ast 树,以及插件逻辑添加.md ├── 49-代码生成string.md ├── 50-代码生成插值类型.md ├── 51-处理element类型.md ├── 51-处理三种联合类型的思考.md ├── 52-实现编译template 为render函数并渲染到页面.md ├── 53.使用pnpm和vitest来改造项目.md ├── 54.实现watchEffect的基础功能.md └── static │ ├── final.png │ ├── left.png │ ├── left_with_complier.png │ └── render.png ├── example ├── apiInject │ ├── App.js │ ├── index.html │ └── main.js ├── compiler-base │ ├── App.js │ ├── index.html │ └── main.js ├── componentSlots │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentUpdate │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── customRender │ ├── App.js │ ├── index.html │ └── main.js ├── getCurrentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── helloWord │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── nextTicker │ ├── App.js │ ├── index.html │ └── main.js ├── patchChildren │ ├── App.js │ ├── ArrayToArray.js │ ├── ArrayToText.js │ ├── TextToArray.js │ ├── TextToText.js │ ├── index.html │ └── main.js └── update │ ├── App.js │ ├── index.html │ └── main.js ├── index.js ├── package.json ├── packages ├── complier-core │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parser.spec.ts │ │ └── transform.spec.ts │ ├── package.json │ └── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── compile.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ └── transforms │ │ ├── transformElement.ts │ │ ├── transformExpression.ts │ │ ├── transformText.ts │ │ └── utils.ts ├── reactivity │ ├── __tests__ │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts │ ├── package.json │ └── src │ │ ├── baseHandler.ts │ │ ├── computed.ts │ │ ├── effect.ts │ │ ├── index.ts │ │ ├── reactive.ts │ │ └── ref.ts ├── runtime-core │ ├── __tests__ │ │ └── apiWatch.spec.ts │ ├── package.json │ └── src │ │ ├── apiInject.ts │ │ ├── apiWatch.ts │ │ ├── component.ts │ │ ├── componentEmit.ts │ │ ├── componentProps.ts │ │ ├── componentPublicInstance.ts │ │ ├── componentSlots.ts │ │ ├── componentUpdateUtils.ts │ │ ├── createApp.ts │ │ ├── h.ts │ │ ├── helpers │ │ └── renderSlots.ts │ │ ├── index.ts │ │ ├── renderer.ts │ │ ├── scheduler.ts │ │ └── vnode.ts ├── runtime-dom │ ├── package.json │ └── src │ │ └── index.ts ├── shared │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── shapeFlags.ts │ │ └── toDisplayString.ts └── vue │ ├── dist │ ├── ass-vue.cjs.js │ └── ass-vue.esm.js │ ├── examples │ ├── apiInject │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── compiler-base │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── componentSlots │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── componentUpdate │ │ ├── App.js │ │ ├── Child.js │ │ ├── index.html │ │ └── main.js │ ├── customRender │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── getCurrentInstance │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── helloWord │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── nextTicker │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── patchChildren │ │ ├── App.js │ │ ├── ArrayToArray.js │ │ ├── ArrayToText.js │ │ ├── TextToArray.js │ │ ├── TextToText.js │ │ ├── index.html │ │ └── main.js │ └── update │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── package.json │ └── src │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.ts ├── tsconfig.json └── vitest.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 自己实现一个简单版本的vue 2 | 3 | - [x] 环境集成-集成jest 做测试 4 | - [x] 实现effect 5 | - [x] 实现effect 返回runner 6 | - [x] 实现effect的scheduler功能 7 | - [x] 实现effect的stop功能 8 | - [x] 实现readonly 功能 9 | - [x] 实现isReadonly和isReactive功能 10 | - [x] 优化stop功能 11 | - [x] 实现reactive和readonly嵌套对象转换功能 12 | - [x] 实现shallowReadonly功能 13 | - [x] 实现isProxy功能 14 | - [x] 实现ref 15 | - [x] 实现isRef和unRef功能函数 16 | - [x] 实现proxyRef 函数 17 | - [x] 实现computed计算属性 18 | 19 | --- 20 | 21 | - [x] 实现初始化component主流程 22 | - [x] 使用rollup 打包库 23 | - [x] 实现element主流程 24 | - [x] 实现组件代理对象 25 | - [x] 实现shapeFlags 26 | - [x] 实现注册组件事件功能 27 | - [x] 实现组件props功能 28 | - [x] 实现组件emit功能 29 | - [x] 实现组件slots功能 30 | - [x] 实现Fragment 和Text类型节点 31 | - [x] 实现getCurrentInstance 32 | - [x] 实现provide-inject 功能 33 | - [x] 实现自定义渲染器custom renderer 34 | - [x] 更新element 流程搭建 35 | - [x] 更新element 的props 36 | - [x] 更新element的children 37 | 38 | --- 39 | 40 | - [x] 更新element的children- 双端对比diff算法(1) 41 | - [x] 更新element的children- 双端对比diff算法(2) 42 | - [x] 更新element的children- 双端对比diff算法(3) 43 | 44 | --- 45 | 46 | - [x] 学习解决bug的处理方式 47 | 48 | --- 49 | 50 | - [x] 实现组件更新功能 51 | - [x] 实现nextTick功能 52 | - [x] 编译模块概述 53 | - [x] 实现解析插值功能 54 | - [x] 实现解析element 55 | - [x] 实现解析text 功能 56 | - [x] 实现解析三种联合类型 57 | - [x] parse的实现原理 58 | - [x] 实现transform功能 59 | - [x] 实现代码生成string类型 60 | - [x] 实现代码生成插值类型 61 | - [x] 实现代码生成三种联合类型 62 | - [x] 实现编译template成render函数 63 | - [x] 实现monorepo 64 | - [x] 实现watchEffect 65 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /docs/05-setup环境-集成jest做单元测试-集成 ts.md: -------------------------------------------------------------------------------- 1 | # 1-1-setup环境-集成jest做单元测试-集成 ts 2 | 3 | ```bash 4 | yarn init -y #初始化项目 5 | ``` 6 | 7 | ```bash 8 | npx tsc --init #集成ts,但是报错了,因为项目里面没有typescript,所以安装typescript 9 | 10 | yarn add typescript --dev #然后再执行一下 npx tsc --init 就生成了tsconfig.json 文件 11 | ``` 12 | 13 | ## 解决使用jest 方法的时候的报错 (test index.spec.ts) 14 | 15 | ```bash 16 | yarn add jest @types/jest --dev #但是报错信息还是没有解决 这时候就要去tsconfig.json 里面配置一下,让ts 使用jest 的类型申明文件 17 | ``` 18 | 19 | >让代码支持隐式的any类型,以及让ts使用jest 的类型声明文件 20 | 21 | ```json 22 | { 23 | ...config, 24 | "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */ 25 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 26 | } 27 | ``` 28 | 29 | ## 使用babel 转化jest 的 import 规范 30 | 31 | > 因为jest是运行在node环境下面的,但是我们开发的环境是esm 规范,所以需要我们使用babel转换一下 32 | 33 | F&Q 为什么不直接在package.json 文件里面指定我们 的type="module" 34 | 35 | > 因为package.json是项目运行的环境,是项目本身的环境,而我们的jest是另外的一个项目,他的规范是自己定义的,而不是能够通过我们写的package.json 文件来规定的,况且jest的代码已经生成好了,我们如果要让他生成好的代码完美适应我们项目本身的环境的话,就还是需要Babel来转义一下 36 | 37 | [jest 官方文档](https://jestjs.io/docs/getting-started) 38 | 39 | ```bash 40 | yarn add --dev babel-jest @babel/core @babel/preset-env #安装转义需要的文件 41 | yarn add --dev @babel/preset-typescript #使用typescript 42 | ``` 43 | 44 | 配置Babel 45 | 46 | ```javascript 47 | //babel.config.js 48 | module.exports = { 49 | presets: [ 50 | ['@babel/preset-env', {targets: {node: 'current'}}], 51 | '@babel/preset-typescript', 52 | ], 53 | }; 54 | ``` 55 | 56 | ## 顺带一提,为了更好的测试请安装,插件jest,jest runner 57 | 58 | [jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) 59 | [jest runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) -------------------------------------------------------------------------------- /docs/07-实现 effect 返回 runner.md: -------------------------------------------------------------------------------- 1 | # 实现effect 返回runner 2 | 3 | 在上一章节已经实现了我们的effect 里面的触发和收集依赖的操作 4 | 5 | > 但是在effect函数的实现里面我们并没有定义返回值,在vue3中我们调用了effect过后会返回一个runner 函数,我们调用这个runner函数的时候就会再次的触发调用传递给effect 的函数,并且这个runner 函数的返回值也是传递进去函数运行后的返回值 6 | 7 | 我们先写测试用例 8 | 9 | ```typescript 10 | //effect.spec.ts 11 | 12 | import { reactive } from "../reactive"; 13 | import { effect } from "../effect"; 14 | describe("effect", () => { 15 | it("should return runner when call effect",()=>{ 16 | //我们调用effect(fn)之后是会返回一个function的runner ,当调用这个function(runner) 的时候,就会再执行一下这个effect 函数,并且这个时候会返回effect 函数执行的值的 17 | let foo =10; 18 | //希望effect 函数执行之后返回的是一个runner函数 19 | const runner=effect(()=>{ 20 | foo++; 21 | return "foo"; 22 | }) 23 | //期待第一次的effect 函数是会调用传递进去的fn的 24 | expect(foo).toBe(11) 25 | //我们把返回函数执行一遍,期待传递进去的函数再次被调用 26 | const r=runner() 27 | expect(foo).toBe(12) 28 | //然后我们还期待runner函数的返回值是传递进去的函数的 返回值 29 | expect(r).toBe("foo") 30 | }) 31 | }); 32 | ``` 33 | 34 | 然后我们来到effect.ts 里面实现相关的逻辑 35 | 36 | ```typescript 37 | 38 | class ReactiveEffect { 39 | private _fn: any; 40 | constructor(fn) { 41 | this._fn = fn; 42 | } 43 | run() { 44 | activeEffect = this; 45 | + let res=this._fn(); 46 | + //实现调用run方法的时候需要得到fn的返回值 47 | + return res; 48 | } 49 | } 50 | //map 对象就像是一个对象,但是这个对象里面的键可以是任何类型的属性 51 | let targetMap = new Map(); 52 | export function track(target, key) { 53 | //取到target 上面存的key值 54 | let depsMap = targetMap.get(target); 55 | 56 | if (!depsMap) { 57 | depsMap = new Map(); 58 | targetMap.set(target, depsMap); 59 | } 60 | let dep = depsMap.get(key); 61 | if (!dep) { 62 | dep = new Set(); 63 | //这里初始化的时候dep就是空 64 | depsMap.set(key, dep); 65 | } 66 | dep.add(activeEffect); 67 | 68 | //因为依赖的项都是不重复的函数,那么可以用set这个数据结构来存储 69 | // let dep =new Set() 70 | //然后把 target key 对应起来 71 | //target=> key => dep 72 | } 73 | export function trigger(target, key) { 74 | let depsMap = targetMap.get(target); 75 | let dep = depsMap.get(key); 76 | for (const effect of dep) { 77 | effect.run(); 78 | } 79 | } 80 | let activeEffect; 81 | export function effect(fn) { 82 | let _effect = new ReactiveEffect(fn); 83 | _effect.run(); 84 | //以当前这个effect的实例作为run 方法的this的指向 85 | + return _effect.run.bind(_effect); 86 | } 87 | ``` -------------------------------------------------------------------------------- /docs/08-实现 effect 的scheduler 功能.md: -------------------------------------------------------------------------------- 1 | # 实现effect 的scheduler 功能 2 | 3 | > effect 的scheduler 功能简介 4 | 1. 通过effect 的第二个参数给定的一个scheduler 的fn 5 | 2. effect 第一次执行的时候 还会执行fn 6 | 3. 当响应式对象更新的时候不会执行fn了而是执行scheduler 7 | 4 .如果说执行runner 的时候会再次执行fn 8 | 9 | --- 10 | 对应的 测试用例写法 11 | 12 | ```typescript 13 | it("scheduler", () => { 14 | //1.通过effect 的第二个参数给定的一个scheduler 的fn 15 | //2. effect 第一次执行的时候 还会执行fn 16 | //3. 当响应式对象更新的时候不会执行fn了而是执行scheduler 17 | //4 .如果说执行runner 的时候会再次执行fn 18 | let dummy; 19 | let run: any; 20 | const scheduler = jest.fn(() => { 21 | run = runner; 22 | }); 23 | const obj = reactive({ foo: 1 }); 24 | const runner = effect( 25 | () => { 26 | dummy = obj.foo; 27 | }, 28 | { 29 | scheduler, 30 | } 31 | ); 32 | //scheduler 就是effect 的第二个参数,在初始化的时候不会被调用 33 | expect(scheduler).not.toHaveBeenCalled() 34 | //然后第一次的时候是会被调用的,调用后dummy 就会被赋值为1 35 | expect(dummy).toBe(1) 36 | obj.foo++; 37 | //当obj 改变的时候会被调用scheduler 38 | expect(scheduler).toHaveBeenCalledTimes(1) 39 | //但是这个时候不会去更新dummy的值 40 | expect(dummy).toBe(1) 41 | //当执行run的时候才会被调用dummy 42 | run(); 43 | expect(dummy).toBe(2) 44 | }); 45 | ``` 46 | 47 | --- 48 | 49 | 对应的effect 代码就不贴了,可以看对应的提交记录 50 | 这里有些值得学习的地方 51 | **scheduler** 这个参数**不会影响依赖收集**的过程,**只会影响触发依赖**的过程, 52 | 但是触发依赖的时候我们不知道当前的scheduler是传递了的还是没传递的, 53 | 而对应的依赖是由**ReactiveEffect**这个构造函数生成的, 54 | 所以我们可以在这个类**初始化**的时候传递这个**scheduler**进到每一个**effect** 实例里面去, 55 | 当我们触发依赖的时候拿出每一个**effect** 然后判断**effect** 有没有**scheduler** 属性,有的话就执行scheduler 没有的话就执行effect.run() 56 | -------------------------------------------------------------------------------- /docs/09-实现 effect 的 stop 功能.md: -------------------------------------------------------------------------------- 1 | # 实现effect 的stop功能 2 | 3 | ## stop 功能 4 | 5 | > stop 的功能简介 6 | 1. effect.ts 会导出一个stop函数,当调用stop函数,并且把runner(也就是effect的返回值传递给他时),再次触发trigger 也就是触发响应式对象值得更新得时候,当前用户传递进来得依赖不会执行(就是effect包裹得函数不会执行) 7 | 2. 当再次调用runner 的时候effect包裹的函数执行 8 | 9 | 对应的测试代码 10 | 11 | ```typescript 12 | it("stop", () => { 13 | let dump; 14 | const obj = reactive({ prop: 1 }); 15 | const runner = effect(() => { 16 | dump = obj.prop; 17 | }); 18 | obj.prop = 2; 19 | expect(dump).toBe(2); 20 | stop(runner); 21 | obj.prop = 3; 22 | expect(dump).toBe(2); 23 | 24 | //stopped effect should still be manually callable 25 | runner(); 26 | expect(dump).toBe(3); 27 | }); 28 | ``` 29 | 30 | --- 31 | 老样子代码就不贴了 32 | **先理一下思路** 33 | 34 | 35 | - 我们要干什么(我们要停止一个函数的运行) 36 | - 停止的函数怎么来的(我们通过effect 函数返回的) 37 | - 我们通过effect 函数处理的函数返回的函数本质是什么(本质就是用户通过effect函数传入进来的那个改变 响应式对象值的方法) 38 | - 我们怎么去拿到这个函数呢(我想我们effect 对象是自己创建的,那么如果传递进去了一个stop方法的话那么我们在对应的实例上也挂载一个一个stop方法,当调用stop(runner)的时候我们对应的去调用实例对象上面的stop 方法不就行了吗) 39 | - stop 方法停止runner 函数的运行,我们需要去收集当前的effect 的deps,那么我们可以通过activeEffect.deps 去收集dep,在收集依赖的时候反向收集一波 40 | - 然后在ReactiveEffect 类里面去实现stop 方法,当调用stop 方法的时候去把deps 里面当前的dep删除掉,那么以后的trigger 就不会执行effect了 41 | 42 | --- 43 | 44 | ## 实现effect 的onStop 功能 45 | 46 | 这个功能很简单,就是通过effect 的第二个参数传递一个onStop 函数进去,再stop 调用后如果有这个函数传递的话运行这个函数如果没有就不运行 47 | 48 | ```typescript 49 | //effect onStop 的测试逻辑 50 | it("onStop", () => { 51 | const obj = reactive({ foo: 1 }); 52 | const onStop = jest.fn(); 53 | let dummy; 54 | const runner = effect( 55 | () => { 56 | dummy = obj.foo; 57 | }, 58 | { onStop } 59 | ); 60 | stop(runner); 61 | expect(onStop).toBeCalledTimes(1); 62 | }); 63 | ``` 64 | 65 | --- 66 | ***让我们来理一下现在这个effect文件的思路*** 67 | 68 | - 首先我们需要一个响应式对象,当get的时候触发track(收集依赖),当set的时候触发trigger(触发依赖) 69 | - 然后我们通过effect函数包裹一个函数表达式,函数表达式里面有我们响应式对象的值被调用的情况,就会触发依赖收集,有因为当前函数被effect包裹那么也会走到effect函数的逻辑 70 | - effect 函数会处理传递进来的函数通过ReactiveEffect类暴露出run,stop,等方法,然后通过effect的第二个参数,options 传递进来的属性也会被ReactiveEffect接收,进行处理 71 | - effect 函数也会返回一个传入函数,其返回值也是和传入函数相同的一个值,并且会在其函数上挂载effect 的全部方法 runner.effect=_effect; 72 | - 而在ReactiveEffect这边的话我们是根据传入进来的参数做了各种操作 run,stop ,scheduler,onStop 并且在调用run方法的时候把整个作用域里面的activeEffect的值改变成了当前的实例 73 | - 那么在收集依赖的时候我们就可以拿到当前的activeEffect 做点事情了,`activeEffect.deps.push(dep);` 我们在activeEffect上面挂载了所有依赖的函数集合 74 | - 而这个函数集合又是通过`dep.add(activeEffect);` 来收集的那么这里可以就类似于这种activeEffect.deps =[activeEffect[],activeEffect[]] 75 | - 还有一个很牛逼的点是通过targetMap 这个Map对象存储了target key 以及key所对应的所有的effect effect 的结构层层递归,就像上面的activeEffect 一样 76 | -------------------------------------------------------------------------------- /docs/10-实现readonly 功能.md: -------------------------------------------------------------------------------- 1 | # readonly 功能的实现 2 | 3 | > 首先我们需要明白的是,readonly 跟reactive 是差不多的只不过通过readonly 创建出来的对象不能被set,就如同这个名字一样(只读) 4 | 5 | 那么这个逻辑就比较简单了,就把之前写好的reactive方法拿下来改个名字删点代码就ok . 6 | 7 | ```typescript 8 | import {reactive} from "../reactive" 9 | describe("reactive",()=>{ 10 | it("happy path",()=>{ 11 | const original={foo:10}; 12 | const observer=reactive(original) 13 | //创建响应式对象,希望响应式对象能读取到之前的值 14 | expect(observer.foo).toBe(10) 15 | //希望响应式对象和原来的值是两个不同的引用 16 | expect(observer).not.toBe(original) 17 | }) 18 | }) 19 | ``` 20 | 21 | ## 代码重构 22 | 23 | - 把get 和set 抽离出去,变成一个高阶函数,传入是否为readonly 返回不同的函数,然后把handler也单独抽离成一个文件,并且为了让代码更语义化,那么可以把 new proxy 这个动作也处理一下 24 | - 另一个需要优化的点就是可以利用缓存的技术来创建handlers,而不是每次都重新创建handlers 25 | 26 | 重构完成的代码 27 | 28 | ```typescript 29 | //reactive.ts 30 | import { mutableHandlers, readonlyHandlers } from "./baseHandler"; 31 | 32 | export function reactive(raw) { 33 | return createActionObject(raw, mutableHandlers); 34 | } 35 | 36 | export function readonly(raw) { 37 | return createActionObject(raw, readonlyHandlers); 38 | } 39 | 40 | function createActionObject(raw, baseHandlers) { 41 | return new Proxy(raw, baseHandlers); 42 | } 43 | ``` 44 | 45 | ```typescript 46 | import { track, trigger } from "./effect"; 47 | const get = createGetter(); 48 | const set = createSetter(); 49 | const readonlyGet=createGetter(true) 50 | 51 | function createGetter(isReadonly = false) { 52 | return function get(target, key) { 53 | const res = Reflect.get(target, key); 54 | if(!isReadonly){ 55 | track(target, key); 56 | } 57 | return res; 58 | }; 59 | } 60 | 61 | function createSetter() { 62 | return function set(target, key, value) { 63 | const res = Reflect.set(target, key, value); 64 | trigger(target, key); 65 | return res; 66 | }; 67 | } 68 | 69 | export const mutableHandlers = { 70 | get, 71 | set, 72 | }; 73 | 74 | export const readonlyHandlers = { 75 | get:readonlyGet, 76 | set(target, key, value) { 77 | console.warn( 78 | `target ${target} is readonly, ${key.toString()} can not be set to ${value}` 79 | ); 80 | return true; 81 | }, 82 | }; 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/11-实现IsReadonly和IsReactive.md: -------------------------------------------------------------------------------- 1 | # 实现IsReadonly 和 IsReactive 2 | 3 | > 这个功能其实很简单的 4 | 5 | **思路** 6 | 我们在读取创建出来的响应式对象的时候,读取任意一个属性都会去触发他的get操作,我们只要在get操作里面拦截相应的值,然后再把传递进去的isReadonly参数返回出去就好了 7 | 8 | 还有一种特殊情况就是说,那个值并不是我们创建的reactive对象,那么该Reactive对象就会是一个undefined 这种情况下直接返回!!undefined 即可,就取一下他的boolean值 9 | 10 | 再者,这两个全局变量的名字很重要,所以用的枚举类型 11 | 12 | -------------------------------------------------------------------------------- /docs/12-优化stop功能.md: -------------------------------------------------------------------------------- 1 | # 优化stop功能 2 | 3 | > 遇到的问题 4 | > 之前通过stop停止了一个runner然后再直接改变reactive对象的值,然后去观察dump 的值是否改变 5 | > 但是现在如果通过obj.foo++这种方式去改变reactive对象的值,那么测试就会不通过 6 | 7 | 新的测试用例 8 | 9 | ```typescript 10 | it("stop", () => { 11 | // 1. effect.ts 会导出一个stop函数,当调用stop函数,并且把runner(也就是effect的返回值传递给他时),再次触发trigger 也就是触发响应式对象值得更新得时候,当前用户传递进来得依赖不会执行(就是effect包裹得函数不会执行) 12 | // 2. 当再次调用runner 的时候effect包裹的函数执行 13 | let dump; 14 | const obj = reactive({ prop: 1 }); 15 | const runner = effect(() => { 16 | dump = obj.prop; 17 | }); 18 | obj.prop = 2; 19 | expect(dump).toBe(2); 20 | stop(runner); 21 | // obj.prop = 3; 22 | //这里会先去走get的操作,从而把清空的依赖重新收集了起来 23 | obj.prop++; 24 | expect(dump).toBe(2); 25 | 26 | //stopped effect should still be manually callable 27 | runner(); 28 | expect(dump).toBe(3); 29 | }); 30 | ``` 31 | 32 | **bug出现的原因分析** 33 | 正常的情况是这样的,我们直接去改变响应式对象的值,那么响应式对象只会去触发set操作,但是我们通过obj.foo++这种方式去改变值的话那么会触发get操作先去读取一遍值,读取值的时候触发了依赖收集,而我们之前的stop方法刚刚把依赖删除了,这时候有因为依赖收集从而有收集了依赖 34 | 35 | **bug解决方案** 36 | 我们再申明一个全局变量shouldTrack来判断当前的这个effect时候应该被收集 37 | 而我们是在ReactEffect 的run方法里面首次去触发依赖收集这个动作的,那么我们可以在run方法那边给shouldTrack来赋值,通过this.active来判断当前的effect的状态,如果是active的状态那么改变shouldTrack的值为true,并且在执行完函数的时候reset掉shouldTrack 38 | 39 | **后续操作** 40 | 然后在track函数的时候判断一下当前的track时候为真,如果是假那么就返回掉 41 | 42 | NOTE:调用了stop方法之后整个ReactEffect的active为false,为false 的话,只能调用run方法才能改变reactive对象的值,但是如果调用了get方法之后,执行用户传递的fn会被收集依赖,我们在执行fn前把shouldtrack的值赋值为true那么到track的时候再判断一下shouldTrack为不为true然后再收集,要是为false就不收集,为true就收集。 -------------------------------------------------------------------------------- /docs/13-实现reactive和readonly嵌套对象转换功能.md: -------------------------------------------------------------------------------- 1 | # 实现reactive和readonly嵌套对象转换功能 2 | 3 | 其实这个功能也很简单,我们在createGetter的时候会去拿到每一个调用了的target[key],我们在拿到这个值的时候判断一下当前对象是否是一个对象,如果是的话那么就再次的去调用reactive函数或者是readonly函数来构造响应式对象 4 | 5 | 瞅一瞅对应的测试 6 | 7 | > reactive的测试 8 | 9 | ```typescript 10 | //reactive.spec.ts 11 | it("nested reactive",()=>{ 12 | const original={ 13 | nested:{ 14 | foo:1 15 | },array:[{bar:2}] 16 | } 17 | const observer=reactive(original) 18 | expect(isReactive(observer.nested)).toBe(true) 19 | expect(isReactive(observer.array)).toBe(true) 20 | expect(isReactive(observer.array[0])).toBe(true) 21 | }) 22 | ``` 23 | 24 | > readonly的测试 25 | 26 | ```typescript 27 | //readonly.spec.ts 28 | it("nested readonly", () => { 29 | const original = { foo: 1, bar: { baz: 2 } }; 30 | const wrapped = readonly(original); 31 | //IsReadonly 32 | expect(isReadonly(wrapped)).toBe(true) 33 | expect(isReadonly(wrapped.bar)).toBe(true) 34 | }); 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/14-实现shallowReadonly功能.md: -------------------------------------------------------------------------------- 1 | # 实现shallowReadonly 功能 2 | 3 | > shallowReadonly 字面意思就是浅的readonly功能 4 | > 是为了性能优化做的api只有最外面一层对象是readonly而其内部是原始的对象 5 | 6 | 我们来到测试这边 7 | 8 | ```typescript 9 | import { isReadonly,shallowReadonly } from "../reactive"; 10 | 11 | describe("shallowReadonly happy path",()=>{ 12 | it("should not make non-reactive properties reactive",()=>{ 13 | const props=shallowReadonly({n:{foo:1}}) 14 | expect(isReadonly(props)).toBe(true) 15 | expect(isReadonly(props.n)).toBe(false) 16 | }) 17 | it("warn then call set", () => { 18 | console.warn = jest.fn(); 19 | const user = shallowReadonly({ age: 10 }); 20 | user.age = 11; 21 | expect(console.warn).toBeCalled(); 22 | }); 23 | }) 24 | ``` 25 | 26 | 然后来到reactive这边发现我们的所有的函数都被我们层层封装了,那么再要创建一个shallowReadonly 还是得重新走一遍流程 27 | 28 | 我们来到baseHandler里面 29 | 照葫芦画瓢 30 | 31 | ```typescript 32 | const readonlyGet = createGetter(true); 33 | const shallowReadonlyGet = createGetter(true, true); 34 | ``` 35 | 36 | 然后改造createGetter方法 37 | 38 | ```typescript 39 | function createGetter(isReadonly = false, shallow = false) { 40 | return function get(target, key) { 41 | const res = Reflect.get(target, key); 42 | if (key === reactiveFlags.IS_READONLY) { 43 | return isReadonly; 44 | } else if (key === reactiveFlags.IS_REACTIVE) { 45 | return !isReadonly; 46 | } 47 | if (shallow) { 48 | return res; 49 | } 50 | if (!isReadonly) { 51 | track(target, key); 52 | } 53 | 54 | if (isObject(res)) { 55 | return isReadonly ? readonly(res) : reactive(res); 56 | } 57 | return res; 58 | }; 59 | } 60 | ``` 61 | 62 | 然后更新一下导出函数就可以了 63 | 64 | ```typescript 65 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet, 66 | }); 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/15-实现isProxy功能.md: -------------------------------------------------------------------------------- 1 | # 实现isProxy功能 2 | 3 | > 实现isProxy 功能其实很简单的,之前就封装了isReactive 和isReadonly方法,那么在isProxy里面再调用这连个方法不就得了 4 | 5 | ```typescript 6 | export const isProxy = (value: any) => { 7 | return isReactive(value) || isReadonly(value); 8 | }; 9 | ``` 10 | 11 | [isProxy官方文档](https://vuejs.org/api/reactivity-utilities.html#isproxy) 12 | -------------------------------------------------------------------------------- /docs/16-实现ref功能.md: -------------------------------------------------------------------------------- 1 | # 实现ref功能 2 | 3 | 我们需要知道ref是什么 4 | [ref 官网链接](https://vuejs.org/api/reactivity-core.html#ref) 5 | 接受一个内部值并返回一个响应式和可变的ref对象,该对象只有一个指向内部值的属性.value 6 | 7 | 通过这句话和官网的例子就知道了这个函数会把一个单独的值,构造成一个响应式对象类似于这种形式 8 | {value:接受的值} 9 | 10 | ```typescript 11 | function ref(value: T): Ref> 12 | 13 | interface Ref { 14 | value: T 15 | } 16 | ``` 17 | 18 | 再来看看我们的单元测试 19 | 20 | ```typescript 21 | import { effect } from "../effect"; 22 | import { ref } from "../ref"; 23 | 24 | describe("ref", () => { 25 | it("happy path", () => { 26 | const a = ref(1); 27 | expect(a.value).toBe(1); 28 | }); 29 | 30 | it("should be reactive", () => { 31 | //创建一个对象 32 | const a = ref(1); 33 | let dummy; 34 | let calls = 0; 35 | //然后去通过effect 做依赖收集 36 | effect(() => { 37 | calls++; 38 | dummy = a.value; 39 | }); 40 | expect(calls).toBe(1); 41 | expect(dummy).toBe(1); 42 | //改变value的值我们希望依赖被触发 43 | a.value = 2; 44 | expect(calls).toBe(2); 45 | expect(dummy).toBe(2); 46 | //same value should not trigger 47 | //如果改变的值和原有的数据相等那么就不进行触发依赖 48 | a.value = 2; 49 | expect(calls).toBe(2); 50 | expect(dummy).toBe(2); 51 | }); 52 | 53 | it("should make nested properties reactive", () => { 54 | const a = ref({ count: 1 }); 55 | let dummy; 56 | effect(() => { 57 | dummy = a.value.count; 58 | }); 59 | expect(dummy).toBe(1); 60 | a.value.count = 2; 61 | expect(dummy).toBe(2); 62 | }); 63 | }); 64 | ``` 65 | 66 | 先来说第一个单元测试吧,我们希望通过ref()函数包裹后我们通过创建出来的新的对象的value值可以得到之前的值 67 | > 让我们简单的来实现一下这个功能 68 | 69 | ```typescript 70 | class RefImpl { 71 | private _value: any; 72 | 73 | constructor(value) { 74 | this._value = value; 75 | } 76 | 77 | get value() { 78 | return this._value; 79 | } 80 | 81 | // set value(newValue) { 82 | // } 83 | } 84 | 85 | export function ref(value) { 86 | return new RefImpl(value); 87 | } 88 | ``` 89 | 90 | 我们简单的创建了一个RefImpl的类来实现我们的get value的时候返回传递进来的value值 91 | 92 | 然后我们再来看看第二个测试,我们希望创建出来的这个对象应该是reactive的,我们通过effect去收集依赖,然后在值改变的时候们希望收集起来的依赖会被触发, 93 | 然后还有一个边缘keys的情况就是说当改变的值是一样的那么我们就不触发收集起来的依赖 94 | 95 | > 之前我们在reactive的时候已经做了依赖收集和触发依赖的逻辑操作了,但是那时候的依赖收集是收集的对象的现在我们收集的是单个值的,所以就不用之前那么麻烦了,我们来做一下逻辑抽离 96 | 97 | ```typescript 98 | //effect.ts 99 | export function isTracking() { 100 | return shouldTrack && activeEffect !== undefined; 101 | } 102 | 103 | export function track(target, key) { 104 | if (!isTracking()) return; 105 | //取到target 上面存的key值 106 | let depsMap = targetMap.get(target); 107 | 108 | if (!depsMap) { 109 | depsMap = new Map(); 110 | targetMap.set(target, depsMap); 111 | } 112 | let dep = depsMap.get(key); 113 | if (!dep) { 114 | dep = new Set(); 115 | //这里初始化的时候dep就是空 116 | depsMap.set(key, dep); 117 | } 118 | trackEffects(dep); 119 | } 120 | 121 | export function trackEffects(dep) { 122 | if (dep.has(activeEffect)) return; 123 | dep.add(activeEffect); 124 | activeEffect.deps.push(dep); 125 | } 126 | 127 | export function trigger(target, key) { 128 | let depsMap = targetMap.get(target); 129 | let dep = depsMap.get(key); 130 | triggerEffects(dep); 131 | } 132 | 133 | export function triggerEffects(dep) { 134 | for (const effect of dep) { 135 | if (effect.scheduler) { 136 | effect.scheduler(); 137 | } else { 138 | effect.run(); 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | --- 145 | 那么在ref.ts 里面我们就可以这么做 146 | 147 | - 在初始化的时候初始化dep为一个空的set数组 148 | - 在get操作的时候把当前的依赖收集进去 149 | - 然后在set的时候把依赖再触发一次,在这个时候需要注意值是否发生了改变,我们需要对比的是原始的值和新值,而不是proxy和新值,所以我们需要再声明一个rawValue来存放之前没改变之前的数据 150 | 151 | ```typescript 152 | import { trackEffects, triggerEffects, isTracking } from "./effect"; 153 | import { reactive } from "./reactive"; 154 | import { hasChanged, isObject } from "./shared"; 155 | 156 | class RefImpl { 157 | private _value: any; 158 | public dep; 159 | private _rawValue: any; 160 | 161 | constructor(value) { 162 | // 1.看看value是不是对象,如果不是直接返回,如果是那么就处理包裹一下 163 | this._rawValue = value; 164 | this._value = convert(value); 165 | this.dep = new Set(); 166 | } 167 | 168 | get value() { 169 | //这里需要收集依赖 170 | trackRefValue(this); 171 | return this._value; 172 | } 173 | 174 | set value(newValue) { 175 | //这里需要触发依赖 176 | //如果对比的话那么对象 177 | if (hasChanged(newValue, this._rawValue)) { 178 | this._rawValue = newValue; 179 | this._value = convert(newValue); 180 | triggerEffects(this.dep); 181 | } 182 | } 183 | } 184 | 185 | function trackRefValue(ref) { 186 | if (isTracking()) { 187 | trackEffects(ref.dep); 188 | } 189 | } 190 | 191 | function convert(value) { 192 | return isObject(value) ? reactive(value) : value; 193 | } 194 | 195 | export function ref(value) { 196 | return new RefImpl(value); 197 | } 198 | ``` 199 | -------------------------------------------------------------------------------- /docs/17-实现isRef和unRef功能.md: -------------------------------------------------------------------------------- 1 | # 实现isRef功能和unRef功能 2 | 3 | ## 实现isRef功能 4 | 5 | > 先说思路,在创建ref对象的时候在实例上面挂载一个__v_isRef属性,在调用isRef的时候我们返回ref.__v_isRef就行了 6 | 7 | [isRef官方地址](https://vuejs.org/api/reactivity-utilities.html#isref) 8 | 测试用例 9 | 10 | ```typescript 11 | it("isRef",() => { 12 | const a=ref(1) 13 | const user=reactive({age:1}) 14 | expect(isRef(a)).toBe(true) 15 | expect(isRef(1)).toBe(false) 16 | expect(isRef(user)).toBe(false) 17 | }) 18 | ``` 19 | 20 | ```typescript 21 | class RefImpl { 22 | private _value: any; 23 | public dep; 24 | //新加的 25 | public __v_isRef=true; 26 | private _rawValue: any; 27 | 28 | constructor(value) { 29 | // 1.看看value是不是对象,如果不是直接返回,如果是那么就处理包裹一下 30 | this._rawValue = value; 31 | this._value = convert(value); 32 | this.dep = new Set(); 33 | } 34 | 35 | get value() { 36 | //这里需要收集依赖 37 | trackRefValue(this); 38 | return this._value; 39 | } 40 | 41 | set value(newValue) { 42 | //这里需要触发依赖 43 | //如果对比的话那么对象 44 | if (hasChanged(newValue, this._rawValue)) { 45 | this._rawValue = newValue; 46 | this._value = convert(newValue); 47 | triggerEffects(this.dep); 48 | } 49 | } 50 | } 51 | export function isRef(value){ 52 | return !!value.__v_isRef 53 | 54 | } 55 | 56 | ``` 57 | 58 | ## 实现unRef功能 59 | 60 | [unRef官方地址](https://vuejs.org/api/reactivity-utilities.html#unref) 61 | 62 | > 先说思路,我们调用unRef的时候,如果是一个ref,那么我们就返回ref.value如果不是一个ref那么就返回原先传递进来的值 63 | 64 | 测试代码 65 | 66 | ```typescript 67 | //ref.spec.ts 68 | it("unRef",() => { 69 | const a=ref(1) 70 | expect(unRef(1)).toBe(1) 71 | }) 72 | ``` 73 | 74 | 实现代码 75 | 76 | ```typescript 77 | export function unRef(ref){ 78 | return isRef(ref)?ref.value:ref; 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/18-实现proxyRefs 功能.md: -------------------------------------------------------------------------------- 1 | # 实现proxyRefs功能 2 | 3 | > proxyRefs 这个函数也是一个工具函数,通过这个函数包裹的包含ref的对象,可以通过object.key 这样的方式直接访问 4 | 5 | ## get 6 | 7 | 请看测试代码 8 | 9 | ```typescript 10 | it("proxyRefs get", () => { 11 | const user = { age: ref(10), name: "xiaoming" }; 12 | const proxyUser=proxyRefs(user) 13 | expect(user.age.value).toBe(10) 14 | expect(proxyUser.age).toBe(10) 15 | expect(proxyUser.name).toBe("xiaoming") 16 | }); 17 | ``` 18 | 19 | 来看ref.ts 的具体实现 20 | 21 | ```typescript 22 | export function proxyRefs(objectWithRefs) { 23 | return new Proxy(objectWithRefs, { 24 | get(target, key) { 25 | //如果是ref的话就返回ref.value,如果不是ref的话那么就返回target.key 26 | return unRef(Reflect.get(target, key)); 27 | }, 28 | // set(target,key,newValue){ 29 | 30 | // } 31 | }); 32 | } 33 | ``` 34 | 35 | ## set 36 | 37 | > 然后我们来分析一下set的时候的逻辑,当原来的值是ref 的时候,并且新值不是ref的时候,那么需要改变原先ref的value值为newValue 38 | 39 | 测试代码 40 | 41 | ```typescript 42 | //ref.spec.ts 43 | it("proxyRefs set", () => { 44 | const user = { age: ref(10), name: "xiaoming" }; 45 | const proxyUser=proxyRefs(user) 46 | expect(user.age.value).toBe(10) 47 | expect(proxyUser.age).toBe(10) 48 | expect(proxyUser.name).toBe("xiaoming") 49 | }); 50 | 51 | 52 | ``` 53 | 54 | 具体实现 55 | 56 | ```typescript 57 | //ref.ts 58 | export function proxyRefs(objectWithRefs) { 59 | return new Proxy(objectWithRefs, { 60 | get(target, key) { 61 | //如果是ref的话就返回ref.value,如果不是ref的话那么就返回target.key 62 | return unRef(Reflect.get(target, key)); 63 | }, 64 | // ref&&newValue 不是ref ref.value=newValue 65 | set(target,key,newValue){ 66 | if(isRef(target[key])&&!isRef(newValue)){ 67 | return (target[key]=newValue) 68 | } 69 | else{ 70 | return Reflect.set(target,key,newValue) 71 | } 72 | } 73 | }); 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/22-element 渲染逻辑.md: -------------------------------------------------------------------------------- 1 | # element 组件的渲染逻辑 2 | 3 | 其实渲染实际的 element 并没有那么很高深的样子 4 | 5 | ```javascript 6 | //创建元素 7 | const el = document.createElement("div"); 8 | //为元素设置属性 9 | el.setAttribute("class", "root"); 10 | //为元素设置children 等属性 11 | el.textContent = "children here"; 12 | //将元素 插入到页面上 13 | document.body.append(el); 14 | ``` 15 | 16 | 主流程搞清楚了,我们需要在patch的时候判断一下当前到底是component 还是实际需要渲染出来的实际元素 17 | 18 | ```typescript 19 | //renderer.ts 20 | function patch(vNode, container) { 21 | //处理组件 22 | if (typeof vNode.type === "string") { 23 | console.log("element 类型"); 24 | processElement(vNode, container); 25 | } else if (isObject(vNode)) { 26 | console.log("component 类型"); 27 | processComponent(vNode, container); 28 | } 29 | } 30 | ``` 31 | 32 | 然后我们去实现一下processElement 方法 33 | 和processComponent 方法一样先挂载mount一下element 34 | 35 | ```typescript 36 | //renderer.ts 37 | function mountElement(vNode, container) { 38 | const { type, children, props } = vNode; 39 | const el = document.createElement(type); 40 | setMountElementAttribute(el, props); 41 | mountChildren(children, el); 42 | container.appendChild(el); 43 | } 44 | ``` 45 | 46 | 正如上面 渲染实际的element 举例那里一样,我们会根据type创建一个element,然后通过setAttribute 设置传递过来的属性props,然后添加children到在我们创建的元素身上,最终我们把这个元素添加到我们的rootContainer身上 47 | 48 | ```typescript 49 | //renderer.ts 50 | function setMountElementAttribute(el, attributes) { 51 | for (const key in attributes) { 52 | const attributeValue = attributes[key]; 53 | const value = Array.isArray(attributeValue) 54 | ? attributeValue.join(" ") 55 | : attributeValue; 56 | el.setAttribute(key, value); 57 | } 58 | } 59 | 60 | function mountChildren(children, container) { 61 | if (typeof children === "string") { 62 | container.textContent = children; 63 | } else if (Array.isArray(children)) { 64 | children.map((v) => { 65 | patch(v, container); 66 | }); 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/23-实现组件代理对象.md: -------------------------------------------------------------------------------- 1 | # 实现组件代理对象 2 | 3 | 需求场景:我们需要在render 里面去 获取this.msg 而msg 是setup 返回的对象里面的属性 4 | 5 | ```javascript 6 | import { h } from "../../lib/ass-vue.esm.js"; 7 | window.self = null; 8 | export const App = { 9 | // .vue 10 | // 11 | //render 12 | 13 | render() { 14 | //ui 逻辑 15 | window.self = this; 16 | return h( 17 | "div", 18 | { id: "root", class: ["red", "blue"] }, 19 | "hi" + this.msg 20 | // [h("p", { class: "red" }, "hi red"), h("p", { class: "blue" }, "hi blue")] 21 | ); 22 | }, 23 | setup() { 24 | return { 25 | msg: "ass-vue", 26 | }; 27 | }, 28 | }; 29 | ``` 30 | 31 | 我们可以在组件初始化的时候去创建一个代理对象,在setup的时候把这个代理对象赋值,然后在调用this.msg 的时候去绑定proxy 到render 上 32 | 33 | 然后我们来改造一下我们的setupStatefulComponent函数 34 | 35 | ```typescript 36 | //component.ts 37 | function setupStatefulComponent(instance) { 38 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 39 | const Component = instance.type; 40 | const { setup } = Component; 41 | 42 | //ctx 43 | const proxy =new Proxy({},{ 44 | get(target,key){ 45 | //setupState 46 | const {setupState}=instance; 47 | if(key in setupState){ 48 | return setupState[key] 49 | } 50 | } 51 | }) 52 | 53 | instance.proxy=proxy; 54 | 55 | if (setup) { 56 | const setupResult = setup(); 57 | handleSetupResult(instance, setupResult); 58 | } 59 | } 60 | ``` 61 | 62 | 然后我们在调用render的时候需要把proxy代理对象取出来绑定到render的this上面 63 | 64 | ```typescript 65 | //renderer.ts 66 | function setupRenderEffect(instance, vnode, container) { 67 | const { proxy } = instance; 68 | const subTree = instance.render.call(proxy); 69 | //subTree 就是虚拟节点树 70 | /* 71 | vnode ->patch 72 | vnode -> element mountElement 73 | */ 74 | patch(subTree, container); 75 | // element=> mount 76 | vnode.el = subTree.el; 77 | } 78 | ``` 79 | 80 | ## 实现$el api(返回根节点) 81 | 82 | 83 | 我们需要在vnode上面去存储这个$el 84 | 85 | ```typescript 86 | //renderer.ts 87 | function mountElement(vnode, container) { 88 | //vnode =>element =>div 89 | const { type, children, props } = vnode; 90 | const el = document.createElement(type); 91 | vnode.el = el; 92 | setMountElementAttribute(el, props); 93 | mountChildren(children, el); 94 | container.appendChild(el); 95 | } 96 | ``` 97 | 98 | 然后再创建vnode的时候去申明这个$el 99 | 100 | ```typescript 101 | //vnode.ts 102 | export function createVNode(type, props?, children?) { 103 | const vnode = { type, props, children, el: null }; 104 | return vnode; 105 | } 106 | ``` 107 | 108 | 然后我们存储了这个$el 然后我们需要去处理这个代理对象的值 109 | 110 | ```typescript 111 | //component.ts 112 | function setupStatefulComponent(instance) { 113 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 114 | const Component = instance.type; 115 | const { setup } = Component; 116 | 117 | //ctx 118 | const proxy =new Proxy({},{ 119 | get(target,key){ 120 | //setupState 121 | const {setupState}=instance; 122 | if(key in setupState){ 123 | return setupState[key] 124 | } 125 | 126 | if(key==="$el"){ 127 | return instance.vnode.el 128 | } 129 | } 130 | }) 131 | 132 | instance.proxy=proxy; 133 | 134 | if (setup) { 135 | const setupResult = setup(); 136 | handleSetupResult(instance, setupResult); 137 | } 138 | } 139 | ``` 140 | 141 | 这个时候去网页上面测试 控制台输入self.$el 是null 142 | 143 | 因为最开始的时候 我们的vnode是一个对象的形式,还没有到真正的渲染div的阶段,所以self.$el 全部都是null 144 | 我们需要等到所有的节点都挂载完成之后再对最开始的vnode进行赋值 145 | 146 | ```typescript 147 | //renderer.ts 148 | function setupRenderEffect(instance, vnode, container) { 149 | const { proxy } = instance; 150 | const subTree = instance.render.call(proxy); 151 | //subTree 就是虚拟节点树 152 | /* 153 | vnode ->patch 154 | vnode -> element mountElement 155 | */ 156 | patch(subTree, container); 157 | // element=> mount 158 | vnode.el = subTree.el; 159 | } 160 | ``` 161 | 162 | ## 抽离proxy 逻辑 163 | 164 | 值得注意的是用ctx传递参数 (有点牛逼哦) 165 | 166 | ```typescript 167 | //componentPublicInstance.ts 168 | const publicPropertiesMap = { 169 | $el: (instance) => instance.vnode.el, 170 | }; 171 | 172 | export const PublicInstanceProxyHandlers = { 173 | get({ _: instance }, key) { 174 | //从setupState里面获取值 175 | const { setupState } = instance; 176 | if (key in setupState) { 177 | return setupState[key]; 178 | } 179 | 180 | if (key in publicPropertiesMap) { 181 | const publicGetter = publicPropertiesMap[key]; 182 | return publicGetter(instance); 183 | } 184 | }, 185 | }; 186 | ``` 187 | 188 | 更改component.ts 里面的逻辑 189 | 190 | ```typescript 191 | // component.ts 192 | function setupStatefulComponent(instance) { 193 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 194 | const Component = instance.type; 195 | const { setup } = Component; 196 | 197 | //ctx 198 | 199 | const proxy = new Proxy ({_:instance},PublicInstanceProxyHandlers) 200 | 201 | instance.proxy=proxy; 202 | 203 | if (setup) { 204 | const setupResult = setup(); 205 | handleSetupResult(instance, setupResult); 206 | } 207 | } 208 | ``` 209 | -------------------------------------------------------------------------------- /docs/24-实现shapeFlags 功能.md: -------------------------------------------------------------------------------- 1 | # 实现shapeFlags 功能 2 | 3 | > 需求场景:vnode 有两个状态,vnode的children也有两个状态,如果每次都去通过是否为string array ,这样来判断的话,不太好,并且难管理 4 | 5 | 所以统一来处理这个场景 6 | 7 | ```typescript 8 | export const shapeFlags ={ 9 | element:0, 10 | status_component:0, 11 | text_children:0, 12 | array_children:0 13 | } 14 | 15 | //如果有某个属性,那么就把对应的值赋为1 就行了(设置值) 16 | //判断某个值存在否,也很简单,通过 shapeFlags.element 这样就可以(读取值) 17 | ``` 18 | 19 | 但是这样的话不够高效,vue3里面是用位运算符来做的这件事 20 | 21 | ```typescript 22 | //shared/shapeFlags.ts 23 | export const enum shapeFlags { 24 | ELEMENT = 1, //0001 25 | STATEFUL_COMPONENT = 1 << 1, //0010 26 | TEXT_CHILDREN = 1 << 2, //0100 27 | ARRAY_CHILDREN = 1 << 3, //1000 28 | } 29 | // 这个对象的两个功能是 设置值,读取值 30 | 31 | // 我们可以通过 0001|1000 这种方式来设置值 32 | // 通过 0001 & 0001 这种方式来读取值 33 | ``` 34 | 35 | 改造之前我们需要再createVNode 上面去初始化一下shapeFlag属性 36 | 37 | ```typescript 38 | // runtime-core/vnode.ts 39 | import { shapeFlags } from "../shared/shapeFlags"; 40 | 41 | export function createVNode(type, props?, children?) { 42 | const vnode = { 43 | type, 44 | props, 45 | children, 46 | el: null, 47 | shapeFlag: getShapeFlag(type), 48 | }; 49 | debugger 50 | // 处理children的flag 51 | if (typeof children === "string") { 52 | vnode.shapeFlag |= shapeFlags.TEXT_CHILDREN; 53 | } else if (Array.isArray(children)) { 54 | vnode.shapeFlag |= shapeFlags.ARRAY_CHILDREN; 55 | } 56 | return vnode; 57 | } 58 | 59 | function getShapeFlag(type) { 60 | return typeof type === "string" 61 | ? shapeFlags.ELEMENT 62 | : shapeFlags.STATEFUL_COMPONENT; 63 | } 64 | 65 | 66 | ``` 67 | 68 | 然后改造之前我们的判断函数 69 | 70 | ```typescript 71 | // runtime-core/renderer.ts 72 | function patch(vnode, container) { 73 | //处理组件 74 | const { shapeFlag } = vnode; 75 | if (shapeFlag & shapeFlags.ELEMENT) { 76 | console.log("element 类型"); 77 | processElement(vnode, container); 78 | } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 79 | console.log("component 类型"); 80 | processComponent(vnode, container); 81 | } 82 | } 83 | 84 | function mountElement(vnode, container) { 85 | //vnode =>element =>div 86 | const { type, children, props, shapeFlag } = vnode; 87 | const el = document.createElement(type); 88 | vnode.el = el; 89 | 90 | setMountElementAttribute(el, props); 91 | 92 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 93 | //判断是否为 string 就是text类型 ,马上要渲染成text节点了 94 | container.textContent = children; 95 | } else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) { 96 | //还是array类型,需要再通过patch 方法处理 97 | mountChildren(children, container); 98 | } 99 | container.appendChild(el); 100 | function mountChildren(children, container) { 101 | children.map((v) => { 102 | patch(v, container); 103 | }); 104 | } 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/25-实现注册组件事件功能.md: -------------------------------------------------------------------------------- 1 | # 实现注册组件事件功能 2 | 3 | 其实注册组件的事件很简单,主要的js 代码就是 4 | 5 | ```javascript 6 | 7 | const el=document.createElement("div") 8 | el.addEventListener("click",()=>{ 9 | console.log("this is click event") 10 | }) 11 | ``` 12 | 13 | 我们在创建元素的时候就应该传递进入对应的参数 14 | 15 | ```typescript 16 | 17 | import { h } from "../../lib/ass-vue.esm.js"; 18 | window.self = null; 19 | export const App = { 20 | // .vue 21 | // 22 | //render 23 | render() { 24 | //ui 逻辑 25 | window.self = this; 26 | return h( 27 | "div", 28 | { 29 | id: "root", 30 | class: ["red", "blue"], 31 | onClick() { 32 | console.log("this is app div onclick"); 33 | }, 34 | onMousedown(){ 35 | console.log("mouseDown,app"); 36 | } 37 | }, 38 | "hi" + this.msg 39 | // [h("p", { class: "red" }, "hi red"), h("p", { class: "blue" }, "hi blue")] 40 | ); 41 | }, 42 | setup() { 43 | return { 44 | msg: "ass-vue", 45 | }; 46 | }, 47 | }; 48 | ``` 49 | 50 | 然后我们去mountElement的时候去处理props的时候区别对待这些事件函数 51 | 52 | ```typescript 53 | function processElement(vnode, container) { 54 | //TODO: updateElement 55 | mountElement(vnode, container); 56 | } 57 | 58 | function mountElement(vnode, container) { 59 | //vnode =>element =>div 60 | const { type, children, props, shapeFlag } = vnode; 61 | const el = document.createElement(type); 62 | vnode.el = el; 63 | 64 | for (const key in props) { 65 | const attributeValue = props[key]; 66 | if (key==="onClick") { 67 | el.addEventListener("click", attributeValue); 68 | }else{ 69 | const _attributeValue=Array.isArray(attributeValue)?attributeValue.join(" "):attributeValue 70 | el.setAttribute(key, _attributeValue); 71 | } 72 | } 73 | 74 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 75 | el.textContent = children; 76 | } else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) { 77 | mountChildren(children, el); 78 | } 79 | container.appendChild(el); 80 | } 81 | ``` 82 | 83 | 然后我们让这个功能变得更通用一些 84 | 85 | ```typescript 86 | function mountElement(vnode, container) { 87 | //vnode =>element =>div 88 | const { type, children, props, shapeFlag } = vnode; 89 | const el = document.createElement(type); 90 | vnode.el = el; 91 | 92 | for (const key in props) { 93 | const attributeValue = props[key]; 94 | const isOn=(eventName:string)=>/^on[A-Z]/.test(eventName) 95 | if (isOn(key)) { 96 | const eventName=key.slice(2).toLocaleLowerCase() 97 | el.addEventListener(eventName, attributeValue); 98 | }else{ 99 | const _attributeValue=Array.isArray(attributeValue)?attributeValue.join(" "):attributeValue 100 | el.setAttribute(key, _attributeValue); 101 | } 102 | } 103 | 104 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 105 | el.textContent = children; 106 | } else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) { 107 | mountChildren(children, el); 108 | } 109 | container.appendChild(el); 110 | } 111 | 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/26-实现组件props功能.md: -------------------------------------------------------------------------------- 1 | # 实现组件props 功能 2 | 3 | 主要分为这么几点 4 | 5 | 1. props是通过setup传递过来的 6 | 2. setup传递过来的参数能在render里面通过this拿到 7 | 3. props不可被修改 8 | 9 | ```javascript 10 | //Foo.js 11 | import { h } from "../../lib/ass-vue.esm.js"; 12 | export const Foo = { 13 | render() { 14 | //1.通过setup 传递过来 15 | //2. 通过setup传递过来的参数能在render里面通过this 拿到, 16 | //3. 通过props 传递过来的参数不可被修改 17 | return h("div", {}, "some text in fool" + this.count); 18 | }, 19 | setup(props) { 20 | console.log(props); 21 | }, 22 | }; 23 | 24 | ``` 25 | 26 | ## 实现通过setup 传递props 参数 27 | 28 | ```typescript 29 | //component.ts 30 | import { initProps } from "./componentProps"; 31 | export function setupComponent(instance) { 32 | //TODO:initSlots 33 | initProps(instance, instance.vnode.props); 34 | //初始化一个有状态的component (有状态的组件和函数组件函数组件是没有任何状态的) 35 | setupStatefulComponent(instance); 36 | } 37 | ``` 38 | 39 | 做兼容处理如果没有串props 那么就给instance.props 赋值为一个空对象 40 | 41 | ```typescript 42 | //componentProps.ts 43 | export function initProps(instance,rawProps){ 44 | instance.props=rawProps||{} 45 | } 46 | ``` 47 | 48 | ## 实现通过render 里面通过this 拿到count(还是通过之前的代理对象给注入到render的this上) 49 | 50 | ```typescript 51 | import { hasOwn } from "../shared"; 52 | 53 | const publicPropertiesMap = { 54 | $el: (instance) => instance.vnode.el, 55 | }; 56 | 57 | export const PublicInstanceProxyHandlers = { 58 | get({ _: instance }, key) { 59 | //从setupState里面获取值 60 | const { setupState, props } = instance; 61 | if (hasOwn(setupState, key)) { 62 | return setupState[key]; 63 | } else if (hasOwn(props, key)) { 64 | return props[key]; 65 | } 66 | 67 | if (key in publicPropertiesMap) { 68 | const publicGetter = publicPropertiesMap[key]; 69 | return publicGetter(instance); 70 | } 71 | }, 72 | }; 73 | ``` 74 | 75 | 改写createReactiveObject 76 | 77 | ```typescript 78 | function createReactiveObject(target, baseHandlers) { 79 | if (!isObject(target)) { 80 | console.warn(`target ${target} 必须是一个对象`); 81 | return target; 82 | } else { 83 | return new Proxy(target, baseHandlers); 84 | } 85 | } 86 | ``` 87 | 88 | ## 实现props 不可被修改 89 | 90 | ```typescript 91 | //component.ts 92 | export function setupComponent(instance) { 93 | //TODO:initSlots 94 | initProps(instance, instance.vnode.props); 95 | //初始化一个有状态的component (有状态的组件和函数组件函数组件是没有任何状态的) 96 | setupStatefulComponent(instance); 97 | } 98 | 99 | function setupStatefulComponent(instance) { 100 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 101 | const Component = instance.type; 102 | const { setup } = Component; 103 | 104 | //ctx 105 | 106 | const proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 107 | 108 | instance.proxy = proxy; 109 | 110 | if (setup) { 111 | //实现传递进去的props 不可被修改 112 | const setupResult = setup(shallowReadonly(instance.props)); 113 | handleSetupResult(instance, setupResult); 114 | } 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/27-实现emit 功能.md: -------------------------------------------------------------------------------- 1 | # 实现组件emit 功能 2 | 3 | > 需求场景需要 在组件里面setup的时候调用emit方法然后在 组件render的时候去调用对应的方法 4 | 5 | 下面以示例代码做个演示 6 | 7 | ```typescript 8 | 9 | //Foo.js 10 | setup(props,{emit}) { 11 | //通过 setup 的第二个参数 处理emit 12 | 13 | const emitAdd = () => { 14 | emit("add",1,2) 15 | emit("add-foo") 16 | console.log("emit add"); 17 | }; 18 | return { emitAdd }; 19 | }, 20 | //App.js 21 | render() { 22 | //ui 逻辑 23 | window.self = this; 24 | return h( 25 | "div", 26 | { 27 | id: "root", 28 | class: ["red"], 29 | }, 30 | [ 31 | h("p", { class: "red" }, "hi red"), 32 | h(Foo, { 33 | count: 1, 34 | onAdd(a, b) { 35 | console.log("on add in app js", a, b); 36 | }, 37 | onAddFoo(){ 38 | console.log("on Add foo in app js") 39 | } 40 | }), 41 | ] 42 | ); 43 | }, 44 | ``` 45 | 46 | 我们之前已经实现了通过setup返回值参数挂载到instance 实例上,所以instance 上面可以调用 emitAdd 方法 47 | 所以我们在渲染button的时候给他加上点击事件事件的名称就为this.emitAdd 48 | 49 | 然后我们把emit声明在instance上面,然后再在setup的时候传递这个emit给setup 50 | 51 | ```typescript 52 | //component.ts 53 | export function createComponentInstance(vnode: any) { 54 | const component = { 55 | vnode, 56 | type: vnode.type, 57 | setupState: {}, 58 | props: {}, 59 | emit: (event) => {}, 60 | }; 61 | //这里有点东西的啊,不想传递第一个参数等到emit调用的时候在传递,先把component填充好 62 | component.emit = emit.bind(null,component); 63 | return component; 64 | } 65 | // 66 | function setupStatefulComponent(instance) { 67 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 68 | const Component = instance.type; 69 | const { setup } = Component; 70 | 71 | //ctx 72 | 73 | const proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 74 | 75 | instance.proxy = proxy; 76 | 77 | if (setup) { 78 | const setupResult = setup(shallowReadonly(instance.props), { 79 | //传递emit给setup 80 | emit: instance.emit, 81 | }); 82 | handleSetupResult(instance, setupResult); 83 | } 84 | } 85 | ``` 86 | 87 | 然后我们再创建一个文件实现emit方法 88 | 89 | ```typescript 90 | //componentEmit 91 | export function emit(instance,event){ 92 | import { camelize, toHandlerKey } from "../shared"; 93 | 94 | export function emit(instance, event, ...arg) { 95 | const { props } = instance; 96 | //TPP 97 | // 先去写一个特定的行为然后再重构成一个通用的行为 98 | 99 | const handlerName = toHandlerKey(camelize(event)); 100 | const handler = props[handlerName]; 101 | handler && handler(...arg); 102 | } 103 | } 104 | 105 | //shared/index.ts 106 | const capitalize = (str: string) => { 107 | return str.charAt(0).toUpperCase() + str.slice(1); 108 | }; 109 | export const toHandlerKey = (str: string) => { 110 | return str ? "on" + capitalize(str) : ""; 111 | }; 112 | 113 | export const camelize = (str: string) => { 114 | return str.replace(/-(\w)/g, (_, c) => { 115 | return c ? c.toUpperCase() : ""; 116 | }); 117 | }; 118 | ``` 119 | 120 | 这一节课最主要学到的方法是 我只想给emit传递一个event参数,但是emit 这个函数需要两个参数,我可以通过`component.emit = emit.bind(null,component)` 这种方式来传递emit 的第一个参数为component 就是组件实例 -------------------------------------------------------------------------------- /docs/29-实现Fragment和Text类型组件渲染.md: -------------------------------------------------------------------------------- 1 | # 实现Fragment 和Text 类型组件渲染 2 | 3 | ## 实现Fragment 组件渲染逻辑 4 | 5 | 1. 判断当前需要渲染的是Fragment,我们之前渲染插槽的时候是去新创建的一个div ,那么节点渲染就会多一层div 6 | 2. 渲染Fragment 就是直接渲染vnode的children 7 | 3. 我们需要给创建给创建Fragment 的 事件做一个特殊处理 8 | 9 | ```typescript 10 | //在render slots 的时候去处理一下这个特殊的类型,然后我们去改写一下渲染vnode的逻辑,让渲染的时候特殊处理一下这个Fragment类型的vnode 11 | import { createVNode, Fragment, } from "../vnode"; 12 | 13 | export function renderSlots(slots, key, props) { 14 | const slot = slots[key]; 15 | if (slot) { 16 | if (typeof slot === "function") { 17 | return createVNode(Fragment, {}, slot(props)); 18 | } 19 | } 20 | } 21 | //vnode.ts 22 | //改写patch 方法判断当前的虚拟节点的类型是否为Fragment 或者其他特殊类型,如果是就特殊处理一下 23 | function patch(vnode, container) { 24 | //处理组件 25 | const { shapeFlag, type } = vnode; 26 | 27 | // 需要特殊处理我们的Fragment 类型的 28 | switch (type) { 29 | case Fragment: 30 | processFragment(vnode, container); 31 | 32 | break; 33 | default: 34 | if (shapeFlag & shapeFlags.ELEMENT) { 35 | // console.log("element 类型"); 36 | processElement(vnode, container); 37 | } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 38 | // console.log("component 类型"); 39 | processComponent(vnode, container); 40 | } 41 | break; 42 | } 43 | } 44 | 45 | //然后再实现一下ProcessFragment 的逻辑,实际上我们只需要去去渲染Fragment类型vnode的children,而不需要使用div去包裹就行了,而我们之前就封装了mountChildren的方法,这个时候就可以拿来使用一下 46 | 47 | function processFragment(vnode, container) { 48 | mountChildren(vnode.children, container); 49 | } 50 | 51 | //现在这样应该就可以渲染了 52 | 53 | ``` 54 | 55 | ## 渲染Text类型组件 56 | 57 | 我们现在如果要在视图上渲染一个Text类型的组件的话,是不行的,我想的是直接传递参数到h的最后一个参数上面,然后再去判断最后一个参数是不是为一个string类型的就行了,但是官方的实现不一样,他是单独把这个抽离成为了一个函数createTextVNode,然后再通过h去渲染 58 | 59 | 这个功能虽然简单但是还是要过一下我们这个流程 60 | 61 | 1. 基于text 创建虚拟节点 62 | 2. 然后patch 会基于这个vnode的类型进行对应的处理 63 | 3. 处理Text节点 64 | 4. document.createTextNode 来创建textNode节点 而节点的内容是vnode的children 属性 65 | 5. 处理传递下来的vnode 对象,把创建好的节点挂载到vnode.el 上面 66 | 67 | ```typescript 68 | // 1. 基于text 创建虚拟节点 69 | import { h, createTextVNode } from "../../lib/ass-vue.esm.js"; 70 | import { Foo } from "./Foo.js"; 71 | export const App = { 72 | name: "App", 73 | render() { 74 | const app = h("div", {}, "App"); 75 | //我们希望在foo 这里传递h 的第三个参数,能被Foo 接收到并且渲染到children里面 76 | const foo = h( 77 | Foo, 78 | {}, 79 | { 80 | header: ({ age }) => [ 81 | h("p", {}, "header" + age), 82 | createTextVNode("你好呀"), 83 | ], 84 | footer: () => h("p", {}, "footer"), 85 | } 86 | ); 87 | 88 | return h("div", {}, [app, foo]); 89 | }, 90 | setup() { 91 | return {}; 92 | }, 93 | }; 94 | 95 | //patch 处理Text节点 96 | function patch(vnode, container) { 97 | //处理组件 98 | const { shapeFlag, type } = vnode; 99 | 100 | // 需要特殊处理我们的Fragment 类型的 101 | switch (type) { 102 | case Fragment: 103 | processFragment(vnode, container); 104 | 105 | break; 106 | case Text: 107 | processText(vnode, container); 108 | break; 109 | 110 | default: 111 | if (shapeFlag & shapeFlags.ELEMENT) { 112 | // console.log("element 类型"); 113 | processElement(vnode, container); 114 | } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 115 | // console.log("component 类型"); 116 | processComponent(vnode, container); 117 | } 118 | break; 119 | } 120 | } 121 | //实现对应的processText方法 122 | //实现创建真实textNode节点,实现把创建好的el属性挂载到虚拟节点的el属性下 123 | function processText(vnode, container) { 124 | const { children } = vnode; 125 | const textNode = (vnode.el = document.createTextNode(children)); 126 | container.append(textNode); 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/30-实现getCurrentInstance功能.md: -------------------------------------------------------------------------------- 1 | # 实现getCurrentInstance 函数 2 | 3 | 顾名思义getCurrentInstance 就是获取当前组件对象实例 4 | 5 | ```javascript 6 | //App.js 7 | import { h, getCurrentInstance } from "../../lib/ass-vue.esm.js"; 8 | import { Foo } from "./Foo.js"; 9 | 10 | export const App = { 11 | name: "App", 12 | render() { 13 | return h("div", {}, [h("p", {}, "currentInstance Demo"), h(Foo)]); 14 | }, 15 | setup() { 16 | const instance = getCurrentInstance(); 17 | console.log("App:", instance); 18 | }, 19 | }; 20 | 21 | //Foo.js 22 | import { h,getCurrentInstance } from "../../lib/ass-vue.esm.js"; 23 | export const Foo = { 24 | name: "Foo", 25 | setup() { 26 | const instance = getCurrentInstance(); 27 | console.log("Foo:", instance); 28 | return {}; 29 | }, 30 | render() { 31 | return h("div", {}, "foo"); 32 | }, 33 | }; 34 | 35 | ``` 36 | 37 | 然后我们去对应创建instance 的时候拿到instance 然后返回就行了 38 | 39 | 1. 去拿到instance实例对象 40 | 2. 需要等到实例对象instance上面的属性初始化完成 41 | 3. 通过调用getCurrentInstance 返回当前实例对象 42 | 43 | ```typescript 44 | //在调用instance setup的时候给currentInstance赋值,这样得到的instance 已经经过了初始化props,初始化slots 挂载了proxy 对象 得到的是一个成熟的instance了 45 | function setupStatefulComponent(instance) { 46 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 47 | const Component = instance.type; 48 | const { setup } = Component; 49 | 50 | //ctx 51 | 52 | const proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 53 | 54 | instance.proxy = proxy; 55 | 56 | if (setup) { 57 | setCurrentInstance(instance) 58 | const setupResult = setup(shallowReadonly(instance.props), { 59 | emit: instance.emit, 60 | }); 61 | setCurrentInstance(null) 62 | handleSetupResult(instance, setupResult); 63 | } 64 | } 65 | //定义全局变量存储当前的currentInstance 66 | let currentInstance = null; 67 | 68 | export function getCurrentInstance() { 69 | return currentInstance; 70 | } 71 | 72 | function setCurrentInstance(instance) { 73 | currentInstance = instance; 74 | } 75 | ``` 76 | 77 | bingo 就是这么简单 78 | -------------------------------------------------------------------------------- /docs/33-更新Element流程搭建.md: -------------------------------------------------------------------------------- 1 | # 更新element 流程搭建 2 | 3 | 更新流程搭建 4 | 5 | ```javascript 6 | import { h, ref } from "../../lib/ass-vue.esm.js"; 7 | 8 | export const App = { 9 | name: "App", 10 | setup() { 11 | const count = ref(0); 12 | const onClick = () => count.value++; 13 | return { 14 | count, 15 | onClick, 16 | }; 17 | }, 18 | render() { 19 | return h("div", { id: "root" }, [ 20 | h("div", {}, "count:" + this.count), 21 | h("button", { onClick: this.onClick }, "click me to add 1"), 22 | ]); 23 | }, 24 | }; 25 | ``` 26 | 27 | 我希望能 在点击按钮的时候视图能发生变化 28 | 29 | 1. 我们希望通过this.count 直接拿到ref 的值,可以使用之前我们包装的proxyRefs函数包裹setup 返回的值 30 | 2. 我们通过ref 创建了一个响应式对象,那么我们可以通过依赖收集的方式来更新视图 31 | 3. 我们需要区分现在是组件初始化阶段还是组件更新阶段 32 | 4. 我们判断更新阶段然后拿到之前的节点的数据然后再做其他的事 33 | 34 | ```typescript 35 | //1.通过 proxy包裹setup返回值实现通过this.count 可以获取到count的值 36 | //component.ts 37 | function handleSetupResult(instance, setupResult) { 38 | //setup => function || object 39 | // function => render 函数 40 | // object=> 注入到组件上下文中 41 | //TODO: function 42 | 43 | if (typeof setupResult === "object") { 44 | instance.setupState = proxyRefs(setupResult); 45 | } 46 | //保证组件的render是一定有值的 47 | finishComponentSetup(instance); 48 | } 49 | //通过effect 把渲染逻辑收集下来 50 | //renderer.ts 51 | function setupRenderEffect(instance, initialVNode, container) { 52 | effect(() => { 53 | const { proxy } = instance; 54 | const subTree = instance.render.call(proxy); 55 | patch( subTree, container, instance); 56 | initialVNode.el = subTree.el; 57 | } 58 | } 59 | 60 | //通过instance 挂载isMount属性区分现在是挂载阶段还是更新阶段 61 | 62 | function setupRenderEffect(instance, initialVNode, container) { 63 | //因为 count 改变的值是一个响应式对象,而我们需要收集到响应式对象改变所触发的依赖 64 | //所以我们在这里收集依赖 65 | //这里也是一次渲染逻辑的终点 66 | effect(() => { 67 | if (!instance.isMounted) { 68 | console.log("mount"); 69 | const { proxy } = instance; 70 | const subTree = instance.render.call(proxy); 71 | patch( subTree, container, instance); 72 | initialVNode.el = subTree.el; 73 | instance.isMounted = true; 74 | } else { 75 | console.log("update") 76 | } 77 | }); 78 | } 79 | 80 | //通过subTree 属性存储当前的subTree 节点树,保存节点树状态 81 | 82 | function setupRenderEffect(instance, initialVNode, container) { 83 | //因为 count 改变的值是一个响应式对象,而我们需要收集到响应式对象改变所触发的依赖 84 | //所以我们在这里收集依赖 85 | //这里也是一次渲染逻辑的终点 86 | effect(() => { 87 | if (!instance.isMounted) { 88 | console.log("mount"); 89 | const { proxy } = instance; 90 | const subTree = (instance.subTree = instance.render.call(proxy)); 91 | patch( subTree, container, instance); 92 | initialVNode.el = subTree.el; 93 | instance.isMounted = true; 94 | } else { 95 | const { proxy } = instance; 96 | const subTree = instance.render.call(proxy); 97 | const prevSubTree = instance.subTree; 98 | instance.subTree = subTree; 99 | console.log("mounted", prevSubTree); 100 | patch( subTree, container, instance); 101 | } 102 | }); 103 | } 104 | 105 | // 改写patch 方法 通过前后两个vnode 对象的不同来渲染节点 106 | function patch(n1, n2, container, parentComponent) { 107 | //处理组件 108 | const { shapeFlag, type } = n2; 109 | 110 | // 需要特殊处理我们的Fragment 类型的 111 | switch (type) { 112 | case Fragment: 113 | processFragment(n1, n2, container, parentComponent); 114 | 115 | break; 116 | case Text: 117 | processText(n1, n2, container); 118 | break; 119 | 120 | default: 121 | if (shapeFlag & shapeFlags.ELEMENT) { 122 | // console.log("element 类型"); 123 | processElement(n1, n2, container, parentComponent); 124 | } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 125 | // console.log("component 类型"); 126 | processComponent(n1, n2, container, parentComponent); 127 | } 128 | break; 129 | } 130 | } 131 | 132 | //然后处理相关函数调用,然后再在processElement 里面通过传递进来参数来区分是否是更新Element,并抽离出更新Element 的逻辑 133 | function processElement(n1, n2, container, parentComponent) { 134 | if (!n1) { 135 | // init 136 | mountElement(null, n2, container, parentComponent); 137 | } else { 138 | //update 139 | patchElement(n1, n2, container, parentComponent); 140 | } 141 | } 142 | 143 | function patchElement(n1, n2, container) { 144 | console.log("patchElement"); 145 | 146 | console.log("n1", n1); 147 | 148 | console.log("n2", n2); 149 | } 150 | ``` 151 | 152 | 简单总结一下,这一章节的流程搭建的难点是 在哪去做依赖 的处理,还好之前我们架构的时候专门弄了一个函数setupRenderEffect 来专门处理所有参数都准备好的时候 处理我们视图的逻辑,这个阶段是从component转化成真实dom的一层也是一个完整流程的最后一个阶段 -------------------------------------------------------------------------------- /docs/34-更新element 的props.md: -------------------------------------------------------------------------------- 1 | # 更新element 的props 2 | 3 | 这一个小节其实很简单,我们需要在element 变化的时候动态去跟新props 4 | 5 | 然后这个情况其实可以勉强分为三个小的情节 6 | 7 | - 我们改变element 的值 8 | - 我们把值改变成undefined 9 | - 我们把值连同key 全部删掉 10 | 11 | 12 | 基于我们之前监听到props 变化,我们申明了一个叫patchProp 的函数,然后我们在里面去调用hostPathProps 方法 13 | 而这个方法是我们实现自定义渲染器传递进来的渲染函数 14 | 15 | ```typescript 16 | function patchElement(n1, n2, container) { 17 | //值得注意的是我们为了保证{} 是同一个引用所以在shared 里面去定义了一个 empty_object 这样一个变量 18 | //然后就是我们需要把旧 的el 属性绑定在新的上面,不然新的instance 上面会没有el属性 19 | 20 | const prevProps = n1.props || EMPTY_OBJECT; 21 | const nextProps = n2.props || EMPTY_OBJECT; 22 | const el = (n2.el = n1.el); 23 | patchProps(el, prevProps, nextProps); 24 | } 25 | 26 | function patchProps(el, prevProps, nextProps) { 27 | //新的props 里面值改变了或者是新的props 里面的值为undefined 28 | if (nextProps !== prevProps) { 29 | //如果新的props 和原来的props 不相同才做更改 30 | for (const key in nextProps) { 31 | //循环新的props 然后拿到新的值对原现的属性尽心更改 32 | // 值得一提的是如果新的props 里面有旧的props 里面没有的属性那么也会添加上 33 | const prevProp = prevProps[key]; 34 | const nextProp = nextProps[key]; 35 | if (prevProp !== nextProp) { 36 | hostPatchProp(el, key, prevProp, nextProp); 37 | } 38 | } 39 | if (prevProps !== EMPTY_OBJECT ){ 40 | //如果之前的props 不为一个空数组的话 41 | for (const key in prevProps) { 42 | //循环之前的数组,然后删除之后的属性 43 | if (!(key in nextProps)) { 44 | hostPatchProp(el, key, prevProps[key], null); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | 然后我们去更新一下我们的hostPatchProp里面的逻辑 53 | 54 | ```typescript 55 | //新增删除属性的方法 56 | function patchProp(el, key, prevVal, nextVal) { 57 | const isOn = (eventName: string) => /^on[A-Z]/.test(eventName); 58 | if (isOn(key)) { 59 | const eventName = key.slice(2).toLocaleLowerCase(); 60 | el.addEventListener(eventName, nextVal); 61 | } else { 62 | const _val = Array.isArray(nextVal) ? nextVal.join(" ") : nextVal; 63 | if (nextVal === undefined || nextVal === null) { 64 | console.log("undefined or null"); 65 | el.removeAttribute(key); 66 | } else { 67 | el.setAttribute(key, _val); 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | 这一节最值得注意的就是el的赋值,需要把之前的el属性赋值给新的vnode -------------------------------------------------------------------------------- /docs/35-更新element 的children.md: -------------------------------------------------------------------------------- 1 | # 更新element 的children 2 | 3 | 首先我们得children 有两种类型一种是text 类型一种是array 类型 4 | 5 | 所以如果讨论所有得情况得话就会有四种情况 6 | 7 | - text => text (把文本直接替换掉就行) 8 | - text => array (先把文本清空然后再挂载array 类型得节点) 9 | - array => text (先卸载array 类型的节点 然后再设置父级的textContent) 10 | - array => array (很复杂需要算法来解决) 11 | 12 | ```javascript 13 | // ArrayToText 14 | import { h, ref } from "../../lib/ass-vue.esm.js"; 15 | 16 | const nextChildren = "newChildren"; 17 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")]; 18 | export default { 19 | name: "ArrayToText", 20 | setup() { 21 | const isChange = ref(false); 22 | window.isChange = isChange; 23 | return { isChange }; 24 | }, 25 | render() { 26 | const self = this; 27 | 28 | return self.isChange === true 29 | ? h("div", {}, nextChildren) 30 | : h("div", {}, prevChildren); 31 | }, 32 | }; 33 | 34 | //App.js 35 | import { h } from "../../lib/ass-vue.esm.js"; 36 | 37 | import ArrayToText from "./ArrayToText.js"; 38 | import TextToText from './TextToText.js' 39 | import TextToArray from "./TextToArray.js"; 40 | export const App = { 41 | name: "App", 42 | setup() {}, 43 | render() { 44 | return h("div", { tId: 1 }, [ 45 | h("p", {}, "主页"), 46 | //老的是Array,新的是text 47 | // h(ArrayToText), 48 | //老的是Text 新的是不同的text 49 | // h(TextToText), 50 | //老的是text 新的是Array 51 | h(TextToArray), 52 | //老的是Array 新的也是Array 53 | // h(ArrayToArray), 54 | ]); 55 | }, 56 | }; 57 | ``` 58 | 59 | 然后我们去render.ts 里面处理相关的逻辑 60 | 61 | ```typescript 62 | //patchElement 通过patchChildren 处理children 的变化 63 | function patchElement(n1, n2, container,parentComponent) { 64 | // console.log("patchElement"); 65 | 66 | // console.log("n1", n1); 67 | 68 | // console.log("n2", n2); 69 | const prevProps = n1.props || EMPTY_OBJECT; 70 | const nextProps = n2.props || EMPTY_OBJECT; 71 | const el = (n2.el = n1.el); 72 | patchChildren(n1, n2, el,parentComponent); 73 | patchProps(el, prevProps, nextProps); 74 | } 75 | 76 | function patchChildren(n1, n2, container,parentComponent) { 77 | //ArrayToText 78 | //先删除Array 然后再 添加文本 79 | const prevShapeFlag = n1.shapeFlag; 80 | const c1 = n1.children; 81 | const nextShapeFlag = n2.shapeFlag; 82 | const c2 = n2.children; 83 | if (nextShapeFlag & shapeFlags.TEXT_CHILDREN) { 84 | if (prevShapeFlag & shapeFlags.ARRAY_CHILDREN) { 85 | //ArrayToText 86 | //卸载children 元素 87 | //需要动态的传入unmountChildren 函数 88 | 89 | unMountChildren(n1.children); 90 | } 91 | if (c1 !== c2) { 92 | hostSetElementText(c2, container); 93 | } 94 | } else { 95 | //新的是一个数组类型的节点 96 | //所以我们需要去判断老的是否为文本节点还是数组 97 | if (prevShapeFlag & shapeFlags.TEXT_CHILDREN) { 98 | //删除之前的文本节点,mount 新的child数组 99 | //这个方法需要动态的传入,customRender 100 | hostSetElementText("", container); 101 | mountChildren(c2,container,parentComponent) 102 | } 103 | } 104 | 105 | console.log("patchChildren", n1, n2); 106 | } 107 | 108 | //把卸载Children单独拎出来 remove 单独的一个children 应该作为一个渲染方法传递进来,可以动态配置 109 | function unMountChildren(children) { 110 | for (let i = 0; i < children.length; i++) { 111 | const el = children[i].el; 112 | //remove 113 | //insert 114 | hostRemove(el); 115 | } 116 | } 117 | //然后去runtime-dom 里面定义相关的函数 118 | //runtime-dom/index.ts 119 | function remove(child) { 120 | const parent = child.parentNode; 121 | if (parent) { 122 | parent.removeChild(child); 123 | } 124 | } 125 | //当然设置文本节点的内容也应该是传递进来的渲染方法 126 | function setElementText(text, container) { 127 | container.textContent = text; 128 | } 129 | 130 | 131 | ``` -------------------------------------------------------------------------------- /docs/36-双端对比算法更新children.md: -------------------------------------------------------------------------------- 1 | # 双端对比算法更新children 2 | 3 | 双端对比算法锁定中间乱序的部分的。 4 | 5 | - 处理左侧 6 | - 处理右侧 7 | - 左侧一样(新的比老的长)把新创建的添加到尾部 8 | - 右侧一样 (新的比老的长) 把新创建的添加到头部 9 | - 左侧 老的比新的长 (删除) 10 | - 右侧 老的比新的长 (删除) 11 | - 中间对比 12 | - 创建新的 (在老的里面不存在,新的里面存在) 13 | - 删除老的 (在老的里面存在,新的里面不存在) 14 | - 移动 (在老的里面存在,新的里面也存在) 15 | 16 | ![alt 左侧对比](./static/left.png) 17 | 18 | (ab)c 19 | (ab)de 20 | 我们现在只需要确定左边位置不相等的元素的位置 21 | 22 | 如果是上面的情况的话,我们可以声明一个指针来记录当前两个children 从第几位开始前后不一样的 23 | 24 | ```typescript 25 | let prevLastChildIndex = prevChildrenArray.length - 1; 26 | let nextLastChildIndex = nextChildrenArray.length - 1; 27 | let pointer = 0; 28 | 29 | function isSomeVNodeType(prevChild, nextChild) { 30 | return ( 31 | prevChild.type === nextChild.type && prevChild.key === nextChild.key 32 | ); 33 | } 34 | //左边 35 | while (pointer <= prevLastChildIndex && pointer <= nextLastChildIndex) { 36 | const prevChild = prevChildrenArray[pointer]; 37 | const nextChild = nextChildrenArray[pointer]; 38 | 39 | if (isSomeVNodeType(prevChild, nextChild)) { 40 | //如果相等的话那么就在执行一下patch 方法递归对比一下 其他节点 41 | patch(prevChild, nextChild, container, parentComponent, anchor); 42 | } else { 43 | //跳出循环 44 | break; 45 | } 46 | //指针后移动 47 | pointer++; 48 | } 49 | ``` 50 | 51 | 如果是右边对比的情况的话 52 | 53 | ca( de ) 54 | b( de ) 55 | 56 | 我们可以从两个children 的长度来入手,这种作法也是很高端的,就是在循环的时候去改变两个children 的最后元素的下标,这样的话,我们判断的时候就不需要引入其他的变量。 57 | 58 | ```typescript 59 | //右边对比 60 | while (pointer <= prevLastChildIndex && pointer <= nextLastChildIndex) { 61 | const rightPrevChild = prevChildrenArray[prevLastChildIndex]; 62 | const rightNextChild = nextChildrenArray[nextLastChildIndex]; 63 | if (isSomeVNodeType(rightNextChild, rightPrevChild)) { 64 | patch( 65 | rightPrevChild, 66 | rightNextChild, 67 | container, 68 | parentComponent, 69 | anchor 70 | ); 71 | } else { 72 | break; 73 | } 74 | 75 | prevLastChildIndex--; 76 | nextLastChildIndex--; 77 | } 78 | 79 | ``` 80 | 81 | 然后我们就可以确定两个children前后不一样的地方的范围了 82 | 之前的不一样children 的范围为 pointer -> preLastChildIndex 83 | 改变后不以样的children 的范围为 pointer -> nextLastChildIndex 84 | 85 | 那上面的例子就是 86 | 之前不一样的就是 0-1 87 | 新的不一样的 0-0 88 | 89 | ```typescript 90 | //如果新的比老的长那么就循环创建新的 91 | //这个判断是结合了左侧的对比以及右侧的对比的,新的比老的长的情况 92 | // 比如 左侧的 prev:(ab) next:(ab)c 93 | //比如右侧的 prev: (ab) next: c(ab) 94 | if (pointer > prevLastChildIndex) { 95 | if (pointer <= nextLastChildIndex) { 96 | //如果新的比老的长 97 | console.log("新的比老的长"); 98 | 99 | const nextPos = nextLastChildIndex + 1; 100 | const anchor = 101 | nextPos < nextChildrenArray.length 102 | ? nextChildrenArray[nextPos].el 103 | : null; 104 | 105 | while (pointer <= nextLastChildIndex) { 106 | //循环创建新的元素,然后添加到指定的锚点的位置 107 | patch( 108 | null, 109 | nextChildrenArray[pointer], 110 | container, 111 | parentComponent, 112 | anchor 113 | ); 114 | pointer++; 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | ```typescript 121 | //如果新的比老的少,那么就删除 122 | //这种情况 也考虑了左侧以及右侧两种情况 123 | // 比如左侧 prev:(ab)c next:(ab) 124 | // 比如右侧 prev:c(ab) next:(ab) 125 | else if (pointer > nextLastChildIndex) { 126 | //老的比新的多 127 | while (pointer <= prevLastChildIndex) { 128 | hostRemove(prevChildrenArray[pointer].el); 129 | pointer++; 130 | } 131 | } 132 | ``` 133 | 134 | ```typescript 135 | else { 136 | // 中间乱序部分的逻辑 137 | } 138 | ``` 139 | 140 | NOTE: 注意在完成这个更新算法的时候,有几个值得注意的点 141 | 142 | 1. 我们申明了一个从左边查找元素不同的指针pointer 143 | 2. 我们通过改变 前后两个children 的最后一个元素的下标的方式来确定了右边的元素的不一样的范围(起始范围) 144 | 3. 我们通过锚点的来解决了,新增加的元素的位置错位的问题,实际的api 调整是,dom 操作的 增加元素的方法是从parent.insert(el) 变成了parent.insertBefore(el,anchor) 145 | -------------------------------------------------------------------------------- /docs/37-双端对比算法更新children-2.md: -------------------------------------------------------------------------------- 1 | # 双端对比算法更新children -2 2 | 3 | 双端对比算法删除老children 里面存在且新children 里面不存在的节点 4 | 5 | 下面的例子 6 | ab(cd)fg 7 | ab(ec)fg 8 | 9 | ```typescript 10 | //中间对比 11 | let s1 = pointer; 12 | let s2 = pointer; 13 | 14 | //map 映射 15 | //如果用户给了key 16 | const keyToNewIndexMap = new Map(); 17 | 18 | for (let i = s2; i <= nextLastChildIndex; i++) { 19 | const nextChild = nextChildrenArray[i]; 20 | keyToNewIndexMap.set(nextChild.key, i); 21 | } 22 | 23 | let newIndex; 24 | for (let i = s1; i <= prevLastChildIndex; i++) { 25 | const prevChild = prevChildrenArray[i]; 26 | //看当前的key在不在老的节点建立的映射表里面 27 | //null undefined 28 | if (patched >= toBePatched) { 29 | hostRemove(prevChild.el); 30 | continue; 31 | } 32 | if (prevChild.key !== null) { 33 | newIndex = keyToNewIndexMap.get(prevChild.key); 34 | //如果用户给了key 35 | } else { 36 | //如果用户没有给key 37 | for (let j = s2; j < nextLastChildIndex; j++) { 38 | if (isSomeVNodeType(prevChild, nextChildrenArray[j])) { 39 | newIndex = j; 40 | break; 41 | } 42 | } 43 | } 44 | 45 | //基于之前的节点去删除元素或者是新增元素 46 | if (newIndex === undefined) { 47 | hostRemove(prevChild.el); 48 | } else { 49 | //如果查找到了 50 | //那么继续去patch 对比 51 | //确定新的元素存在 52 | if (newIndex >= maxNewIndexSoFar) { 53 | maxNewIndexSoFar = newIndex; 54 | } else { 55 | moved = true; 56 | } 57 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 58 | 59 | patch( 60 | prevChild, 61 | nextChildrenArray[newIndex], 62 | container, 63 | parentComponent, 64 | null 65 | ); 66 | patched++; 67 | } 68 | } 69 | 70 | ``` 71 | 72 | NOTE: 首先需要明确思路,我们现在只需要删除老的里面存在的元素,不需要管其他的 73 | 74 | 1. 现在我们已经定位到了需要对比的范围,我们就需要根据这个范围来优化我们的对比的算法 75 | 2. 然后我们需要优化对比的性能,如果循环的对比那么算法的时间复杂度为O(n) 如果我们需要优化我们的对比逻辑那么我们可以考虑把需要对比的数组映射成一个 map 数组,这样映射过后时间复杂度为 O(1) 76 | 3. 我们对新的children 数组进行遍历,然后对map 数组进行循环赋值 77 | 78 | ```typescript 79 | for (let i = s2; i <= nextLastChildIndex; i++) { 80 | const nextChild = nextChildrenArray[i]; 81 | keyToNewIndexMap.set(nextChild.key, i); 82 | } 83 | ``` 84 | 85 | 4. 然后我们对老的children(prevChildren) 进行循环的对比,如果之前的children 里面存在key,那么对比一下那么就给newIndex 赋值为当前prevChildren的key 如果用户没有指定key 的话,那么就只有循环的对比,循环范围为新的children 不同的范围 86 | 87 | ```typescript 88 | if (prevChild.key !== null) { 89 | newIndex = keyToNewIndexMap.get(prevChild.key); 90 | //如果用户给了key 91 | } else { 92 | //如果用户没有给key 93 | for (let j = s2; j < nextLastChildIndex; j++) { 94 | if (isSomeVNodeType(prevChild, nextChildrenArray[j])) { 95 | newIndex = j; 96 | break; 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | 5. 然后基于我们找到的newIndex 去删除老的节点,这时候需要需要注意,newIndex 是在新节点有并且老节点里面也有的元素,如果当前的newIndex 为undefined 那么就需要删除老节点里面对应的元素,如果查找到了newIndex 那么就需要我们循环的去调用patch 方法去看看元素还有啥不同的 103 | 104 | ```typescript 105 | if (newIndex === undefined) { 106 | hostRemove(prevChild.el); 107 | } else { 108 | //如果查找到了 109 | //那么继续去patch 对比 110 | //确定新的元素存在 111 | patch( 112 | prevChild, 113 | nextChildrenArray[newIndex], 114 | container, 115 | parentComponent, 116 | null 117 | ); 118 | patched++; 119 | } 120 | ``` 121 | 122 | 6. 我们需要删除元素的时候需要传入的是实际的el 元素,*prevChild.el* 123 | 124 | 7. 如果我们新新的里面的children 里面的所有的元素都对比完了,那么就可以直接删除老的children 里面的元素我们声明总共需要patch的最大的patched数量, 然后patched 的数量初始化为0 然后在查找到对应的元素并处理完之后patched++ 125 | 126 | ```typescript 127 | //新节点需要最多对比的数量 128 | //我们声明总共需要patch的最大的patched数量,然后patched 的数量初始化为0 129 | //然后在查找到对应的元素并处理完之后patched++ 130 | let toBePatched = nextLastChildIndex - pointer + 1; 131 | let patched = 0; 132 | if (patched >= toBePatched) { 133 | hostRemove(prevChild.el); 134 | continue; 135 | } 136 | 137 | if (newIndex === undefined) { 138 | hostRemove(prevChild.el); 139 | } else { 140 | //如果查找到了 141 | //那么继续去patch 对比 142 | //确定新的元素存在 143 | if (newIndex >= maxNewIndexSoFar) { 144 | maxNewIndexSoFar = newIndex; 145 | } else { 146 | moved = true; 147 | } 148 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 149 | 150 | patch( 151 | prevChild, 152 | nextChildrenArray[newIndex], 153 | container, 154 | parentComponent, 155 | null 156 | ); 157 | patched++; 158 | } 159 | ``` 160 | -------------------------------------------------------------------------------- /docs/39-学习犹大处理问题的思路.md: -------------------------------------------------------------------------------- 1 | # 学习犹大处理问题的思路 2 | 3 | - 先写一个测试让错误显现出来 4 | - 然后再通过代码去调试错误 5 | - 再修复代码 6 | 7 | ```javascript 8 | //写一个demo 让bug 可以复现 9 | import { h, ref } from "../../lib/ass-vue.esm.js"; 10 | 11 | const prevChildren=[ 12 | h("p", { key: "A" }, "A"), 13 | h("p", "C"), 14 | h("p", { key: "B" }, "B"), 15 | h("p", { key: "D" }, "D"), 16 | ] 17 | 18 | const nextChildren=[ 19 | h("p", { key: "A" }, "A"), 20 | h("p", { key: "B" }, "B"), 21 | h("p", "C"), 22 | h("p", { key: "D" }, "D"), 23 | ] 24 | export default { 25 | name: "ArrayToArray", 26 | setup() { 27 | const isChange = ref(false); 28 | window.isChange = isChange; 29 | 30 | return { isChange }; 31 | }, 32 | render() { 33 | const self = this; 34 | 35 | return self.isChange === true 36 | ? h("div", {}, nextChildren) 37 | : h("div", {}, prevChildren); 38 | }, 39 | }; 40 | 41 | ``` 42 | 43 | 我们的demo 需要满足移动逻辑,并且我们的key 必须是没有的 44 | 然后我们循环 中间不一样的部分 我们会先循环到b 然后nextLastIndex 就等于j了,就不会再进行循环了 45 | 这个时候就不会给newIndex赋值,所以我们的newIndex就等于undefined 就会移除之前的 child,这时候我们还会循环的patch新数组里面的元素 所以就会新生成元素 46 | 而我们需要的当前的c 元素是我们移动之后产生的而不是我们新创建的元素 47 | 这个时候我们就需要更改一下循环的条件 48 | 49 | ```typescript 50 | let newIndex; 51 | for (let i = s1; i <= prevLastChildIndex; i++) { 52 | const prevChild = prevChildrenArray[i]; 53 | //看当前的key在不在老的节点建立的映射表里面 54 | //null undefined 55 | if (patched >= toBePatched) { 56 | hostRemove(prevChild.el); 57 | continue; 58 | } 59 | if (prevChild.key !== null) { 60 | newIndex = keyToNewIndexMap.get(prevChild.key); 61 | //如果用户给了key 62 | } else { 63 | //如果用户没有给key 64 | for (let j = s2; j <= nextLastChildIndex; j++) { 65 | if (isSomeVNodeType(prevChild, nextChildrenArray[j])) { 66 | newIndex = j; 67 | break; 68 | } 69 | } 70 | } 71 | 72 | //基于之前的节点去删除元素或者是新增元素 73 | if (newIndex === undefined) { 74 | hostRemove(prevChild.el); 75 | } else { 76 | //如果查找到了 77 | //那么继续去patch 对比 78 | //确定新的元素存在 79 | if (newIndex >= maxNewIndexSoFar) { 80 | maxNewIndexSoFar = newIndex; 81 | } else { 82 | moved = true; 83 | } 84 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 85 | 86 | patch( 87 | prevChild, 88 | nextChildrenArray[newIndex], 89 | container, 90 | parentComponent, 91 | null 92 | ); 93 | patched++; 94 | } 95 | } 96 | 97 | ``` -------------------------------------------------------------------------------- /docs/42-编译模块概述.md: -------------------------------------------------------------------------------- 1 | # 编译模块概述 2 | 3 | - 我们需要解析类似于 *
{{message}}
* 这样的代码 4 | - parse 处理我们用户写的string 然后处理这个 string 为一个抽象语法树 5 | 6 | >为什么需要ast 抽象语法树呢,以为我们对string 操作很不方便,所以我们需要把string 变成一个抽象语法树 7 | 8 | - transform 处理我们的抽象语法树生成的节点,编辑节点 9 | 10 | - ast => codegen 通过我们的ast 抽象语法树上面的数据去拼接我们的字符串 然后我们就会生成一个render 函数 11 | 12 | - render 函数就会去生成我们的dom节点 13 | 14 | ```js 15 |
hi! {{message}}
16 | ``` 17 | 18 | 上面这一小段代码就是我们需要实现的目标,我们可以看到上面的template 有三种类型 ,element(原生的dom节点) ,text(文本) ,slot(插值), 19 | -------------------------------------------------------------------------------- /docs/43-实现解析插值功能.md: -------------------------------------------------------------------------------- 1 | # 实现解析插值功能 2 | 3 | 现在插值的情况是这样的 {{message}} 4 | 5 | 我们现在需要处理了这个字符串,然后把他变成抽象语法树,一个对象,我们先写一个测试 6 | 7 | ```typescript 8 | // parser.spec.ts 9 | import { NodeTypes } from "../src/ast"; 10 | import { baseParse } from "../src/parse"; 11 | describe("Parser", () => { 12 | describe("interpolation", () => { 13 | test("simple interpolation", () => { 14 | const ast = baseParse("{{ message }}"); 15 | 16 | //root 17 | expect(ast.children[0]).toStrictEqual({ 18 | type: NodeTypes.INTERPOLATION, 19 | content: { 20 | type: NodeTypes.SIMPLE_EXPRESSION, 21 | content: "message", 22 | }, 23 | }); 24 | }); 25 | }); 26 | }); 27 | ``` 28 | 29 | 在处理这个字符串的时候我们需要注意以下几点 30 | 31 | - 我们可以创建一个全局的context 对象,然后source里面是我们的要处理的数据 32 | - 我们可以先让我们的测试通过,然后再来重构我们的代码 33 | - 然后我们在处理字符的时候是先截取前面的 {{ 然后把 我们的context 删除我们处理过的 {{, 34 | - 然后截取我们的 message 35 | - 然后推进我们的context (就是删除我们的message}} ) 36 | - 总的来说就是我们处理了多少字符串就删除多少的context 37 | 38 | ```typescript 39 | //这个函数很有意思,这个函数会改变我们传递进去的context的值 40 | function advanceBy(context: any, length: number) { 41 | context.source = context.source.slice(length); 42 | } 43 | 44 | ``` 45 | 46 | > 注:我并不是很理解为什么这个语法树要是这样的结构,而两层type又是干什么用的,以及children属性是干什么的 47 | -------------------------------------------------------------------------------- /docs/44- 实现解析element类型.md: -------------------------------------------------------------------------------- 1 | # 实现解析element 类型 2 | 3 | 逻辑还是和之前的处理插值类型一样的 4 | 5 | - 先判断什么情况下需要解析我们的element类型(开始标志为左尖括号的 时候需要解析我们的element类型“<”) 6 | 7 | ```typescript 8 | if (s.startsWith("{{")) { 9 | node = parseInterpolation(context); 10 | } else if (s[0] === "<") { 11 | //解析
12 | if (/[a-z]/i.test(s[1])) { 13 | node = parseElement(context); 14 | } 15 | } 16 | ``` 17 | 18 | - 我们需要知道这个element到底是什么类型的element ,通过正则匹配找到这个tag, 19 | 20 | ```typescript 21 | const reg = new RegExp(/^<([a-z]*)/i); 22 | const match: any = reg.exec(context.source); 23 | const tag = match[1]; 24 | ``` 25 | 26 | - 推进我们已经处理完的字符串 27 | 28 | ```typescript 29 | 30 | advanceBy(context, match[0].length); 31 | //删除我们的左边的右边的闭合尖括号 32 | advanceBy(context, 1); 33 | ``` 34 | 35 | - 然后如果是结束标识的话就直接返回 36 | 37 | ```typescript 38 | 39 | if (type === TagType.End) return; 40 | 41 | //如果不是结束标识的话就返回对应解析出来的tag 42 | return { 43 | type: NodeTypes.ELEMENT, 44 | tag, 45 | }; 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /docs/45-实现解析text类型.md: -------------------------------------------------------------------------------- 1 | # 实现解析text类型 2 | 3 | 和之前一样 4 | 5 | - text 类型是默认的类型,如果不是插值也不是我们的element 类型,那么就默认是text类型 6 | - 思路还是解析我们的context.source 然后返回 7 | - 然后处理完成之后我们再推进我们的context 8 | 9 | ```typescript 10 | describe("text", () => { 11 | test("simple text", () => { 12 | const ast = baseParse("some text"); 13 | expect(ast.children[0]).toStrictEqual({ 14 | type: NodeTypes.TEXT, 15 | content: "some text", 16 | }); 17 | }); 18 | }); 19 | 20 | ``` 21 | 22 | 然后我们来实现他 23 | 24 | 抽离出单独的处理text的函数 parseText 25 | 26 | 27 | ```typescript 28 | function parseText(context: any): any { 29 | //1.获取当前的内容 30 | //推进字符串 31 | const content = parseTextData(context,context.source.length); 32 | // console.log(context.source,"source"); 33 | return { 34 | type: NodeTypes.TEXT, 35 | content, 36 | } 37 | } 38 | 39 | function parseTextData(context: any,length) { 40 | const content = context.source.slice(0, length); 41 | advanceBy(context, content.length); 42 | return content; 43 | } 44 | ``` 45 | 46 | > 解析我们的text类型的应该是最简单的,我们的content就是传递过来的context.source,然后我们推进的距离也是content 的距离,就很舒服 47 | 48 | -------------------------------------------------------------------------------- /docs/46-实现三种联合类型的解析.md: -------------------------------------------------------------------------------- 1 | # 实现三种联合类型的解析 2 | 3 | 在跟着敲了一遍代码之后,其实我总结出来了下面几个点是需要我们我们处理的 4 | 5 | 1. 首先我们之前处理的逻辑都是单个的逻辑,所以我们需要循环的递归处理我们的字符串 6 | 7 | ```typescript 8 | //循环递归的处理我们的字符串, 9 | function parseChildren(context, ancestors) { 10 | const nodes: any = []; 11 | while (!isEnd(context, ancestors)) { 12 | let node; 13 | const s = context.source; 14 | if (s.startsWith("{{")) { 15 | node = parseInterpolation(context); 16 | } else if (s[0] === "<") { 17 | //解析
18 | if (/[a-z]/i.test(s[1])) { 19 | node = parseElement(context, ancestors); 20 | } 21 | } 22 | if (node === undefined) { 23 | //解析text类型 24 | node = parseText(context); 25 | } 26 | nodes.push(node); 27 | } 28 | return nodes; 29 | } 30 | 31 | ``` 32 | 33 | 2. 我们需要知道,我们应该在什么地方停止我们的单个的类型处理,然后跳出循环,去进行一个新的循环,或者直接结束掉我们的解析 34 | 35 | ```typescript 36 | function isEnd(context, ancestors) { 37 | //当遇到借宿标签的时候就是end 38 | //当context.source 没有值的时候就是end 39 | const s = context.source; 40 | if (s.startsWith("= 0; i--) { 43 | const tag = ancestors[i].tag; 44 | if (startsWithEndTagOpen(s, tag)) { 45 | return true; 46 | } 47 | } 48 | } 49 | // if (ancestors && s.startsWith(``)) { 50 | // return true; 51 | // } 52 | return !s; 53 | } 54 | ``` 55 | 56 | 3. 我在完成这个解析过程中遇到了一个bug,排查了一晚,这里再做一下记录 57 | 58 | ```typescript 59 | function advanceBy(context: any, length: number) { 60 | context.source = context.source.slice(length); 61 | } 62 | function parseInterpolation(context) { 63 | //{{message}} 64 | const openDelimiter = "{{"; 65 | const closeDelimiter = "}}"; 66 | 67 | //从{{ 开始找}dd 68 | const closeIndex = context.source.indexOf( 69 | closeDelimiter, 70 | openDelimiter.length 71 | ); 72 | 73 | //剔除掉{{ 74 | advanceBy(context, openDelimiter.length); 75 | 76 | const rawContentLength = closeIndex - openDelimiter.length; 77 | 78 | //得到message 截取得到 79 | const rawContent = parseTextData(context, rawContentLength); 80 | 81 | const content = rawContent.trim(); 82 | 83 | //推进 提出掉 message}} 84 | // console.log("context", context); 85 | advanceBy(context, closeDelimiter.length); 86 | 87 | return { 88 | type: NodeTypes.INTERPOLATION, 89 | content: { 90 | type: NodeTypes.SIMPLE_EXPRESSION, 91 | content: content, 92 | }, 93 | }; 94 | } 95 | ``` 96 | 97 | 现在这个是改正过后的代码, 之前的代码是因为我推进得太多了,导致下一个循环开始的时候拿到的context.source 为空 98 | 99 | 我那时候忘记了*parseTextData* 这个函数会自动的给我推进我的context 导致推进多了,实际上我只需要再parseInterpolation里面显示的推进 两次插值的开始标志以及插值的结束标志就行了 100 | 101 | 4. 然后就是我们的两个edge case 第一个是嵌套标签的问题,通过isEnd 里面构造结束标识的方法来实现我们的判断结束位置的逻辑,并通过*element.children=parseChildren(context,ancestors)* 来实现递归调用我们的parseChildren 来解析我们当前element 的子节点 102 | 103 | ```typescript 104 | function parseElement(context, ancestors) { 105 | const element: any = parseTag(context, TagType.Start); 106 | //收集我们的element 107 | ancestors.push(element); 108 | element.children = parseChildren(context, ancestors); 109 | //弹出我们的处理完的element 110 | ancestors.pop(); 111 | 112 | //判断结束标签是否和开始标签一样;如果一样就销毁掉,如果不一样就抛出错误 113 | 114 | if (startsWithEndTagOpen(context.source, element.tag)) { 115 | parseTag(context, TagType.End); 116 | } else { 117 | throw new Error(`缺少结束标签:${element.tag}`); 118 | } 119 | return element; 120 | } 121 | ``` 122 | 123 | >Note:总之这一小节细节很多,但是方法总比困难多,这一节还是收获很多,很佩服前辈们能想到这方法来解析字符串,也明白了,为什么那些api 都要遵守一定的规范,这样就可以找到特殊的序列去解析节点,然后再抽象成语法树, 124 | 125 | --- 126 | 127 | > 还有就是我们的根节点本身也是一个element 类型的节点所以会有children属性,而插值类型的两层type,第一层的type是标识我们当前的的节点是一个插值表达式,第二层的type则表示我们插值里面的节点到底是什么类型的,因为之前只模拟了text 类型的节点,所以想当然的以为插值里面只能有text类型, 128 | 129 | day day up!!! 130 | -------------------------------------------------------------------------------- /docs/47-浅析parse 原理.md: -------------------------------------------------------------------------------- 1 | # 浅析parse原理 2 | 3 | 在之前我们去实现baseParse 函数的时候我们其实是用了一定的技术的,这个技术叫做"有限状态机",我们用这个方法来逐个处理我们的字符串,然后解析成我们的ast 抽象语法树 4 | 5 | ## 有限状态机 6 | 7 | 概念: 简单的理解就是 现在有一个状态如果你给两个不同的条件,现在这个状态就会演变成两个不同的其他或者当前的状态(读取一组数据,然后根据这些数据来改变成不同的状态) 8 | 9 | 反观我们的parse 逻辑也可抽象成一个状态机 10 | 11 | 我们需要解析的字符串 "
hi,{{message}}
" 12 | 13 | context ==>如果以"<"开头并且context 的第二个字符为a-z 就走解析parseElement 的逻辑,然后我们去处理element element 类型的 会去调用解析tag的逻辑,然后处理过后返回一个表示element 的对象,这个element上面如果还有children的话还可以调用parseChildren 来解析element 上的children *element.children=parseChildren(context,ancestors)* 14 | 15 | 处理完element 之后状态变回初始状态,然后再去判断现在结束没有,现在很明显是没有结束的,所以下面就会默认的去解析,hi,这个文本节点,处理完文本节点, 16 | 17 | 遇到 {{ 标识符,表示当前的循环该停止,状态变成初始状态,然后我们又因为{{是解析插值的标志,所以状态就会顺着这个条件跳转到解析插值的函数里面去,然后插值解析完, 遇到结束的tag 标签推进我们的context,然后再次回到初始状态 18 | 19 | 20 | ## 利用有限状态机来实现正则匹配相关 21 | 22 | ```javascript 23 | // /abc/.text('abc') 24 | 25 | function test(string){ 26 | function waitForA(char){ 27 | if(char==="a"){ 28 | return waitForB; 29 | } 30 | return waitForA 31 | } 32 | function waitForB(char){ 33 | if(char==="b"){ 34 | return waitForC; 35 | } 36 | return waitForA; 37 | } 38 | function waitForC(char){ 39 | if(char==="c"){ 40 | return end; 41 | } 42 | return waitForA 43 | } 44 | 45 | function end(){ 46 | return end; 47 | } 48 | 49 | for(let i=0;i 我希望我得到通过*"
hi,{{message}}
"*这个 字符串生成的ast树,然后改变ast 里面text 节点的值为 hi,mini-vue 8 | 9 | - 把大象放冰箱的第一步 (找到我们的text节点) 10 | 11 | - 把大象放冰箱的第二部 (改变我们的text 的值) 12 | 13 | 通过深度优先的算法来遍历我们的ast树,得到我们的text 节点 14 | 15 | ```typescript 16 | //深度优先遍历树 17 | export function transform (root){ 18 | travelNode(root) 19 | } 20 | 21 | function travelNode(node){ 22 | const children=node.children; 23 | console.log("children") 24 | if(children){ 25 | for(let i;i { 60 | const ast = baseParse("
hi,{{message}}
"); 61 | const plugin = (node) => { 62 | if (node.type === NodeTypes.TEXT) { 63 | node.content = node.content + "mini-vue"; 64 | } 65 | }; 66 | //通过插件注入的方法来动态的控制我们的代码执行 (就是我们的处理函数插件化,把写在travelNode 里面的逻辑抽离了出来) 67 | transform(ast, { nodeTransforms: [plugin] }); 68 | const nodeText = ast.children[0].children[0]; 69 | expect(nodeText.content).toBe("hi,mini-vue"); 70 | }); 71 | ``` 72 | 73 | 再回到我们的transform 函数里面,改写函数,支持配置扩展 74 | 75 | ```typescript 76 | export function transform(root: any, options: any) { 77 | const context = createTransformContext(root, options); 78 | 79 | travelNode(root, context); 80 | } 81 | 82 | function createTransformContext(root: any, options: any) { 83 | const context = { 84 | root, 85 | nodeTransforms: options.nodeTransforms || [], 86 | }; 87 | return context; 88 | } 89 | function travelNode(node,context){ 90 | const children=node.children; 91 | console.log("children",children) 92 | //执行我们的transforms 对应定义的插件逻辑, 93 | const nodeTransforms = context.nodeTransforms; 94 | for (let i = 0; i < nodeTransforms.length; i++) { 95 | const nodeTransform = nodeTransforms[i]; 96 | nodeTransform(node); 97 | } 98 | 99 | 100 | if(children){ 101 | for(let i;iNOTE:值得注意的几个点 134 | 135 | 1. 利用深度优先遍历ast 节点树的时候,循环调用函数,(travelNode,和transform 是分开的,利用transform 这个函数做总的逻辑的处理,插件的初始化处理,以及其他) 136 | 2. travelNode 的处理函数抽离,抽离到外部传入 137 | 3. 递归处理我们的children ,如果children 存在,遍历children,然后循环调用travelNode 来处理children 138 | 139 | -------------------------------------------------------------------------------- /docs/49-代码生成string.md: -------------------------------------------------------------------------------- 1 | # 实现代码生成string 2 | 3 | 这个小节的目标就是通过我们的ast树来生成我们的可运行代码的一个阶段 4 | 我们最终要实现的目标就是编译我们的string 类型为一个函数可执行的函数 5 | [template explorer vue](https://template-explorer.vuejs.org/#eyJzcmMiOiJoaSIsIm9wdGlvbnMiOnt9fQ==) 6 | 7 | 我们期待我们的输出一个render 方法 像这样 8 | 9 | ![alt=render 方法](./static/render.png) 10 | 11 | 然后我们可以去代码里面去伪实现一下,让测试通过. 12 | 13 | 但是这一小节我并不打算贴好多代码来进行逻辑的阐述,因为这个功能实在是很简单,但是这里的逻辑要封装成一个可复用,可拓展的逻辑还是需要好好设计一下的 14 | 15 | 下面就让我们来跟着写好的代码一起来感受一下如何设计这一块的代码,以及各个模块之间的功能划分 16 | 17 | 测试代码 18 | 19 | ```typescript 20 | import { generator } from "../src/codegen"; 21 | import { baseParse } from "../src/parse"; 22 | import { transform } from "../src/transform"; 23 | 24 | describe('codeGen', () => { 25 | it("string",() => { 26 | const ast=baseParse("hi"); 27 | 28 | transform(ast) 29 | 30 | const {code}=generator(ast) 31 | 32 | expect(code).toMatchSnapshot() 33 | 34 | }) 35 | }); 36 | ``` 37 | 38 | 主要代码实现逻辑 39 | 40 | ```typescript 41 | export function generator(ast) { 42 | const context = createCodegenContext(); 43 | const { push } = context; 44 | const args = ["_ctx", "_cache"]; 45 | push("return "); 46 | const functionName = "render"; 47 | const signature = args.join(", "); 48 | 49 | push(`function ${functionName}(${signature}){`); 50 | push("return "); 51 | genNode(ast.codegenNode, context); 52 | push("}"); 53 | return { 54 | code: context.code, 55 | }; 56 | } 57 | 58 | function genNode(node: any, context: any) { 59 | const { push } = context; 60 | push(`'${node.content}'`); 61 | } 62 | 63 | function createCodegenContext() { 64 | const context = { 65 | code: "", 66 | push(source) { 67 | context.code += source; 68 | }, 69 | }; 70 | return context; 71 | } 72 | 73 | 74 | // return function render(_ctx,cache){ 75 | // return "hi" 76 | // } 77 | ``` 78 | 79 | 1. 在测试的时候通过transform(ast) 处理ast 来得到经过transform函数处理过的ast;这个时候ast树上面就挂载了我们需要处理的codegenNode这个节点;做这个操作是因为,在generator 这个函数中,只处理ast 相关的逻辑,但是我们的层级结构是有点复杂的,我们要很了解这个层级结构才知道我们该处理的那个dom节点到底是那一个,所以我们就可以先在transform 里面先处理我们的ast树,然后挂载相应的需要的属性,然后再传入到generator 里面进行处理 80 | 81 | 2. 在进行函数处理的时候抽离出我们需要的内容,然后以及处理对象需要用到的方法,然后再去简便的处理这个内容 82 | 83 | ```typescript 84 | const context = createCodegenContext(); 85 | 86 | const { push } = context; 87 | function createCodegenContext() { 88 | const context = { 89 | code: "", 90 | push(source) { 91 | context.code += source; 92 | }, 93 | }; 94 | return context; 95 | } 96 | ``` 97 | 98 | 3. 处理函数名以及函数参数 99 | 100 | ```typescript 101 | const args = ["_ctx", "_cache"]; 102 | 103 | const functionName = "render"; 104 | const signature = args.join(", "); 105 | 106 | push(`function ${functionName}(${signature}){`); 107 | ``` 108 | 109 | 4. 处理函数主体逻辑 110 | 111 | ```typescript 112 | push("return "); 113 | genNode(ast.codegenNode, context); 114 | push("}"); 115 | function genNode(node: any, context: any) { 116 | const { push } = context; 117 | push(`'${node.content}'`); 118 | } 119 | ``` 120 | 121 | > Note 122 | 123 | - 这一小节还介绍了测试的调试技巧中的 *toMatchSnapshot* 的用法 124 | - 核心的点是ast树转化逻辑的迁移到transform 125 | - 代码的核心技术手段有生成context,以及处理context 的方法方便全局调用 126 | - 代码的核心技术手段还有参数的数组化,方便拓展 127 | - 代码的核心手段 拆分处理node 的函数体逻辑(genNode),方便以后拓展 128 | -------------------------------------------------------------------------------- /docs/50-代码生成插值类型.md: -------------------------------------------------------------------------------- 1 | # 代码生成插值类型 2 | 3 | 代码生成插值类型 4 | 我们的目标是 5 | {{message}} 6 | 转化成 7 | "const {toDisplayString:_toDisplayString}=Vue 8 | return function render(_ctx, _cache){return_toDisplayString(_ctx.message)}" 9 | 10 | 伪实现还是挺简单的 11 | 12 | 我们简单来分析一下我们相对于之前处理过text 来说这里面要附加的东西 13 | 14 | 1. 需要引入一个toDisplayString 函数 15 | 2. 需要给toDisplayString 函数给一个别名 16 | 3. 返回的函数的名字还是一样的 17 | 4. 返回的函数参数也是一样的 18 | 5. 返回的函数体有变化 19 | 6. 返回的函数体里面是返回的一个_toDisplayString处理的函数 20 | 7. 返回的函数的参数是message 21 | 8. 返回的函数的参数是有从_ctx 里面拿出来的 22 | 23 | 下面我们根据代码来讲解一下这一个小节的主要逻辑 24 | 25 | ```typescript 26 | //处理导入的函数; 27 | genFunctionPreamble(ast, context); 28 | function genFunctionPreamble(ast, context) { 29 | const { push } = context; 30 | const VueBinging = "Vue"; 31 | 32 | // const helpers=["toDisplayString"] 33 | const aliasHelper = (s) => `${helperMapName[s]}:_${helperMapName[s]}`; 34 | if (ast.helpers.length) { 35 | push(`const {${ast.helpers.map(aliasHelper).join(", ")}}=${VueBinging}`); 36 | } 37 | push("\n"); 38 | push("return "); 39 | } 40 | ``` 41 | 42 | > 在transform 里面处理ast节点的是否我们需要判断当前的节点,然后配置需要用到的函数 43 | 44 | ```typescript 45 | function travelNode(node: any, context) { 46 | const nodeTransforms = context.nodeTransforms; 47 | for (let i = 0; i < nodeTransforms.length; i++) { 48 | const nodeTransform = nodeTransforms[i]; 49 | nodeTransform(node); 50 | } 51 | 52 | switch (node.type) { 53 | case NodeTypes.INTERPOLATION: 54 | context.helper(TO_DISPLAY_STRING) 55 | break; 56 | case NodeTypes.ELEMENT: 57 | case NodeTypes.ROOT: 58 | travelChildren(node,context); 59 | break; 60 | default: 61 | break; 62 | } 63 | 64 | } 65 | ``` 66 | 67 | 一些细节的东西,我发现了context 的好处,我们可以把方法放到context 上面,然后在后续的处理中调用,舒服的很 68 | 69 | 我们忽略没有变化的部分 70 | 直接到返回的函数体里面有变化 71 | 72 | ```typescript 73 | function genNode(node: any, context: any) { 74 | switch (node.type) { 75 | case NodeTypes.TEXT: 76 | genText(node, context); 77 | break; 78 | case NodeTypes.INTERPOLATION: 79 | genInterpolation(node, context); 80 | break; 81 | case NodeTypes.SIMPLE_EXPRESSION: 82 | genExpress(node, context); 83 | break; 84 | default: 85 | break; 86 | } 87 | } 88 | function genInterpolation(node, context) { 89 | const { push, helper } = context; 90 | push(`${helper(TO_DISPLAY_STRING)}(`); 91 | genNode(node.content, context); 92 | push(")"); 93 | } 94 | function genExpress(node: any, context: any) { 95 | const { push } = context; 96 | push(`${node.content}`); 97 | } 98 | ``` 99 | 100 | >添加了处理插值的函数体,以及插值里面还有 content 节点,然后循环的传递给genNode 处理Text 节点类型,顺便添加了处理Express类型的方法; 101 | 102 | 我们通过之前设计的plugin 结构来处理我们的的_ctx.message 103 | 104 | ```typescript 105 | import { NodeTypes } from "../ast"; 106 | 107 | export function transformExpression(node) { 108 | if (node.type === NodeTypes.INTERPOLATION) { 109 | node.content = processExpression(node.content); 110 | } 111 | } 112 | 113 | function processExpression(node) { 114 | node.content = `_ctx.` + node.content; 115 | return node; 116 | } 117 | ``` 118 | 119 | > 这个流程是在 transform 里面,插件先运行,然后把处理好的node 再交给我们的codegen 处理 120 | 121 | ```typescript 122 | for (let i = 0; i < nodeTransforms.length; i++) { 123 | const nodeTransform = nodeTransforms[i]; 124 | nodeTransform(node); 125 | } 126 | ``` 127 | 128 | 其实还有一些小细节的优化,比如我们把函数参数放在一个特定的文件夹里面 129 | 130 | ```typescript 131 | export const TO_DISPLAY_STRING = Symbol("toDisplayString"); 132 | 133 | export const helperMapName = { 134 | [TO_DISPLAY_STRING]: "toDisplayString", 135 | }; 136 | ``` 137 | 138 | 我们把处理TO_DISPLAY_STRING 的逻辑放在context 上面 139 | 140 | ```typescript 141 | //helper 就是处理我们的函数和函数别名的方法, 142 | function createCodegenContext() { 143 | const context = { 144 | code: "", 145 | push(source) { 146 | context.code += source; 147 | }, 148 | helper(key) { 149 | return `_${helperMapName[key]}`; 150 | }, 151 | }; 152 | return context; 153 | } 154 | ``` 155 | 156 | 虽然说我还不是很懂这些代码,但是现在看着这些代码的结构,卧槽,真的好舒服,代码的文件结构划分的很清楚,在transform 里面做转化的事情,给ast树上面绑定特定的属性方法,以及代理插件的逻辑, 157 | 在codegen 里面就处理我们的转化后的ast ,但是我们可以根据需求在ast 上面拿到我们需要的参数,这些参数是通过transform 这一层做了处理的,所以现在实现起来codegen 还是特别舒服的 158 | 159 | -------------------------------------------------------------------------------- /docs/51-处理element类型.md: -------------------------------------------------------------------------------- 1 | # 处理element 类型 2 | 3 | 处理element 类型还是和处理插值类型一样分三步走 4 | 5 | 1. 处理我们的函数的参数, 6 | 2. 处理我们的函数的主体逻辑 7 | 3. 处理我们的tag 标签 8 | 9 | ```typescript 10 | it.only("element",() => { 11 | const ast =baseParse("
") 12 | //注入插件的里面 13 | transform(ast,{nodeTransforms:[transformElement]}) 14 | const {code}=generator(ast) 15 | expect(code).toMatchSnapshot() 16 | }) 17 | ``` 18 | 19 | ```typescript 20 | //处理函数需要导入的函数名 21 | //transformElement 22 | import { NodeTypes } from "../ast"; 23 | import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers"; 24 | 25 | 26 | export default function transformElement (node,context) { 27 | if(node.type=== NodeTypes.ELEMENT){ 28 | context.helper(CREATE_ELEMENT_VNODE) 29 | } 30 | } 31 | 32 | 33 | ``` 34 | 35 | 36 | 处理函数的主要逻辑以及我们的tag获取; 37 | 38 | ````typescript 39 | function genElement(node,context){ 40 | const {push ,helper}=context; 41 | const {tag}=node 42 | push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}")`) 43 | } 44 | ``` 45 | 46 | 解析element 还是很简单的, 但是不要忘了我们的解析函数名字和别名的操作是通过plugin 这种方式来添加的; 47 | -------------------------------------------------------------------------------- /docs/52-实现编译template 为render函数并渲染到页面.md: -------------------------------------------------------------------------------- 1 | # 实现编译template 成render函数并渲染到页面上 2 | 3 | - 编译template 是在声明式组件没有传递我们的render函数的时候的操作 4 | - 简单一点就是我们替换掉我们的component 的render函数就行了 5 | - 为了解耦不能直接调用我们的complier 函数然后返回函数主题逻辑直接赋值给我们的component的render 函数 6 | 7 | 1. 处理我们的complier-core 的导出逻辑到全局的Vue 出口 8 | 9 | ```typescript 10 | //compile.ts 11 | import { generator } from "./codegen"; 12 | import { baseParse } from "./parse"; 13 | import { transform } from "./transform"; 14 | import transformElement from "./transforms/transformElement"; 15 | import { transformExpression } from "./transforms/transformExpression"; 16 | import { transformText } from "./transforms/transformText"; 17 | 18 | export function baseCompile(template){ 19 | const ast: any = baseParse(template); 20 | transform(ast, { 21 | nodeTransforms: [transformExpression, transformElement, transformText], 22 | }); 23 | return generator(ast); 24 | } 25 | //index.ts 26 | export * from "./compile"; 27 | ``` 28 | 29 | 2. 在我们的component 模块申明compiler 状态以及注册compiler 函数的方法 30 | 31 | ```typescript 32 | //注册组件,在index里面去调用注册我们的compiler; 33 | let compiler; 34 | export function registerRuntimeCompiler(_compiler) { 35 | compiler = _compiler; 36 | } 37 | 38 | 39 | ``` 40 | 41 | 3. 在入口处注册我们的compiler 42 | 43 | ```typescript 44 | function compileToFunction(template) { 45 | const { code } = baseCompile(template); 46 | //这里的code 就是我们代码compile 生成的代码;第一个参数是我们的函数参数 47 | const render = new Function("Vue", code)(runtimeDom); 48 | return render; 49 | } 50 | 51 | registerRuntimeCompiler(compileToFunction); 52 | 53 | 54 | ``` 55 | 56 | 4. 在我们的component里面去使用我们注册好了的compiler 函数来重新生成我们的render 函数 57 | 58 | ```typescript 59 | function finishComponentSetup(instance) { 60 | const Component = instance.type; 61 | //假设是一定有render的 62 | //在这里去调用我们的render 函数 63 | //但是如果在这里面去引用我们的complier 模块的东西,这样会造成依赖的循环依赖;vue 是可以直接存在于运行时的,如果引入了complier 模块的话就不干净了; 64 | //注册了compiler 然后在这里使用 65 | if (compiler && !Component.render) { 66 | if (Component.template) { 67 | Component.render = compiler(Component.template); 68 | } 69 | } 70 | 71 | instance.render = Component.render; 72 | } 73 | ``` 74 | 75 | 然后我们还需要去声明一下我们解析需要用到的toDisplayString 和createElementVNode函数 76 | 77 | bingo: 78 | 79 | 然后你就会在页面上看见这,努力了两个月的成果, 80 | 81 | ![alt 最终效果图](./static/final.png) 82 | 83 | >Note: 话不多说,找崔哥拿书 84 | -------------------------------------------------------------------------------- /docs/53.使用pnpm和vitest来改造项目.md: -------------------------------------------------------------------------------- 1 | # 使用pnpm 和vitest 来改造项目 2 | 3 | ## 使用pnpm 来重新组织项目 4 | 5 | 一些粗浅的对pnpm 的认识 6 | 7 | - 通过npm 发包的方式让各个模块更好的隔离,方便单独的发包,以及版本管理 8 | - 上手难度比较大,目录结构比较复杂 9 | - 每个模块都会有一个package.json 文件 10 | 11 | 12 | 抽离complier-core 等模块,最重要的是弄清楚,各个模块的依赖关系,然后处理导入导出逻辑,进行各个模块的精细化处理 13 | 14 | 按照下面几个步骤处理 15 | 16 | 1. 先在根目录下面建立pnpm-workspace.yaml 文件指定我们的包的路径 17 | 18 | ```yaml 19 | packages: 20 | - "packages/*" 21 | ``` 22 | 23 | 2. 然后再在每个包里面通过`pnpm init` 建立初始化的package.json 文件,然后分析 这个模块需要依赖什么模块,然后通过 安装share 模块到 complier-core 模块,当然我们需要先给我们的share和complier-core 模块重新命名,嘿嘿 24 | 25 | ```bash 26 | pnpm install @ass-vue/share -F complier-core 27 | ``` 28 | 29 | 3. 然后检查我们的代码,看有无报错,以及错误,如果有路径错误,就替换成相应的模块的名称就好了 30 | 31 | 32 | ## 通过vitest 替换jest 33 | 34 | vitest和jest 不同,我们需要手动的导入it description 等函数,可以通过配置文件改变其为全局引入的方式 35 | 36 | ```json 37 | //vitest.config.json 38 | import path, { resolve } from "path"; 39 | import { defineConfig } from "vitest/config"; 40 | 41 | export default defineConfig({ 42 | test: { 43 | globals: true, 44 | }, 45 | resolve: { 46 | alias: [ 47 | { 48 | find: /@ass-vue\/(\w*)/, 49 | replacement: path.resolve(__dirname, "packages") + "/$1/src", 50 | }, 51 | ], 52 | }, 53 | }); 54 | ``` 55 | 56 | 然后通过rollup.config.ts 改变我们的 打包后的相关代码存放地 57 | 58 | ```ts 59 | import typescript from "@rollup/plugin-typescript"; 60 | export default { 61 | input: "packages/vue/src/index.ts", 62 | output: [ 63 | { 64 | formate: "cjs", 65 | file: "packages/vue/dist/ass-vue.cjs.js", 66 | }, 67 | 68 | { 69 | formate: "es", 70 | file: "packages/vue/dist/ass-vue.esm.js", 71 | }, 72 | ], 73 | plugins: [typescript()], 74 | }; 75 | 76 | ``` 77 | 78 | 通过 vi 来生成函数 79 | 80 | ```ts 81 | const cleanup=vi.fn() 82 | ``` 83 | 84 | 删除我们之前安装的jest 依赖 85 | 86 | 改变tsconfig.ts里面的types 为vitest,改变全局的packages.json 里面的test 命令为vitest 87 | 88 | bingo -------------------------------------------------------------------------------- /docs/54.实现watchEffect的基础功能.md: -------------------------------------------------------------------------------- 1 | # 实现watchEffect 的基础功能 2 | 3 | [官方地址](https://vuejs.org/api/reactivity-core.html#watcheffect) 4 | 5 | 编写代码实现我们的简单的三个功能 6 | 7 | 1. 当响应式对象改变的时候我们的watchEffect 里面的effect 相应的值也会变换 8 | 2. watchEffect 会返回一个stop 函数,调用了之后effect 的值effect 对应的值不会改变 9 | 3. 调用了stop 之后穿人的clear 函数会再被执行一次,再每次重新渲染之前会去执行一下我们的cleanup 函数 10 | 11 | ```ts 12 | import { reactive } from "@ass-vue/reactivity"; 13 | import { nextTick } from "../src/scheduler"; 14 | 15 | import { watchEffect } from "../src/apiWatch"; 16 | 17 | describe("api: watch", () => { 18 | it("effect", async () => { 19 | const state = reactive({ count: 0 }); 20 | 21 | let dummy; 22 | watchEffect(() => { 23 | dummy = state.count; 24 | }); 25 | 26 | expect(dummy).toBe(0); 27 | state.count++; 28 | await nextTick(); 29 | expect(dummy).toBe(1); 30 | }); 31 | 32 | it("stop the watcher(effect)", async () => { 33 | const state = reactive({ count: 0 }); 34 | let dummy; 35 | const stop: any = watchEffect(() => { 36 | dummy = state.count; 37 | }); 38 | 39 | expect(dummy).toBe(0); 40 | 41 | stop(); 42 | state.count++; 43 | await nextTick(); 44 | 45 | expect(dummy).toBe(0); 46 | }); 47 | 48 | it("cleanup registration (effect)",async () => { 49 | const state = reactive({ count: 0 }); 50 | 51 | const cleanup=vi.fn() 52 | let dummy; 53 | const stop=watchEffect((onCleanup) => { 54 | onCleanup(cleanup) 55 | dummy = state.count; 56 | }); 57 | 58 | expect(dummy).toBe(0); 59 | state.count++; 60 | await nextTick(); 61 | //发生更新逻辑之后再调用 62 | expect(cleanup).toBeCalledTimes(1) 63 | expect(dummy).toBe(1); 64 | stop() 65 | //当stop 掉了之后cleanup还会被调用一次 66 | expect(cleanup).toBeCalledTimes(2) 67 | }) 68 | }); 69 | ``` 70 | 71 | 对应的apiWatch 代码 72 | 73 | ```ts 74 | import { ReactiveEffect } from "../../reactivity/src/effect"; 75 | 76 | import { queueFlushCb } from "./scheduler"; 77 | export function watchEffect(source) { 78 | // source 函数会被添加到组件渲染前调用 79 | let cleanup; 80 | function job(cleanupFn) { 81 | effect.run(); 82 | } 83 | 84 | const onCleanup = function (cleanupFn) { 85 | cleanup = effect.onStop=() => { cleanupFn() } 86 | }; 87 | 88 | function getter() { 89 | if (cleanup) { 90 | cleanup(); 91 | } 92 | source(onCleanup); 93 | } 94 | const effect = new ReactiveEffect(getter, () => { 95 | queueFlushCb(job); 96 | // effect.run(); 97 | }); 98 | //一上来的时候这个source 是要被调用的 99 | effect.run(); 100 | return () => { 101 | effect.stop(); 102 | }; 103 | } 104 | ``` 105 | 106 | 对应需要改变的 scheduler.ts 里面的逻辑 107 | 108 | ```ts 109 | const queue: any[] = []; 110 | const activePreFlushCbs: any[] = []; 111 | const promiseResolve = Promise.resolve(); 112 | 113 | let isFlushPending = false; 114 | export function queueJobs(job) { 115 | if (!queue.includes(job)) { 116 | queue.push(job); 117 | } 118 | 119 | queueFlush(); 120 | } 121 | 122 | function queueFlush() { 123 | //取出job放到微任务里面执行 124 | if (isFlushPending) { 125 | return; 126 | } 127 | isFlushPending = true; 128 | nextTick(flushJob); 129 | } 130 | 131 | export function queueFlushCb(job) { 132 | activePreFlushCbs.push(job); 133 | 134 | queueFlush() 135 | } 136 | function flushJob() { 137 | isFlushPending = false; 138 | //组件渲染之前 139 | flushPreFlushCbs(); 140 | 141 | let job; 142 | while ((job = queue.shift())) { 143 | job && job(); 144 | } 145 | } 146 | 147 | function flushPreFlushCbs() { 148 | for (let i = 0; i < activePreFlushCbs.length; i++) { 149 | activePreFlushCbs[i](); 150 | } 151 | } 152 | 153 | export function nextTick(fn?) { 154 | return fn ? promiseResolve.then(fn) : promiseResolve; 155 | } 156 | ``` -------------------------------------------------------------------------------- /docs/static/final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aoshisen/ass_vue/e679953cd962dc246864906e2e643269bcf0147a/docs/static/final.png -------------------------------------------------------------------------------- /docs/static/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aoshisen/ass_vue/e679953cd962dc246864906e2e643269bcf0147a/docs/static/left.png -------------------------------------------------------------------------------- /docs/static/left_with_complier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aoshisen/ass_vue/e679953cd962dc246864906e2e643269bcf0147a/docs/static/left_with_complier.png -------------------------------------------------------------------------------- /docs/static/render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aoshisen/ass_vue/e679953cd962dc246864906e2e643269bcf0147a/docs/static/render.png -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from "../../lib/ass-vue.esm.js"; 2 | 3 | const Provider = { 4 | name: "Provider", 5 | setup() { 6 | provide("foo", "fooVal"), provide("bar", "barVal"); 7 | const foo = inject("foo"); 8 | return { foo }; 9 | }, 10 | render() { 11 | return h("div", {}, [ 12 | h("p", {}, `provider foo:${this.foo}`), 13 | h(ProviderTwo), 14 | ]); 15 | }, 16 | }; 17 | 18 | const ProviderTwo = { 19 | name: "ProviderTwo", 20 | setup() { 21 | provide("foo", "fooTwo Val"); 22 | const foo = inject("foo"); 23 | return { foo }; 24 | }, 25 | render() { 26 | return h("div", {}, [h("p", {}, `providerTwo-${this.foo}`), h(Consumer)]); 27 | }, 28 | }; 29 | const Consumer = { 30 | name: "Consumer", 31 | setup() { 32 | const foo = inject("foo"); 33 | const bar = inject("bar"); 34 | // const baz=inject("baz","bazDefault") 35 | const baz = inject("baz", () => { 36 | return "ass"; 37 | }); 38 | return { foo, bar, baz }; 39 | }, 40 | render() { 41 | return h("div", {}, `Consumer:-${this.foo}-${this.bar}-${this.baz}`); 42 | }, 43 | }; 44 | 45 | export default { 46 | name: "App", 47 | setup() {}, 48 | render() { 49 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]); 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/apiInject/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import App from "./App.js"; 4 | 5 | const rootContainer = document.getElementById("app"); 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /example/compiler-base/App.js: -------------------------------------------------------------------------------- 1 | export const App = { 2 | name: "App", 3 | template: `
hi,{{message}}
`, 4 | setup() { 5 | return { 6 | message: "Ass", 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /example/compiler-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/compiler-base/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from "../../lib/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | export const App = { 4 | name: "App", 5 | render() { 6 | const app = h("div", {}, "App"); 7 | //我们希望在foo 这里传递h 的第三个参数,能被Foo 接收到并且渲染到children里面 8 | const foo = h( 9 | Foo, 10 | {}, 11 | { 12 | header: ({ age }) => [ 13 | h("p", {}, "header" + age), 14 | createTextVNode("你好呀"), 15 | ], 16 | footer: () => h("p", {}, "footer"), 17 | } 18 | ); 19 | 20 | return h("div", {}, [app, foo]); 21 | }, 22 | setup() { 23 | return {}; 24 | }, 25 | }; 26 | 27 | //处理组件,处理element 28 | -------------------------------------------------------------------------------- /example/componentSlots/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | export const Foo = { 3 | setup() { 4 | return {}; 5 | }, 6 | render() { 7 | const foo = h("p", {}, "foo"); 8 | console.log(this.$slots); 9 | // 1.获取到要渲染的节点 10 | // 2.获取到要渲染的位置 11 | return h("div", {}, [ 12 | renderSlots(this.$slots, "header", { age: 10000 }), 13 | foo, 14 | renderSlots(this.$slots, "footer"), 15 | ]); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /example/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | 3 | import Child from "./Child.js"; 4 | export const App = { 5 | name: "App", 6 | setup() { 7 | const msg = ref("123"); 8 | const count = ref(1); 9 | window.msg = msg; 10 | const changeChildProps = () => { 11 | msg.value = "245555"; 12 | }; 13 | const changeCount = () => { 14 | count.value++; 15 | }; 16 | return { msg, changeChildProps, changeCount, count }; 17 | }, 18 | render() { 19 | return h("div", {}, [ 20 | h("div", {}, "你好"), 21 | h("button", { onClick: this.changeChildProps }, "change child props"), 22 | h(Child, { 23 | msg: this.msg, 24 | }), 25 | h("button", { onClick: this.changeCount }, "change self count"), 26 | h("p", {}, "count" + this.count), 27 | ]); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /example/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h, ref, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | const Child = { 3 | setup() { 4 | return {}; 5 | }, 6 | 7 | render() { 8 | return h("p", {}, "child-props-msg" + this.$props.msg); 9 | }, 10 | }; 11 | export default Child; 12 | -------------------------------------------------------------------------------- /example/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/customRender/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | export const App = { 3 | setup() { 4 | return { 5 | x: 100, 6 | y: 100, 7 | }; 8 | }, 9 | render() { 10 | return h("rect", { x: this.x, y: this.y }); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /example/customRender/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/customRender/main.js: -------------------------------------------------------------------------------- 1 | import { createRender } from "../../lib/ass-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | console.log(PIXI); 4 | 5 | const game = new PIXI.Application({ 6 | width: 500, 7 | height: 500, 8 | }); 9 | 10 | document.body.appendChild(game.view); 11 | 12 | const createElement = (type) => { 13 | if (type === "rect") { 14 | const rect = new PIXI.Graphics(); 15 | rect.beginFill(0xff0000); 16 | rect.drawRect(0, 0, 100, 100); 17 | rect.endFill(); 18 | return rect; 19 | } 20 | }; 21 | 22 | const patchProp = (el, key, val) => { 23 | el[key] = val; 24 | }; 25 | 26 | const insert = (el, parent) => { 27 | parent.addChild(el); 28 | }; 29 | 30 | const renderer = createRender({ 31 | createElement, 32 | patchProp, 33 | insert, 34 | }); 35 | 36 | renderer.createApp(App).mount(game.stage); 37 | -------------------------------------------------------------------------------- /example/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | return h("div", {}, [h("p", {}, "currentInstance Demo"), h(Foo)]); 8 | }, 9 | setup() { 10 | const instance = getCurrentInstance(); 11 | console.log("App:", instance); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /example/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h,getCurrentInstance } from "../../lib/ass-vue.esm.js"; 2 | export const Foo = { 3 | name: "Foo", 4 | setup() { 5 | const instance = getCurrentInstance(); 6 | console.log("Foo:", instance); 7 | return {}; 8 | }, 9 | render() { 10 | return h("div", {}, "foo"); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /example/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/helloWord/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | window.self = null; 4 | export const App = { 5 | // .vue 6 | // 7 | //render 8 | render() { 9 | //ui 逻辑 10 | window.self = this; 11 | return h( 12 | "div", 13 | { 14 | id: "root", 15 | class: ["red"], 16 | // onClick() { 17 | // console.log("this is app div onclick"); 18 | // }, 19 | // onMousedown() { 20 | // console.log("mouseDown,app"); 21 | // }, 22 | }, 23 | // "hi" + this.msg, 24 | // [h("p", { class: "red" }, "hi red"), h("p", { class: "blue" }, "hi blue")], 25 | [ 26 | h("p", { class: "red" }, "hi red"), 27 | h(Foo, { 28 | count: 1, 29 | onAdd(a, b) { 30 | console.log("on add in app js", a, b); 31 | }, 32 | onAddFoo(){ 33 | console.log("on Add foo in app js") 34 | } 35 | }), 36 | ] 37 | ); 38 | }, 39 | setup() { 40 | return { 41 | msg: "ass-vue", 42 | }; 43 | }, 44 | }; 45 | 46 | //处理组件,处理element 47 | -------------------------------------------------------------------------------- /example/helloWord/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | export const Foo = { 3 | setup() { 4 | return {}; 5 | }, 6 | render() { 7 | const foo = h("p", {}, "Foo"); 8 | //我们需要在这里接收上面传递过来的slots 然后把他加入到h 函数渲染的函数里面去 9 | //其实slots 就是当前虚拟节点的children 10 | return h("div", {}, [foo,this.$slots,]); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /example/helloWord/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/helloWord/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, getCurrentInstance,nextTick } from "../../lib/ass-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | setup() { 6 | const count = ref(1); 7 | const instance = getCurrentInstance(); 8 | async function onClick() { 9 | for (let i = 0; i < 100; i++) { 10 | console.log("update"); 11 | count.value = i; 12 | } 13 | debugger; 14 | 15 | //在这里因为是异步任务所以拿不到最新的instance 16 | console.log(instance, "instance"); 17 | nextTick(()=>{ 18 | //在nextTick中拿到最新的instance 19 | console.log(instance, "instance"); 20 | }) 21 | //或者使用这样使用 22 | await nextTick(); 23 | console.log(instance,"instance"); 24 | 25 | } return { onClick, count }; 26 | }, 27 | render() { 28 | { 29 | const button = h("button", { onClick: this.onClick }, "update"); 30 | const p = h("p", {}, "count" + this.count); 31 | return h("div", {}, [button, p]); 32 | } 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /example/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | 3 | import ArrayToText from "./ArrayToText.js"; 4 | import TextToText from './TextToText.js' 5 | // import TextToArray from "./TextToArray.js"; 6 | import ArrayToArray from "./ArrayToArray.js"; 7 | export const App = { 8 | name: "App", 9 | setup() {}, 10 | render() { 11 | return h("div", { tId: 1 }, [ 12 | h("p", {}, "主页"), 13 | //老的是Array,新的是text 14 | // h(ArrayToText), 15 | //老的是Text 新的是不同的text 16 | // h(TextToText), 17 | //老的是text 新的是Array 18 | // h(TextToArray), 19 | //老的是Array 新的也是Array 20 | h(ArrayToArray), 21 | ]); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const nextChildren = "newChildren"; 4 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")]; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const prevChildren = "oldChildren"; 4 | const nextChildren = [h("div", {}, "New"), h("div", {}, "Children")]; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const nextChildren = "newChildren"; 4 | const prevChildren = "oldChildren"; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | setup() { 6 | const count = ref(0); 7 | const onClick = () => count.value++; 8 | const props = ref({ 9 | foo: "foo", 10 | bar: "bar", 11 | }); 12 | const onChangePropsDemo1 = () => { 13 | props.value.foo = "new-foo"; 14 | }; 15 | const onChangePropsDemo2 = () => { 16 | props.value.foo = undefined; 17 | }; 18 | const onChangePropsDemo3 = () => { 19 | props.value = { foo: "foo" }; 20 | }; 21 | return { 22 | count, 23 | onClick, 24 | onChangePropsDemo1, 25 | onChangePropsDemo2, 26 | onChangePropsDemo3, 27 | props, 28 | }; 29 | }, 30 | render() { 31 | return h("div", { id: "root", foo: this.props.foo, bar: this.props.bar }, [ 32 | h("div", {}, "count:" + this.count), 33 | h("button", { onClick: this.onClick }, "click me to add 1"), 34 | h( 35 | "button", 36 | { onClick: this.onChangePropsDemo1 }, 37 | "changePropsDemo props" 38 | ), 39 | h( 40 | "button", 41 | { onClick: this.onChangePropsDemo2 }, 42 | "changeProps to null or undefined" 43 | ), 44 | h("button", { onClick: this.onChangePropsDemo3 }, "delete Props"), 45 | ]); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /example/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/update/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function createVNode(type, props, children) { 4 | const vNode = { type, props, children }; 5 | return vNode; 6 | } 7 | 8 | function createComponentInstance(vNode) { 9 | const component = { 10 | vNode, 11 | type: vNode.type, 12 | }; 13 | return component; 14 | } 15 | function setupComponent(instance) { 16 | //TODO:initProps 17 | //TODO:initSlots 18 | //初始化一个有状态的component (有状态的组件和函数组件函数组件是没有任何状态的) 19 | setupStatefulComponent(instance); 20 | } 21 | function setupStatefulComponent(instance) { 22 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 23 | const Component = instance.type; 24 | const { setup } = Component; 25 | if (setup) { 26 | const setupResult = setup(); 27 | handleSetupResult(instance, setupResult); 28 | } 29 | } 30 | function handleSetupResult(instance, setupResult) { 31 | //setup => function || object 32 | // function => render 函数 33 | // object=> 注入到组件上下文中 34 | //TODO: function 35 | if (typeof setupResult === "object") { 36 | instance.setupState = setupResult; 37 | } 38 | //保证组件的render是一定有值的 39 | finishComponentSetup(instance); 40 | } 41 | function finishComponentSetup(instance) { 42 | const Component = instance.type; 43 | //假设是一定有render的 44 | instance.render = Component.render; 45 | } 46 | 47 | function render(vNode, container) { 48 | //render 的时候啥也不干,就去调用patch方法 49 | //方便进行递归的处理 50 | patch(vNode); 51 | } 52 | function patch(vNode, container) { 53 | //处理组件 54 | // 55 | //element 56 | if (typeof vNode.type === "object") { 57 | console.log("component 类型"); 58 | processComponent(vNode); 59 | } 60 | else { 61 | console.log("element 类型"); 62 | } 63 | } 64 | function processComponent(vNode, container) { 65 | //处理组件 66 | //先去mountComponent 67 | mountComponent(vNode); 68 | } 69 | function mountComponent(vNode, container) { 70 | /* 71 | 1. 通过vNode 创建组件实例对象 72 | 2. 通过组件实例对象来初始化组件(component) 处理props 处理slot 处理当前组件调用setup返回出来的值 73 | 3. 创建renderEffect 74 | */ 75 | const instance = createComponentInstance(vNode); 76 | setupComponent(instance); 77 | setupRenderEffect(instance); 78 | } 79 | function setupRenderEffect(instance, container) { 80 | const subTree = instance.render(); 81 | //subTree 就是虚拟节点树 82 | /* 83 | vNode ->patch 84 | vNode -> element mountElement 85 | */ 86 | patch(subTree); 87 | } 88 | 89 | function createApp(rootComponent) { 90 | return { 91 | //接收一个根容器,在页面上存在的元素节点 92 | mount(rootContainer) { 93 | // 现在把所有的component转化成VNode 94 | //先把所有的东西转化成一个虚拟节点VNode 95 | //之后所有的逻辑操作都会基于虚拟节点做操作 96 | const vNode = createVNode(rootComponent); 97 | render(vNode); 98 | } 99 | }; 100 | } 101 | 102 | function h(type, props, children) { 103 | return createVNode(type, props, children); 104 | } 105 | 106 | exports.createApp = createApp; 107 | exports.h = h; 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ass_vue", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "main": "lib/ass-vue.cjs.js", 6 | "module": "lib/ass-vue.esm.js", 7 | "devDependencies": { 8 | "@babel/core": "^7.21.8", 9 | "@babel/preset-env": "^7.21.5", 10 | "@babel/preset-typescript": "^7.21.5", 11 | "@rollup/plugin-typescript": "^10.0.1", 12 | "rollup": "^3.21.6", 13 | "tslib": "^2.5.0", 14 | "typescript": "^4.9.5", 15 | "vitest": "^0.31.0" 16 | }, 17 | "scripts": { 18 | "test": "vitest", 19 | "build": "rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/complier-core/__tests__/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`codeGen > element 1`] = ` 4 | "const {toDisplayString:_toDisplayString, createElementVNode:_createElementVNode}=Vue 5 | return function render(_ctx, _cache) {return _createElementVNode('div',null,'hi,' + _toDisplayString(_ctx.message))}" 6 | `; 7 | 8 | exports[`codeGen > interpolation 1`] = ` 9 | "const {toDisplayString:_toDisplayString}=Vue 10 | return function render(_ctx, _cache) {return _toDisplayString(_ctx.message)}" 11 | `; 12 | 13 | exports[`codeGen > string 1`] = ` 14 | " 15 | return function render(_ctx, _cache) {return 'hi'}" 16 | `; 17 | 18 | exports[`codeGen element 1`] = ` 19 | "const {toDisplayString:_toDisplayString, createElementVnode:_createElementVnode}=Vue 20 | return function render(_ctx, _cache) {return _createElementVnode('div',null,'hi,' + _toDisplayString(_ctx.message))}" 21 | `; 22 | 23 | exports[`codeGen interpolation 1`] = ` 24 | "const {toDisplayString:_toDisplayString}=Vue 25 | return function render(_ctx, _cache) {return _toDisplayString(_ctx.message)}" 26 | `; 27 | 28 | exports[`codeGen string 1`] = ` 29 | " 30 | return function render(_ctx, _cache) {return 'hi'}" 31 | `; 32 | -------------------------------------------------------------------------------- /packages/complier-core/__tests__/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { generator } from "../src/codegen"; 2 | import { baseParse } from "../src/parse"; 3 | import { transform } from "../src/transform"; 4 | import transformElement from "../src/transforms/transformElement"; 5 | import { transformExpression } from "../src/transforms/transformExpression"; 6 | import { transformText } from "../src/transforms/transformText"; 7 | 8 | // import { describe, it, expect } from "vitest"; 9 | describe("codeGen", () => { 10 | it("string", () => { 11 | const ast = baseParse("hi"); 12 | 13 | transform(ast); 14 | 15 | const { code } = generator(ast); 16 | 17 | expect(code).toMatchSnapshot(); 18 | }); 19 | 20 | it("interpolation", () => { 21 | const ast = baseParse("{{message}}"); 22 | 23 | transform(ast, { nodeTransforms: [transformExpression] }); 24 | 25 | const { code } = generator(ast); 26 | 27 | expect(code).toMatchSnapshot(); 28 | }); 29 | 30 | it("element", () => { 31 | const ast: any = baseParse("
hi,{{message}}
"); 32 | transform(ast, { 33 | nodeTransforms: [transformExpression, transformElement, transformText], 34 | }); 35 | console.log("ast", ast.codegenNode.children); 36 | const { code } = generator(ast); 37 | expect(code).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/complier-core/__tests__/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | describe("Parser", () => { 4 | describe("interpolation", () => { 5 | test("simple interpolation", () => { 6 | const ast = baseParse("{{ message }}"); 7 | 8 | //root 9 | expect(ast.children[0]).toStrictEqual({ 10 | type: NodeTypes.INTERPOLATION, 11 | content: { 12 | type: NodeTypes.SIMPLE_EXPRESSION, 13 | content: "message", 14 | }, 15 | }); 16 | }); 17 | }); 18 | 19 | describe("element", () => { 20 | test("simple element div", () => { 21 | const ast = baseParse("
"); 22 | expect(ast.children[0]).toStrictEqual({ 23 | type: NodeTypes.ELEMENT, 24 | tag: "div", 25 | children: [], 26 | }); 27 | }); 28 | }); 29 | 30 | describe("text", () => { 31 | test("simple text", () => { 32 | const ast = baseParse("some text"); 33 | expect(ast.children[0]).toStrictEqual({ 34 | type: NodeTypes.TEXT, 35 | content: "some text", 36 | }); 37 | }); 38 | }); 39 | 40 | test("happy path", () => { 41 | const ast = baseParse("
hi,{{message}}
"); 42 | expect(ast.children[0]).toStrictEqual({ 43 | type: NodeTypes.ELEMENT, 44 | tag: "div", 45 | children: [ 46 | { type: NodeTypes.TEXT, content: "hi," }, 47 | { 48 | type: NodeTypes.INTERPOLATION, 49 | content: { 50 | type: NodeTypes.SIMPLE_EXPRESSION, 51 | content: "message", 52 | }, 53 | }, 54 | ], 55 | }); 56 | }); 57 | 58 | test("Nested Element", () => { 59 | const ast = baseParse("

hi

{{message}}
"); 60 | expect(ast.children[0]).toStrictEqual({ 61 | type: NodeTypes.ELEMENT, 62 | tag: "div", 63 | children: [ 64 | { 65 | type: NodeTypes.ELEMENT, 66 | tag: "p", 67 | children: [ 68 | { 69 | type: NodeTypes.TEXT, 70 | content: "hi", 71 | }, 72 | ], 73 | }, 74 | { 75 | type: NodeTypes.INTERPOLATION, 76 | content: { 77 | type: NodeTypes.SIMPLE_EXPRESSION, 78 | content: "message", 79 | }, 80 | }, 81 | ], 82 | }); 83 | }); 84 | 85 | //记录我们已经解析过的tag标签,如果存在在list里面就可以结束当前的解析,如果没有的话就抛出错误, 86 | test("should throw error when lack end tag", () => { 87 | // baseParse("
"); 88 | expect(() => { 89 | baseParse("
"); 90 | }).toThrow("缺少结束标签:span"); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/complier-core/__tests__/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | import { transform } from "../src/transform"; 4 | 5 | describe("transform", () => { 6 | it("happy path", () => { 7 | const ast = baseParse("
hi,{{message}}
"); 8 | const plugin = (node) => { 9 | if (node.type === NodeTypes.TEXT) { 10 | node.content = node.content + "mini-vue"; 11 | } 12 | }; 13 | //通过插件注入的方法来动态的控制我们的代码执行 (就是我们的处理函数插件化,把写在travelNode 里面的逻辑抽离了出来) 14 | transform(ast, { nodeTransforms: [plugin] }); 15 | const nodeText = ast.children[0].children[0]; 16 | expect(nodeText.content).toBe("hi,mini-vue"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/complier-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ass-vue/complier-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@ass-vue/shared": "workspace:^" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/complier-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | import { CREATE_ELEMENT_VNODE } from "./runtimeHelpers"; 2 | 3 | export const enum NodeTypes { 4 | INTERPOLATION, 5 | SIMPLE_EXPRESSION, 6 | ELEMENT, 7 | TEXT, 8 | ROOT, 9 | COMPOUND_EXPRESSION, 10 | } 11 | 12 | export function createVNodeCall(context, tag, props, children) { 13 | context.helper(CREATE_ELEMENT_VNODE); 14 | return { 15 | type: NodeTypes.ELEMENT, 16 | tag, 17 | props, 18 | children, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/complier-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "@ass-vue/shared"; 2 | import { NodeTypes } from "./ast"; 3 | import { 4 | CREATE_ELEMENT_VNODE, 5 | TO_DISPLAY_STRING, 6 | helperMapName, 7 | } from "./runtimeHelpers"; 8 | 9 | export function generator(ast) { 10 | const context = createCodegenContext(); 11 | const { push } = context; 12 | const args = ["_ctx", "_cache"]; 13 | 14 | genFunctionPreamble(ast, context); 15 | 16 | const functionName = "render"; 17 | const signature = args.join(", "); 18 | 19 | push(`function ${functionName}(${signature}) {`); 20 | push("return "); 21 | genNode(ast.codegenNode, context); 22 | push("}"); 23 | return { 24 | code: context.code, 25 | }; 26 | } 27 | 28 | function genFunctionPreamble(ast, context) { 29 | const { push } = context; 30 | const VueBinging = "Vue"; 31 | 32 | const aliasHelper = (s) => `${helperMapName[s]}:_${helperMapName[s]}`; 33 | if (ast.helpers.length) { 34 | push(`const {${ast.helpers.map(aliasHelper).join(", ")}}=${VueBinging}`); 35 | } 36 | push("\n"); 37 | push("return "); 38 | } 39 | 40 | function genNode(node: any, context: any) { 41 | // console.log("genNode>>>>>>", node); 42 | switch (node.type) { 43 | case NodeTypes.TEXT: 44 | genText(node, context); 45 | break; 46 | case NodeTypes.INTERPOLATION: 47 | genInterpolation(node, context); 48 | break; 49 | case NodeTypes.SIMPLE_EXPRESSION: 50 | genExpress(node, context); 51 | break; 52 | case NodeTypes.COMPOUND_EXPRESSION: 53 | genCompoundExpression(node, context); 54 | break; 55 | case NodeTypes.ELEMENT: 56 | genElement(node, context); 57 | break; 58 | default: 59 | break; 60 | } 61 | } 62 | 63 | function genElement(node, context) { 64 | const { push, helper } = context; 65 | const { tag, children, props } = node; 66 | push(`${helper(CREATE_ELEMENT_VNODE)}(`); 67 | // genNode(children, context); 68 | const nodeList = genNullable([tag, props, children]); 69 | genNodeList(nodeList, context); 70 | push(")"); 71 | } 72 | 73 | function genNullable(args: any) { 74 | return args.map((arg) => arg || "null"); 75 | } 76 | 77 | function genNodeList(nodes, context) { 78 | for (let i = 0; i < nodes.length; i++) { 79 | const node = nodes[i]; 80 | if (isString(node)) { 81 | context.push(node); 82 | } else { 83 | genNode(node, context); 84 | } 85 | if (i < nodes.length - 1) { 86 | context.push(","); 87 | } 88 | } 89 | } 90 | function genText(node, context) { 91 | const { push } = context; 92 | push(`'${node.content}'`); 93 | } 94 | 95 | function genInterpolation(node, context) { 96 | const { push, helper } = context; 97 | push(`${helper(TO_DISPLAY_STRING)}(`); 98 | genNode(node.content, context); 99 | push(")"); 100 | } 101 | 102 | function createCodegenContext() { 103 | const context = { 104 | code: "", 105 | push(source) { 106 | context.code += source; 107 | }, 108 | helper(key) { 109 | return `_${helperMapName[key]}`; 110 | }, 111 | }; 112 | return context; 113 | } 114 | 115 | function genExpress(node: any, context: any) { 116 | context.push(`${node.content}`); 117 | } 118 | 119 | function genCompoundExpression(node: any, context: any) { 120 | const { push } = context; 121 | for (let i = 0; i < node.children.length; i++) { 122 | const child = node.children[i]; 123 | if (isString(child)) { 124 | push(child); 125 | } else { 126 | genNode(child, context); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/complier-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { generator } from "./codegen"; 2 | import { baseParse } from "./parse"; 3 | import { transform } from "./transform"; 4 | import transformElement from "./transforms/transformElement"; 5 | import { transformExpression } from "./transforms/transformExpression"; 6 | import { transformText } from "./transforms/transformText"; 7 | 8 | export function baseCompile(template){ 9 | const ast: any = baseParse(template); 10 | transform(ast, { 11 | nodeTransforms: [transformExpression, transformElement, transformText], 12 | }); 13 | return generator(ast); 14 | } -------------------------------------------------------------------------------- /packages/complier-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./compile"; 2 | -------------------------------------------------------------------------------- /packages/complier-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | 3 | const enum TagType { 4 | Start, 5 | End, 6 | } 7 | 8 | export function baseParse(content: string) { 9 | const context = createContext(content); 10 | return createRoot(parseChildren(context, [])); 11 | } 12 | 13 | function createRoot(children) { 14 | return { children, type: NodeTypes.ROOT }; 15 | } 16 | 17 | function parseChildren(context, ancestors) { 18 | const nodes: any = []; 19 | while (!isEnd(context, ancestors)) { 20 | let node; 21 | const s = context.source; 22 | if (s.startsWith("{{")) { 23 | node = parseInterpolation(context); 24 | } else if (s.startsWith("<")) { 25 | //解析
26 | if (/[a-z]/i.test(s[1])) { 27 | node = parseElement(context, ancestors); 28 | } 29 | } else { 30 | //解析text类型 31 | node = parseText(context); 32 | } 33 | nodes.push(node); 34 | } 35 | return nodes; 36 | } 37 | 38 | function isEnd(context, ancestors) { 39 | //当遇到借宿标签的时候就是end 40 | //当context.source 没有值的时候就是end 41 | const s = context.source; 42 | if (s.startsWith("= 0; i--) { 45 | const tag = ancestors[i].tag; 46 | if (startsWithEndTagOpen(s, tag)) { 47 | return true; 48 | } 49 | } 50 | } 51 | return !s; 52 | } 53 | 54 | function parseElement(context, ancestors) { 55 | const element: any = parseTag(context, TagType.Start); 56 | //收集我们的element 57 | ancestors.push(element); 58 | element.children = parseChildren(context, ancestors); 59 | //弹出我们的处理完的element 60 | ancestors.pop(); 61 | 62 | //判断结束标签是否和开始标签一样;如果一样就销毁掉,如果不一样就抛出错误 63 | 64 | if (startsWithEndTagOpen(context.source, element.tag)) { 65 | parseTag(context, TagType.End); 66 | } else { 67 | throw new Error(`缺少结束标签:${element.tag}`); 68 | } 69 | return element; 70 | } 71 | 72 | function startsWithEndTagOpen(source, tag) { 73 | return ( 74 | source.startsWith(" index) { 160 | endIndex = index; 161 | } 162 | } 163 | 164 | const content = parseTextData(context, endIndex); 165 | return { 166 | type: NodeTypes.TEXT, 167 | content, 168 | }; 169 | } 170 | 171 | function parseTextData(context: any, length) { 172 | const content = context.source.slice(0, length); 173 | advanceBy(context, content.length); 174 | return content; 175 | } 176 | -------------------------------------------------------------------------------- /packages/complier-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const TO_DISPLAY_STRING = Symbol("toDisplayString"); 2 | export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode"); 3 | 4 | export const helperMapName = { 5 | [TO_DISPLAY_STRING]: "toDisplayString", 6 | [CREATE_ELEMENT_VNODE]: "createElementVNode", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/complier-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | import { TO_DISPLAY_STRING } from "./runtimeHelpers"; 3 | 4 | //传入nodeTransform 属性到options 里面,就会执行transform 然后改造我们的node 5 | export function transform(root: any, options: any = {}) { 6 | const context = createTransformContext(root, options); 7 | travelNode(root, context); 8 | createRootCodegen(root, context); 9 | root.helpers = [...context.helpers.keys()]; 10 | } 11 | 12 | //rootCodegen Node for codegen 13 | function createRootCodegen(root: any, context) { 14 | const { children } = root; 15 | const child = children[0]; 16 | if (child.type === NodeTypes.ELEMENT && child.codegenNode) { 17 | const codegenNode = child.codegenNode; 18 | root.codegenNode = codegenNode; 19 | } else { 20 | root.codegenNode = child; 21 | } 22 | } 23 | 24 | function createTransformContext(root: any, options: any) { 25 | const context = { 26 | root, 27 | nodeTransforms: options.nodeTransforms || [], 28 | helpers: new Map(), 29 | helper(key) { 30 | context.helpers.set(key, 1); 31 | }, 32 | }; 33 | return context; 34 | } 35 | 36 | function travelNode(node: any, context) { 37 | // console.log("travelNode>>>>>>>>>>>",node); 38 | //因为调用的时候会去改变我们的text 类型的结构,变成我们的复合类型,所以我们设计一下,先把我们的 复合类型函数收集起来,然后等我们的text 节点类型处理完成之后然后再去处理复合类型的函数 39 | 40 | const exitFn: any = []; 41 | 42 | const nodeTransforms = context.nodeTransforms; 43 | 44 | for (let i = 0; i < nodeTransforms.length; i++) { 45 | const nodeTransform = nodeTransforms[i]; 46 | const onExit = nodeTransform(node, context); 47 | if (onExit) { 48 | exitFn.push(onExit); 49 | } 50 | } 51 | // console.log("travelNode>>>>end",node) 52 | switch (node.type) { 53 | case NodeTypes.INTERPOLATION: 54 | context.helper(TO_DISPLAY_STRING); 55 | break; 56 | case NodeTypes.ELEMENT: 57 | case NodeTypes.ROOT: 58 | travelChildren(node, context); 59 | break; 60 | default: 61 | break; 62 | } 63 | 64 | //这里的设计非常巧妙,刚开始的时候我们的i是length ,然后循环一次i减少1,然后我们如果到0的时候也不会越界; 65 | let i = exitFn.length; 66 | while (i--) { 67 | exitFn[i](); 68 | } 69 | } 70 | 71 | function travelChildren(parent: any, context: any) { 72 | parent.children.forEach((node) => { 73 | travelNode(node, context); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /packages/complier-core/src/transforms/transformElement.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes, createVNodeCall } from "../ast"; 2 | 3 | export default function transformElement(node, context) { 4 | if (node.type === NodeTypes.ELEMENT) { 5 | return () => { 6 | // context.helper(CREATE_ELEMENT_VNODE); 7 | 8 | //tag 9 | const vnodeTag = `'${node.tag}'`; 10 | 11 | //props 12 | let vnodeProps; 13 | 14 | //children 15 | const children = node.children; 16 | 17 | const vnodeChildren = children[0]; 18 | 19 | node.codegenNode = createVNodeCall( 20 | context, 21 | vnodeTag, 22 | vnodeProps, 23 | vnodeChildren 24 | ); 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/complier-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | // ok 2 | import { NodeTypes } from "../ast"; 3 | 4 | export function transformExpression(node) { 5 | //复合类型节点不会进来 6 | if (node.type === NodeTypes.INTERPOLATION) { 7 | node.content = processExpression(node.content); 8 | } 9 | } 10 | 11 | function processExpression(node) { 12 | node.content = `_ctx.${node.content}` ; 13 | return node; 14 | } 15 | -------------------------------------------------------------------------------- /packages/complier-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../ast"; 2 | import { isText } from "./utils"; 3 | 4 | //因为需要延后执行,所以我们返回一个函数 5 | export function transformText(node) { 6 | if (node.type === NodeTypes.ELEMENT) { 7 | return () => { 8 | let currentContainer; 9 | const { children } = node; 10 | for (let i = 0; i < children.length; i++) { 11 | const child = children[i]; 12 | //判断当前的child 是不是普通的 hi 或者是插值类型 13 | if (isText(child)) { 14 | //找到下一个 15 | for (let j = i + 1; j < children.length; j++) { 16 | const next = children[j]; 17 | if (isText(next)) { 18 | if (!currentContainer) { 19 | //初始化 20 | currentContainer = children[i] = { 21 | type: NodeTypes.COMPOUND_EXPRESSION, 22 | children: [child], 23 | }; 24 | } 25 | currentContainer.children.push(" + "); 26 | currentContainer.children.push(next); 27 | children.splice(j, 1); 28 | //数组删除了数组结构发生了变化,保证指向正确 29 | j--; 30 | } else { 31 | currentContainer = undefined; 32 | break; 33 | } 34 | } 35 | } 36 | } 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/complier-core/src/transforms/utils.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../ast"; 2 | 3 | export function isText(node) { 4 | return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT; 5 | } 6 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../src/computed"; 2 | 3 | import { reactive } from "../src/reactive"; 4 | 5 | import {vi} from "vitest" 6 | describe("computed", () => { 7 | it("happy path", () => { 8 | //ref 9 | //.value 10 | //1.缓存 11 | const user = reactive({ 12 | age: 1, 13 | }); 14 | 15 | const age = computed(() => { 16 | return user.age; 17 | }); 18 | 19 | expect(age.value).toBe(1); 20 | }); 21 | 22 | it("should compute lazily", () => { 23 | const value = reactive({ foo: 1 }); 24 | const getter = vi.fn(() => { 25 | return value.foo; 26 | }); 27 | 28 | const cValue=computed(getter) 29 | 30 | //lazy 31 | expect(getter).not.toHaveBeenCalled() 32 | expect(cValue.value).toBe(1) 33 | expect(getter).toHaveBeenCalledTimes(1) 34 | 35 | //should not computed again 36 | cValue.value 37 | expect(getter).toHaveBeenCalledTimes(1) 38 | 39 | //should not compute until needed 40 | //在set 的时候会去触发依赖 41 | value.foo=2 42 | expect(getter).toHaveBeenCalledTimes(1) 43 | 44 | // // //now it should compute 45 | expect(cValue.value).toBe(2) 46 | expect(getter).toHaveBeenCalledTimes(2) 47 | 48 | // // //should not compute again 49 | cValue.value 50 | expect(getter).toHaveBeenCalledTimes(2) 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../src/reactive"; 2 | import { effect, stop } from "../src/effect"; 3 | import {vi} from "vitest" 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | //创建一个响应式对象 7 | const user = reactive({ 8 | age: 10, 9 | }); 10 | 11 | let nextAge; 12 | 13 | //init 14 | effect(() => { 15 | //在effect函数里面去改变响应式对象的值,期望响应式对象的值能够通过user这个之前创建的变量拿到 16 | nextAge = user.age + 1; 17 | }); 18 | //期望响应式对象的值为11 19 | expect(nextAge).toBe(11); 20 | //update 21 | user.age++; 22 | // //期望effect包裹的函数能够在user的set操作完成之后触发 23 | expect(nextAge).toBe(12); 24 | }); 25 | 26 | it("should return runner when call effect", () => { 27 | //我们调用effect(fn)之后是会返回一个function的runner ,当调用这个function(runner) 的时候,就会再执行一下这个effect 函数,并且这个时候会返回effect 函数执行的值的 28 | let foo = 10; 29 | //希望effect 函数执行之后返回的是一个runner函数 30 | const runner = effect(() => { 31 | foo++; 32 | return "foo"; 33 | }); 34 | //期待第一次的effect 函数是会调用传递进去的fn的 35 | expect(foo).toBe(11); 36 | //我们把返回函数执行一遍,期待传递进去的函数再次被调用 37 | const r = runner(); 38 | expect(foo).toBe(12); 39 | //然后我们还期待runner函数的返回值是传递进去的函数的 返回值 40 | expect(r).toBe("foo"); 41 | }); 42 | 43 | it("scheduler", () => { 44 | //1.通过effect 的第二个参数给定的一个scheduler 的fn 45 | //2. effect 第一次执行的时候 还会执行fn 46 | //3. 当响应式对象更新的时候不会执行fn了而是执行scheduler 47 | //4 .如果说执行runner 的时候会再次执行fn 48 | let dummy; 49 | let run: any; 50 | const scheduler = vi.fn(() => { 51 | run = runner; 52 | }); 53 | const obj = reactive({ foo: 1 }); 54 | const runner = effect( 55 | () => { 56 | dummy = obj.foo; 57 | }, 58 | { 59 | scheduler, 60 | } 61 | ); 62 | //scheduler 就是effect 的第二个参数,在初始化的时候不会被调用 63 | expect(scheduler).not.toHaveBeenCalled(); 64 | //然后第一次的时候是会被调用的,调用后dummy 就会被赋值为1 65 | expect(dummy).toBe(1); 66 | obj.foo++; 67 | //当obj 改变的时候会被调用scheduler 68 | expect(scheduler).toHaveBeenCalledTimes(1); 69 | //但是这个时候不会去更新dummy的值 70 | expect(dummy).toBe(1); 71 | //当执行run的时候才会被调用dummy 72 | run(); 73 | expect(dummy).toBe(2); 74 | }); 75 | 76 | it("stop", () => { 77 | // 1. effect.ts 会导出一个stop函数,当调用stop函数,并且把runner(也就是effect的返回值传递给他时),再次触发trigger 也就是触发响应式对象值得更新得时候,当前用户传递进来得依赖不会执行(就是effect包裹得函数不会执行) 78 | // 2. 当再次调用runner 的时候effect包裹的函数执行 79 | let dump; 80 | const obj = reactive({ prop: 1 }); 81 | const runner = effect(() => { 82 | dump = obj.prop; 83 | }); 84 | obj.prop = 2; 85 | expect(dump).toBe(2); 86 | stop(runner); 87 | // obj.prop = 3; 88 | //这里会先去走get的操作,从而把清空的依赖重新收集了起来 89 | obj.prop++; 90 | expect(dump).toBe(2); 91 | 92 | //stopped effect should still be manually callable 93 | runner(); 94 | expect(dump).toBe(3); 95 | }); 96 | 97 | it("onStop", () => { 98 | const obj = reactive({ foo: 1 }); 99 | const onStop = vi.fn(); 100 | let dummy; 101 | const runner = effect( 102 | () => { 103 | dummy = obj.foo; 104 | }, 105 | { onStop } 106 | ); 107 | stop(runner); 108 | expect(onStop).toBeCalledTimes(1); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, reactive } from "../src/reactive"; 2 | describe("reactive", () => { 3 | it("happy path", () => { 4 | const original = { foo: 10 }; 5 | const observer = reactive(original); 6 | //创建响应式对象,希望响应式对象能读取到之前的值 7 | expect(observer.foo).toBe(10); 8 | //希望响应式对象和原来的值是两个不同的引用 9 | expect(observer).not.toBe(original); 10 | //IsReactive 11 | expect(isReactive(original)).toBe(false); 12 | 13 | expect(isReactive(observer)).toBe(true); 14 | 15 | expect(isProxy(observer)).toBe(true) 16 | }); 17 | it("nested reactive",()=>{ 18 | const original={ 19 | nested:{ 20 | foo:1 21 | },array:[{bar:2}] 22 | } 23 | const observer=reactive(original) 24 | expect(isReactive(observer.nested)).toBe(true) 25 | expect(isReactive(observer.array)).toBe(true) 26 | expect(isReactive(observer.array[0])).toBe(true) 27 | }) 28 | }); 29 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { readonly, isReadonly,isProxy } from "../src/reactive"; 2 | import {vi} from "vitest" 3 | describe("readonly", () => { 4 | it("happy path", () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | //IsReadonly 8 | expect(isReadonly(wrapped)).toBe(true); 9 | expect(wrapped).not.toBe(original); 10 | expect(wrapped.foo).toBe(1); 11 | expect(isProxy(wrapped)).toBe(true) 12 | }); 13 | it("warn then call set", () => { 14 | console.warn = vi.fn(); 15 | const user = readonly({ age: 10 }); 16 | user.age = 11; 17 | expect(console.warn).toBeCalled(); 18 | }); 19 | it("nested readonly", () => { 20 | const original = { foo: 1, bar: { baz: 2 } }; 21 | const wrapped = readonly(original); 22 | //IsReadonly 23 | expect(isReadonly(wrapped)).toBe(true) 24 | expect(isReadonly(wrapped.bar)).toBe(true) 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../src/effect"; 2 | import { reactive } from "../src/reactive"; 3 | import { ref, isRef, unRef, proxyRefs } from "../src/ref"; 4 | 5 | describe("ref", () => { 6 | it("happy path", () => { 7 | const a = ref(1); 8 | expect(a.value).toBe(1); 9 | }); 10 | 11 | it("should be reactive", () => { 12 | //创建一个对象 13 | const a = ref(1); 14 | let dummy; 15 | let calls = 0; 16 | //然后去通过effect 做依赖收集 17 | effect(() => { 18 | calls++; 19 | dummy = a.value; 20 | }); 21 | expect(calls).toBe(1); 22 | expect(dummy).toBe(1); 23 | //改变value的值我们希望依赖被触发 24 | a.value = 2; 25 | expect(calls).toBe(2); 26 | expect(dummy).toBe(2); 27 | //same value should not trigger 28 | //如果改变的值和原有的数据相等那么就不进行触发依赖 29 | a.value = 2; 30 | expect(calls).toBe(2); 31 | expect(dummy).toBe(2); 32 | }); 33 | 34 | it("should make nested properties reactive", () => { 35 | const a = ref({ count: 1 }); 36 | let dummy; 37 | effect(() => { 38 | dummy = a.value.count; 39 | }); 40 | expect(dummy).toBe(1); 41 | a.value.count = 2; 42 | expect(dummy).toBe(2); 43 | }); 44 | 45 | it("isRef", () => { 46 | const a = ref(1); 47 | const user = reactive({ age: 1 }); 48 | expect(isRef(a)).toBe(true); 49 | expect(isRef(1)).toBe(false); 50 | expect(isRef(user)).toBe(false); 51 | }); 52 | 53 | it("unRef", () => { 54 | const a = ref(1); 55 | expect(unRef(a)).toBe(1); 56 | expect(unRef(1)).toBe(1); 57 | }); 58 | 59 | it("proxyRefs get", () => { 60 | const user = { age: ref(10), name: "xiaoming" }; 61 | const proxyUser = proxyRefs(user); 62 | expect(user.age.value).toBe(10); 63 | expect(proxyUser.age).toBe(10); 64 | expect(proxyUser.name).toBe("xiaoming"); 65 | }); 66 | 67 | it("proxyRefs set", () => { 68 | const user = { age: ref(10), name: "xiaoming" }; 69 | const proxyUser = proxyRefs(user); 70 | expect(user.age.value).toBe(10); 71 | expect(proxyUser.age).toBe(10); 72 | expect(proxyUser.name).toBe("xiaoming"); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../src/reactive"; 2 | import { vi } from "vitest"; 3 | 4 | describe("shallowReadonly happy path", () => { 5 | it("should not make non-reactive properties reactive", () => { 6 | const props = shallowReadonly({ n: { foo: 1 } }); 7 | expect(isReadonly(props)).toBe(true); 8 | expect(isReadonly(props.n)).toBe(false); 9 | }); 10 | it("warn then call set", () => { 11 | console.warn = vi.fn(); 12 | const user = shallowReadonly({ age: 10 }); 13 | user.age = 11; 14 | expect(console.warn).toBeCalled(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ass-vue/reactivity", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@ass-vue/shared": "workspace:^" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactivity/src/baseHandler.ts: -------------------------------------------------------------------------------- 1 | import { track, trigger } from "./effect"; 2 | import { reactive, readonly } from "./reactive"; 3 | import { extend, isObject } from "@ass-vue/shared"; 4 | const get = createGetter(); 5 | const set = createSetter(); 6 | const readonlyGet = createGetter(true); 7 | const shallowReadonlyGet = createGetter(true, true); 8 | 9 | export const enum reactiveFlags { 10 | IS_READONLY = "__v_isReadonly", 11 | IS_REACTIVE = "__v_isReactive", 12 | } 13 | 14 | function createGetter(isReadonly = false, shallow = false) { 15 | return function get(target, key) { 16 | const res = Reflect.get(target, key); 17 | if (key === reactiveFlags.IS_READONLY) { 18 | return isReadonly; 19 | } else if (key === reactiveFlags.IS_REACTIVE) { 20 | return !isReadonly; 21 | } 22 | if (shallow) { 23 | return res; 24 | } 25 | if (!isReadonly) { 26 | track(target, key); 27 | } 28 | 29 | if (isObject(res)) { 30 | return isReadonly ? readonly(res) : reactive(res); 31 | } 32 | return res; 33 | }; 34 | } 35 | 36 | function createSetter() { 37 | return function set(target, key, value) { 38 | const res = Reflect.set(target, key, value); 39 | trigger(target, key); 40 | return res; 41 | }; 42 | } 43 | 44 | export const mutableHandlers = { 45 | get, 46 | set, 47 | }; 48 | 49 | export const readonlyHandlers = { 50 | get: readonlyGet, 51 | set(target, key, value) { 52 | console.warn( 53 | `target ${target} is readonly, ${key.toString()} can not be set to ${value}` 54 | ); 55 | return true; 56 | }, 57 | }; 58 | 59 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 60 | get: shallowReadonlyGet, 61 | }); 62 | -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _getter: any; 5 | private _dirty: boolean = true; 6 | private _value: any; 7 | private _effect: any; 8 | constructor(getter) { 9 | this._getter = getter; 10 | this._effect = new ReactiveEffect(getter, () => { 11 | if (!this._dirty) { 12 | this._dirty = true; 13 | } 14 | }); 15 | } 16 | get value() { 17 | if (this._dirty) { 18 | this._dirty = false; 19 | //在run 的时候会去收集依赖 20 | this._value = this._effect.run(); 21 | } 22 | return this._value; 23 | } 24 | } 25 | 26 | export function computed(getter) { 27 | return new ComputedRefImpl(getter); 28 | } 29 | -------------------------------------------------------------------------------- /packages/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "@ass-vue/shared"; 2 | //创建activeEffect 的实例的类 3 | 4 | //map 对象就像是一个对象,但是这个对象里面的键可以是任何类型的属性 5 | let targetMap = new Map(); 6 | //当前的target key 值被 get 的时候 的函数的自定义包装 7 | let activeEffect; 8 | let shouldTrack; 9 | 10 | export class ReactiveEffect { 11 | private _fn: any; 12 | public scheduler?: any; 13 | public deps = []; 14 | public active = true; 15 | public onStop?: () => void; 16 | constructor(fn, scheduler?: any) { 17 | this._fn = fn; 18 | this.scheduler = scheduler; 19 | } 20 | run() { 21 | activeEffect = this; 22 | //实现调用run方法的时候需要得到fn的返回值 23 | //在这里的时候会收集依赖 24 | //用shouldTrack来做区分 25 | if (!this.active) { 26 | return this._fn(); 27 | } 28 | shouldTrack = true; 29 | const result = this._fn(); 30 | shouldTrack = false; 31 | return result; 32 | } 33 | stop() { 34 | //this就是当前active 的runner 35 | if (this.active) { 36 | cleanupEffect(this); 37 | if (this.onStop) { 38 | this.onStop(); 39 | } 40 | this.active = false; 41 | } 42 | } 43 | } 44 | 45 | function cleanupEffect(effect: any) { 46 | //每一个dep都是一个set对象 47 | effect.deps.forEach((dep: any) => dep.delete(effect)); 48 | effect.deps.length = 0; 49 | } 50 | 51 | export function isTracking() { 52 | return shouldTrack && activeEffect !== undefined; 53 | } 54 | 55 | export function track(target, key) { 56 | if (!isTracking()) return; 57 | //取到target 上面存的key值 58 | let depsMap = targetMap.get(target); 59 | 60 | if (!depsMap) { 61 | depsMap = new Map(); 62 | targetMap.set(target, depsMap); 63 | } 64 | let dep = depsMap.get(key); 65 | if (!dep) { 66 | dep = new Set(); 67 | //这里初始化的时候dep就是空 68 | depsMap.set(key, dep); 69 | } 70 | //添加当前活动的effect 71 | //只有当调用effect 的时候,才会生成activeEffect 72 | trackEffects(dep); 73 | 74 | //得到了类似于下面这种结构 75 | // 1. target 通过target 存储自身 76 | // 2. target 通过target 拿到自身 77 | // 3. target key 通过target的key值存储在target 里面 78 | // 4. target key 通过target拿到targetMap 然后通过targetMap.get(key)来拿到自身 79 | // 5. target key 可以通过add 方法添加自身到指定的地方,以及对自身的属性再进行操作; 80 | // 6. 相当于建立了一个对应的关系,连了个线,各自的数据储存在各自的位置,现在这个map数据结构只是把内存地址相互关联了一下 81 | 82 | //因为依赖的项都是不重复的函数,那么可以用set这个数据结构来存储 83 | // let dep =new Set() 84 | //然后把 target key 对应起来 85 | //target=> key => dep 86 | } 87 | 88 | export function trackEffects(dep) { 89 | if (dep.has(activeEffect)) return; 90 | dep.add(activeEffect); 91 | activeEffect.deps.push(dep); 92 | } 93 | 94 | export function trigger(target, key) { 95 | let depsMap = targetMap.get(target); 96 | let dep = depsMap.get(key); 97 | triggerEffects(dep); 98 | } 99 | 100 | export function triggerEffects(dep) { 101 | //触发所有收集起来的effect 102 | for (const effect of dep) { 103 | if (effect.scheduler) { 104 | effect.scheduler(); 105 | } else { 106 | effect.run(); 107 | } 108 | } 109 | } 110 | 111 | export function effect(fn, options: any = {}) { 112 | let _effect = new ReactiveEffect(fn, options.scheduler); 113 | 114 | // _effect.onStop = options.onStop; 115 | // Object.assign(_effect,options) 116 | extend(_effect, options); 117 | //在run 的时候顺带绑定activeEffect 为当前活动的effect 118 | _effect.run(); 119 | 120 | const runner: any = _effect.run.bind(_effect); 121 | //把effect 挂载到runner 上面好通过stop方法停止 122 | runner.effect = _effect; 123 | //以当前这个effect的实例作为run 方法的this的指向 124 | return runner; 125 | } 126 | 127 | export function stop(runner) { 128 | runner.effect.stop(); 129 | } 130 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export {ref,proxyRefs,isRef,unRef} from "./ref" 2 | export {shallowReadonly,isProxy,isReactive,isReadonly,reactive,readonly} from "./reactive" 3 | export {effect} from "./effect" 4 | -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "@ass-vue/shared"; 2 | import { 3 | mutableHandlers, 4 | reactiveFlags, 5 | readonlyHandlers, 6 | shallowReadonlyHandlers, 7 | } from "./baseHandler"; 8 | 9 | export function reactive(raw) { 10 | return createReactiveObject(raw, mutableHandlers); 11 | } 12 | 13 | export function readonly(raw) { 14 | return createReactiveObject(raw, readonlyHandlers); 15 | } 16 | 17 | export function shallowReadonly(raw) { 18 | return createReactiveObject(raw, shallowReadonlyHandlers); 19 | } 20 | function createReactiveObject(target, baseHandlers) { 21 | if (!isObject(target)) { 22 | console.warn(`target ${target} 必须是一个对象`); 23 | return target; 24 | } else { 25 | return new Proxy(target, baseHandlers); 26 | } 27 | } 28 | 29 | export const isReadonly = (value: any) => { 30 | return !!value[reactiveFlags.IS_READONLY]; 31 | }; 32 | 33 | export const isReactive = (value: any) => { 34 | return !!value[reactiveFlags.IS_REACTIVE]; 35 | }; 36 | 37 | export const isProxy = (value: any) => { 38 | return isReactive(value) || isReadonly(value); 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { trackEffects, triggerEffects, isTracking } from "./effect"; 2 | import { reactive } from "./reactive"; 3 | import { hasChanged, isObject } from "@ass-vue/shared"; 4 | 5 | class RefImpl { 6 | private _value: any; 7 | public dep; 8 | public __v_isRef = true; 9 | private _rawValue: any; 10 | 11 | constructor(value) { 12 | // 1.看看value是不是对象,如果不是直接返回,如果是那么就处理包裹一下 13 | this._rawValue = value; 14 | this._value = convert(value); 15 | this.dep = new Set(); 16 | } 17 | 18 | get value() { 19 | //这里需要收集依赖 20 | trackRefValue(this); 21 | return this._value; 22 | } 23 | 24 | set value(newValue) { 25 | //这里需要触发依赖 26 | //如果对比的话那么对象 27 | if (hasChanged(newValue, this._rawValue)) { 28 | this._rawValue = newValue; 29 | this._value = convert(newValue); 30 | triggerEffects(this.dep); 31 | } 32 | } 33 | } 34 | 35 | function trackRefValue(ref) { 36 | if (isTracking()) { 37 | trackEffects(ref.dep); 38 | } 39 | } 40 | 41 | function convert(value) { 42 | return isObject(value) ? reactive(value) : value; 43 | } 44 | 45 | export function isRef(value) { 46 | return !!value.__v_isRef; 47 | } 48 | 49 | export function unRef(ref) { 50 | return isRef(ref) ? ref.value : ref; 51 | } 52 | 53 | export function ref(value) { 54 | return new RefImpl(value); 55 | } 56 | 57 | export function proxyRefs(objectWithRefs) { 58 | return new Proxy(objectWithRefs, { 59 | get(target, key) { 60 | //如果是ref的话就返回ref.value,如果不是ref的话那么就返回target.key 61 | return unRef(Reflect.get(target, key)); 62 | }, 63 | set(target, key, newValue) { 64 | if (isRef(target[key]) && !isRef(newValue)) { 65 | return (target[key].value = newValue); 66 | } else { 67 | return Reflect.set(target, key, newValue); 68 | } 69 | }, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /packages/runtime-core/__tests__/apiWatch.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "@ass-vue/reactivity"; 2 | import { nextTick } from "../src/scheduler"; 3 | 4 | import { watchEffect } from "../src/apiWatch"; 5 | 6 | describe("api: watch", () => { 7 | it("effect", async () => { 8 | const state = reactive({ count: 0 }); 9 | 10 | let dummy; 11 | watchEffect(() => { 12 | dummy = state.count; 13 | }); 14 | 15 | expect(dummy).toBe(0); 16 | state.count++; 17 | await nextTick(); 18 | expect(dummy).toBe(1); 19 | }); 20 | 21 | it("stop the watcher(effect)", async () => { 22 | const state = reactive({ count: 0 }); 23 | let dummy; 24 | const stop: any = watchEffect(() => { 25 | dummy = state.count; 26 | }); 27 | 28 | expect(dummy).toBe(0); 29 | 30 | stop(); 31 | state.count++; 32 | await nextTick(); 33 | 34 | expect(dummy).toBe(0); 35 | }); 36 | 37 | it("cleanup registration (effect)",async () => { 38 | const state = reactive({ count: 0 }); 39 | 40 | const cleanup=vi.fn() 41 | let dummy; 42 | const stop=watchEffect((onCleanup) => { 43 | onCleanup(cleanup) 44 | dummy = state.count; 45 | }); 46 | 47 | expect(dummy).toBe(0); 48 | state.count++; 49 | await nextTick(); 50 | //发生更新逻辑之后再调用 51 | expect(cleanup).toBeCalledTimes(1) 52 | expect(dummy).toBe(1); 53 | stop() 54 | //当stop 掉了之后cleanup还会被调用一次 55 | expect(cleanup).toBeCalledTimes(2) 56 | }) 57 | }); 58 | -------------------------------------------------------------------------------- /packages/runtime-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ass-vue/runtime-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@ass-vue/reactivity": "workspace:^", 14 | "@ass-vue/shared": "workspace:^" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | export function provide(key, value) { 4 | // 存数据 5 | let currentInstance: any = getCurrentInstance(); 6 | if (currentInstance) { 7 | let { provides } = currentInstance; 8 | let parentProvides = currentInstance.parent.provides; 9 | 10 | //原型链对接 11 | if (provides === parentProvides) { 12 | provides = currentInstance.provides = Object.create(parentProvides); 13 | } 14 | // init 15 | provides[key] = value; 16 | } 17 | } 18 | export function inject(key, defaultValue) { 19 | //取数据 20 | //取父级组件的instance的providers 21 | const currentInstance: any = getCurrentInstance(); 22 | if (currentInstance) { 23 | const parentProvides = currentInstance.parent.provides; 24 | if (key in parentProvides) { 25 | return parentProvides[key]; 26 | } else if (defaultValue) { 27 | if (typeof defaultValue === "function") { 28 | return defaultValue(); 29 | } else if (typeof defaultValue === "string") { 30 | return defaultValue; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiWatch.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "../../reactivity/src/effect"; 2 | 3 | import { queueFlushCb } from "./scheduler"; 4 | export function watchEffect(source) { 5 | // source 函数会被添加到组件渲染前调用 6 | let cleanup; 7 | function job(cleanupFn) { 8 | effect.run(); 9 | } 10 | 11 | const onCleanup = function (cleanupFn) { 12 | cleanup = effect.onStop=() => { cleanupFn() } 13 | }; 14 | 15 | function getter() { 16 | if (cleanup) { 17 | cleanup(); 18 | } 19 | source(onCleanup); 20 | } 21 | const effect = new ReactiveEffect(getter, () => { 22 | queueFlushCb(job); 23 | // effect.run(); 24 | }); 25 | //一上来的时候这个source 是要被调用的 26 | effect.run(); 27 | return () => { 28 | effect.stop(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import { shallowReadonly, proxyRefs } from "@ass-vue/reactivity"; 2 | import { emit } from "./componentEmit"; 3 | import { initProps } from "./componentProps"; 4 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 5 | import { initSlots } from "./componentSlots"; 6 | 7 | export function createComponentInstance(vnode: any, parent) { 8 | const component = { 9 | vnode, 10 | type: vnode.type, 11 | next: null, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | provides: parent ? parent.provides : {}, 16 | parent, 17 | isMounted: false, 18 | subTree: {}, 19 | emit: (event) => {}, 20 | }; 21 | //这里有点东西的啊,不想传递第一个参数等到emit调用的时候在传递,先把component填充好 22 | //填充emit的第一个参数 23 | 24 | component.emit = emit.bind(null, component); 25 | return component; 26 | } 27 | 28 | export function setupComponent(instance) { 29 | initProps(instance, instance.vnode.props); 30 | initSlots(instance, instance.vnode.children); 31 | //初始化一个有状态的component (有状态的组件和函数组件函数组件是没有任何状态的) 32 | setupStatefulComponent(instance); 33 | } 34 | 35 | function setupStatefulComponent(instance) { 36 | //在最开始的时候很简单,去调用setup 拿到setup的返回值就可以了 37 | const Component = instance.type; 38 | const { setup } = Component; 39 | 40 | //ctx 41 | 42 | const proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 43 | 44 | instance.proxy = proxy; 45 | 46 | if (setup) { 47 | setCurrentInstance(instance); 48 | const setupResult = setup(shallowReadonly(instance.props), { 49 | emit: instance.emit, 50 | }); 51 | setCurrentInstance(null); 52 | handleSetupResult(instance, setupResult); 53 | } 54 | } 55 | 56 | function handleSetupResult(instance, setupResult) { 57 | //setup => function || object 58 | // function => render 函数 59 | // object=> 注入到组件上下文中 60 | //TODO: function 61 | 62 | if (typeof setupResult === "object") { 63 | instance.setupState = proxyRefs(setupResult); 64 | } 65 | //保证组件的render是一定有值的 66 | finishComponentSetup(instance); 67 | } 68 | 69 | function finishComponentSetup(instance) { 70 | const Component = instance.type; 71 | //假设是一定有render的 72 | //在这里去调用我们的render 函数 73 | //但是如果在这里面去引用我们的complier 模块的东西,这样会造成依赖的循环依赖;vue 是可以直接存在于运行时的,如果引入了complier 模块的话就不干净了; 74 | //注册了compiler 然后在这里使用 75 | if (compiler && !Component.render) { 76 | if (Component.template) { 77 | Component.render = compiler(Component.template); 78 | } 79 | } 80 | 81 | instance.render = Component.render; 82 | } 83 | 84 | let currentInstance = null; 85 | 86 | export function getCurrentInstance() { 87 | return currentInstance; 88 | } 89 | 90 | function setCurrentInstance(instance) { 91 | currentInstance = instance; 92 | } 93 | 94 | //注册组件,在index里面去调用注册我们的compiler; 95 | let compiler; 96 | export function registerRuntimeCompiler(_compiler) { 97 | compiler = _compiler; 98 | } 99 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "@ass-vue/shared"; 2 | 3 | export function emit(instance, event, ...arg) { 4 | const { props } = instance; 5 | //TPP 6 | // 先去写一个特定的行为然后再重构成一个通用的行为 7 | 8 | const handlerName = toHandlerKey(camelize(event)); 9 | const handler = props[handlerName]; 10 | handler && handler(...arg); 11 | } 12 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance,rawProps){ 2 | instance.props=rawProps||{} 3 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "@ass-vue/shared"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (instance) => instance.vnode.el, 5 | $slots: (instance) => instance.slots, 6 | $props: (instance) => instance.props, 7 | }; 8 | 9 | export const PublicInstanceProxyHandlers = { 10 | get({ _: instance }, key) { 11 | //从setupState里面获取值 12 | const { setupState, props } = instance; 13 | if (hasOwn(setupState, key)) { 14 | return setupState[key]; 15 | } else if (hasOwn(props, key)) { 16 | return props[key]; 17 | } 18 | 19 | if (key in publicPropertiesMap) { 20 | const publicGetter = publicPropertiesMap[key]; 21 | return publicGetter(instance); 22 | } 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { shapeFlags } from "@ass-vue/shared"; 2 | 3 | export function initSlots(instance, childrenObject) { 4 | // instance.slots = Array.isArray(children) ? children : [children]; 5 | const { vnode } = instance; 6 | if (vnode.shapeFlag & shapeFlags.SLOT_CHILDREN) { 7 | normalizeSlotObject(instance.slots, childrenObject); 8 | } 9 | } 10 | 11 | function normalizeSlotObject(slots, childrenObject) { 12 | for (const key in childrenObject) { 13 | const slot = childrenObject[key]; 14 | slots[key] = (props) => normalizeSlotValue(slot(props)); 15 | } 16 | } 17 | 18 | function normalizeSlotValue(slot) { 19 | return Array.isArray(slot) ? slot : [slot]; 20 | } 21 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export default function shouldUpdateComponent(n1, n2) { 2 | const { props: prevProps } = n1; 3 | const { props: nextProps } = n2; 4 | for (let key in nextProps) { 5 | if (prevProps[key]!== nextProps[key]) { 6 | return true; 7 | } 8 | } 9 | return false; 10 | } 11 | -------------------------------------------------------------------------------- /packages/runtime-core/src/createApp.ts: -------------------------------------------------------------------------------- 1 | // import {render} from './renderer' 2 | import { createVNode } from "./vnode"; 3 | 4 | export function createAppAPI(render) { 5 | return function createApp(rootComponent) { 6 | // 这时候的rootComponent 还是初始的状态 7 | // 就是一个对象里面有render方法,和setup方法等 8 | return { 9 | //接收一个根容器,在页面上存在的元素节点 10 | mount(rootContainer) { 11 | // 现在把所有的component转化成VNode 12 | //先把所有的东西转化成一个虚拟节点VNode 13 | //之后所有的逻辑操作都会基于虚拟节点做操作 14 | const vNode = createVNode(rootComponent); 15 | render(vNode, rootContainer); 16 | }, 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | export function h(type, props?, children?) { 3 | return createVNode(type, props, children); 4 | } 5 | -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment, } from "../vnode"; 2 | 3 | export function renderSlots(slots, key, props) { 4 | const slot = slots[key]; 5 | if (slot) { 6 | if (typeof slot === "function") { 7 | return createVNode(Fragment, {}, slot(props)); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from "./h"; 2 | 3 | export { renderSlots } from "./helpers/renderSlots"; 4 | 5 | export { createTextVNode, createElementVNode } from "./vnode"; 6 | 7 | export { getCurrentInstance, registerRuntimeCompiler } from "./component"; 8 | 9 | export { provide, inject } from "./apiInject"; 10 | 11 | export { createRender } from "./renderer"; 12 | 13 | export { nextTick } from "./scheduler"; 14 | 15 | export { toDisplayString } from "@ass-vue/shared"; 16 | 17 | export * from "@ass-vue/reactivity"; 18 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = []; 2 | const activePreFlushCbs: any[] = []; 3 | const promiseResolve = Promise.resolve(); 4 | 5 | let isFlushPending = false; 6 | export function queueJobs(job) { 7 | if (!queue.includes(job)) { 8 | queue.push(job); 9 | } 10 | 11 | queueFlush(); 12 | } 13 | 14 | function queueFlush() { 15 | //取出job放到微任务里面执行 16 | if (isFlushPending) { 17 | return; 18 | } 19 | isFlushPending = true; 20 | nextTick(flushJob); 21 | } 22 | 23 | export function queueFlushCb(job) { 24 | activePreFlushCbs.push(job); 25 | 26 | queueFlush() 27 | } 28 | function flushJob() { 29 | isFlushPending = false; 30 | //组件渲染之前 31 | flushPreFlushCbs(); 32 | 33 | let job; 34 | while ((job = queue.shift())) { 35 | job && job(); 36 | } 37 | } 38 | 39 | function flushPreFlushCbs() { 40 | for (let i = 0; i < activePreFlushCbs.length; i++) { 41 | activePreFlushCbs[i](); 42 | } 43 | } 44 | 45 | export function nextTick(fn?) { 46 | return fn ? promiseResolve.then(fn) : promiseResolve; 47 | } 48 | -------------------------------------------------------------------------------- /packages/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { shapeFlags } from "@ass-vue/shared"; 2 | 3 | export const Fragment = Symbol("Fragment"); 4 | export const Text = Symbol("Text"); 5 | export { createVNode as createElementVNode }; 6 | export function createVNode(type, props?, children?) { 7 | //下面的就是初始的type 8 | // { 9 | // render() { 10 | // return h( 11 | // "div", 12 | // { 13 | // id: "root", 14 | // class: ["red"], 15 | // onClick() { 16 | // console.log("this is app div onclick"); 17 | // }, 18 | // onMousedown() { 19 | // console.log("mouseDown,app"); 20 | // }, 21 | // }, 22 | // [h("p", { class: "red" }, "hi red"), h(Foo, { count: 1 })] 23 | // ); 24 | // }, 25 | // setup() { 26 | // return { 27 | // msg: "ass-vue", 28 | // }; 29 | // }, 30 | // }; 31 | 32 | const vnode = { 33 | type, 34 | props, 35 | children, 36 | component: null, 37 | el: null, 38 | key: props && props.key, 39 | shapeFlag: getShapeFlag(type), 40 | }; 41 | // 处理children的flag 42 | if (typeof children === "string") { 43 | vnode.shapeFlag |= shapeFlags.TEXT_CHILDREN; 44 | } else if (Array.isArray(children)) { 45 | vnode.shapeFlag |= shapeFlags.ARRAY_CHILDREN; 46 | } 47 | 48 | //现在这个vnode 可以标识element 类型也可标识 stateful_component 类型的组件 49 | //也可以在vnode上面直接体现children 的类型,是需要即将渲染的text_children 还是需要path继续处理的array-children 50 | 51 | //处理slots 的flog 52 | if (vnode.shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 53 | if (typeof children === "object") { 54 | vnode.shapeFlag |= shapeFlags.SLOT_CHILDREN; 55 | } 56 | } 57 | return vnode; 58 | } 59 | 60 | export function createTextVNode(text) { 61 | return createVNode(Text, {}, text); 62 | } 63 | 64 | function getShapeFlag(type) { 65 | return typeof type === "string" 66 | ? shapeFlags.ELEMENT 67 | : shapeFlags.STATEFUL_COMPONENT; 68 | } 69 | -------------------------------------------------------------------------------- /packages/runtime-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ass-vue/runtime-dom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@ass-vue/runtime-core": "workspace:^" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRender } from "@ass-vue/runtime-core"; 2 | 3 | function createElement(type) { 4 | return document.createElement(type); 5 | } 6 | 7 | function patchProp(el, key, prevVal, nextVal) { 8 | const isOn = (eventName: string) => /^on[A-Z]/.test(eventName); 9 | if (isOn(key)) { 10 | const eventName = key.slice(2).toLocaleLowerCase(); 11 | el.addEventListener(eventName, nextVal); 12 | } else { 13 | const _val = Array.isArray(nextVal) ? nextVal.join(" ") : nextVal; 14 | if (nextVal === undefined || nextVal === null) { 15 | console.log("undefined or null"); 16 | el.removeAttribute(key); 17 | } else { 18 | el.setAttribute(key, _val); 19 | } 20 | } 21 | } 22 | 23 | function insert(el, container,anchor) { 24 | return container.insertBefore(el,anchor||null); 25 | } 26 | 27 | function remove(child) { 28 | const parent = child.parentNode; 29 | if (parent) { 30 | parent.removeChild(child); 31 | } 32 | } 33 | function setElementText(text, container) { 34 | container.textContent = text; 35 | } 36 | 37 | const renderer: any = createRender({ 38 | insert, 39 | patchProp, 40 | createElement, 41 | remove, 42 | setElementText, 43 | }); 44 | 45 | //通过createApp 把dom 创建元素的方法默认传递给createApp 46 | export function createApp(...args) { 47 | return renderer.createApp(...args); 48 | } 49 | 50 | //如果要自定义渲染函数的话那么还需要通过createRender 然后再通过renderer.createApp() 创建元素 51 | export * from "@ass-vue/runtime-core"; -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ass-vue/shared", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | export const EMPTY_OBJECT = {}; 3 | 4 | export function isObject(val) { 5 | return val !== null && typeof val === "object"; 6 | } 7 | 8 | export function isString(val) { 9 | return typeof val === "string"; 10 | } 11 | 12 | export function hasChanged(value, newValue) { 13 | return !Object.is(newValue, value); 14 | } 15 | 16 | export function hasOwn(obj, key) { 17 | return Object.prototype.hasOwnProperty.call(obj, key); 18 | } 19 | 20 | //emit function 21 | const capitalize = (str: string) => { 22 | return str.charAt(0).toUpperCase() + str.slice(1); 23 | }; 24 | export const toHandlerKey = (str: string) => { 25 | return str ? "on" + capitalize(str) : ""; 26 | }; 27 | 28 | //add-foo -addFoo 29 | export const camelize = (str: string) => { 30 | return str.replace(/-(\w)/g, (_, c) => { 31 | return c ? c.toUpperCase() : ""; 32 | }); 33 | }; 34 | 35 | export * from "./shapeFlags"; 36 | 37 | export * from "./toDisplayString"; 38 | -------------------------------------------------------------------------------- /packages/shared/src/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum shapeFlags { 2 | ELEMENT = 1, //0001 3 | STATEFUL_COMPONENT = 1 << 1, //0010 4 | TEXT_CHILDREN = 1 << 2, //0100 5 | ARRAY_CHILDREN = 1 << 3, //1000 6 | SLOT_CHILDREN = 1 << 4, 7 | } 8 | -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(val) { 2 | return String(val); 3 | } 4 | -------------------------------------------------------------------------------- /packages/vue/examples/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from "../../lib/ass-vue.esm.js"; 2 | 3 | const Provider = { 4 | name: "Provider", 5 | setup() { 6 | provide("foo", "fooVal"), provide("bar", "barVal"); 7 | const foo = inject("foo"); 8 | return { foo }; 9 | }, 10 | render() { 11 | return h("div", {}, [ 12 | h("p", {}, `provider foo:${this.foo}`), 13 | h(ProviderTwo), 14 | ]); 15 | }, 16 | }; 17 | 18 | const ProviderTwo = { 19 | name: "ProviderTwo", 20 | setup() { 21 | provide("foo", "fooTwo Val"); 22 | const foo = inject("foo"); 23 | return { foo }; 24 | }, 25 | render() { 26 | return h("div", {}, [h("p", {}, `providerTwo-${this.foo}`), h(Consumer)]); 27 | }, 28 | }; 29 | const Consumer = { 30 | name: "Consumer", 31 | setup() { 32 | const foo = inject("foo"); 33 | const bar = inject("bar"); 34 | // const baz=inject("baz","bazDefault") 35 | const baz = inject("baz", () => { 36 | return "ass"; 37 | }); 38 | return { foo, bar, baz }; 39 | }, 40 | render() { 41 | return h("div", {}, `Consumer:-${this.foo}-${this.bar}-${this.baz}`); 42 | }, 43 | }; 44 | 45 | export default { 46 | name: "App", 47 | setup() {}, 48 | render() { 49 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]); 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/vue/examples/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/apiInject/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import App from "./App.js"; 4 | 5 | const rootContainer = document.getElementById("app"); 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/App.js: -------------------------------------------------------------------------------- 1 | export const App = { 2 | name: "App", 3 | template: `
hi,{{message}}
`, 4 | setup() { 5 | return { 6 | message: "Ass", 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../dist/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from "../../lib/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | export const App = { 4 | name: "App", 5 | render() { 6 | const app = h("div", {}, "App"); 7 | //我们希望在foo 这里传递h 的第三个参数,能被Foo 接收到并且渲染到children里面 8 | const foo = h( 9 | Foo, 10 | {}, 11 | { 12 | header: ({ age }) => [ 13 | h("p", {}, "header" + age), 14 | createTextVNode("你好呀"), 15 | ], 16 | footer: () => h("p", {}, "footer"), 17 | } 18 | ); 19 | 20 | return h("div", {}, [app, foo]); 21 | }, 22 | setup() { 23 | return {}; 24 | }, 25 | }; 26 | 27 | //处理组件,处理element 28 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlots/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | export const Foo = { 3 | setup() { 4 | return {}; 5 | }, 6 | render() { 7 | const foo = h("p", {}, "foo"); 8 | console.log(this.$slots); 9 | // 1.获取到要渲染的节点 10 | // 2.获取到要渲染的位置 11 | return h("div", {}, [ 12 | renderSlots(this.$slots, "header", { age: 10000 }), 13 | foo, 14 | renderSlots(this.$slots, "footer"), 15 | ]); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | 3 | import Child from "./Child.js"; 4 | export const App = { 5 | name: "App", 6 | setup() { 7 | const msg = ref("123"); 8 | const count = ref(1); 9 | window.msg = msg; 10 | const changeChildProps = () => { 11 | msg.value = "245555"; 12 | }; 13 | const changeCount = () => { 14 | count.value++; 15 | }; 16 | return { msg, changeChildProps, changeCount, count }; 17 | }, 18 | render() { 19 | return h("div", {}, [ 20 | h("div", {}, "你好"), 21 | h("button", { onClick: this.changeChildProps }, "change child props"), 22 | h(Child, { 23 | msg: this.msg, 24 | }), 25 | h("button", { onClick: this.changeCount }, "change self count"), 26 | h("p", {}, "count" + this.count), 27 | ]); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h, ref, renderSlots } from "../../lib/ass-vue.esm.js"; 2 | const Child = { 3 | setup() { 4 | return {}; 5 | }, 6 | 7 | render() { 8 | return h("p", {}, "child-props-msg" + this.$props.msg); 9 | }, 10 | }; 11 | export default Child; 12 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/customRender/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | export const App = { 3 | setup() { 4 | return { 5 | x: 100, 6 | y: 100, 7 | }; 8 | }, 9 | render() { 10 | return h("rect", { x: this.x, y: this.y }); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/vue/examples/customRender/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/vue/examples/customRender/main.js: -------------------------------------------------------------------------------- 1 | import { createRender } from "../../lib/ass-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | console.log(PIXI); 4 | 5 | const game = new PIXI.Application({ 6 | width: 500, 7 | height: 500, 8 | }); 9 | 10 | document.body.appendChild(game.view); 11 | 12 | const createElement = (type) => { 13 | if (type === "rect") { 14 | const rect = new PIXI.Graphics(); 15 | rect.beginFill(0xff0000); 16 | rect.drawRect(0, 0, 100, 100); 17 | rect.endFill(); 18 | return rect; 19 | } 20 | }; 21 | 22 | const patchProp = (el, key, val) => { 23 | el[key] = val; 24 | }; 25 | 26 | const insert = (el, parent) => { 27 | parent.addChild(el); 28 | }; 29 | 30 | const renderer = createRender({ 31 | createElement, 32 | patchProp, 33 | insert, 34 | }); 35 | 36 | renderer.createApp(App).mount(game.stage); 37 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | return h("div", {}, [h("p", {}, "currentInstance Demo"), h(Foo)]); 8 | }, 9 | setup() { 10 | const instance = getCurrentInstance(); 11 | console.log("App:", instance); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h,getCurrentInstance } from "../../lib/ass-vue.esm.js"; 2 | export const Foo = { 3 | name: "Foo", 4 | setup() { 5 | const instance = getCurrentInstance(); 6 | console.log("Foo:", instance); 7 | return {}; 8 | }, 9 | render() { 10 | return h("div", {}, "foo"); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/helloWord/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/ass-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | window.self = null; 4 | export const App = { 5 | // .vue 6 | // 7 | //render 8 | render() { 9 | //ui 逻辑 10 | window.self = this; 11 | return h( 12 | "div", 13 | { 14 | id: "root", 15 | class: ["red"], 16 | // onClick() { 17 | // console.log("this is app div onclick"); 18 | // }, 19 | // onMousedown() { 20 | // console.log("mouseDown,app"); 21 | // }, 22 | }, 23 | // "hi" + this.msg, 24 | // [h("p", { class: "red" }, "hi red"), h("p", { class: "blue" }, "hi blue")], 25 | [ 26 | h("p", { class: "red" }, "hi red"), 27 | h(Foo, { 28 | count: 1, 29 | onAdd(a, b) { 30 | console.log("on add in app js", a, b); 31 | }, 32 | onAddFoo(){ 33 | console.log("on Add foo in app js") 34 | } 35 | }), 36 | ] 37 | ); 38 | }, 39 | setup() { 40 | return { 41 | msg: "ass-vue", 42 | }; 43 | }, 44 | }; 45 | 46 | //处理组件,处理element 47 | -------------------------------------------------------------------------------- /packages/vue/examples/helloWord/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/ass-vue.esm.js"; 2 | export const Foo = { 3 | setup() { 4 | return {}; 5 | }, 6 | render() { 7 | const foo = h("p", {}, "Foo"); 8 | //我们需要在这里接收上面传递过来的slots 然后把他加入到h 函数渲染的函数里面去 9 | //其实slots 就是当前虚拟节点的children 10 | return h("div", {}, [foo,this.$slots,]); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/vue/examples/helloWord/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/helloWord/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../dist/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, getCurrentInstance,nextTick } from "../../lib/ass-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | setup() { 6 | const count = ref(1); 7 | const instance = getCurrentInstance(); 8 | async function onClick() { 9 | for (let i = 0; i < 100; i++) { 10 | console.log("update"); 11 | count.value = i; 12 | } 13 | debugger; 14 | 15 | //在这里因为是异步任务所以拿不到最新的instance 16 | console.log(instance, "instance"); 17 | nextTick(()=>{ 18 | //在nextTick中拿到最新的instance 19 | console.log(instance, "instance"); 20 | }) 21 | //或者使用这样使用 22 | await nextTick(); 23 | console.log(instance,"instance"); 24 | 25 | } return { onClick, count }; 26 | }, 27 | render() { 28 | { 29 | const button = h("button", { onClick: this.onClick }, "update"); 30 | const p = h("p", {}, "count" + this.count); 31 | return h("div", {}, [button, p]); 32 | } 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/ass-vue.esm.js"; 2 | 3 | import ArrayToText from "./ArrayToText.js"; 4 | import TextToText from './TextToText.js' 5 | // import TextToArray from "./TextToArray.js"; 6 | import ArrayToArray from "./ArrayToArray.js"; 7 | export const App = { 8 | name: "App", 9 | setup() {}, 10 | render() { 11 | return h("div", { tId: 1 }, [ 12 | h("p", {}, "主页"), 13 | //老的是Array,新的是text 14 | // h(ArrayToText), 15 | //老的是Text 新的是不同的text 16 | // h(TextToText), 17 | //老的是text 新的是Array 18 | // h(TextToArray), 19 | //老的是Array 新的也是Array 20 | h(ArrayToArray), 21 | ]); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const nextChildren = "newChildren"; 4 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")]; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const prevChildren = "oldChildren"; 4 | const nextChildren = [h("div", {}, "New"), h("div", {}, "Children")]; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | const nextChildren = "newChildren"; 4 | const prevChildren = "oldChildren"; 5 | export default { 6 | name: "ArrayToText", 7 | setup() { 8 | const isChange = ref(false); 9 | window.isChange = isChange; 10 | return { isChange }; 11 | }, 12 | render() { 13 | const self = this; 14 | 15 | return self.isChange === true 16 | ? h("div", {}, nextChildren) 17 | : h("div", {}, prevChildren); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/examples/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/ass-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | setup() { 6 | const count = ref(0); 7 | const onClick = () => count.value++; 8 | const props = ref({ 9 | foo: "foo", 10 | bar: "bar", 11 | }); 12 | const onChangePropsDemo1 = () => { 13 | props.value.foo = "new-foo"; 14 | }; 15 | const onChangePropsDemo2 = () => { 16 | props.value.foo = undefined; 17 | }; 18 | const onChangePropsDemo3 = () => { 19 | props.value = { foo: "foo" }; 20 | }; 21 | return { 22 | count, 23 | onClick, 24 | onChangePropsDemo1, 25 | onChangePropsDemo2, 26 | onChangePropsDemo3, 27 | props, 28 | }; 29 | }, 30 | render() { 31 | return h("div", { id: "root", foo: this.props.foo, bar: this.props.bar }, [ 32 | h("div", {}, "count:" + this.count), 33 | h("button", { onClick: this.onClick }, "click me to add 1"), 34 | h( 35 | "button", 36 | { onClick: this.onChangePropsDemo1 }, 37 | "changePropsDemo props" 38 | ), 39 | h( 40 | "button", 41 | { onClick: this.onChangePropsDemo2 }, 42 | "changeProps to null or undefined" 43 | ), 44 | h("button", { onClick: this.onChangePropsDemo3 }, "delete Props"), 45 | ]); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/vue/examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/update/main.js: -------------------------------------------------------------------------------- 1 | //vue3 2 | import { createApp } from "../../lib/ass-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer=document.getElementById("app") 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ass-vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@ass-vue/complier-core": "workspace:^", 14 | "@ass-vue/runtime-dom": "workspace:^" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | // ass-vue 的出口文件 2 | export * from "@ass-vue/runtime-dom" 3 | import { baseCompile } from "@ass-vue/complier-core"; 4 | import * as runtimeDom from "@ass-vue/runtime-dom"; 5 | import { registerRuntimeCompiler } from "@ass-vue/runtime-dom"; 6 | function compileToFunction(template) { 7 | const { code } = baseCompile(template); 8 | //这里的code 就是我们代码compile 生成的代码;第一个参数是我们的函数参数 9 | const render = new Function("Vue", code)(runtimeDom); 10 | return render; 11 | } 12 | 13 | registerRuntimeCompiler(compileToFunction); 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | export default { 3 | input: "packages/vue/src/index.ts", 4 | output: [ 5 | { 6 | formate: "cjs", 7 | file: "packages/vue/dist/ass-vue.cjs.js", 8 | }, 9 | 10 | { 11 | formate: "es", 12 | file: "packages/vue/dist/ass-vue.esm.js", 13 | }, 14 | ], 15 | plugins: [typescript()], 16 | }; 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path, { resolve } from "path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | resolve: { 9 | alias: [ 10 | { 11 | find: /@ass-vue\/(\w*)/, 12 | replacement: path.resolve(__dirname, "packages") + "/$1/src", 13 | }, 14 | ], 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------