├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── note ├── reactivity │ ├── assets │ │ ├── computed_flow.png │ │ ├── init_flow.png │ │ ├── readonly_flow.png │ │ ├── ref_flow.png │ │ ├── scheduler_flow.png │ │ └── shallowReactive_flow.png │ ├── computed.md │ ├── index.md │ ├── readonly.md │ ├── ref.md │ ├── scheduler.md │ └── shallowReadonly.md └── runtime-core │ ├── assets │ └── createApp_flow.png │ └── createApp.md ├── package.json ├── packages ├── compiler-core │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parse.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 │ │ ├── shallowReactive.spec.ts │ │ ├── shallowReadonly.spec.ts │ │ └── shallowRef.spec.ts │ ├── package.json │ └── src │ │ ├── baseHandlers.ts │ │ ├── computed.ts │ │ ├── effect.ts │ │ ├── index.ts │ │ ├── reactive.ts │ │ └── ref.ts ├── runtime-core │ ├── package.json │ └── src │ │ ├── apiInject.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 │ │ ├── ShapeFlags.ts │ │ ├── index.ts │ │ └── toDisplayString.ts └── vue │ ├── dist │ ├── tiny-vue3.cjs.js │ └── tiny-vue3.esm.js │ ├── examples │ ├── apiInject │ │ ├── App.js │ │ └── index.html │ ├── compiler-base │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── componentEmit │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── componentSlot │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── componentUpdate │ │ ├── App.js │ │ ├── Child.js │ │ ├── index.html │ │ └── main.js │ ├── currentInstance │ │ ├── App.js │ │ ├── Foo.js │ │ ├── index.html │ │ └── main.js │ ├── customRenderer │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── helloworld │ │ ├── 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.js ├── tsconfig.json └── vitest.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

tiny vue3

2 | 3 |
4 | 5 | **English** | [中文](./README.zh-CN.md) 6 | 7 |
8 | 9 | ### Overview 10 | 11 | Tiny version of the Vue3 based on TDD, in order to learning deeply Vue3 and clarify the core logic 12 | 13 | ### Feature 14 | 15 | **reactivity** 16 | 17 | - [x] implement reactive 18 | - [x] effect.scheduler 19 | - [x] effect.stop 20 | - [x] track dependency collection 21 | - [x] trigger triggering dependencies 22 | - [x] implement readonly 23 | - [x] isReactive 24 | - [x] isReadonly 25 | - [x] nested reactive 26 | - [x] shallowReadonly 27 | - [x] shallowReactive 28 | - [x] isProxy 29 | - [x] implement ref 30 | - [x] shallowRef 31 | - [x] proxyRefs 32 | - [x] implement computed 33 | 34 | **runtime-core** 35 | 36 | - [x] component type 37 | - [x] element type 38 | - [x] props 39 | - [x] props 和 context 40 | - [x] component emit 41 | - [x] proxy 42 | - [x] setup 43 | - [x] nextTick 44 | - [x] getCurrentInstance 45 | - [x] provide/inject 46 | - [x] slots 47 | - [x] Fragment node component 48 | - [x] Text node component 49 | - [x] $el 50 | 51 | **compiler-core** 52 | 53 | - [x] parse slots 54 | - [x] parse element 55 | - [x] parse text 56 | 57 | **runtime-dom** 58 | 59 | - [x] custom renderer 60 | 61 | **monorepo & vitest** 62 | 63 | - [x] refactor by pnpm monorepo 64 | - [x] vitest replace jest 65 | 66 | ### License 67 | 68 | Tiny-vue3 is [MIT licensed](./LICENSE). 69 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

tiny vue3

2 | 3 |
4 | 5 | [English](./README.md) | **中文** 6 | 7 |
8 | 9 | ### 概述 10 | 11 | 基于 TDD 实现 vue3 模型简易版本,用于深入学习 vue3,理清 vue3 的核心逻辑 12 | 13 | > `note` 目录下为学习笔记,用于加深各个环节的重点印象 14 | 15 | ### 功能实现 16 | 17 | **reactivity** 18 | 19 | - [x] reactive 的实现 20 | - [x] 支持 effect.scheduler 21 | - [x] 支持 effect.stop 22 | - [x] track 依赖收集 23 | - [x] trigger 触发依赖 24 | - [x] readonly 的实现 25 | - [x] 支持 isReactive 26 | - [x] 支持 isReadonly 27 | - [x] 支持嵌套 reactive 28 | - [x] 支持 shallowReadonly 29 | - [x] 支持 shallowReactive 30 | - [x] 支持 isProxy 31 | - [x] ref 的实现 32 | - [x] 支持 shallowRef 33 | - [x] 支持 proxyRefs 34 | - [x] computed 的实现 35 | 36 | **runtime-core** 37 | 38 | - [x] 支持组件类型 39 | - [x] 支持 element 类型 40 | - [x] 初始化 props 41 | - [x] setup 可获取 props 和 context 42 | - [x] 支持 component emit 43 | - [x] 支持 proxy 44 | - [x] 可以在 render 函数中获取 setup 返回的对象 45 | - [x] nextTick 的实现 46 | - [x] 支持 getCurrentInstance 47 | - [x] 支持 provide/inject 48 | - [x] 支持最基础的 slots 49 | - [x] 支持 Fragment 类型节点 50 | - [x] 支持 Text 类型节点 51 | - [x] 支持 $el api 52 | 53 | **compiler-core** 54 | 55 | - [x] 解析插值 56 | - [x] 解析 element 57 | - [x] 解析 text 58 | 59 | **runtime-dom** 60 | 61 | - [x] 支持 custom renderer 62 | 63 | **monorepo & vitest** 64 | 65 | - [x] pnpm monorepo 改造 66 | - [x] vitest 替换 jest 67 | 68 | ### License 69 | 70 | 本项目基于 [MIT licensed](./LICENSE). -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /note/reactivity/assets/computed_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/computed_flow.png -------------------------------------------------------------------------------- /note/reactivity/assets/init_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/init_flow.png -------------------------------------------------------------------------------- /note/reactivity/assets/readonly_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/readonly_flow.png -------------------------------------------------------------------------------- /note/reactivity/assets/ref_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/ref_flow.png -------------------------------------------------------------------------------- /note/reactivity/assets/scheduler_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/scheduler_flow.png -------------------------------------------------------------------------------- /note/reactivity/assets/shallowReactive_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/reactivity/assets/shallowReactive_flow.png -------------------------------------------------------------------------------- /note/reactivity/computed.md: -------------------------------------------------------------------------------- 1 | # computed 2 | 3 | ## 流程 4 | ![流程](./assets/computed_flow.png) 5 | 6 | computed 计算属性的特征就是缓存,在 get value 的时候才去考虑是否需要更新值。 7 | keypiont 是 ReactiveEffect 类传入 scheduler 函数代替 getter 函数执行, 8 | ```typescript 9 | class ComputedRefImpl { 10 | ... 11 | private _effect: ReactiveEffect; 12 | constructor(getter) { 13 | this._effect = new ReactiveEffect(getter, () => { 14 | if (!this._dirty) { 15 | this._dirty = true; 16 | } 17 | }); 18 | } 19 | ... 20 | } 21 | ``` 22 | 在 getter 依赖的属性或值发生变化时,触发的是 scheduler 函数,标记 _dirty 为 true 则在下一轮 get value 的时候会重新计算 _value 的值 -------------------------------------------------------------------------------- /note/reactivity/index.md: -------------------------------------------------------------------------------- 1 | # reactivity 笔记 2 | 3 | ## 流程 4 | ![flow](./assets/init_flow.png) 5 | 6 | ## reactive 7 | 8 | 响应式的入口,本质是通过 Proxy 返回一个新的响应式对象,该对象在 getter 和 setter 里分别做依赖收集(track)和依赖触发(trigger) 9 | 10 | ## effect 11 | 12 | 接受一个 function 函数,调用时会实例化一个 ReactiveEffect,并立即触发该函数,如果该函数里触发 getter,那么会把该实例收集起来 13 | 14 | update(返回值): ReactiveEffect 初始化时立即触发会把当前 funciton 函数返回,称为 runner,后续可以再 ReactiveEffect 外部主动触发;同时也可以把 function 的值返回 15 | 16 | update(scheduler): effect 可以有第二个参数 options 对象,传入 scheduler 函数可以在触发依赖的时候执行 scheduler 而不是执行 runner 17 | 18 | update(stop/onStop): stop 可以在外部把 runner 函数停止,即在依赖触发的时不运行 function 函数。逻辑是 track 的时候 ReactiveEffect 也把 deps 收集起来、runner 挂载 ReactiveEffect 实例。在调用 stop 的时候通过 runner -> ReactiveEffect -> deps -> 遍历 delete effect(还增加一个 active 锁防止 stop 多次调用); 19 | effect 第二个参数 options 对象可以传入 onStop 函数,可以在外部执行 stop 的时候执行 20 | 21 | ## track 22 | 23 | 在 getter 的时候,分别通过 target 和 key 作唯一的 Map 映射,把当前 ReactiveEffect 实例收集到 dep 里 24 | 25 | ## trigger 26 | 27 | 在 setter 的时候,通过 target 和 key 的唯一映射拿到依赖收集的 dep,触发每一个 ReactiveEffect 实例的 run 方法 28 | 29 | update(scheduler): 当 ReactiveEffect 实例有 scheduler 函数时,scheduler 函数代替 run 函数执行 -------------------------------------------------------------------------------- /note/reactivity/readonly.md: -------------------------------------------------------------------------------- 1 | # readonly 2 | 3 | ## 流程 4 | ![flow](./assets/readonly_flow.png) 5 | 6 | 如命名 readonly 一样言简意赅,readonly 创建一个不能修改内部属性的响应式对象。 7 | 8 | 介绍一下 TDD 的流程: 9 | 1. 先写测试用例 10 | 2. 实现测试用例的功能 11 | 3. 确保测试用例通过的情况下重构 12 | 13 | 实现 readonly 功能的同时,把 reactive 和 readonly 的代码复用重构到 baseHandlers 文件里。 14 | 优化点:get 和 set 函数在初始化时创建,后续再调用 reactive/readonly 都是同一个对象,不用重新创建(readonlyGet 同理)。 -------------------------------------------------------------------------------- /note/reactivity/ref.md: -------------------------------------------------------------------------------- 1 | # ref 2 | 3 | ## 流程 4 | ![flow](./assets/ref_flow.png) 5 | 6 | ref 可以接受基本类型也可以接受引用类型。我们知道 Proxy 返回响应式对象的前提是需要参数是对象,基本类型如何监听呢? 7 | 所以 ref 才会以 `.value` 的形式来生成,在 get value 的时候收集依赖,在 set value 的时候触发依赖 -------------------------------------------------------------------------------- /note/reactivity/scheduler.md: -------------------------------------------------------------------------------- 1 | # scheduler 2 | 3 | ## 流程 4 | ![flow](./assets/scheduler_flow.png) 5 | 6 | 7 | 谈谈 constructor 参数里 scheduler 的修饰符,这节实现的逻辑如下 8 | ```typescript 9 | class ReactiveEffect { 10 | private fn; 11 | 12 | constructor(_fn, public scheduler?) { 13 | this.fn = _fn; 14 | } 15 | ... 16 | } 17 | ``` 18 | 19 | 1. `?` 代表可选属性大家都很常见,补充另一个操作符 `!`,其代表非空断言,表示强制解析,使用后类型推断会排除 `null` 和 `undefined` 20 | ```typscript 21 | function foo (maybeString: string | undefined | null) { 22 | const res: string = maybeString; // Error 23 | cosnt res2: string = maybeString!; // Ok 24 | } 25 | ``` 26 | 27 | 2. `public` 相当于创建了同名的成员变量,即 28 | ```typescript 29 | class ReactiveEffect { 30 | private fn; 31 | public scheduler; 32 | 33 | constructor(_fn, scheduler?) { 34 | this.fn = _fn; 35 | this.scheduler = scheduler; 36 | } 37 | ... 38 | } 39 | ``` 40 | 41 | 以此类推,`private`、`readonly`、`protected` 也是可行的 42 | ```typescript 43 | class ReactiveEffect { 44 | // private fn; // 可省略 45 | 46 | constructor(private fn, public scheduler?) { 47 | // this.fn = _fn; // 可省略 48 | } 49 | ... 50 | } 51 | ``` -------------------------------------------------------------------------------- /note/reactivity/shallowReadonly.md: -------------------------------------------------------------------------------- 1 | # shallowReactive 2 | 3 | 4 | ## 流程 5 | ![flow](./assets/shallowReactive_flow.png) 6 | 7 | shallowReactive 和 shallowReadonly 的子对象不会转换为响应式,适用于只会变化外层属性的场景,可以减少多次 Proxy,减少性能开销 -------------------------------------------------------------------------------- /note/runtime-core/assets/createApp_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psilocine/tiny-vue3/771f80fd685be15a33472e9d8e48a31eb73e7e6c/note/runtime-core/assets/createApp_flow.png -------------------------------------------------------------------------------- /note/runtime-core/createApp.md: -------------------------------------------------------------------------------- 1 | # createApp 2 | 3 | ## 流程 4 | 5 | ![flow](./assets/createApp_flow.png) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "1.0.0", 4 | "description": "A tiny vue3 library.", 5 | "author": "psilolau@foxmail.com", 6 | "scripts": { 7 | "test": "vitest", 8 | "build": "rollup -c rollup.config.js" 9 | }, 10 | "keywords": [ 11 | "vue3", 12 | "vue", 13 | "simplify vue", 14 | "mini vue", 15 | "tiny vue" 16 | ], 17 | "devDependencies": { 18 | "@babel/core": "^7.17.8", 19 | "@babel/preset-env": "^7.16.11", 20 | "@babel/preset-typescript": "^7.16.7", 21 | "@rollup/plugin-typescript": "^8.3.1", 22 | "rollup": "^2.70.1", 23 | "tslib": "^2.3.1", 24 | "typescript": "^4.6.3", 25 | "vitest": "^0.22.1" 26 | }, 27 | "dependencies": { 28 | "@types/jest": "^29.0.3" 29 | }, 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`codegen element: 4 | "const { createElementVNode: _createElementVNode } = Vue 5 | return function render(_ctx, _cache){return }" 6 | 1`] = ` 7 | "const { createElementVNode: _createElementVNode } = Vue 8 | return function render(_ctx, _cache){return _createElementVNode(\\"div\\"), null, 'hi,' + _toDisplayString(message))}" 9 | `; 10 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { generate } 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 | describe('codegen', () => { 9 | it('string', () => { 10 | const ast = baseParse('hi'); 11 | 12 | transform(ast) 13 | 14 | const { code } = generate(ast); 15 | // 快照 16 | // 1. 抓 bug 17 | // 2. 有意 18 | expect(code).toMatchInlineSnapshot(` 19 | "const { createElementVNode: _createElementVNode } = Vue 20 | return function render(_ctx, _cache){return 'hi'}" 21 | `) 22 | }); 23 | 24 | it('interpolation', () => { 25 | const ast = baseParse('{{message}}') 26 | transform(ast, { 27 | nodeTransforms: [transformExpression] 28 | }); 29 | const { code } = generate(ast); 30 | expect(code).toMatchInlineSnapshot(` 31 | "const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue 32 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 33 | `); 34 | }); 35 | 36 | 37 | it("element", () => { 38 | const ast: any = baseParse("
hi,{{message}}
"); 39 | transform(ast, { 40 | nodeTransforms: [transformExpression, transformElement, transformText], 41 | }); 42 | 43 | const { code } = generate(ast); 44 | expect(code).toMatchInlineSnapshot(` 45 | "const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue 46 | return function render(_ctx, _cache){return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.message))}" 47 | `); 48 | }); 49 | }); -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | 4 | describe("Parse", () => { 5 | describe("interpolation", () => { 6 | test("simple interpolation", () => { 7 | const ast = baseParse("{{ message }}"); 8 | 9 | // root 10 | expect(ast.children[0]).toStrictEqual({ 11 | type: NodeTypes.INTERPOLATION, 12 | content: { 13 | type: NodeTypes.SIMPLE_EXPRESSION, 14 | content: "message", 15 | }, 16 | }); 17 | }); 18 | }); 19 | describe("element", () => { 20 | test("simple element div", () => { 21 | const ast = baseParse("
"); 22 | 23 | expect(ast.children[0]).toStrictEqual({ 24 | type: NodeTypes.ELEMENT, 25 | tag: "div", 26 | children: [], 27 | }); 28 | }); 29 | }); 30 | 31 | describe("text", () => { 32 | it("simple text", () => { 33 | const ast = baseParse("some text"); 34 | 35 | expect(ast.children[0]).toStrictEqual({ 36 | type: NodeTypes.TEXT, 37 | content: "some text", 38 | }); 39 | }); 40 | }); 41 | 42 | test("hello world", () => { 43 | const ast = baseParse("
hi,{{message}}
"); 44 | 45 | expect(ast.children[0]).toStrictEqual({ 46 | type: NodeTypes.ELEMENT, 47 | tag: "div", 48 | children: [ 49 | { 50 | type: NodeTypes.TEXT, 51 | content: "hi,", 52 | }, 53 | { 54 | type: NodeTypes.INTERPOLATION, 55 | content: { 56 | type: NodeTypes.SIMPLE_EXPRESSION, 57 | content: "message", 58 | }, 59 | }, 60 | ], 61 | }); 62 | }); 63 | 64 | 65 | test("Nested element", () => { 66 | const ast = baseParse("

hi

{{message}}
"); 67 | 68 | expect(ast.children[0]).toStrictEqual({ 69 | type: NodeTypes.ELEMENT, 70 | tag: "div", 71 | children: [ 72 | { 73 | type: NodeTypes.ELEMENT, 74 | tag: "p", 75 | children: [ 76 | { 77 | type: NodeTypes.TEXT, 78 | content: "hi", 79 | }, 80 | ], 81 | }, 82 | { 83 | type: NodeTypes.INTERPOLATION, 84 | content: { 85 | type: NodeTypes.SIMPLE_EXPRESSION, 86 | content: "message", 87 | }, 88 | }, 89 | ], 90 | }); 91 | }); 92 | 93 | test("should throw error when lack end tag", () => { 94 | expect(() => { 95 | baseParse("
"); 96 | }).toThrow(`缺少结束标签:span`); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/compiler-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 | transform(ast, { 14 | nodeTransforms: [plugin], 15 | }); 16 | const nodeText = ast.children[0].children[0] 17 | expect(nodeText.content).toBe('hi, mini-vue') 18 | }); 19 | }); -------------------------------------------------------------------------------- /packages/compiler-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue3/compiler-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@tiny-vue3/shared": "workspace:^1.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/compiler-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 | 15 | return { 16 | type: NodeTypes.ELEMENT, 17 | tag, 18 | props, 19 | children, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "@tiny-vue3/shared"; 2 | import { NodeTypes } from "./ast"; 3 | import { 4 | CREATE_ELEMENT_VNODE, 5 | helperMapName, 6 | TO_DISPLAY_STRING, 7 | } from "./runtimeHelpers"; 8 | 9 | export function generate(ast) { 10 | const context = createCodegenContext(); 11 | const { push } = context; 12 | 13 | genFunctionPreamble(ast, context); 14 | 15 | const functionName = "render"; 16 | const args = ["_ctx", "_cache"]; 17 | const signature = args.join(", "); 18 | 19 | push(`function ${functionName}(${signature}){`); 20 | push(`return `); 21 | genNode(ast.codegenNode, context); 22 | push("}"); 23 | 24 | return { 25 | code: context.code, 26 | }; 27 | } 28 | 29 | function genFunctionPreamble(ast: any, context: any) { 30 | const { push } = context; 31 | const VueBinging = "Vue"; 32 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}`; 33 | if (ast.helpers.length > 0) { 34 | push( 35 | `const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinging}` 36 | ); 37 | } 38 | push("\n"); 39 | push("return "); 40 | } 41 | 42 | function createCodegenContext(): any { 43 | const context = { 44 | code: "", 45 | push(source) { 46 | context.code += source; 47 | }, 48 | helper(key) { 49 | return `_${helperMapName[key]}`; 50 | }, 51 | }; 52 | 53 | return context; 54 | } 55 | 56 | function genNode(node: any, context) { 57 | switch (node.type) { 58 | case NodeTypes.TEXT: 59 | genText(node, context); 60 | break; 61 | 62 | case NodeTypes.INTERPOLATION: 63 | genInterpolation(node, context); 64 | break; 65 | 66 | case NodeTypes.SIMPLE_EXPRESSION: 67 | genExpression(node, context); 68 | break; 69 | 70 | case NodeTypes.ELEMENT: 71 | genElement(node, context); 72 | 73 | break; 74 | case NodeTypes.COMPOUND_EXPRESSION: 75 | genCompoundExpression(node, context); 76 | break; 77 | default: 78 | break; 79 | } 80 | } 81 | 82 | function genCompoundExpression(node, context) { 83 | const { push } = context; 84 | const { children } = node; 85 | 86 | for (let i = 0; i < children.length; i++) { 87 | const child = children[i]; 88 | if (isString(child)) { 89 | push(child); 90 | } else { 91 | genNode(child, context); 92 | } 93 | } 94 | } 95 | 96 | function genElement(node, context) { 97 | const { push, helper } = context; 98 | const { tag, children, props } = node; 99 | push(`${helper(CREATE_ELEMENT_VNODE)}(`); 100 | genNodeList(genNullable([tag, props, children]), context); 101 | push(")"); 102 | } 103 | 104 | function genNodeList(nodes, context) { 105 | const { push } = context; 106 | for (let i = 0; i < nodes.length; i++) { 107 | const node = nodes[i]; 108 | if (isString(node)) { 109 | push(node); 110 | } else { 111 | genNode(node, context); 112 | } 113 | 114 | if (i < nodes.length - 1) { 115 | push(", "); 116 | } 117 | } 118 | } 119 | 120 | function genNullable(args: any) { 121 | return args.map((arg) => arg || "null"); 122 | } 123 | 124 | function genText(node: any, context: any) { 125 | const { push } = context; 126 | push(`'${node.content}'`); 127 | } 128 | 129 | function genInterpolation(node: any, context: any) { 130 | const { push, helper } = context; 131 | push(`${helper(TO_DISPLAY_STRING)}(`); 132 | genNode(node.content, context); 133 | push(")"); 134 | } 135 | 136 | function genExpression(node: any, context: any) { 137 | const { push } = context; 138 | push(`${node.content}`); 139 | } 140 | -------------------------------------------------------------------------------- /packages/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { generate } 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 | 14 | return generate(ast); 15 | } 16 | -------------------------------------------------------------------------------- /packages/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' -------------------------------------------------------------------------------- /packages/compiler-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 = createParserContent(content); 10 | 11 | return createRoot(parseChildren(context, [])); 12 | } 13 | function parseChildren(context, ancestors) { 14 | const nodes: any = []; 15 | while (!isEnd(context, ancestors)) { 16 | let node; 17 | const s = context.source; 18 | if (s.startsWith("{{")) { 19 | node = parseInterpolation(context); 20 | } else if (s[0] === "<") { 21 | if (/[a-z]/i.test(s[1])) { 22 | node = parseElement(context, ancestors); 23 | } 24 | } 25 | 26 | if (!node) { 27 | node = parseText(context); 28 | } 29 | 30 | nodes.push(node); 31 | } 32 | return nodes; 33 | } 34 | 35 | function isEnd(context, ancestors) { 36 | // 2. 遇到结束标签 37 | const s = context.source; 38 | if (s.startsWith("= 0; --i) { 40 | const tag = ancestors[i].tag; 41 | if (startsWithEndTagOpen(s, tag)) { 42 | return true; 43 | } 44 | } 45 | } 46 | // 1. source 有值 47 | return !context.source; 48 | } 49 | function parseText(context: any) { 50 | let endIndex = context.source.length; 51 | let endTokens = ["<", "{{"]; 52 | 53 | for (let i = 0; i < endTokens.length; i++) { 54 | const index = context.source.indexOf(endTokens[i]); 55 | if (index !== -1 && endIndex > index) { 56 | endIndex = index; 57 | } 58 | } 59 | // 1. 获取 context 60 | const content = parseTextData(context, endIndex); 61 | 62 | console.log("---------", content); 63 | 64 | return { 65 | type: NodeTypes.TEXT, 66 | content, 67 | }; 68 | } 69 | 70 | function parseTextData(context: any, length) { 71 | const content = context.source.slice(0, length); 72 | // 2. 推进 73 | advanceBy(context, length); 74 | 75 | return content; 76 | } 77 | 78 | function parseElement(context: any, ancestors) { 79 | // Implement 80 | // 1. 解析 tag 81 | const element: any = parseTag(context, TagType.Start); 82 | ancestors.push(element); 83 | element.children = parseChildren(context, ancestors); 84 | ancestors.pop(); 85 | 86 | if (startsWithEndTagOpen(context.source, element.tag)) { 87 | parseTag(context, TagType.End); 88 | } else { 89 | throw new Error(`缺少结束标签:${element.tag}`); 90 | } 91 | 92 | return element; 93 | } 94 | 95 | function startsWithEndTagOpen(source, tag) { 96 | return ( 97 | source.startsWith(" { 6 | // tag 7 | const vnodeTag = `'${node.tag}'`; 8 | 9 | // props 10 | let vnodeProps; 11 | 12 | // children 13 | const children = node.children; 14 | let vnodeChildren = children[0]; 15 | 16 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../ast"; 2 | 3 | export function transformExpression(node) { 4 | if (node.type === NodeTypes.INTERPOLATION) { 5 | node.content = processExpression(node.content); 6 | } 7 | } 8 | function processExpression(node: any) { 9 | node.content = `_ctx.${node.content}`; 10 | return node; 11 | } 12 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../ast"; 2 | import { isText } from "../utils"; 3 | 4 | export function transformText(node) { 5 | if (node.type === NodeTypes.ELEMENT) { 6 | return () => { 7 | const { children } = node; 8 | for (let i = 0; i < children.length; i++) { 9 | const child = children[i]; 10 | let currentContainer; 11 | if (isText(child)) { 12 | for (let j = i + 1; j < children.length; j++) { 13 | const next = children[j]; 14 | if (isText(next)) { 15 | if (!currentContainer) { 16 | currentContainer = children[i] = { 17 | type: NodeTypes.COMPOUND_EXPRESSION, 18 | children: [child], 19 | }; 20 | } 21 | currentContainer.children.push(" + "); 22 | currentContainer.children.push(next); 23 | children.splice(j, 1); 24 | j--; 25 | } else { 26 | currentContainer = undefined; 27 | break; 28 | } 29 | } 30 | } 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | 3 | export function isText(node) { 4 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION; 5 | } 6 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | import { computed } from "../src/computed"; 4 | import { reactive } from "../src/reactive"; 5 | 6 | describe("computed", () => { 7 | it("happy path", () => { 8 | const user = reactive({ 9 | age: 1, 10 | }); 11 | 12 | const age = computed(() => { 13 | return user.age; 14 | }); 15 | 16 | expect(age.value).toBe(1); 17 | }); 18 | 19 | it("should compute lazily", () => { 20 | const value = reactive({ 21 | foo: 1, 22 | }); 23 | const getter = vi.fn(() => { 24 | return value.foo; 25 | }); 26 | const cValue = computed(getter); 27 | 28 | // lazy 29 | expect(getter).not.toHaveBeenCalled(); 30 | 31 | expect(cValue.value).toBe(1); 32 | expect(getter).toHaveBeenCalledTimes(1); 33 | 34 | // should not compute again 35 | cValue.value; // get 36 | expect(getter).toHaveBeenCalledTimes(1); 37 | 38 | // // should not compute until needed 39 | value.foo = 2; 40 | expect(getter).toHaveBeenCalledTimes(1); 41 | 42 | // // now it should compute 43 | expect(cValue.value).toBe(2); 44 | expect(getter).toHaveBeenCalledTimes(2); 45 | 46 | // // should not compute again 47 | cValue.value; 48 | expect(getter).toHaveBeenCalledTimes(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | import { reactive } from "../src/reactive"; 4 | import { effect, stop } from "../src/effect"; 5 | 6 | describe("effect", () => { 7 | it("happy path", () => { 8 | const user = reactive({ 9 | age: 10, 10 | }); 11 | 12 | let nextAge; 13 | effect(() => { 14 | nextAge = user.age + 1; 15 | }); 16 | 17 | expect(nextAge).toBe(11); 18 | 19 | // update 20 | user.age++; 21 | expect(nextAge).toBe(12); 22 | }); 23 | 24 | it("should return runner when call effect", () => { 25 | // 当调用 runner 的时候可以重新执行 effect.run 26 | // runner 的返回值就是用户给的 fn 的返回值 27 | let foo = 0; 28 | const runner = effect(() => { 29 | foo++; 30 | return foo; 31 | }); 32 | 33 | expect(foo).toBe(1); 34 | runner(); 35 | expect(foo).toBe(2); 36 | expect(runner()).toBe(3); 37 | }); 38 | 39 | it("scheduler", () => { 40 | let dummy; 41 | let run: any; 42 | const scheduler = vi.fn(() => { 43 | run = runner; 44 | }); 45 | const obj = reactive({ foo: 1 }); 46 | const runner = effect( 47 | () => { 48 | dummy = obj.foo; 49 | }, 50 | { scheduler } 51 | ); 52 | expect(scheduler).not.toHaveBeenCalled(); 53 | expect(dummy).toBe(1); 54 | // should be called on first trigger 55 | obj.foo++; 56 | expect(scheduler).toHaveBeenCalledTimes(1); 57 | // // should not run yet 58 | expect(dummy).toBe(1); 59 | // // manually run 60 | run(); 61 | // // should have run 62 | expect(dummy).toBe(2); 63 | }); 64 | 65 | it("stop", () => { 66 | let dummy; 67 | const obj = reactive({ prop: 1 }); 68 | const runner = effect(() => { 69 | dummy = obj.prop; 70 | }); 71 | obj.prop = 2; 72 | expect(dummy).toBe(2); 73 | stop(runner); 74 | // obj.prop = 3; 75 | obj.prop++; 76 | expect(dummy).toBe(2); 77 | 78 | // stopped effect should still be manually callable 79 | runner(); 80 | expect(dummy).toBe(3); 81 | }); 82 | 83 | it("onStop", () => { 84 | const obj = reactive({ 85 | foo: 1, 86 | }); 87 | const onStop = vi.fn(); 88 | let dummy; 89 | const runner = effect( 90 | () => { 91 | dummy = obj.foo; 92 | }, 93 | { 94 | onStop, 95 | } 96 | ); 97 | 98 | stop(runner); 99 | expect(onStop).toBeCalledTimes(1); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive, isProxy } from "../src/reactive"; 2 | describe("reactive", () => { 3 | it("happy path", () => { 4 | const original = { foo: 1 }; 5 | const observed = reactive(original); 6 | expect(observed).not.toBe(original); 7 | expect(observed.foo).toBe(1); 8 | expect(isReactive(observed)).toBe(true); 9 | expect(isReactive(original)).toBe(false); 10 | expect(isProxy(observed)).toBe(true); 11 | }); 12 | 13 | test("nested reactives", () => { 14 | const original = { 15 | nested: { 16 | foo: 1, 17 | }, 18 | array: [{ bar: 2 }], 19 | }; 20 | const observed = reactive(original); 21 | expect(isReactive(observed.nested)).toBe(true); 22 | expect(isReactive(observed.array)).toBe(true); 23 | expect(isReactive(observed.array[0])).toBe(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | import { isReadonly, readonly, isProxy } from "../src/reactive"; 4 | 5 | describe("readonly", () => { 6 | it("should make nested values readonly", () => { 7 | const original = { foo: 1, bar: { baz: 2 } }; 8 | const wrapped = readonly(original); 9 | expect(wrapped).not.toBe(original); 10 | expect(isReadonly(wrapped)).toBe(true); 11 | expect(isReadonly(original)).toBe(false); 12 | expect(isReadonly(wrapped.bar)).toBe(true); 13 | expect(isReadonly(original.bar)).toBe(false); 14 | expect(isProxy(wrapped)).toBe(true); 15 | 16 | expect(wrapped.foo).toBe(1); 17 | }); 18 | 19 | it("should call console.warn when set", () => { 20 | console.warn = vi.fn(); 21 | const user = readonly({ 22 | age: 10, 23 | }); 24 | 25 | user.age = 11; 26 | expect(console.warn).toHaveBeenCalled(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /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 | describe("ref", () => { 5 | it("happy path", () => { 6 | const a: any = ref(1); 7 | expect(a.value).toBe(1); 8 | }); 9 | 10 | it("should be reactive", () => { 11 | const a:any = ref(1); 12 | let dummy; 13 | let calls = 0; 14 | effect(() => { 15 | calls++; 16 | dummy = a.value; 17 | }); 18 | expect(calls).toBe(1); 19 | expect(dummy).toBe(1); 20 | a.value = 2; 21 | expect(calls).toBe(2); 22 | expect(dummy).toBe(2); 23 | // same value should not trigger 24 | a.value = 2; 25 | expect(calls).toBe(2); 26 | expect(dummy).toBe(2); 27 | }); 28 | 29 | it("should make nested properties reactive", () => { 30 | const a:any = ref({ 31 | count: 1, 32 | }); 33 | let dummy; 34 | effect(() => { 35 | dummy = a.value.count; 36 | }); 37 | expect(dummy).toBe(1); 38 | a.value.count = 2; 39 | expect(dummy).toBe(2); 40 | }); 41 | 42 | it("isRef", () => { 43 | const a = ref(1); 44 | const user = reactive({ 45 | age: 1, 46 | }); 47 | expect(isRef(a)).toBe(true); 48 | expect(isRef(1)).toBe(false); 49 | expect(isRef(user)).toBe(false); 50 | }); 51 | 52 | it("unref", () => { 53 | const a = ref(1); 54 | expect(unref(a)).toBe(1); 55 | expect(unref(1)).toBe(1); 56 | }); 57 | 58 | 59 | it("proxyRefs", () => { 60 | const user: any = { 61 | age: ref(10), 62 | name: "xiaohong", 63 | }; 64 | 65 | const proxyUser = proxyRefs(user); 66 | expect(user.age.value).toBe(10); 67 | expect(proxyUser.age).toBe(10); 68 | expect(proxyUser.name).toBe("xiaohong"); 69 | 70 | proxyUser.age = 20; 71 | 72 | expect(proxyUser.age).toBe(20); 73 | expect(user.age.value).toBe(20); 74 | 75 | proxyUser.age = ref(10); 76 | expect(proxyUser.age).toBe(10); 77 | expect(user.age.value).toBe(10); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, shallowReactive } from "../src/reactive"; 2 | import { effect } from '../src/effect' 3 | 4 | describe("shallowReactive", () => { 5 | test("should not make non-reactive properties reactive", () => { 6 | let dummy; 7 | let dummy2; 8 | const props = shallowReactive({ foo: 1, nested: { bar: 1 } }); 9 | 10 | effect(() => { 11 | dummy = props.foo; 12 | dummy2 = props.nested.bar; 13 | }); 14 | expect(isReactive(props)).toBe(true); 15 | expect(isReactive(props.nested)).toBe(false); 16 | 17 | props.foo = 2; 18 | expect(dummy).toBe(2); 19 | 20 | props.nested.bar++; 21 | expect(dummy2).toBe(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | import { isReadonly, shallowReadonly } from "../src/reactive"; 4 | 5 | describe("shallowReadonly", () => { 6 | test("should not make non-reactive properties reactive", () => { 7 | const props = shallowReadonly({ n: { foo: 1 } }); 8 | expect(isReadonly(props)).toBe(true); 9 | expect(isReadonly(props.n)).toBe(false); 10 | }); 11 | 12 | it("should call console.warn when set", () => { 13 | console.warn = vi.fn(); 14 | const user = shallowReadonly({ 15 | age: 10, 16 | }); 17 | 18 | user.age = 11; 19 | expect(console.warn).toHaveBeenCalled(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowRef.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive } from "../src/reactive"; 2 | import { shallowRef, isRef } from "../src/ref"; 3 | import { effect } from "../src/effect"; 4 | 5 | describe("shallowRef", () => { 6 | test("should not make non-reactive properties reactive", () => { 7 | let dummy: any; 8 | const props: any = shallowRef({ foo: 1 }); 9 | 10 | effect(() => { 11 | dummy = props.value.foo || "can't find"; 12 | }); 13 | expect(isRef(props)).toBe(true); 14 | expect(isReactive(props.value)).toBe(false); 15 | expect(isReactive(props.value.foo)).toBe(false); 16 | 17 | props.value.foo = 2; 18 | expect(dummy).toBe(1); 19 | 20 | props.value = {}; 21 | expect(dummy).toBe("can't find"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue3/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 | "@tiny-vue3/shared": "workspace:^1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "@tiny-vue3/shared"; 2 | 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | import { track, trigger } from "./effect"; 5 | 6 | const get = createGetter(); 7 | const set = createSetter(); 8 | const readonlyGet = createGetter(true); 9 | const shallowReactiveGet = createGetter(false, true) 10 | const shallowReadonlyGet = createGetter(true, true); 11 | 12 | function createGetter(isReadonly = false, shallow = false) { 13 | return function (target, key) { 14 | if (key === ReactiveFlags.IS_REACTIVE) { 15 | return !isReadonly; 16 | } else if (key === ReactiveFlags.IS_READONLY) { 17 | return isReadonly; 18 | } 19 | 20 | const res = Reflect.get(target, key); 21 | 22 | if (!isReadonly) { 23 | // 依赖收集 24 | track(target, key); 25 | } 26 | 27 | if (shallow) { 28 | return res; 29 | } 30 | 31 | if (isObject(res)) { 32 | return isReadonly ? readonly(res) : reactive(res); 33 | } 34 | 35 | return res; 36 | }; 37 | } 38 | 39 | function createSetter() { 40 | return function set(target, key, value) { 41 | const res = Reflect.set(target, key, value); 42 | 43 | // 依赖触发 44 | trigger(target, key); 45 | return res; 46 | }; 47 | } 48 | 49 | export const mutableHandlers = { 50 | get, 51 | set, 52 | }; 53 | 54 | export const shallowReactiveHandlers = extend({}, mutableHandlers, { 55 | get: shallowReactiveGet, 56 | }); 57 | 58 | export const readonlyHandlers = { 59 | get: readonlyGet, 60 | set(target, key) { 61 | console.warn( 62 | `key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, 63 | target 64 | ); 65 | 66 | return true; 67 | }, 68 | }; 69 | 70 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 71 | get: shallowReadonlyGet, 72 | }); 73 | -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _dirty: boolean = true; 5 | private _value: any; 6 | private _effect: ReactiveEffect; 7 | constructor(getter) { 8 | this._effect = new ReactiveEffect(getter, () => { 9 | if (!this._dirty) { 10 | this._dirty = true; 11 | } 12 | }); 13 | } 14 | 15 | get value() { 16 | if (this._dirty) { 17 | this._dirty = false; 18 | this._value = this._effect.run(); 19 | } 20 | return this._value; 21 | } 22 | } 23 | 24 | export function computed(getter) { 25 | return new ComputedRefImpl(getter); 26 | } 27 | -------------------------------------------------------------------------------- /packages/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "@tiny-vue3/shared"; 2 | 3 | let activeEffect; 4 | let shouldTrack = false; 5 | export class ReactiveEffect { 6 | private _fn; 7 | deps = []; 8 | active = true; 9 | onStop?: () => void; 10 | 11 | constructor(fn, public scheduler?) { 12 | this._fn = fn; 13 | } 14 | 15 | run() { 16 | if (!this.active) { 17 | return this._fn(); 18 | } 19 | // 应该收集 20 | activeEffect = this; 21 | shouldTrack = true; 22 | const r = this._fn(); 23 | 24 | // 重置 25 | shouldTrack = false; 26 | 27 | return r; 28 | } 29 | 30 | stop() { 31 | if (this.active) { 32 | cleanupEffect(this); 33 | if (this.onStop) { 34 | this.onStop(); 35 | } 36 | this.active = false; 37 | } 38 | } 39 | } 40 | 41 | function cleanupEffect(effect) { 42 | effect.deps.forEach((dep: any) => { 43 | dep.delete(effect); 44 | }); 45 | 46 | // 把 effect.deps 清空 47 | effect.deps.length = 0; 48 | } 49 | 50 | const targetMap = new Map(); 51 | export function track(target, key) { 52 | if (!isTracking()) return; 53 | 54 | // target -> key -> dep 55 | let depsMap = targetMap.get(target); 56 | if (!depsMap) { 57 | depsMap = new Map(); 58 | targetMap.set(target, depsMap); 59 | } 60 | 61 | let dep = depsMap.get(key); 62 | if (!dep) { 63 | dep = new Set(); 64 | depsMap.set(key, dep); 65 | } 66 | trackEffects(dep); 67 | } 68 | 69 | export function trackEffects(dep) { 70 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 71 | if (dep.has(activeEffect)) return; 72 | 73 | dep.add(activeEffect); 74 | activeEffect.deps.push(dep); 75 | } 76 | 77 | export function isTracking() { 78 | return shouldTrack && activeEffect !== undefined; 79 | } 80 | 81 | export function trigger(target, key) { 82 | const depsMap = targetMap.get(target); 83 | const dep = depsMap.get(key); 84 | triggerEffects(dep); 85 | } 86 | 87 | export function triggerEffects(dep) { 88 | for (const effect of dep) { 89 | if (effect.scheduler) { 90 | effect.scheduler(); 91 | } else { 92 | effect.run(); 93 | } 94 | } 95 | } 96 | 97 | export function effect(fn, options: any = {}) { 98 | const _effect = new ReactiveEffect(fn, options.scheduler); 99 | extend(_effect, options); 100 | 101 | _effect.run(); 102 | 103 | const runner: any = _effect.run.bind(_effect); 104 | runner.effect = _effect; 105 | return runner; 106 | } 107 | 108 | export function stop(runner) { 109 | runner.effect.stop(); 110 | } 111 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, proxyRefs, unref, isRef } from "./ref"; 2 | 3 | export { 4 | reactive, 5 | readonly, 6 | shallowReactive, 7 | shallowReadonly, 8 | isReactive, 9 | isReadonly, 10 | isProxy, 11 | } from "./reactive"; 12 | 13 | export { effect, stop } from './effect' -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "@tiny-vue3/shared"; 2 | import { 3 | mutableHandlers, 4 | readonlyHandlers, 5 | shallowReactiveHandlers, 6 | shallowReadonlyHandlers, 7 | } from "./baseHandlers"; 8 | 9 | export const enum ReactiveFlags { 10 | IS_REACTIVE = "__v_isReactive", 11 | IS_READONLY = "__v_isReadonly", 12 | } 13 | 14 | export function reactive(raw) { 15 | return createReactiveObject(raw, mutableHandlers); 16 | } 17 | 18 | export function shallowReactive(raw) { 19 | return createReactiveObject(raw, shallowReactiveHandlers); 20 | } 21 | 22 | export function readonly(raw) { 23 | return createReactiveObject(raw, readonlyHandlers); 24 | } 25 | 26 | export function shallowReadonly(raw) { 27 | return createReactiveObject(raw, shallowReadonlyHandlers); 28 | } 29 | 30 | function createReactiveObject(target, baseHandlerss) { 31 | if (!isObject(target)) { 32 | console.warn(`target ${target} 必须是一个对象`); 33 | return target; 34 | } 35 | return new Proxy(target, baseHandlerss); 36 | } 37 | 38 | export function isReactive(value) { 39 | return !!value[ReactiveFlags.IS_REACTIVE]; 40 | } 41 | 42 | export function isReadonly(value) { 43 | return !!value[ReactiveFlags.IS_READONLY]; 44 | } 45 | 46 | export function isProxy(value) { 47 | return isReactive(value) || isReadonly(value); 48 | } 49 | 50 | export const toReactive = (value) => { 51 | return isObject(value) ? reactive(value) : value; 52 | }; 53 | 54 | export const toReadonly = (value) => { 55 | return isObject(value) ? readonly(value) : value; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "@tiny-vue3/shared"; 2 | 3 | import { reactive } from "./reactive"; 4 | import { isTracking, trackEffects, triggerEffects } from "./effect"; 5 | 6 | class RefImpl { 7 | private _value; 8 | private dep; 9 | private _rawValue: any; 10 | public readonly __v_isRef = true; 11 | constructor(value, public readonly __v_isShallow) { 12 | this._value = __v_isShallow ? value : convert(value); 13 | this._rawValue = value; 14 | this.dep = new Set(); 15 | } 16 | 17 | get value() { 18 | trackRefValue(this); 19 | return this._value; 20 | } 21 | 22 | set value(newValue) { 23 | if (hasChanged(this._rawValue, newValue)) { 24 | this._value = this.__v_isShallow ? newValue : convert(newValue); 25 | this._rawValue = newValue; 26 | triggerEffects(this.dep); 27 | } 28 | } 29 | } 30 | 31 | function createRef(rawValue: unknown, shallow:boolean) { 32 | if (isRef(rawValue)) { 33 | return rawValue 34 | } 35 | return new RefImpl(rawValue, shallow); 36 | } 37 | 38 | export function ref(value) { 39 | return createRef(value, false); 40 | } 41 | 42 | export function shallowRef(value) { 43 | return createRef(value, true); 44 | } 45 | 46 | function convert(value) { 47 | return isObject(value) ? reactive(value) : value; 48 | } 49 | 50 | function trackRefValue(ref) { 51 | if (isTracking()) { 52 | trackEffects(ref.dep); 53 | } 54 | } 55 | 56 | export function isRef(ref) { 57 | return !!ref.__v_isRef; 58 | } 59 | 60 | export function unref(ref) { 61 | return isRef(ref) ? ref.value : ref; 62 | } 63 | 64 | export function proxyRefs(objectWithRefs) { 65 | return new Proxy(objectWithRefs, { 66 | get(target, key) { 67 | return unref(Reflect.get(target, key)); 68 | }, 69 | set(target, key, value) { 70 | if (isRef(target[key]) && !isRef(value)) { 71 | return (target[key].value = value); 72 | } else { 73 | return Reflect.set(target, key, value); 74 | } 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /packages/runtime-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue3/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 | "@tiny-vue3/reactivity": "workspace:^1.0.0", 14 | "@tiny-vue3/shared": "workspace:^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | export function provide(key, value) { 4 | // 存 5 | const currentInstance: any = getCurrentInstance(); 6 | 7 | if (currentInstance) { 8 | let { provides } = currentInstance; 9 | const parentProvides = currentInstance.parent.provides; 10 | 11 | // init 12 | if (provides === parentProvides) { 13 | provides = currentInstance.provides = Object.create(parentProvides); 14 | } 15 | 16 | provides[key] = value; 17 | } 18 | } 19 | 20 | export function inject(key, defaultValue) { 21 | // 取 22 | const currentInstance: any = getCurrentInstance(); 23 | 24 | if (currentInstance) { 25 | const parentProvides = currentInstance.parent.provides; 26 | if (key in parentProvides) { 27 | return parentProvides[key]; 28 | } else if (typeof defaultValue === "function") { 29 | return defaultValue(); 30 | } 31 | 32 | return defaultValue; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs, shallowReadonly } from "@tiny-vue3/reactivity"; 2 | 3 | import { emit } from "./componentEmit"; 4 | import { initProps } from "./componentProps"; 5 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 6 | import { initSlots } from "./componentSlots"; 7 | 8 | export function createComponentInstance(vnode, parent) { 9 | console.log("createComponentInstance", parent); 10 | const component = { 11 | vnode, 12 | type: vnode.type, 13 | next: null, 14 | setupState: {}, 15 | props: {}, 16 | slots: {}, 17 | provides: parent ? parent.provides : {}, 18 | parent, 19 | isMounted: false, 20 | subTree: {}, 21 | emit: () => {}, 22 | }; 23 | 24 | component.emit = emit.bind(null, component) as any; 25 | 26 | return component; 27 | } 28 | 29 | export function setupComponent(instance) { 30 | initProps(instance, instance.vnode.props); 31 | initSlots(instance, instance.vnode.children); 32 | 33 | setupStatefulComponent(instance); 34 | } 35 | 36 | function setupStatefulComponent(instance: any) { 37 | const Component = instance.type; 38 | 39 | // ctx 40 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 41 | 42 | const { setup } = Component; 43 | 44 | if (setup) { 45 | setCurrentInstance(instance); 46 | const setupResult = setup(shallowReadonly(instance.props), { 47 | emit: instance.emit, 48 | }); 49 | 50 | setCurrentInstance(null); 51 | 52 | handleSetupResult(instance, setupResult); 53 | } 54 | } 55 | 56 | function handleSetupResult(instance, setupResult: any) { 57 | // function Object 58 | // TODO function 渲染函数 59 | 60 | if (typeof setupResult === "object") { 61 | instance.setupState = proxyRefs(setupResult); 62 | } 63 | 64 | finishComponentSetup(instance); 65 | } 66 | 67 | function finishComponentSetup(instance: any) { 68 | const Component = instance.type; 69 | 70 | if (compiler && !Component.render) { 71 | if (Component.template) { 72 | Component.render = compiler(Component.template); 73 | } 74 | } 75 | 76 | instance.render = Component.render; 77 | } 78 | 79 | let currentInstance = null; 80 | export function getCurrentInstance() { 81 | return currentInstance; 82 | } 83 | 84 | export function setCurrentInstance(instance) { 85 | currentInstance = instance; 86 | } 87 | 88 | let compiler; 89 | 90 | export function registerRuntimeCompiler(_compiler) { 91 | compiler = _compiler; 92 | } 93 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "@tiny-vue3/shared"; 2 | 3 | export function emit(instance, event, ...args) { 4 | console.log("emit", event); 5 | 6 | // instance.props -> event 7 | const { props } = instance; 8 | 9 | // TPP 10 | // 先去写特定行为 -> 重构成通用行为 11 | // add 12 | const handlerName = toHandlerKey(camelize(event)); 13 | const handler = props[handlerName]; 14 | 15 | handler && handler(...args); 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | 3 | instance.props = rawProps || {}; 4 | // attrs 5 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "@tiny-vue3/shared"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | $props: (i) => i.props, 7 | }; 8 | 9 | export const PublicInstanceProxyHandlers = { 10 | get({ _: instance }, key) { 11 | // setupState 12 | const { setupState, props } = instance; 13 | // if (key in setupState) { 14 | // return setupState[key]; 15 | // } 16 | 17 | if (hasOwn(setupState, key)) { 18 | return setupState[key]; 19 | } else if (hasOwn(props, key)) { 20 | return props[key]; 21 | } 22 | 23 | const publicGetter = publicPropertiesMap[key]; 24 | if (publicGetter) { 25 | return publicGetter(instance); 26 | } 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "@tiny-vue3/shared"; 2 | 3 | export function initSlots(instance, children) { 4 | // children object 5 | // instance.slots = Array.isArray(children) ? children : [children]; 6 | const { vnode } = instance; 7 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 8 | normalizeObjectSlots(children, instance.slots); 9 | } 10 | } 11 | 12 | function normalizeObjectSlots(children, slots) { 13 | for (const key in children) { 14 | const value = children[key]; 15 | 16 | slots[key] = (props) => normalizeSlotValue(value(props)); 17 | } 18 | } 19 | 20 | function normalizeSlotValue(value) { 21 | return Array.isArray(value) ? value : [value]; 22 | } 23 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(prevVNode, nextVNode) { 2 | const { props: prevProps } = prevVNode; 3 | const { props: nextProps } = nextVNode; 4 | 5 | for (const key in nextProps) { 6 | if (nextProps[key] !== prevProps[key]) { 7 | return true; 8 | } 9 | } 10 | 11 | return false; 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime-core/src/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function createAppAPI(render) { 4 | return function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | // 先 vode 8 | // 基于 vode 做处理 9 | 10 | const vnode = createVNode(rootComponent); 11 | 12 | render(vnode, rootContainer); 13 | }, 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function h (type, props?, children?) { 4 | return createVNode(type, props, children) 5 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, name, props) { 4 | // vnode 5 | const slot = slots[name]; 6 | 7 | if (slot) { 8 | console.log("slot", slot); 9 | if (typeof slot === 'function') { 10 | return createVNode(Fragment, {}, slot(props)); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from "./h"; 2 | export { renderSlots } from "./helpers/renderSlots"; 3 | export { createTextVNode, createElementVNode } from "./vnode"; 4 | export { getCurrentInstance, registerRuntimeCompiler } from "./component"; 5 | export { provide, inject } from './apiInject'; 6 | export { createRenderer } from './renderer'; 7 | export { nextTick } from './scheduler'; 8 | export { toDisplayString } from '@tiny-vue3/shared' 9 | export * from "@tiny-vue3/reactivity"; 10 | -------------------------------------------------------------------------------- /packages/runtime-core/src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "@tiny-vue3/reactivity"; 2 | import { EMPTY_OBJ, ShapeFlags } from "@tiny-vue3/shared"; 3 | import { createComponentInstance, setupComponent } from "./component"; 4 | import { shouldUpdateComponent } from "./componentUpdateUtils"; 5 | import { createAppAPI } from "./createApp"; 6 | import { queueJobs } from "./scheduler"; 7 | import { Fragment, Text } from "./vnode"; 8 | 9 | export function createRenderer(options) { 10 | const { 11 | createElement: hostCreateElement, 12 | patchProp: hostPatchProp, 13 | insert: hostInsert, 14 | remove: hostRemove, 15 | setElementText: hostSetElementText, 16 | } = options; 17 | 18 | function render(vnode, container) { 19 | patch(null, vnode, container, null, null); 20 | } 21 | 22 | // n1 -> old 23 | // n2 -> new 24 | function patch(n1, n2, container, parentComponent, anchor) { 25 | console.log('n1', n1) 26 | console.log("n2", n2); 27 | const { type, shapeFlag } = n2; 28 | 29 | switch (type) { 30 | case Fragment: 31 | processFragment(n1, n2, container, parentComponent, anchor); 32 | break; 33 | case Text: 34 | processText(n1, n2, container); 35 | break; 36 | 37 | default: 38 | if (shapeFlag & ShapeFlags.ELEMENT) { 39 | processElement(n1, n2, container, parentComponent, anchor); 40 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 41 | processComponent(n1, n2, container, parentComponent, anchor); 42 | } 43 | break; 44 | } 45 | } 46 | 47 | function processText(n1, n2: any, container: any) { 48 | const { children } = n2; 49 | const textNode = (n2.el = document.createTextNode(children)); 50 | container.append(textNode); 51 | } 52 | 53 | function processFragment( 54 | n1, 55 | n2: any, 56 | container: any, 57 | parentComponent, 58 | anchor 59 | ) { 60 | mountChildren(n2.children, container, parentComponent, anchor); 61 | } 62 | 63 | function processElement( 64 | n1, 65 | n2: any, 66 | container: any, 67 | parentComponent, 68 | anchor 69 | ) { 70 | if (!n1) { 71 | mountElement(n1, n2, container, parentComponent, anchor); 72 | } else { 73 | patchElement(n1, n2, container, parentComponent, anchor); 74 | } 75 | } 76 | 77 | function patchElement(n1, n2, container, parentComponent, anchor) { 78 | const oldProps = n1.props || EMPTY_OBJ; 79 | const newProps = n2.props || EMPTY_OBJ; 80 | 81 | const el = (n2.el = n1.el); 82 | patchChildren(n1, n2, el, parentComponent, anchor); 83 | patchProps(el, oldProps, newProps); 84 | } 85 | 86 | function patchChildren(n1, n2, container, parentComponent, anchor) { 87 | const { shapeFlag, children: c2 } = n2; 88 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 89 | 90 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 91 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 92 | // 1. 把老的 children 清空 93 | unmountChildren(n1.children); 94 | } 95 | if (c1 !== c2) { 96 | hostSetElementText(container, c2); 97 | } 98 | } else { 99 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 100 | hostSetElementText(container, ""); 101 | mountChildren(c2, container, parentComponent, anchor); 102 | } else { 103 | // array diff array 104 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 105 | } 106 | } 107 | } 108 | 109 | function patchKeyedChildren( 110 | c1, 111 | c2, 112 | container, 113 | parentComponent, 114 | parentAnchor 115 | ) { 116 | let i = 0; 117 | const l2 = c2.length; 118 | let e1 = c1.length - 1; 119 | let e2 = l2 - 1; 120 | 121 | function isSomeVNodeType(n1, n2) { 122 | return n1.type === n2.type && n1.key === n2.key; 123 | } 124 | 125 | // 左侧 126 | while (i <= e1 && i <= e2) { 127 | const n1 = c1[i]; 128 | const n2 = c2[i]; 129 | 130 | if (isSomeVNodeType(n1, n2)) { 131 | patch(n1, n2, container, parentComponent, parentAnchor); 132 | } else { 133 | break; 134 | } 135 | i++; 136 | } 137 | 138 | // 右侧 139 | while (i <= e1 && i <= e2) { 140 | const n1 = c1[e1]; 141 | const n2 = c2[e2]; 142 | 143 | if (isSomeVNodeType(n1, n2)) { 144 | patch(n1, n2, container, parentComponent, parentAnchor); 145 | } else { 146 | break; 147 | } 148 | e1--; 149 | e2--; 150 | } 151 | 152 | // 新的比老的多 创建 153 | if (i > e1) { 154 | // 左侧右侧逻辑共用 155 | if (i <= e2) { 156 | const nextPos = e2 + 1; 157 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 158 | while (i <= e2) { 159 | patch(null, c2[i], container, parentComponent, anchor); 160 | i++; 161 | } 162 | } 163 | } else if (i > e2) { 164 | // 新的比老的少 删除 左侧右侧逻辑共用 165 | while (i <= e1) { 166 | hostRemove(c1[i].el); 167 | i++; 168 | } 169 | } else { 170 | // 中间对比 171 | let s1 = i; 172 | let s2 = i; 173 | 174 | const toBePatched = e2 - s2 + 1; 175 | let patched = 0; 176 | const keyToNewIndexMap = new Map(); 177 | const newIndexToOldIndexMap = new Array(toBePatched); 178 | let moved = false; 179 | let maxNewIndexSoFar = 0; 180 | 181 | for (let i = 0; i < toBePatched; i++) { 182 | newIndexToOldIndexMap[i] = 0; 183 | } 184 | 185 | for (let i = s2; i <= e2; i++) { 186 | const nextChild = c2[i]; 187 | keyToNewIndexMap.set(nextChild.key, i); 188 | } 189 | 190 | for (let i = s1; i <= e1; i++) { 191 | const prevChild = c1[i]; 192 | 193 | if (patched >= toBePatched) { 194 | // patched 的数量大于等于 新的中间节点数量,直接把剩余的老的中间节点删除 195 | hostRemove(prevChild.el); 196 | continue; 197 | } 198 | 199 | let newIndex; 200 | // null undefined 201 | if (prevChild.key != null) { 202 | newIndex = keyToNewIndexMap.get(prevChild.key); 203 | } else { 204 | for (let j = s2; j <= e2; j++) { 205 | if (isSomeVNodeType(prevChild, c2[j])) { 206 | newIndex = j; 207 | 208 | break; 209 | } 210 | } 211 | } 212 | 213 | if (newIndex === undefined) { 214 | hostRemove(prevChild.el); 215 | } else { 216 | if (newIndex >= maxNewIndexSoFar) { 217 | maxNewIndexSoFar = newIndex; 218 | } else { 219 | moved = true; 220 | } 221 | 222 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 223 | patch(prevChild, c2[newIndex], container, parentComponent, null); 224 | patched++; 225 | } 226 | } 227 | 228 | const increasingNewIndexSequence = moved 229 | ? getSequence(newIndexToOldIndexMap) 230 | : []; 231 | let j = increasingNewIndexSequence.length - 1; 232 | 233 | for (let i = toBePatched - 1; i >= 0; i--) { 234 | const nextIndex = i + s2; 235 | const nextChild = c2[nextIndex]; 236 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 237 | 238 | if (newIndexToOldIndexMap[i] === 0) { 239 | patch(null, nextChild, container, parentComponent, anchor); 240 | } else if (moved) { 241 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 242 | hostInsert(nextChild.el, container, anchor); 243 | } else { 244 | j--; 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | function unmountChildren(children) { 252 | for (let i = 0; i < children.length; i++) { 253 | const el = children[i].el; 254 | // remove 255 | hostRemove(el); 256 | } 257 | } 258 | 259 | function patchProps(el, oldProps, newProps) { 260 | if (oldProps !== newProps) { 261 | for (const key in newProps) { 262 | const prevProp = oldProps[key]; 263 | const nextProp = newProps[key]; 264 | 265 | if (prevProp !== nextProp) { 266 | hostPatchProp(el, key, prevProp, nextProp); 267 | } 268 | } 269 | 270 | if (oldProps !== EMPTY_OBJ) { 271 | for (const key in oldProps) { 272 | if (!(key in newProps)) { 273 | hostPatchProp(el, key, oldProps[key], null); 274 | } 275 | } 276 | } 277 | } 278 | } 279 | 280 | function mountElement(n1, n2: any, container: any, parentComponent, anchor) { 281 | //canvas 282 | // new Element() 283 | const el = (n2.el = hostCreateElement(n2.type)); 284 | 285 | const { children, shapeFlag } = n2; 286 | 287 | // children 288 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 289 | el.textContent = children; 290 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 291 | mountChildren(n2.children, el, parentComponent, anchor); 292 | } 293 | 294 | // props 295 | const { props } = n2; 296 | for (const key in props) { 297 | const val = props[key]; 298 | hostPatchProp(el, key, null, val); 299 | } 300 | // canvs 301 | // el.x = 10 302 | 303 | // container.append(el); 304 | // addChild() 305 | hostInsert(el, container, anchor); 306 | } 307 | 308 | function mountChildren(children, container, parentComponent, anchor) { 309 | children.forEach((v) => { 310 | patch(null, v, container, parentComponent, anchor); 311 | }); 312 | } 313 | 314 | function processComponent( 315 | n1, 316 | n2: any, 317 | container: any, 318 | parentComponent, 319 | anchor 320 | ) { 321 | if (!n1) { 322 | mountComponent(n2, container, parentComponent, anchor); 323 | } else { 324 | updateComponent(n1, n2); 325 | } 326 | } 327 | 328 | function updateComponent(n1, n2) { 329 | const instance = (n2.component = n1.component); 330 | if (shouldUpdateComponent(n1, n2)) { 331 | instance.next = n2; 332 | instance.update(); 333 | } else { 334 | n2.el = n1.el; 335 | instance.vnode = n2; 336 | } 337 | } 338 | 339 | function mountComponent( 340 | initialVNode: any, 341 | container, 342 | parentComponent, 343 | anchor 344 | ) { 345 | const instance = (initialVNode.component = createComponentInstance( 346 | initialVNode, 347 | parentComponent 348 | )); 349 | 350 | setupComponent(instance); 351 | setupRenderEffect(instance, initialVNode, container, anchor); 352 | } 353 | 354 | function setupRenderEffect(instance: any, initialVNode, container, anchor) { 355 | instance.update = effect(() => { 356 | if (!instance.isMounted) { 357 | console.log("init"); 358 | const { proxy } = instance; 359 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 360 | 361 | patch(null, subTree, container, instance, anchor); 362 | 363 | initialVNode.el = subTree.el; 364 | 365 | instance.isMounted = true; 366 | } else { 367 | console.log("update"); 368 | // 需要一个 vnode 369 | const { next, vnode } = instance; 370 | if (next) { 371 | next.el = vnode.el; 372 | updateComponentPreRender(instance, next); 373 | } 374 | const { proxy } = instance; 375 | const subTree = instance.render.call(proxy, proxy); 376 | const prevSubTree = instance.subTree; 377 | instance.subTree = subTree; 378 | 379 | patch(prevSubTree, subTree, container, instance, anchor); 380 | } 381 | }, { 382 | scheduler() { 383 | console.log('update scheduler') 384 | queueJobs(instance.update); 385 | } 386 | }); 387 | } 388 | 389 | return { 390 | createApp: createAppAPI(render), 391 | }; 392 | } 393 | 394 | function updateComponentPreRender(instance, nextVNode) { 395 | instance.vnode = nextVNode; 396 | instance.next = null; 397 | instance.props = nextVNode.props; 398 | } 399 | 400 | // https://en.wikipedia.org/wiki/Longest_increasing_subsequence 401 | function getSequence(arr: number[]): number[] { 402 | const p = arr.slice(); 403 | const result = [0]; 404 | let i, j, u, v, c; 405 | const len = arr.length; 406 | for (i = 0; i < len; i++) { 407 | const arrI = arr[i]; 408 | if (arrI !== 0) { 409 | j = result[result.length - 1]; 410 | if (arr[j] < arrI) { 411 | p[i] = j; 412 | result.push(i); 413 | continue; 414 | } 415 | u = 0; 416 | v = result.length - 1; 417 | while (u < v) { 418 | c = (u + v) >> 1; 419 | if (arr[result[c]] < arrI) { 420 | u = c + 1; 421 | } else { 422 | v = c; 423 | } 424 | } 425 | if (arrI < arr[result[u]]) { 426 | if (u > 0) { 427 | p[i] = result[u - 1]; 428 | } 429 | result[u] = i; 430 | } 431 | } 432 | } 433 | u = result.length; 434 | v = result[u - 1]; 435 | while (u-- > 0) { 436 | result[u] = v; 437 | v = p[v]; 438 | } 439 | return result; 440 | } 441 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = []; 2 | 3 | const p = Promise.resolve(); 4 | let isFlushPending = false; 5 | 6 | export function nextTick(fn) { 7 | return fn ? p.then(fn) : p; 8 | } 9 | 10 | export function queueJobs(job) { 11 | if (!queue.includes(job)) { 12 | queue.push(job); 13 | } 14 | 15 | queueFlush(); 16 | } 17 | 18 | function queueFlush() { 19 | if (isFlushPending) return; 20 | isFlushPending = true; 21 | 22 | nextTick(flushJob); 23 | } 24 | 25 | function flushJob() { 26 | isFlushPending = false; 27 | let job; 28 | while ((job = queue.shift())) { 29 | job && job(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "@tiny-vue3/shared"; 2 | 3 | export const Fragment = Symbol("Fragment"); 4 | export const Text = Symbol("Text"); 5 | 6 | export { 7 | createVNode as createElementVNode 8 | } 9 | 10 | export function createVNode(type, props?, children?) { 11 | const vnode = { 12 | type, 13 | props, 14 | children, 15 | component: null, 16 | key: props && props.key, 17 | shapeFlag: getShapeFlag(type), 18 | el: null, 19 | }; 20 | 21 | // chilren 22 | if (typeof children === "string") { 23 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 24 | } else if (Array.isArray(children)) { 25 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 26 | } 27 | 28 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 29 | if (typeof children === "object") { 30 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 31 | } 32 | } 33 | 34 | return vnode; 35 | } 36 | 37 | export function createTextVNode(text: string) { 38 | return createVNode(Text, {}, text); 39 | } 40 | 41 | function getShapeFlag(type) { 42 | return typeof type === "string" 43 | ? ShapeFlags.ELEMENT 44 | : ShapeFlags.STATEFUL_COMPONENT; 45 | } 46 | -------------------------------------------------------------------------------- /packages/runtime-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue3/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 | "@tiny-vue3/runtime-core": "workspace:^1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "@tiny-vue3/runtime-core"; 2 | 3 | function createElement(type) { 4 | console.log("createElement ----------"); 5 | return document.createElement(type); 6 | } 7 | function patchProp(el, key, prevVal, nextVal) { 8 | console.log("patchProp ----------"); 9 | const isOn = (key: string) => /^on[A-Z]/.test(key); 10 | if (isOn(key)) { 11 | const event = key.slice(2).toLowerCase(); 12 | el.addEventListener(event, nextVal); 13 | } else { 14 | if (nextVal === undefined || nextVal === null) { 15 | el.removeAttribute(key); 16 | } else { 17 | el.setAttribute(key, nextVal); 18 | } 19 | } 20 | } 21 | function insert(child, parent, anchor) { 22 | console.log("insert ----------", anchor); 23 | parent.insertBefore(child, anchor || null); 24 | } 25 | 26 | function remove(child) { 27 | console.log("remove ----------", child); 28 | const parent = child.parentNode; 29 | 30 | if (parent) { 31 | parent.removeChild(child); 32 | } 33 | } 34 | 35 | function setElementText(el, text) { 36 | el.textContent = text; 37 | } 38 | 39 | const renderer: any = createRenderer({ 40 | createElement, 41 | patchProp, 42 | insert, 43 | remove, 44 | setElementText, 45 | }); 46 | 47 | export function createApp(...args) { 48 | return renderer.createApp(...args); 49 | } 50 | 51 | export * from "@tiny-vue3/runtime-core"; 52 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue3/shared", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/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/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | // 0000 2 | // 0001 -> element 3 | // 0010 -> stateful_component 4 | // 0100 -> text_children 5 | // 1000 -> array_children 6 | export const enum ShapeFlags { 7 | ELEMENT = 1, 8 | STATEFUL_COMPONENT = 1 << 1, 9 | TEXT_CHILDREN = 1 << 2, 10 | ARRAY_CHILDREN = 1 << 3, 11 | SLOT_CHILDREN = 1 << 4 12 | } 13 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toDisplayString' 2 | 3 | export const extend = Object.assign; 4 | export const EMPTY_OBJ = {}; 5 | 6 | export const isObject = (value) => { 7 | return value !== null && typeof value === "object"; 8 | }; 9 | 10 | export const isString = (value) => typeof value === "string"; 11 | 12 | export const hasChanged = (val, newValue) => { 13 | return !Object.is(val, newValue); 14 | }; 15 | 16 | export const hasOwn = (val, key) => 17 | Object.prototype.hasOwnProperty.call(val, key); 18 | 19 | export const camelize = (str: string) => { 20 | return str.replace(/-(\w)/g, (_, c: string) => { 21 | return c ? c.toUpperCase() : ""; 22 | }); 23 | }; 24 | export const capitalize = (str: string) => { 25 | return str.charAt(0).toUpperCase() + str.slice(1); 26 | }; 27 | 28 | export const toHandlerKey = (str: string) => { 29 | return str ? "on" + capitalize(str) : ""; 30 | }; 31 | 32 | export { ShapeFlags } from './ShapeFlags' -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(value) { 2 | return String(value) 3 | } -------------------------------------------------------------------------------- /packages/vue/dist/tiny-vue3.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | const Fragment = Symbol("Fragment"); 6 | const Text = Symbol("Text"); 7 | function createVNode(type, props, children) { 8 | const vnode = { 9 | type, 10 | props, 11 | children, 12 | component: null, 13 | key: props && props.key, 14 | shapeFlag: getShapeFlag(type), 15 | el: null, 16 | }; 17 | // chilren 18 | if (typeof children === "string") { 19 | vnode.shapeFlag |= 4 /* ShapeFlags.TEXT_CHILDREN */; 20 | } 21 | else if (Array.isArray(children)) { 22 | vnode.shapeFlag |= 8 /* ShapeFlags.ARRAY_CHILDREN */; 23 | } 24 | if (vnode.shapeFlag & 2 /* ShapeFlags.STATEFUL_COMPONENT */) { 25 | if (typeof children === "object") { 26 | vnode.shapeFlag |= 16 /* ShapeFlags.SLOT_CHILDREN */; 27 | } 28 | } 29 | return vnode; 30 | } 31 | function createTextVNode(text) { 32 | return createVNode(Text, {}, text); 33 | } 34 | function getShapeFlag(type) { 35 | return typeof type === "string" 36 | ? 1 /* ShapeFlags.ELEMENT */ 37 | : 2 /* ShapeFlags.STATEFUL_COMPONENT */; 38 | } 39 | 40 | function h(type, props, children) { 41 | return createVNode(type, props, children); 42 | } 43 | 44 | function renderSlots(slots, name, props) { 45 | // vnode 46 | const slot = slots[name]; 47 | if (slot) { 48 | console.log("slot", slot); 49 | if (typeof slot === 'function') { 50 | return createVNode(Fragment, {}, slot(props)); 51 | } 52 | } 53 | } 54 | 55 | function toDisplayString(value) { 56 | return String(value); 57 | } 58 | 59 | const extend = Object.assign; 60 | const EMPTY_OBJ = {}; 61 | const isObject = (value) => { 62 | return value !== null && typeof value === "object"; 63 | }; 64 | const isString = (value) => typeof value === "string"; 65 | const hasChanged = (val, newValue) => { 66 | return !Object.is(val, newValue); 67 | }; 68 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 69 | const camelize = (str) => { 70 | return str.replace(/-(\w)/g, (_, c) => { 71 | return c ? c.toUpperCase() : ""; 72 | }); 73 | }; 74 | const capitalize = (str) => { 75 | return str.charAt(0).toUpperCase() + str.slice(1); 76 | }; 77 | const toHandlerKey = (str) => { 78 | return str ? "on" + capitalize(str) : ""; 79 | }; 80 | 81 | let activeEffect; 82 | let shouldTrack = false; 83 | class ReactiveEffect { 84 | constructor(fn, scheduler) { 85 | this.scheduler = scheduler; 86 | this.deps = []; 87 | this.active = true; 88 | this._fn = fn; 89 | } 90 | run() { 91 | if (!this.active) { 92 | return this._fn(); 93 | } 94 | // 应该收集 95 | activeEffect = this; 96 | shouldTrack = true; 97 | const r = this._fn(); 98 | // 重置 99 | shouldTrack = false; 100 | return r; 101 | } 102 | stop() { 103 | if (this.active) { 104 | cleanupEffect(this); 105 | if (this.onStop) { 106 | this.onStop(); 107 | } 108 | this.active = false; 109 | } 110 | } 111 | } 112 | function cleanupEffect(effect) { 113 | effect.deps.forEach((dep) => { 114 | dep.delete(effect); 115 | }); 116 | // 把 effect.deps 清空 117 | effect.deps.length = 0; 118 | } 119 | const targetMap = new Map(); 120 | function track(target, key) { 121 | if (!isTracking()) 122 | return; 123 | // target -> key -> dep 124 | let depsMap = targetMap.get(target); 125 | if (!depsMap) { 126 | depsMap = new Map(); 127 | targetMap.set(target, depsMap); 128 | } 129 | let dep = depsMap.get(key); 130 | if (!dep) { 131 | dep = new Set(); 132 | depsMap.set(key, dep); 133 | } 134 | trackEffects(dep); 135 | } 136 | function trackEffects(dep) { 137 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 138 | if (dep.has(activeEffect)) 139 | return; 140 | dep.add(activeEffect); 141 | activeEffect.deps.push(dep); 142 | } 143 | function isTracking() { 144 | return shouldTrack && activeEffect !== undefined; 145 | } 146 | function trigger(target, key) { 147 | const depsMap = targetMap.get(target); 148 | const dep = depsMap.get(key); 149 | triggerEffects(dep); 150 | } 151 | function triggerEffects(dep) { 152 | for (const effect of dep) { 153 | if (effect.scheduler) { 154 | effect.scheduler(); 155 | } 156 | else { 157 | effect.run(); 158 | } 159 | } 160 | } 161 | function effect(fn, options = {}) { 162 | const _effect = new ReactiveEffect(fn, options.scheduler); 163 | extend(_effect, options); 164 | _effect.run(); 165 | const runner = _effect.run.bind(_effect); 166 | runner.effect = _effect; 167 | return runner; 168 | } 169 | function stop(runner) { 170 | runner.effect.stop(); 171 | } 172 | 173 | const get = createGetter(); 174 | const set = createSetter(); 175 | const readonlyGet = createGetter(true); 176 | const shallowReactiveGet = createGetter(false, true); 177 | const shallowReadonlyGet = createGetter(true, true); 178 | function createGetter(isReadonly = false, shallow = false) { 179 | return function (target, key) { 180 | if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) { 181 | return !isReadonly; 182 | } 183 | else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) { 184 | return isReadonly; 185 | } 186 | const res = Reflect.get(target, key); 187 | if (!isReadonly) { 188 | // 依赖收集 189 | track(target, key); 190 | } 191 | if (shallow) { 192 | return res; 193 | } 194 | if (isObject(res)) { 195 | return isReadonly ? readonly(res) : reactive(res); 196 | } 197 | return res; 198 | }; 199 | } 200 | function createSetter() { 201 | return function set(target, key, value) { 202 | const res = Reflect.set(target, key, value); 203 | // 依赖触发 204 | trigger(target, key); 205 | return res; 206 | }; 207 | } 208 | const mutableHandlers = { 209 | get, 210 | set, 211 | }; 212 | const shallowReactiveHandlers = extend({}, mutableHandlers, { 213 | get: shallowReactiveGet, 214 | }); 215 | const readonlyHandlers = { 216 | get: readonlyGet, 217 | set(target, key) { 218 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 219 | return true; 220 | }, 221 | }; 222 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 223 | get: shallowReadonlyGet, 224 | }); 225 | 226 | function reactive(raw) { 227 | return createReactiveObject(raw, mutableHandlers); 228 | } 229 | function shallowReactive(raw) { 230 | return createReactiveObject(raw, shallowReactiveHandlers); 231 | } 232 | function readonly(raw) { 233 | return createReactiveObject(raw, readonlyHandlers); 234 | } 235 | function shallowReadonly(raw) { 236 | return createReactiveObject(raw, shallowReadonlyHandlers); 237 | } 238 | function createReactiveObject(target, baseHandlerss) { 239 | if (!isObject(target)) { 240 | console.warn(`target ${target} 必须是一个对象`); 241 | return target; 242 | } 243 | return new Proxy(target, baseHandlerss); 244 | } 245 | function isReactive(value) { 246 | return !!value["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */]; 247 | } 248 | function isReadonly(value) { 249 | return !!value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]; 250 | } 251 | function isProxy(value) { 252 | return isReactive(value) || isReadonly(value); 253 | } 254 | 255 | class RefImpl { 256 | constructor(value, __v_isShallow) { 257 | this.__v_isShallow = __v_isShallow; 258 | this.__v_isRef = true; 259 | this._value = __v_isShallow ? value : convert(value); 260 | this._rawValue = value; 261 | this.dep = new Set(); 262 | } 263 | get value() { 264 | trackRefValue(this); 265 | return this._value; 266 | } 267 | set value(newValue) { 268 | if (hasChanged(this._rawValue, newValue)) { 269 | this._value = this.__v_isShallow ? newValue : convert(newValue); 270 | this._rawValue = newValue; 271 | triggerEffects(this.dep); 272 | } 273 | } 274 | } 275 | function createRef(rawValue, shallow) { 276 | if (isRef(rawValue)) { 277 | return rawValue; 278 | } 279 | return new RefImpl(rawValue, shallow); 280 | } 281 | function ref(value) { 282 | return createRef(value, false); 283 | } 284 | function convert(value) { 285 | return isObject(value) ? reactive(value) : value; 286 | } 287 | function trackRefValue(ref) { 288 | if (isTracking()) { 289 | trackEffects(ref.dep); 290 | } 291 | } 292 | function isRef(ref) { 293 | return !!ref.__v_isRef; 294 | } 295 | function unref(ref) { 296 | return isRef(ref) ? ref.value : ref; 297 | } 298 | function proxyRefs(objectWithRefs) { 299 | return new Proxy(objectWithRefs, { 300 | get(target, key) { 301 | return unref(Reflect.get(target, key)); 302 | }, 303 | set(target, key, value) { 304 | if (isRef(target[key]) && !isRef(value)) { 305 | return (target[key].value = value); 306 | } 307 | else { 308 | return Reflect.set(target, key, value); 309 | } 310 | }, 311 | }); 312 | } 313 | 314 | function emit(instance, event, ...args) { 315 | console.log("emit", event); 316 | // instance.props -> event 317 | const { props } = instance; 318 | // TPP 319 | // 先去写特定行为 -> 重构成通用行为 320 | // add 321 | const handlerName = toHandlerKey(camelize(event)); 322 | const handler = props[handlerName]; 323 | handler && handler(...args); 324 | } 325 | 326 | function initProps(instance, rawProps) { 327 | instance.props = rawProps || {}; 328 | // attrs 329 | } 330 | 331 | const publicPropertiesMap = { 332 | $el: (i) => i.vnode.el, 333 | $slots: (i) => i.slots, 334 | $props: (i) => i.props, 335 | }; 336 | const PublicInstanceProxyHandlers = { 337 | get({ _: instance }, key) { 338 | // setupState 339 | const { setupState, props } = instance; 340 | // if (key in setupState) { 341 | // return setupState[key]; 342 | // } 343 | if (hasOwn(setupState, key)) { 344 | return setupState[key]; 345 | } 346 | else if (hasOwn(props, key)) { 347 | return props[key]; 348 | } 349 | const publicGetter = publicPropertiesMap[key]; 350 | if (publicGetter) { 351 | return publicGetter(instance); 352 | } 353 | }, 354 | }; 355 | 356 | function initSlots(instance, children) { 357 | // children object 358 | // instance.slots = Array.isArray(children) ? children : [children]; 359 | const { vnode } = instance; 360 | if (vnode.shapeFlag & 16 /* ShapeFlags.SLOT_CHILDREN */) { 361 | normalizeObjectSlots(children, instance.slots); 362 | } 363 | } 364 | function normalizeObjectSlots(children, slots) { 365 | for (const key in children) { 366 | const value = children[key]; 367 | slots[key] = (props) => normalizeSlotValue(value(props)); 368 | } 369 | } 370 | function normalizeSlotValue(value) { 371 | return Array.isArray(value) ? value : [value]; 372 | } 373 | 374 | function createComponentInstance(vnode, parent) { 375 | console.log("createComponentInstance", parent); 376 | const component = { 377 | vnode, 378 | type: vnode.type, 379 | next: null, 380 | setupState: {}, 381 | props: {}, 382 | slots: {}, 383 | provides: parent ? parent.provides : {}, 384 | parent, 385 | isMounted: false, 386 | subTree: {}, 387 | emit: () => { }, 388 | }; 389 | component.emit = emit.bind(null, component); 390 | return component; 391 | } 392 | function setupComponent(instance) { 393 | initProps(instance, instance.vnode.props); 394 | initSlots(instance, instance.vnode.children); 395 | setupStatefulComponent(instance); 396 | } 397 | function setupStatefulComponent(instance) { 398 | const Component = instance.type; 399 | // ctx 400 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 401 | const { setup } = Component; 402 | if (setup) { 403 | setCurrentInstance(instance); 404 | const setupResult = setup(shallowReadonly(instance.props), { 405 | emit: instance.emit, 406 | }); 407 | setCurrentInstance(null); 408 | handleSetupResult(instance, setupResult); 409 | } 410 | } 411 | function handleSetupResult(instance, setupResult) { 412 | // function Object 413 | // TODO function 渲染函数 414 | if (typeof setupResult === "object") { 415 | instance.setupState = proxyRefs(setupResult); 416 | } 417 | finishComponentSetup(instance); 418 | } 419 | function finishComponentSetup(instance) { 420 | const Component = instance.type; 421 | if (compiler && !Component.render) { 422 | if (Component.template) { 423 | Component.render = compiler(Component.template); 424 | } 425 | } 426 | instance.render = Component.render; 427 | } 428 | let currentInstance = null; 429 | function getCurrentInstance() { 430 | return currentInstance; 431 | } 432 | function setCurrentInstance(instance) { 433 | currentInstance = instance; 434 | } 435 | let compiler; 436 | function registerRuntimeCompiler(_compiler) { 437 | compiler = _compiler; 438 | } 439 | 440 | function provide(key, value) { 441 | // 存 442 | const currentInstance = getCurrentInstance(); 443 | if (currentInstance) { 444 | let { provides } = currentInstance; 445 | const parentProvides = currentInstance.parent.provides; 446 | // init 447 | if (provides === parentProvides) { 448 | provides = currentInstance.provides = Object.create(parentProvides); 449 | } 450 | provides[key] = value; 451 | } 452 | } 453 | function inject(key, defaultValue) { 454 | // 取 455 | const currentInstance = getCurrentInstance(); 456 | if (currentInstance) { 457 | const parentProvides = currentInstance.parent.provides; 458 | if (key in parentProvides) { 459 | return parentProvides[key]; 460 | } 461 | else if (typeof defaultValue === "function") { 462 | return defaultValue(); 463 | } 464 | return defaultValue; 465 | } 466 | } 467 | 468 | function shouldUpdateComponent(prevVNode, nextVNode) { 469 | const { props: prevProps } = prevVNode; 470 | const { props: nextProps } = nextVNode; 471 | for (const key in nextProps) { 472 | if (nextProps[key] !== prevProps[key]) { 473 | return true; 474 | } 475 | } 476 | return false; 477 | } 478 | 479 | function createAppAPI(render) { 480 | return function createApp(rootComponent) { 481 | return { 482 | mount(rootContainer) { 483 | // 先 vode 484 | // 基于 vode 做处理 485 | const vnode = createVNode(rootComponent); 486 | render(vnode, rootContainer); 487 | }, 488 | }; 489 | }; 490 | } 491 | 492 | const queue = []; 493 | const p = Promise.resolve(); 494 | let isFlushPending = false; 495 | function nextTick(fn) { 496 | return fn ? p.then(fn) : p; 497 | } 498 | function queueJobs(job) { 499 | if (!queue.includes(job)) { 500 | queue.push(job); 501 | } 502 | queueFlush(); 503 | } 504 | function queueFlush() { 505 | if (isFlushPending) 506 | return; 507 | isFlushPending = true; 508 | nextTick(flushJob); 509 | } 510 | function flushJob() { 511 | isFlushPending = false; 512 | let job; 513 | while ((job = queue.shift())) { 514 | job && job(); 515 | } 516 | } 517 | 518 | function createRenderer(options) { 519 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 520 | function render(vnode, container) { 521 | patch(null, vnode, container, null, null); 522 | } 523 | // n1 -> old 524 | // n2 -> new 525 | function patch(n1, n2, container, parentComponent, anchor) { 526 | console.log('n1', n1); 527 | console.log("n2", n2); 528 | const { type, shapeFlag } = n2; 529 | switch (type) { 530 | case Fragment: 531 | processFragment(n1, n2, container, parentComponent, anchor); 532 | break; 533 | case Text: 534 | processText(n1, n2, container); 535 | break; 536 | default: 537 | if (shapeFlag & 1 /* ShapeFlags.ELEMENT */) { 538 | processElement(n1, n2, container, parentComponent, anchor); 539 | } 540 | else if (shapeFlag & 2 /* ShapeFlags.STATEFUL_COMPONENT */) { 541 | processComponent(n1, n2, container, parentComponent, anchor); 542 | } 543 | break; 544 | } 545 | } 546 | function processText(n1, n2, container) { 547 | const { children } = n2; 548 | const textNode = (n2.el = document.createTextNode(children)); 549 | container.append(textNode); 550 | } 551 | function processFragment(n1, n2, container, parentComponent, anchor) { 552 | mountChildren(n2.children, container, parentComponent, anchor); 553 | } 554 | function processElement(n1, n2, container, parentComponent, anchor) { 555 | if (!n1) { 556 | mountElement(n1, n2, container, parentComponent, anchor); 557 | } 558 | else { 559 | patchElement(n1, n2, container, parentComponent, anchor); 560 | } 561 | } 562 | function patchElement(n1, n2, container, parentComponent, anchor) { 563 | const oldProps = n1.props || EMPTY_OBJ; 564 | const newProps = n2.props || EMPTY_OBJ; 565 | const el = (n2.el = n1.el); 566 | patchChildren(n1, n2, el, parentComponent, anchor); 567 | patchProps(el, oldProps, newProps); 568 | } 569 | function patchChildren(n1, n2, container, parentComponent, anchor) { 570 | const { shapeFlag, children: c2 } = n2; 571 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 572 | if (shapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 573 | if (prevShapeFlag & 8 /* ShapeFlags.ARRAY_CHILDREN */) { 574 | // 1. 把老的 children 清空 575 | unmountChildren(n1.children); 576 | } 577 | if (c1 !== c2) { 578 | hostSetElementText(container, c2); 579 | } 580 | } 581 | else { 582 | if (prevShapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 583 | hostSetElementText(container, ""); 584 | mountChildren(c2, container, parentComponent, anchor); 585 | } 586 | else { 587 | // array diff array 588 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 589 | } 590 | } 591 | } 592 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 593 | let i = 0; 594 | const l2 = c2.length; 595 | let e1 = c1.length - 1; 596 | let e2 = l2 - 1; 597 | function isSomeVNodeType(n1, n2) { 598 | return n1.type === n2.type && n1.key === n2.key; 599 | } 600 | // 左侧 601 | while (i <= e1 && i <= e2) { 602 | const n1 = c1[i]; 603 | const n2 = c2[i]; 604 | if (isSomeVNodeType(n1, n2)) { 605 | patch(n1, n2, container, parentComponent, parentAnchor); 606 | } 607 | else { 608 | break; 609 | } 610 | i++; 611 | } 612 | // 右侧 613 | while (i <= e1 && i <= e2) { 614 | const n1 = c1[e1]; 615 | const n2 = c2[e2]; 616 | if (isSomeVNodeType(n1, n2)) { 617 | patch(n1, n2, container, parentComponent, parentAnchor); 618 | } 619 | else { 620 | break; 621 | } 622 | e1--; 623 | e2--; 624 | } 625 | // 新的比老的多 创建 626 | if (i > e1) { 627 | // 左侧右侧逻辑共用 628 | if (i <= e2) { 629 | const nextPos = e2 + 1; 630 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 631 | while (i <= e2) { 632 | patch(null, c2[i], container, parentComponent, anchor); 633 | i++; 634 | } 635 | } 636 | } 637 | else if (i > e2) { 638 | // 新的比老的少 删除 左侧右侧逻辑共用 639 | while (i <= e1) { 640 | hostRemove(c1[i].el); 641 | i++; 642 | } 643 | } 644 | else { 645 | // 中间对比 646 | let s1 = i; 647 | let s2 = i; 648 | const toBePatched = e2 - s2 + 1; 649 | let patched = 0; 650 | const keyToNewIndexMap = new Map(); 651 | const newIndexToOldIndexMap = new Array(toBePatched); 652 | let moved = false; 653 | let maxNewIndexSoFar = 0; 654 | for (let i = 0; i < toBePatched; i++) { 655 | newIndexToOldIndexMap[i] = 0; 656 | } 657 | for (let i = s2; i <= e2; i++) { 658 | const nextChild = c2[i]; 659 | keyToNewIndexMap.set(nextChild.key, i); 660 | } 661 | for (let i = s1; i <= e1; i++) { 662 | const prevChild = c1[i]; 663 | if (patched >= toBePatched) { 664 | // patched 的数量大于等于 新的中间节点数量,直接把剩余的老的中间节点删除 665 | hostRemove(prevChild.el); 666 | continue; 667 | } 668 | let newIndex; 669 | // null undefined 670 | if (prevChild.key != null) { 671 | newIndex = keyToNewIndexMap.get(prevChild.key); 672 | } 673 | else { 674 | for (let j = s2; j <= e2; j++) { 675 | if (isSomeVNodeType(prevChild, c2[j])) { 676 | newIndex = j; 677 | break; 678 | } 679 | } 680 | } 681 | if (newIndex === undefined) { 682 | hostRemove(prevChild.el); 683 | } 684 | else { 685 | if (newIndex >= maxNewIndexSoFar) { 686 | maxNewIndexSoFar = newIndex; 687 | } 688 | else { 689 | moved = true; 690 | } 691 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 692 | patch(prevChild, c2[newIndex], container, parentComponent, null); 693 | patched++; 694 | } 695 | } 696 | const increasingNewIndexSequence = moved 697 | ? getSequence(newIndexToOldIndexMap) 698 | : []; 699 | let j = increasingNewIndexSequence.length - 1; 700 | for (let i = toBePatched - 1; i >= 0; i--) { 701 | const nextIndex = i + s2; 702 | const nextChild = c2[nextIndex]; 703 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 704 | if (newIndexToOldIndexMap[i] === 0) { 705 | patch(null, nextChild, container, parentComponent, anchor); 706 | } 707 | else if (moved) { 708 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 709 | hostInsert(nextChild.el, container, anchor); 710 | } 711 | else { 712 | j--; 713 | } 714 | } 715 | } 716 | } 717 | } 718 | function unmountChildren(children) { 719 | for (let i = 0; i < children.length; i++) { 720 | const el = children[i].el; 721 | // remove 722 | hostRemove(el); 723 | } 724 | } 725 | function patchProps(el, oldProps, newProps) { 726 | if (oldProps !== newProps) { 727 | for (const key in newProps) { 728 | const prevProp = oldProps[key]; 729 | const nextProp = newProps[key]; 730 | if (prevProp !== nextProp) { 731 | hostPatchProp(el, key, prevProp, nextProp); 732 | } 733 | } 734 | if (oldProps !== EMPTY_OBJ) { 735 | for (const key in oldProps) { 736 | if (!(key in newProps)) { 737 | hostPatchProp(el, key, oldProps[key], null); 738 | } 739 | } 740 | } 741 | } 742 | } 743 | function mountElement(n1, n2, container, parentComponent, anchor) { 744 | //canvas 745 | // new Element() 746 | const el = (n2.el = hostCreateElement(n2.type)); 747 | const { children, shapeFlag } = n2; 748 | // children 749 | if (shapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 750 | el.textContent = children; 751 | } 752 | else if (shapeFlag & 8 /* ShapeFlags.ARRAY_CHILDREN */) { 753 | mountChildren(n2.children, el, parentComponent, anchor); 754 | } 755 | // props 756 | const { props } = n2; 757 | for (const key in props) { 758 | const val = props[key]; 759 | hostPatchProp(el, key, null, val); 760 | } 761 | // canvs 762 | // el.x = 10 763 | // container.append(el); 764 | // addChild() 765 | hostInsert(el, container, anchor); 766 | } 767 | function mountChildren(children, container, parentComponent, anchor) { 768 | children.forEach((v) => { 769 | patch(null, v, container, parentComponent, anchor); 770 | }); 771 | } 772 | function processComponent(n1, n2, container, parentComponent, anchor) { 773 | if (!n1) { 774 | mountComponent(n2, container, parentComponent, anchor); 775 | } 776 | else { 777 | updateComponent(n1, n2); 778 | } 779 | } 780 | function updateComponent(n1, n2) { 781 | const instance = (n2.component = n1.component); 782 | if (shouldUpdateComponent(n1, n2)) { 783 | instance.next = n2; 784 | instance.update(); 785 | } 786 | else { 787 | n2.el = n1.el; 788 | instance.vnode = n2; 789 | } 790 | } 791 | function mountComponent(initialVNode, container, parentComponent, anchor) { 792 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 793 | setupComponent(instance); 794 | setupRenderEffect(instance, initialVNode, container, anchor); 795 | } 796 | function setupRenderEffect(instance, initialVNode, container, anchor) { 797 | instance.update = effect(() => { 798 | if (!instance.isMounted) { 799 | console.log("init"); 800 | const { proxy } = instance; 801 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 802 | patch(null, subTree, container, instance, anchor); 803 | initialVNode.el = subTree.el; 804 | instance.isMounted = true; 805 | } 806 | else { 807 | console.log("update"); 808 | // 需要一个 vnode 809 | const { next, vnode } = instance; 810 | if (next) { 811 | next.el = vnode.el; 812 | updateComponentPreRender(instance, next); 813 | } 814 | const { proxy } = instance; 815 | const subTree = instance.render.call(proxy, proxy); 816 | const prevSubTree = instance.subTree; 817 | instance.subTree = subTree; 818 | patch(prevSubTree, subTree, container, instance, anchor); 819 | } 820 | }, { 821 | scheduler() { 822 | console.log('update scheduler'); 823 | queueJobs(instance.update); 824 | } 825 | }); 826 | } 827 | return { 828 | createApp: createAppAPI(render), 829 | }; 830 | } 831 | function updateComponentPreRender(instance, nextVNode) { 832 | instance.vnode = nextVNode; 833 | instance.next = null; 834 | instance.props = nextVNode.props; 835 | } 836 | // https://en.wikipedia.org/wiki/Longest_increasing_subsequence 837 | function getSequence(arr) { 838 | const p = arr.slice(); 839 | const result = [0]; 840 | let i, j, u, v, c; 841 | const len = arr.length; 842 | for (i = 0; i < len; i++) { 843 | const arrI = arr[i]; 844 | if (arrI !== 0) { 845 | j = result[result.length - 1]; 846 | if (arr[j] < arrI) { 847 | p[i] = j; 848 | result.push(i); 849 | continue; 850 | } 851 | u = 0; 852 | v = result.length - 1; 853 | while (u < v) { 854 | c = (u + v) >> 1; 855 | if (arr[result[c]] < arrI) { 856 | u = c + 1; 857 | } 858 | else { 859 | v = c; 860 | } 861 | } 862 | if (arrI < arr[result[u]]) { 863 | if (u > 0) { 864 | p[i] = result[u - 1]; 865 | } 866 | result[u] = i; 867 | } 868 | } 869 | } 870 | u = result.length; 871 | v = result[u - 1]; 872 | while (u-- > 0) { 873 | result[u] = v; 874 | v = p[v]; 875 | } 876 | return result; 877 | } 878 | 879 | function createElement(type) { 880 | console.log("createElement ----------"); 881 | return document.createElement(type); 882 | } 883 | function patchProp(el, key, prevVal, nextVal) { 884 | console.log("patchProp ----------"); 885 | const isOn = (key) => /^on[A-Z]/.test(key); 886 | if (isOn(key)) { 887 | const event = key.slice(2).toLowerCase(); 888 | el.addEventListener(event, nextVal); 889 | } 890 | else { 891 | if (nextVal === undefined || nextVal === null) { 892 | el.removeAttribute(key); 893 | } 894 | else { 895 | el.setAttribute(key, nextVal); 896 | } 897 | } 898 | } 899 | function insert(child, parent, anchor) { 900 | console.log("insert ----------", anchor); 901 | parent.insertBefore(child, anchor || null); 902 | } 903 | function remove(child) { 904 | console.log("remove ----------", child); 905 | const parent = child.parentNode; 906 | if (parent) { 907 | parent.removeChild(child); 908 | } 909 | } 910 | function setElementText(el, text) { 911 | el.textContent = text; 912 | } 913 | const renderer = createRenderer({ 914 | createElement, 915 | patchProp, 916 | insert, 917 | remove, 918 | setElementText, 919 | }); 920 | function createApp(...args) { 921 | return renderer.createApp(...args); 922 | } 923 | 924 | var runtimeDom = /*#__PURE__*/Object.freeze({ 925 | __proto__: null, 926 | createApp: createApp, 927 | h: h, 928 | renderSlots: renderSlots, 929 | createTextVNode: createTextVNode, 930 | createElementVNode: createVNode, 931 | getCurrentInstance: getCurrentInstance, 932 | registerRuntimeCompiler: registerRuntimeCompiler, 933 | provide: provide, 934 | inject: inject, 935 | createRenderer: createRenderer, 936 | nextTick: nextTick, 937 | toDisplayString: toDisplayString, 938 | ref: ref, 939 | proxyRefs: proxyRefs, 940 | unref: unref, 941 | isRef: isRef, 942 | reactive: reactive, 943 | readonly: readonly, 944 | shallowReactive: shallowReactive, 945 | shallowReadonly: shallowReadonly, 946 | isReactive: isReactive, 947 | isReadonly: isReadonly, 948 | isProxy: isProxy, 949 | effect: effect, 950 | stop: stop 951 | }); 952 | 953 | const TO_DISPLAY_STRING = Symbol("toDisplayString"); 954 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 955 | const helperMapName = { 956 | [TO_DISPLAY_STRING]: "toDisplayString", 957 | [CREATE_ELEMENT_VNODE]: 'createElementVNode' 958 | }; 959 | 960 | function generate(ast) { 961 | const context = createCodegenContext(); 962 | const { push } = context; 963 | genFunctionPreamble(ast, context); 964 | const functionName = "render"; 965 | const args = ["_ctx", "_cache"]; 966 | const signature = args.join(", "); 967 | push(`function ${functionName}(${signature}){`); 968 | push(`return `); 969 | genNode(ast.codegenNode, context); 970 | push("}"); 971 | return { 972 | code: context.code, 973 | }; 974 | } 975 | function genFunctionPreamble(ast, context) { 976 | const { push } = context; 977 | const VueBinging = "Vue"; 978 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}`; 979 | if (ast.helpers.length > 0) { 980 | push(`const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinging}`); 981 | } 982 | push("\n"); 983 | push("return "); 984 | } 985 | function createCodegenContext() { 986 | const context = { 987 | code: "", 988 | push(source) { 989 | context.code += source; 990 | }, 991 | helper(key) { 992 | return `_${helperMapName[key]}`; 993 | }, 994 | }; 995 | return context; 996 | } 997 | function genNode(node, context) { 998 | switch (node.type) { 999 | case 3 /* NodeTypes.TEXT */: 1000 | genText(node, context); 1001 | break; 1002 | case 0 /* NodeTypes.INTERPOLATION */: 1003 | genInterpolation(node, context); 1004 | break; 1005 | case 1 /* NodeTypes.SIMPLE_EXPRESSION */: 1006 | genExpression(node, context); 1007 | break; 1008 | case 2 /* NodeTypes.ELEMENT */: 1009 | genElement(node, context); 1010 | break; 1011 | case 5 /* NodeTypes.COMPOUND_EXPRESSION */: 1012 | genCompoundExpression(node, context); 1013 | break; 1014 | } 1015 | } 1016 | function genCompoundExpression(node, context) { 1017 | const { push } = context; 1018 | const { children } = node; 1019 | for (let i = 0; i < children.length; i++) { 1020 | const child = children[i]; 1021 | if (isString(child)) { 1022 | push(child); 1023 | } 1024 | else { 1025 | genNode(child, context); 1026 | } 1027 | } 1028 | } 1029 | function genElement(node, context) { 1030 | const { push, helper } = context; 1031 | const { tag, children, props } = node; 1032 | push(`${helper(CREATE_ELEMENT_VNODE)}(`); 1033 | genNodeList(genNullable([tag, props, children]), context); 1034 | push(")"); 1035 | } 1036 | function genNodeList(nodes, context) { 1037 | const { push } = context; 1038 | for (let i = 0; i < nodes.length; i++) { 1039 | const node = nodes[i]; 1040 | if (isString(node)) { 1041 | push(node); 1042 | } 1043 | else { 1044 | genNode(node, context); 1045 | } 1046 | if (i < nodes.length - 1) { 1047 | push(", "); 1048 | } 1049 | } 1050 | } 1051 | function genNullable(args) { 1052 | return args.map((arg) => arg || "null"); 1053 | } 1054 | function genText(node, context) { 1055 | const { push } = context; 1056 | push(`'${node.content}'`); 1057 | } 1058 | function genInterpolation(node, context) { 1059 | const { push, helper } = context; 1060 | push(`${helper(TO_DISPLAY_STRING)}(`); 1061 | genNode(node.content, context); 1062 | push(")"); 1063 | } 1064 | function genExpression(node, context) { 1065 | const { push } = context; 1066 | push(`${node.content}`); 1067 | } 1068 | 1069 | function baseParse(content) { 1070 | const context = createParserContent(content); 1071 | return createRoot(parseChildren(context, [])); 1072 | } 1073 | function parseChildren(context, ancestors) { 1074 | const nodes = []; 1075 | while (!isEnd(context, ancestors)) { 1076 | let node; 1077 | const s = context.source; 1078 | if (s.startsWith("{{")) { 1079 | node = parseInterpolation(context); 1080 | } 1081 | else if (s[0] === "<") { 1082 | if (/[a-z]/i.test(s[1])) { 1083 | node = parseElement(context, ancestors); 1084 | } 1085 | } 1086 | if (!node) { 1087 | node = parseText(context); 1088 | } 1089 | nodes.push(node); 1090 | } 1091 | return nodes; 1092 | } 1093 | function isEnd(context, ancestors) { 1094 | // 2. 遇到结束标签 1095 | const s = context.source; 1096 | if (s.startsWith("= 0; --i) { 1098 | const tag = ancestors[i].tag; 1099 | if (startsWithEndTagOpen(s, tag)) { 1100 | return true; 1101 | } 1102 | } 1103 | } 1104 | // 1. source 有值 1105 | return !context.source; 1106 | } 1107 | function parseText(context) { 1108 | let endIndex = context.source.length; 1109 | let endTokens = ["<", "{{"]; 1110 | for (let i = 0; i < endTokens.length; i++) { 1111 | const index = context.source.indexOf(endTokens[i]); 1112 | if (index !== -1 && endIndex > index) { 1113 | endIndex = index; 1114 | } 1115 | } 1116 | // 1. 获取 context 1117 | const content = parseTextData(context, endIndex); 1118 | console.log("---------", content); 1119 | return { 1120 | type: 3 /* NodeTypes.TEXT */, 1121 | content, 1122 | }; 1123 | } 1124 | function parseTextData(context, length) { 1125 | const content = context.source.slice(0, length); 1126 | // 2. 推进 1127 | advanceBy(context, length); 1128 | return content; 1129 | } 1130 | function parseElement(context, ancestors) { 1131 | // Implement 1132 | // 1. 解析 tag 1133 | const element = parseTag(context, 0 /* TagType.Start */); 1134 | ancestors.push(element); 1135 | element.children = parseChildren(context, ancestors); 1136 | ancestors.pop(); 1137 | if (startsWithEndTagOpen(context.source, element.tag)) { 1138 | parseTag(context, 1 /* TagType.End */); 1139 | } 1140 | else { 1141 | throw new Error(`缺少结束标签:${element.tag}`); 1142 | } 1143 | return element; 1144 | } 1145 | function startsWithEndTagOpen(source, tag) { 1146 | return (source.startsWith(" { 1270 | // tag 1271 | const vnodeTag = `'${node.tag}'`; 1272 | // props 1273 | let vnodeProps; 1274 | // children 1275 | const children = node.children; 1276 | let vnodeChildren = children[0]; 1277 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 1278 | }; 1279 | } 1280 | } 1281 | 1282 | function transformExpression(node) { 1283 | if (node.type === 0 /* NodeTypes.INTERPOLATION */) { 1284 | node.content = processExpression(node.content); 1285 | } 1286 | } 1287 | function processExpression(node) { 1288 | node.content = `_ctx.${node.content}`; 1289 | return node; 1290 | } 1291 | 1292 | function isText(node) { 1293 | return node.type === 3 /* NodeTypes.TEXT */ || node.type === 0 /* NodeTypes.INTERPOLATION */; 1294 | } 1295 | 1296 | function transformText(node) { 1297 | if (node.type === 2 /* NodeTypes.ELEMENT */) { 1298 | return () => { 1299 | const { children } = node; 1300 | for (let i = 0; i < children.length; i++) { 1301 | const child = children[i]; 1302 | let currentContainer; 1303 | if (isText(child)) { 1304 | for (let j = i + 1; j < children.length; j++) { 1305 | const next = children[j]; 1306 | if (isText(next)) { 1307 | if (!currentContainer) { 1308 | currentContainer = children[i] = { 1309 | type: 5 /* NodeTypes.COMPOUND_EXPRESSION */, 1310 | children: [child], 1311 | }; 1312 | } 1313 | currentContainer.children.push(" + "); 1314 | currentContainer.children.push(next); 1315 | children.splice(j, 1); 1316 | j--; 1317 | } 1318 | else { 1319 | currentContainer = undefined; 1320 | break; 1321 | } 1322 | } 1323 | } 1324 | } 1325 | }; 1326 | } 1327 | } 1328 | 1329 | function baseCompile(template) { 1330 | const ast = baseParse(template); 1331 | transform(ast, { 1332 | nodeTransforms: [transformExpression, transformElement, transformText], 1333 | }); 1334 | return generate(ast); 1335 | } 1336 | 1337 | function compileToFunction(template) { 1338 | const { code } = baseCompile(template); 1339 | const render = new Function("Vue", code)(runtimeDom); 1340 | return render; 1341 | } 1342 | registerRuntimeCompiler(compileToFunction); 1343 | 1344 | exports.createApp = createApp; 1345 | exports.createElementVNode = createVNode; 1346 | exports.createRenderer = createRenderer; 1347 | exports.createTextVNode = createTextVNode; 1348 | exports.effect = effect; 1349 | exports.getCurrentInstance = getCurrentInstance; 1350 | exports.h = h; 1351 | exports.inject = inject; 1352 | exports.isProxy = isProxy; 1353 | exports.isReactive = isReactive; 1354 | exports.isReadonly = isReadonly; 1355 | exports.isRef = isRef; 1356 | exports.nextTick = nextTick; 1357 | exports.provide = provide; 1358 | exports.proxyRefs = proxyRefs; 1359 | exports.reactive = reactive; 1360 | exports.readonly = readonly; 1361 | exports.ref = ref; 1362 | exports.registerRuntimeCompiler = registerRuntimeCompiler; 1363 | exports.renderSlots = renderSlots; 1364 | exports.shallowReactive = shallowReactive; 1365 | exports.shallowReadonly = shallowReadonly; 1366 | exports.stop = stop; 1367 | exports.toDisplayString = toDisplayString; 1368 | exports.unref = unref; 1369 | -------------------------------------------------------------------------------- /packages/vue/dist/tiny-vue3.esm.js: -------------------------------------------------------------------------------- 1 | const Fragment = Symbol("Fragment"); 2 | const Text = Symbol("Text"); 3 | function createVNode(type, props, children) { 4 | const vnode = { 5 | type, 6 | props, 7 | children, 8 | component: null, 9 | key: props && props.key, 10 | shapeFlag: getShapeFlag(type), 11 | el: null, 12 | }; 13 | // chilren 14 | if (typeof children === "string") { 15 | vnode.shapeFlag |= 4 /* ShapeFlags.TEXT_CHILDREN */; 16 | } 17 | else if (Array.isArray(children)) { 18 | vnode.shapeFlag |= 8 /* ShapeFlags.ARRAY_CHILDREN */; 19 | } 20 | if (vnode.shapeFlag & 2 /* ShapeFlags.STATEFUL_COMPONENT */) { 21 | if (typeof children === "object") { 22 | vnode.shapeFlag |= 16 /* ShapeFlags.SLOT_CHILDREN */; 23 | } 24 | } 25 | return vnode; 26 | } 27 | function createTextVNode(text) { 28 | return createVNode(Text, {}, text); 29 | } 30 | function getShapeFlag(type) { 31 | return typeof type === "string" 32 | ? 1 /* ShapeFlags.ELEMENT */ 33 | : 2 /* ShapeFlags.STATEFUL_COMPONENT */; 34 | } 35 | 36 | function h(type, props, children) { 37 | return createVNode(type, props, children); 38 | } 39 | 40 | function renderSlots(slots, name, props) { 41 | // vnode 42 | const slot = slots[name]; 43 | if (slot) { 44 | console.log("slot", slot); 45 | if (typeof slot === 'function') { 46 | return createVNode(Fragment, {}, slot(props)); 47 | } 48 | } 49 | } 50 | 51 | function toDisplayString(value) { 52 | return String(value); 53 | } 54 | 55 | const extend = Object.assign; 56 | const EMPTY_OBJ = {}; 57 | const isObject = (value) => { 58 | return value !== null && typeof value === "object"; 59 | }; 60 | const isString = (value) => typeof value === "string"; 61 | const hasChanged = (val, newValue) => { 62 | return !Object.is(val, newValue); 63 | }; 64 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 65 | const camelize = (str) => { 66 | return str.replace(/-(\w)/g, (_, c) => { 67 | return c ? c.toUpperCase() : ""; 68 | }); 69 | }; 70 | const capitalize = (str) => { 71 | return str.charAt(0).toUpperCase() + str.slice(1); 72 | }; 73 | const toHandlerKey = (str) => { 74 | return str ? "on" + capitalize(str) : ""; 75 | }; 76 | 77 | let activeEffect; 78 | let shouldTrack = false; 79 | class ReactiveEffect { 80 | constructor(fn, scheduler) { 81 | this.scheduler = scheduler; 82 | this.deps = []; 83 | this.active = true; 84 | this._fn = fn; 85 | } 86 | run() { 87 | if (!this.active) { 88 | return this._fn(); 89 | } 90 | // 应该收集 91 | activeEffect = this; 92 | shouldTrack = true; 93 | const r = this._fn(); 94 | // 重置 95 | shouldTrack = false; 96 | return r; 97 | } 98 | stop() { 99 | if (this.active) { 100 | cleanupEffect(this); 101 | if (this.onStop) { 102 | this.onStop(); 103 | } 104 | this.active = false; 105 | } 106 | } 107 | } 108 | function cleanupEffect(effect) { 109 | effect.deps.forEach((dep) => { 110 | dep.delete(effect); 111 | }); 112 | // 把 effect.deps 清空 113 | effect.deps.length = 0; 114 | } 115 | const targetMap = new Map(); 116 | function track(target, key) { 117 | if (!isTracking()) 118 | return; 119 | // target -> key -> dep 120 | let depsMap = targetMap.get(target); 121 | if (!depsMap) { 122 | depsMap = new Map(); 123 | targetMap.set(target, depsMap); 124 | } 125 | let dep = depsMap.get(key); 126 | if (!dep) { 127 | dep = new Set(); 128 | depsMap.set(key, dep); 129 | } 130 | trackEffects(dep); 131 | } 132 | function trackEffects(dep) { 133 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 134 | if (dep.has(activeEffect)) 135 | return; 136 | dep.add(activeEffect); 137 | activeEffect.deps.push(dep); 138 | } 139 | function isTracking() { 140 | return shouldTrack && activeEffect !== undefined; 141 | } 142 | function trigger(target, key) { 143 | const depsMap = targetMap.get(target); 144 | const dep = depsMap.get(key); 145 | triggerEffects(dep); 146 | } 147 | function triggerEffects(dep) { 148 | for (const effect of dep) { 149 | if (effect.scheduler) { 150 | effect.scheduler(); 151 | } 152 | else { 153 | effect.run(); 154 | } 155 | } 156 | } 157 | function effect(fn, options = {}) { 158 | const _effect = new ReactiveEffect(fn, options.scheduler); 159 | extend(_effect, options); 160 | _effect.run(); 161 | const runner = _effect.run.bind(_effect); 162 | runner.effect = _effect; 163 | return runner; 164 | } 165 | function stop(runner) { 166 | runner.effect.stop(); 167 | } 168 | 169 | const get = createGetter(); 170 | const set = createSetter(); 171 | const readonlyGet = createGetter(true); 172 | const shallowReactiveGet = createGetter(false, true); 173 | const shallowReadonlyGet = createGetter(true, true); 174 | function createGetter(isReadonly = false, shallow = false) { 175 | return function (target, key) { 176 | if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) { 177 | return !isReadonly; 178 | } 179 | else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) { 180 | return isReadonly; 181 | } 182 | const res = Reflect.get(target, key); 183 | if (!isReadonly) { 184 | // 依赖收集 185 | track(target, key); 186 | } 187 | if (shallow) { 188 | return res; 189 | } 190 | if (isObject(res)) { 191 | return isReadonly ? readonly(res) : reactive(res); 192 | } 193 | return res; 194 | }; 195 | } 196 | function createSetter() { 197 | return function set(target, key, value) { 198 | const res = Reflect.set(target, key, value); 199 | // 依赖触发 200 | trigger(target, key); 201 | return res; 202 | }; 203 | } 204 | const mutableHandlers = { 205 | get, 206 | set, 207 | }; 208 | const shallowReactiveHandlers = extend({}, mutableHandlers, { 209 | get: shallowReactiveGet, 210 | }); 211 | const readonlyHandlers = { 212 | get: readonlyGet, 213 | set(target, key) { 214 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 215 | return true; 216 | }, 217 | }; 218 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 219 | get: shallowReadonlyGet, 220 | }); 221 | 222 | function reactive(raw) { 223 | return createReactiveObject(raw, mutableHandlers); 224 | } 225 | function shallowReactive(raw) { 226 | return createReactiveObject(raw, shallowReactiveHandlers); 227 | } 228 | function readonly(raw) { 229 | return createReactiveObject(raw, readonlyHandlers); 230 | } 231 | function shallowReadonly(raw) { 232 | return createReactiveObject(raw, shallowReadonlyHandlers); 233 | } 234 | function createReactiveObject(target, baseHandlerss) { 235 | if (!isObject(target)) { 236 | console.warn(`target ${target} 必须是一个对象`); 237 | return target; 238 | } 239 | return new Proxy(target, baseHandlerss); 240 | } 241 | function isReactive(value) { 242 | return !!value["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */]; 243 | } 244 | function isReadonly(value) { 245 | return !!value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]; 246 | } 247 | function isProxy(value) { 248 | return isReactive(value) || isReadonly(value); 249 | } 250 | 251 | class RefImpl { 252 | constructor(value, __v_isShallow) { 253 | this.__v_isShallow = __v_isShallow; 254 | this.__v_isRef = true; 255 | this._value = __v_isShallow ? value : convert(value); 256 | this._rawValue = value; 257 | this.dep = new Set(); 258 | } 259 | get value() { 260 | trackRefValue(this); 261 | return this._value; 262 | } 263 | set value(newValue) { 264 | if (hasChanged(this._rawValue, newValue)) { 265 | this._value = this.__v_isShallow ? newValue : convert(newValue); 266 | this._rawValue = newValue; 267 | triggerEffects(this.dep); 268 | } 269 | } 270 | } 271 | function createRef(rawValue, shallow) { 272 | if (isRef(rawValue)) { 273 | return rawValue; 274 | } 275 | return new RefImpl(rawValue, shallow); 276 | } 277 | function ref(value) { 278 | return createRef(value, false); 279 | } 280 | function convert(value) { 281 | return isObject(value) ? reactive(value) : value; 282 | } 283 | function trackRefValue(ref) { 284 | if (isTracking()) { 285 | trackEffects(ref.dep); 286 | } 287 | } 288 | function isRef(ref) { 289 | return !!ref.__v_isRef; 290 | } 291 | function unref(ref) { 292 | return isRef(ref) ? ref.value : ref; 293 | } 294 | function proxyRefs(objectWithRefs) { 295 | return new Proxy(objectWithRefs, { 296 | get(target, key) { 297 | return unref(Reflect.get(target, key)); 298 | }, 299 | set(target, key, value) { 300 | if (isRef(target[key]) && !isRef(value)) { 301 | return (target[key].value = value); 302 | } 303 | else { 304 | return Reflect.set(target, key, value); 305 | } 306 | }, 307 | }); 308 | } 309 | 310 | function emit(instance, event, ...args) { 311 | console.log("emit", event); 312 | // instance.props -> event 313 | const { props } = instance; 314 | // TPP 315 | // 先去写特定行为 -> 重构成通用行为 316 | // add 317 | const handlerName = toHandlerKey(camelize(event)); 318 | const handler = props[handlerName]; 319 | handler && handler(...args); 320 | } 321 | 322 | function initProps(instance, rawProps) { 323 | instance.props = rawProps || {}; 324 | // attrs 325 | } 326 | 327 | const publicPropertiesMap = { 328 | $el: (i) => i.vnode.el, 329 | $slots: (i) => i.slots, 330 | $props: (i) => i.props, 331 | }; 332 | const PublicInstanceProxyHandlers = { 333 | get({ _: instance }, key) { 334 | // setupState 335 | const { setupState, props } = instance; 336 | // if (key in setupState) { 337 | // return setupState[key]; 338 | // } 339 | if (hasOwn(setupState, key)) { 340 | return setupState[key]; 341 | } 342 | else if (hasOwn(props, key)) { 343 | return props[key]; 344 | } 345 | const publicGetter = publicPropertiesMap[key]; 346 | if (publicGetter) { 347 | return publicGetter(instance); 348 | } 349 | }, 350 | }; 351 | 352 | function initSlots(instance, children) { 353 | // children object 354 | // instance.slots = Array.isArray(children) ? children : [children]; 355 | const { vnode } = instance; 356 | if (vnode.shapeFlag & 16 /* ShapeFlags.SLOT_CHILDREN */) { 357 | normalizeObjectSlots(children, instance.slots); 358 | } 359 | } 360 | function normalizeObjectSlots(children, slots) { 361 | for (const key in children) { 362 | const value = children[key]; 363 | slots[key] = (props) => normalizeSlotValue(value(props)); 364 | } 365 | } 366 | function normalizeSlotValue(value) { 367 | return Array.isArray(value) ? value : [value]; 368 | } 369 | 370 | function createComponentInstance(vnode, parent) { 371 | console.log("createComponentInstance", parent); 372 | const component = { 373 | vnode, 374 | type: vnode.type, 375 | next: null, 376 | setupState: {}, 377 | props: {}, 378 | slots: {}, 379 | provides: parent ? parent.provides : {}, 380 | parent, 381 | isMounted: false, 382 | subTree: {}, 383 | emit: () => { }, 384 | }; 385 | component.emit = emit.bind(null, component); 386 | return component; 387 | } 388 | function setupComponent(instance) { 389 | initProps(instance, instance.vnode.props); 390 | initSlots(instance, instance.vnode.children); 391 | setupStatefulComponent(instance); 392 | } 393 | function setupStatefulComponent(instance) { 394 | const Component = instance.type; 395 | // ctx 396 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 397 | const { setup } = Component; 398 | if (setup) { 399 | setCurrentInstance(instance); 400 | const setupResult = setup(shallowReadonly(instance.props), { 401 | emit: instance.emit, 402 | }); 403 | setCurrentInstance(null); 404 | handleSetupResult(instance, setupResult); 405 | } 406 | } 407 | function handleSetupResult(instance, setupResult) { 408 | // function Object 409 | // TODO function 渲染函数 410 | if (typeof setupResult === "object") { 411 | instance.setupState = proxyRefs(setupResult); 412 | } 413 | finishComponentSetup(instance); 414 | } 415 | function finishComponentSetup(instance) { 416 | const Component = instance.type; 417 | if (compiler && !Component.render) { 418 | if (Component.template) { 419 | Component.render = compiler(Component.template); 420 | } 421 | } 422 | instance.render = Component.render; 423 | } 424 | let currentInstance = null; 425 | function getCurrentInstance() { 426 | return currentInstance; 427 | } 428 | function setCurrentInstance(instance) { 429 | currentInstance = instance; 430 | } 431 | let compiler; 432 | function registerRuntimeCompiler(_compiler) { 433 | compiler = _compiler; 434 | } 435 | 436 | function provide(key, value) { 437 | // 存 438 | const currentInstance = getCurrentInstance(); 439 | if (currentInstance) { 440 | let { provides } = currentInstance; 441 | const parentProvides = currentInstance.parent.provides; 442 | // init 443 | if (provides === parentProvides) { 444 | provides = currentInstance.provides = Object.create(parentProvides); 445 | } 446 | provides[key] = value; 447 | } 448 | } 449 | function inject(key, defaultValue) { 450 | // 取 451 | const currentInstance = getCurrentInstance(); 452 | if (currentInstance) { 453 | const parentProvides = currentInstance.parent.provides; 454 | if (key in parentProvides) { 455 | return parentProvides[key]; 456 | } 457 | else if (typeof defaultValue === "function") { 458 | return defaultValue(); 459 | } 460 | return defaultValue; 461 | } 462 | } 463 | 464 | function shouldUpdateComponent(prevVNode, nextVNode) { 465 | const { props: prevProps } = prevVNode; 466 | const { props: nextProps } = nextVNode; 467 | for (const key in nextProps) { 468 | if (nextProps[key] !== prevProps[key]) { 469 | return true; 470 | } 471 | } 472 | return false; 473 | } 474 | 475 | function createAppAPI(render) { 476 | return function createApp(rootComponent) { 477 | return { 478 | mount(rootContainer) { 479 | // 先 vode 480 | // 基于 vode 做处理 481 | const vnode = createVNode(rootComponent); 482 | render(vnode, rootContainer); 483 | }, 484 | }; 485 | }; 486 | } 487 | 488 | const queue = []; 489 | const p = Promise.resolve(); 490 | let isFlushPending = false; 491 | function nextTick(fn) { 492 | return fn ? p.then(fn) : p; 493 | } 494 | function queueJobs(job) { 495 | if (!queue.includes(job)) { 496 | queue.push(job); 497 | } 498 | queueFlush(); 499 | } 500 | function queueFlush() { 501 | if (isFlushPending) 502 | return; 503 | isFlushPending = true; 504 | nextTick(flushJob); 505 | } 506 | function flushJob() { 507 | isFlushPending = false; 508 | let job; 509 | while ((job = queue.shift())) { 510 | job && job(); 511 | } 512 | } 513 | 514 | function createRenderer(options) { 515 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 516 | function render(vnode, container) { 517 | patch(null, vnode, container, null, null); 518 | } 519 | // n1 -> old 520 | // n2 -> new 521 | function patch(n1, n2, container, parentComponent, anchor) { 522 | console.log('n1', n1); 523 | console.log("n2", n2); 524 | const { type, shapeFlag } = n2; 525 | switch (type) { 526 | case Fragment: 527 | processFragment(n1, n2, container, parentComponent, anchor); 528 | break; 529 | case Text: 530 | processText(n1, n2, container); 531 | break; 532 | default: 533 | if (shapeFlag & 1 /* ShapeFlags.ELEMENT */) { 534 | processElement(n1, n2, container, parentComponent, anchor); 535 | } 536 | else if (shapeFlag & 2 /* ShapeFlags.STATEFUL_COMPONENT */) { 537 | processComponent(n1, n2, container, parentComponent, anchor); 538 | } 539 | break; 540 | } 541 | } 542 | function processText(n1, n2, container) { 543 | const { children } = n2; 544 | const textNode = (n2.el = document.createTextNode(children)); 545 | container.append(textNode); 546 | } 547 | function processFragment(n1, n2, container, parentComponent, anchor) { 548 | mountChildren(n2.children, container, parentComponent, anchor); 549 | } 550 | function processElement(n1, n2, container, parentComponent, anchor) { 551 | if (!n1) { 552 | mountElement(n1, n2, container, parentComponent, anchor); 553 | } 554 | else { 555 | patchElement(n1, n2, container, parentComponent, anchor); 556 | } 557 | } 558 | function patchElement(n1, n2, container, parentComponent, anchor) { 559 | const oldProps = n1.props || EMPTY_OBJ; 560 | const newProps = n2.props || EMPTY_OBJ; 561 | const el = (n2.el = n1.el); 562 | patchChildren(n1, n2, el, parentComponent, anchor); 563 | patchProps(el, oldProps, newProps); 564 | } 565 | function patchChildren(n1, n2, container, parentComponent, anchor) { 566 | const { shapeFlag, children: c2 } = n2; 567 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 568 | if (shapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 569 | if (prevShapeFlag & 8 /* ShapeFlags.ARRAY_CHILDREN */) { 570 | // 1. 把老的 children 清空 571 | unmountChildren(n1.children); 572 | } 573 | if (c1 !== c2) { 574 | hostSetElementText(container, c2); 575 | } 576 | } 577 | else { 578 | if (prevShapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 579 | hostSetElementText(container, ""); 580 | mountChildren(c2, container, parentComponent, anchor); 581 | } 582 | else { 583 | // array diff array 584 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 585 | } 586 | } 587 | } 588 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 589 | let i = 0; 590 | const l2 = c2.length; 591 | let e1 = c1.length - 1; 592 | let e2 = l2 - 1; 593 | function isSomeVNodeType(n1, n2) { 594 | return n1.type === n2.type && n1.key === n2.key; 595 | } 596 | // 左侧 597 | while (i <= e1 && i <= e2) { 598 | const n1 = c1[i]; 599 | const n2 = c2[i]; 600 | if (isSomeVNodeType(n1, n2)) { 601 | patch(n1, n2, container, parentComponent, parentAnchor); 602 | } 603 | else { 604 | break; 605 | } 606 | i++; 607 | } 608 | // 右侧 609 | while (i <= e1 && i <= e2) { 610 | const n1 = c1[e1]; 611 | const n2 = c2[e2]; 612 | if (isSomeVNodeType(n1, n2)) { 613 | patch(n1, n2, container, parentComponent, parentAnchor); 614 | } 615 | else { 616 | break; 617 | } 618 | e1--; 619 | e2--; 620 | } 621 | // 新的比老的多 创建 622 | if (i > e1) { 623 | // 左侧右侧逻辑共用 624 | if (i <= e2) { 625 | const nextPos = e2 + 1; 626 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 627 | while (i <= e2) { 628 | patch(null, c2[i], container, parentComponent, anchor); 629 | i++; 630 | } 631 | } 632 | } 633 | else if (i > e2) { 634 | // 新的比老的少 删除 左侧右侧逻辑共用 635 | while (i <= e1) { 636 | hostRemove(c1[i].el); 637 | i++; 638 | } 639 | } 640 | else { 641 | // 中间对比 642 | let s1 = i; 643 | let s2 = i; 644 | const toBePatched = e2 - s2 + 1; 645 | let patched = 0; 646 | const keyToNewIndexMap = new Map(); 647 | const newIndexToOldIndexMap = new Array(toBePatched); 648 | let moved = false; 649 | let maxNewIndexSoFar = 0; 650 | for (let i = 0; i < toBePatched; i++) { 651 | newIndexToOldIndexMap[i] = 0; 652 | } 653 | for (let i = s2; i <= e2; i++) { 654 | const nextChild = c2[i]; 655 | keyToNewIndexMap.set(nextChild.key, i); 656 | } 657 | for (let i = s1; i <= e1; i++) { 658 | const prevChild = c1[i]; 659 | if (patched >= toBePatched) { 660 | // patched 的数量大于等于 新的中间节点数量,直接把剩余的老的中间节点删除 661 | hostRemove(prevChild.el); 662 | continue; 663 | } 664 | let newIndex; 665 | // null undefined 666 | if (prevChild.key != null) { 667 | newIndex = keyToNewIndexMap.get(prevChild.key); 668 | } 669 | else { 670 | for (let j = s2; j <= e2; j++) { 671 | if (isSomeVNodeType(prevChild, c2[j])) { 672 | newIndex = j; 673 | break; 674 | } 675 | } 676 | } 677 | if (newIndex === undefined) { 678 | hostRemove(prevChild.el); 679 | } 680 | else { 681 | if (newIndex >= maxNewIndexSoFar) { 682 | maxNewIndexSoFar = newIndex; 683 | } 684 | else { 685 | moved = true; 686 | } 687 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 688 | patch(prevChild, c2[newIndex], container, parentComponent, null); 689 | patched++; 690 | } 691 | } 692 | const increasingNewIndexSequence = moved 693 | ? getSequence(newIndexToOldIndexMap) 694 | : []; 695 | let j = increasingNewIndexSequence.length - 1; 696 | for (let i = toBePatched - 1; i >= 0; i--) { 697 | const nextIndex = i + s2; 698 | const nextChild = c2[nextIndex]; 699 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 700 | if (newIndexToOldIndexMap[i] === 0) { 701 | patch(null, nextChild, container, parentComponent, anchor); 702 | } 703 | else if (moved) { 704 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 705 | hostInsert(nextChild.el, container, anchor); 706 | } 707 | else { 708 | j--; 709 | } 710 | } 711 | } 712 | } 713 | } 714 | function unmountChildren(children) { 715 | for (let i = 0; i < children.length; i++) { 716 | const el = children[i].el; 717 | // remove 718 | hostRemove(el); 719 | } 720 | } 721 | function patchProps(el, oldProps, newProps) { 722 | if (oldProps !== newProps) { 723 | for (const key in newProps) { 724 | const prevProp = oldProps[key]; 725 | const nextProp = newProps[key]; 726 | if (prevProp !== nextProp) { 727 | hostPatchProp(el, key, prevProp, nextProp); 728 | } 729 | } 730 | if (oldProps !== EMPTY_OBJ) { 731 | for (const key in oldProps) { 732 | if (!(key in newProps)) { 733 | hostPatchProp(el, key, oldProps[key], null); 734 | } 735 | } 736 | } 737 | } 738 | } 739 | function mountElement(n1, n2, container, parentComponent, anchor) { 740 | //canvas 741 | // new Element() 742 | const el = (n2.el = hostCreateElement(n2.type)); 743 | const { children, shapeFlag } = n2; 744 | // children 745 | if (shapeFlag & 4 /* ShapeFlags.TEXT_CHILDREN */) { 746 | el.textContent = children; 747 | } 748 | else if (shapeFlag & 8 /* ShapeFlags.ARRAY_CHILDREN */) { 749 | mountChildren(n2.children, el, parentComponent, anchor); 750 | } 751 | // props 752 | const { props } = n2; 753 | for (const key in props) { 754 | const val = props[key]; 755 | hostPatchProp(el, key, null, val); 756 | } 757 | // canvs 758 | // el.x = 10 759 | // container.append(el); 760 | // addChild() 761 | hostInsert(el, container, anchor); 762 | } 763 | function mountChildren(children, container, parentComponent, anchor) { 764 | children.forEach((v) => { 765 | patch(null, v, container, parentComponent, anchor); 766 | }); 767 | } 768 | function processComponent(n1, n2, container, parentComponent, anchor) { 769 | if (!n1) { 770 | mountComponent(n2, container, parentComponent, anchor); 771 | } 772 | else { 773 | updateComponent(n1, n2); 774 | } 775 | } 776 | function updateComponent(n1, n2) { 777 | const instance = (n2.component = n1.component); 778 | if (shouldUpdateComponent(n1, n2)) { 779 | instance.next = n2; 780 | instance.update(); 781 | } 782 | else { 783 | n2.el = n1.el; 784 | instance.vnode = n2; 785 | } 786 | } 787 | function mountComponent(initialVNode, container, parentComponent, anchor) { 788 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 789 | setupComponent(instance); 790 | setupRenderEffect(instance, initialVNode, container, anchor); 791 | } 792 | function setupRenderEffect(instance, initialVNode, container, anchor) { 793 | instance.update = effect(() => { 794 | if (!instance.isMounted) { 795 | console.log("init"); 796 | const { proxy } = instance; 797 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 798 | patch(null, subTree, container, instance, anchor); 799 | initialVNode.el = subTree.el; 800 | instance.isMounted = true; 801 | } 802 | else { 803 | console.log("update"); 804 | // 需要一个 vnode 805 | const { next, vnode } = instance; 806 | if (next) { 807 | next.el = vnode.el; 808 | updateComponentPreRender(instance, next); 809 | } 810 | const { proxy } = instance; 811 | const subTree = instance.render.call(proxy, proxy); 812 | const prevSubTree = instance.subTree; 813 | instance.subTree = subTree; 814 | patch(prevSubTree, subTree, container, instance, anchor); 815 | } 816 | }, { 817 | scheduler() { 818 | console.log('update scheduler'); 819 | queueJobs(instance.update); 820 | } 821 | }); 822 | } 823 | return { 824 | createApp: createAppAPI(render), 825 | }; 826 | } 827 | function updateComponentPreRender(instance, nextVNode) { 828 | instance.vnode = nextVNode; 829 | instance.next = null; 830 | instance.props = nextVNode.props; 831 | } 832 | // https://en.wikipedia.org/wiki/Longest_increasing_subsequence 833 | function getSequence(arr) { 834 | const p = arr.slice(); 835 | const result = [0]; 836 | let i, j, u, v, c; 837 | const len = arr.length; 838 | for (i = 0; i < len; i++) { 839 | const arrI = arr[i]; 840 | if (arrI !== 0) { 841 | j = result[result.length - 1]; 842 | if (arr[j] < arrI) { 843 | p[i] = j; 844 | result.push(i); 845 | continue; 846 | } 847 | u = 0; 848 | v = result.length - 1; 849 | while (u < v) { 850 | c = (u + v) >> 1; 851 | if (arr[result[c]] < arrI) { 852 | u = c + 1; 853 | } 854 | else { 855 | v = c; 856 | } 857 | } 858 | if (arrI < arr[result[u]]) { 859 | if (u > 0) { 860 | p[i] = result[u - 1]; 861 | } 862 | result[u] = i; 863 | } 864 | } 865 | } 866 | u = result.length; 867 | v = result[u - 1]; 868 | while (u-- > 0) { 869 | result[u] = v; 870 | v = p[v]; 871 | } 872 | return result; 873 | } 874 | 875 | function createElement(type) { 876 | console.log("createElement ----------"); 877 | return document.createElement(type); 878 | } 879 | function patchProp(el, key, prevVal, nextVal) { 880 | console.log("patchProp ----------"); 881 | const isOn = (key) => /^on[A-Z]/.test(key); 882 | if (isOn(key)) { 883 | const event = key.slice(2).toLowerCase(); 884 | el.addEventListener(event, nextVal); 885 | } 886 | else { 887 | if (nextVal === undefined || nextVal === null) { 888 | el.removeAttribute(key); 889 | } 890 | else { 891 | el.setAttribute(key, nextVal); 892 | } 893 | } 894 | } 895 | function insert(child, parent, anchor) { 896 | console.log("insert ----------", anchor); 897 | parent.insertBefore(child, anchor || null); 898 | } 899 | function remove(child) { 900 | console.log("remove ----------", child); 901 | const parent = child.parentNode; 902 | if (parent) { 903 | parent.removeChild(child); 904 | } 905 | } 906 | function setElementText(el, text) { 907 | el.textContent = text; 908 | } 909 | const renderer = createRenderer({ 910 | createElement, 911 | patchProp, 912 | insert, 913 | remove, 914 | setElementText, 915 | }); 916 | function createApp(...args) { 917 | return renderer.createApp(...args); 918 | } 919 | 920 | var runtimeDom = /*#__PURE__*/Object.freeze({ 921 | __proto__: null, 922 | createApp: createApp, 923 | h: h, 924 | renderSlots: renderSlots, 925 | createTextVNode: createTextVNode, 926 | createElementVNode: createVNode, 927 | getCurrentInstance: getCurrentInstance, 928 | registerRuntimeCompiler: registerRuntimeCompiler, 929 | provide: provide, 930 | inject: inject, 931 | createRenderer: createRenderer, 932 | nextTick: nextTick, 933 | toDisplayString: toDisplayString, 934 | ref: ref, 935 | proxyRefs: proxyRefs, 936 | unref: unref, 937 | isRef: isRef, 938 | reactive: reactive, 939 | readonly: readonly, 940 | shallowReactive: shallowReactive, 941 | shallowReadonly: shallowReadonly, 942 | isReactive: isReactive, 943 | isReadonly: isReadonly, 944 | isProxy: isProxy, 945 | effect: effect, 946 | stop: stop 947 | }); 948 | 949 | const TO_DISPLAY_STRING = Symbol("toDisplayString"); 950 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 951 | const helperMapName = { 952 | [TO_DISPLAY_STRING]: "toDisplayString", 953 | [CREATE_ELEMENT_VNODE]: 'createElementVNode' 954 | }; 955 | 956 | function generate(ast) { 957 | const context = createCodegenContext(); 958 | const { push } = context; 959 | genFunctionPreamble(ast, context); 960 | const functionName = "render"; 961 | const args = ["_ctx", "_cache"]; 962 | const signature = args.join(", "); 963 | push(`function ${functionName}(${signature}){`); 964 | push(`return `); 965 | genNode(ast.codegenNode, context); 966 | push("}"); 967 | return { 968 | code: context.code, 969 | }; 970 | } 971 | function genFunctionPreamble(ast, context) { 972 | const { push } = context; 973 | const VueBinging = "Vue"; 974 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}`; 975 | if (ast.helpers.length > 0) { 976 | push(`const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinging}`); 977 | } 978 | push("\n"); 979 | push("return "); 980 | } 981 | function createCodegenContext() { 982 | const context = { 983 | code: "", 984 | push(source) { 985 | context.code += source; 986 | }, 987 | helper(key) { 988 | return `_${helperMapName[key]}`; 989 | }, 990 | }; 991 | return context; 992 | } 993 | function genNode(node, context) { 994 | switch (node.type) { 995 | case 3 /* NodeTypes.TEXT */: 996 | genText(node, context); 997 | break; 998 | case 0 /* NodeTypes.INTERPOLATION */: 999 | genInterpolation(node, context); 1000 | break; 1001 | case 1 /* NodeTypes.SIMPLE_EXPRESSION */: 1002 | genExpression(node, context); 1003 | break; 1004 | case 2 /* NodeTypes.ELEMENT */: 1005 | genElement(node, context); 1006 | break; 1007 | case 5 /* NodeTypes.COMPOUND_EXPRESSION */: 1008 | genCompoundExpression(node, context); 1009 | break; 1010 | } 1011 | } 1012 | function genCompoundExpression(node, context) { 1013 | const { push } = context; 1014 | const { children } = node; 1015 | for (let i = 0; i < children.length; i++) { 1016 | const child = children[i]; 1017 | if (isString(child)) { 1018 | push(child); 1019 | } 1020 | else { 1021 | genNode(child, context); 1022 | } 1023 | } 1024 | } 1025 | function genElement(node, context) { 1026 | const { push, helper } = context; 1027 | const { tag, children, props } = node; 1028 | push(`${helper(CREATE_ELEMENT_VNODE)}(`); 1029 | genNodeList(genNullable([tag, props, children]), context); 1030 | push(")"); 1031 | } 1032 | function genNodeList(nodes, context) { 1033 | const { push } = context; 1034 | for (let i = 0; i < nodes.length; i++) { 1035 | const node = nodes[i]; 1036 | if (isString(node)) { 1037 | push(node); 1038 | } 1039 | else { 1040 | genNode(node, context); 1041 | } 1042 | if (i < nodes.length - 1) { 1043 | push(", "); 1044 | } 1045 | } 1046 | } 1047 | function genNullable(args) { 1048 | return args.map((arg) => arg || "null"); 1049 | } 1050 | function genText(node, context) { 1051 | const { push } = context; 1052 | push(`'${node.content}'`); 1053 | } 1054 | function genInterpolation(node, context) { 1055 | const { push, helper } = context; 1056 | push(`${helper(TO_DISPLAY_STRING)}(`); 1057 | genNode(node.content, context); 1058 | push(")"); 1059 | } 1060 | function genExpression(node, context) { 1061 | const { push } = context; 1062 | push(`${node.content}`); 1063 | } 1064 | 1065 | function baseParse(content) { 1066 | const context = createParserContent(content); 1067 | return createRoot(parseChildren(context, [])); 1068 | } 1069 | function parseChildren(context, ancestors) { 1070 | const nodes = []; 1071 | while (!isEnd(context, ancestors)) { 1072 | let node; 1073 | const s = context.source; 1074 | if (s.startsWith("{{")) { 1075 | node = parseInterpolation(context); 1076 | } 1077 | else if (s[0] === "<") { 1078 | if (/[a-z]/i.test(s[1])) { 1079 | node = parseElement(context, ancestors); 1080 | } 1081 | } 1082 | if (!node) { 1083 | node = parseText(context); 1084 | } 1085 | nodes.push(node); 1086 | } 1087 | return nodes; 1088 | } 1089 | function isEnd(context, ancestors) { 1090 | // 2. 遇到结束标签 1091 | const s = context.source; 1092 | if (s.startsWith("= 0; --i) { 1094 | const tag = ancestors[i].tag; 1095 | if (startsWithEndTagOpen(s, tag)) { 1096 | return true; 1097 | } 1098 | } 1099 | } 1100 | // 1. source 有值 1101 | return !context.source; 1102 | } 1103 | function parseText(context) { 1104 | let endIndex = context.source.length; 1105 | let endTokens = ["<", "{{"]; 1106 | for (let i = 0; i < endTokens.length; i++) { 1107 | const index = context.source.indexOf(endTokens[i]); 1108 | if (index !== -1 && endIndex > index) { 1109 | endIndex = index; 1110 | } 1111 | } 1112 | // 1. 获取 context 1113 | const content = parseTextData(context, endIndex); 1114 | console.log("---------", content); 1115 | return { 1116 | type: 3 /* NodeTypes.TEXT */, 1117 | content, 1118 | }; 1119 | } 1120 | function parseTextData(context, length) { 1121 | const content = context.source.slice(0, length); 1122 | // 2. 推进 1123 | advanceBy(context, length); 1124 | return content; 1125 | } 1126 | function parseElement(context, ancestors) { 1127 | // Implement 1128 | // 1. 解析 tag 1129 | const element = parseTag(context, 0 /* TagType.Start */); 1130 | ancestors.push(element); 1131 | element.children = parseChildren(context, ancestors); 1132 | ancestors.pop(); 1133 | if (startsWithEndTagOpen(context.source, element.tag)) { 1134 | parseTag(context, 1 /* TagType.End */); 1135 | } 1136 | else { 1137 | throw new Error(`缺少结束标签:${element.tag}`); 1138 | } 1139 | return element; 1140 | } 1141 | function startsWithEndTagOpen(source, tag) { 1142 | return (source.startsWith(" { 1266 | // tag 1267 | const vnodeTag = `'${node.tag}'`; 1268 | // props 1269 | let vnodeProps; 1270 | // children 1271 | const children = node.children; 1272 | let vnodeChildren = children[0]; 1273 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 1274 | }; 1275 | } 1276 | } 1277 | 1278 | function transformExpression(node) { 1279 | if (node.type === 0 /* NodeTypes.INTERPOLATION */) { 1280 | node.content = processExpression(node.content); 1281 | } 1282 | } 1283 | function processExpression(node) { 1284 | node.content = `_ctx.${node.content}`; 1285 | return node; 1286 | } 1287 | 1288 | function isText(node) { 1289 | return node.type === 3 /* NodeTypes.TEXT */ || node.type === 0 /* NodeTypes.INTERPOLATION */; 1290 | } 1291 | 1292 | function transformText(node) { 1293 | if (node.type === 2 /* NodeTypes.ELEMENT */) { 1294 | return () => { 1295 | const { children } = node; 1296 | for (let i = 0; i < children.length; i++) { 1297 | const child = children[i]; 1298 | let currentContainer; 1299 | if (isText(child)) { 1300 | for (let j = i + 1; j < children.length; j++) { 1301 | const next = children[j]; 1302 | if (isText(next)) { 1303 | if (!currentContainer) { 1304 | currentContainer = children[i] = { 1305 | type: 5 /* NodeTypes.COMPOUND_EXPRESSION */, 1306 | children: [child], 1307 | }; 1308 | } 1309 | currentContainer.children.push(" + "); 1310 | currentContainer.children.push(next); 1311 | children.splice(j, 1); 1312 | j--; 1313 | } 1314 | else { 1315 | currentContainer = undefined; 1316 | break; 1317 | } 1318 | } 1319 | } 1320 | } 1321 | }; 1322 | } 1323 | } 1324 | 1325 | function baseCompile(template) { 1326 | const ast = baseParse(template); 1327 | transform(ast, { 1328 | nodeTransforms: [transformExpression, transformElement, transformText], 1329 | }); 1330 | return generate(ast); 1331 | } 1332 | 1333 | function compileToFunction(template) { 1334 | const { code } = baseCompile(template); 1335 | const render = new Function("Vue", code)(runtimeDom); 1336 | return render; 1337 | } 1338 | registerRuntimeCompiler(compileToFunction); 1339 | 1340 | export { createApp, createVNode as createElementVNode, createRenderer, createTextVNode, effect, getCurrentInstance, h, inject, isProxy, isReactive, isReadonly, isRef, nextTick, provide, proxyRefs, reactive, readonly, ref, registerRuntimeCompiler, renderSlots, shallowReactive, shallowReadonly, stop, toDisplayString, unref }; 1341 | -------------------------------------------------------------------------------- /packages/vue/examples/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | const Provider = { 4 | name: "Provider", 5 | setup() { 6 | provide("foo", "fooVal"); 7 | provide("bar", "barVal"); 8 | }, 9 | render() { 10 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]); 11 | }, 12 | }; 13 | 14 | const ProviderTwo = { 15 | name: "ProviderTwo", 16 | setup() { 17 | provide("foo", "fooValTwo"); 18 | const foo = inject("foo"); 19 | 20 | return { 21 | foo, 22 | }; 23 | }, 24 | render() { 25 | return h("div", {}, [ 26 | h("p", {}, `ProviderTwo foo: ${this.foo}`), 27 | h(Consumer), 28 | ]); 29 | }, 30 | }; 31 | 32 | const Consumer = { 33 | name: "Comsumer", 34 | setup() { 35 | const foo = inject("foo"); 36 | const bar = inject("bar"); 37 | // const baz = inject("baz", "bazDefault"); 38 | const baz = inject("baz", () => "bazDefault"); 39 | 40 | return { 41 | foo, 42 | bar, 43 | baz, 44 | }; 45 | }, 46 | 47 | render() { 48 | return h("div", {}, `Consumer: - ${this.foo} - ${this.bar} - ${this.baz}`); 49 | }, 50 | }; 51 | 52 | export default { 53 | name: "App", 54 | setup() {}, 55 | render() { 56 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/vue/examples/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/App.js: -------------------------------------------------------------------------------- 1 | import { ref } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | template: `
hi,{{count}}
`, 6 | setup() { 7 | const count = (window.count = ref(1)); 8 | return { 9 | count, 10 | }; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null; 5 | export const App = { 6 | name: "App", 7 | render() { 8 | // emit 9 | return h("div", {}, [ 10 | h("div", {}, "App"), 11 | h(Foo, { 12 | // on + Event 13 | onAdd(a, b) { 14 | console.log("onAdd", a, b); 15 | }, 16 | // add-foo 17 | onAddFoo(a, b) { 18 | console.log('onAdd', a, b); 19 | } 20 | }), 21 | ]); 22 | }, 23 | setup() { 24 | return {}; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | const emitAdd = () => { 6 | console.log('emit add') 7 | emit('add', 1, 2) 8 | emit("add-foo", 1, 2); 9 | }; 10 | 11 | return { 12 | emitAdd, 13 | }; 14 | }, 15 | 16 | render() { 17 | const btn = h( 18 | "button", 19 | { 20 | onClick: this.emitAdd, 21 | }, 22 | "emitAdd" 23 | ); 24 | 25 | const foo = h("p", {}, "foo"); 26 | return h("div", {}, [foo, btn]); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from "../../dist/tiny-vue3.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null; 5 | export const App = { 6 | name: "App", 7 | render() { 8 | // slot 9 | const app = h("div", {}, "App"); 10 | // const foo = h(Foo, {}, h("p", {}, "123")); 11 | // const foo = h(Foo, {}, [h("p", {}, "123"), h("p", {}, "456")]); 12 | const foo = h( 13 | Foo, 14 | {}, 15 | { 16 | header: ({ age }) => [ 17 | h("p", {}, "header" + age), 18 | createTextVNode("你好呀"), 19 | ], 20 | footer: () => h("p", {}, "footer"), 21 | } 22 | ); 23 | 24 | return h("div", {}, [app, foo]); 25 | }, 26 | setup() { 27 | return {}; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | return {}; 6 | }, 7 | 8 | render() { 9 | const foo = h("p", {}, "foo"); 10 | 11 | // Foo vnode children 12 | console.log(this.$slots); 13 | // child -> vnode 14 | // vnode 15 | 16 | // renderSlots 17 | // 1. 获取到要渲染的元素 18 | // 2. 获取到渲染的位置 19 | const age = 18; 20 | return h("div", {}, [ 21 | renderSlots(this.$slots, "header", { 22 | age 23 | }), 24 | foo, 25 | renderSlots(this.$slots, "footer"), 26 | ]); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../dist/tiny-vue3.esm.js"; 2 | import Child from "./Child.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | setup() { 7 | const msg = ref("123"); 8 | const count = ref(1); 9 | 10 | window.msg = msg; 11 | 12 | const changeChildProps = () => { 13 | msg.value = "456"; 14 | }; 15 | 16 | const changeCount = () => { 17 | count.value++; 18 | }; 19 | 20 | return { msg, changeChildProps, changeCount, count }; 21 | }, 22 | 23 | render() { 24 | return h("div", {}, [ 25 | h("div", {}, "你好"), 26 | h( 27 | "button", 28 | { 29 | onClick: this.changeChildProps, 30 | }, 31 | "change child props" 32 | ), 33 | h(Child, { 34 | msg: this.msg, 35 | }), 36 | h( 37 | "button", 38 | { 39 | onClick: this.changeCount, 40 | }, 41 | "change self count" 42 | ), 43 | h("p", {}, "count: " + this.count), 44 | ]); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | export default { 3 | name: "Child", 4 | setup(props, { emit }) {}, 5 | render(proxy) { 6 | return h("div", {}, [h("div", {}, "child - props - msg: " + this.$props.msg)]); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /packages/vue/examples/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../dist/tiny-vue3.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null; 5 | export const App = { 6 | name: "App", 7 | render() { 8 | return h("div", {}, [h("p", {}, "currentInstance demo"), h(Foo)]); 9 | }, 10 | setup() { 11 | const instance = getCurrentInstance(); 12 | console.log("App:", instance); 13 | return {}; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/vue/examples/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const Foo = { 4 | name: 'Foo', 5 | setup(props, { emit }) { 6 | const instance = getCurrentInstance(); 7 | console.log("Foo:", instance); 8 | return {}; 9 | }, 10 | 11 | render() { 12 | return h("div", {}, "foo"); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/vue/examples/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/examples/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const App = { 4 | setup() { 5 | return { 6 | x: 100, 7 | y: 100, 8 | }; 9 | }, 10 | render() { 11 | return h("rect", { x: this.x, y: this.y }); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../../dist/tiny-vue3.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.append(game.view); 11 | 12 | const renderer = createRenderer({ 13 | createElement(type) { 14 | if (type === "rect") { 15 | const rect = new PIXI.Graphics(); 16 | rect.beginFill(0xff0000); 17 | rect.drawRect(0, 0, 100, 100); 18 | rect.endFill(); 19 | 20 | return rect; 21 | } 22 | }, 23 | patchProp(el, key, val) { 24 | el[key] = val; 25 | }, 26 | insert(el, parent) { 27 | parent.addChild(el); 28 | }, 29 | }); 30 | 31 | renderer.createApp(App).mount(game.stage); 32 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null; 5 | export const App = { 6 | render() { 7 | window.self = this; 8 | // ui 9 | return h( 10 | "div", 11 | { 12 | id: "root", 13 | class: ["red", "hard"], 14 | onClick() { 15 | console.log("click"); 16 | }, 17 | }, 18 | // setupState 19 | // this.$el 20 | [ 21 | h("div", {}, "hi, " + this.msg), 22 | h(Foo, { 23 | count: 1, 24 | }), 25 | ] 26 | // string 27 | // "hi, tiny-vue3" 28 | // Array 29 | // [h("p", { class: "red" }, "hi"), h("p", { class: "blue" }, "tiny-vue3")] 30 | ); 31 | }, 32 | setup() { 33 | return { 34 | msg: "tiny-vue3-haha22", 35 | }; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props) { 5 | // props count 6 | // shallow readonly 7 | props.count++; 8 | console.log(props); 9 | }, 10 | 11 | render() { 12 | return h("div", {}, "foo: " + this.count); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | getCurrentInstance, 5 | nextTick, 6 | } from "../../dist/tiny-vue3.esm.js"; 7 | 8 | export default { 9 | name: "App", 10 | setup() { 11 | const count = ref(1); 12 | const instance = getCurrentInstance(); 13 | 14 | async function onClick() { 15 | for (let i = 0; i < 100; i++) { 16 | console.log("update"); 17 | count.value = i; 18 | } 19 | 20 | // console.log(instance); 21 | // nextTick(() => { 22 | // console.log(instance); 23 | // }); 24 | 25 | await nextTick() 26 | console.log(instance) 27 | } 28 | 29 | return { 30 | onClick, 31 | count, 32 | }; 33 | }, 34 | render() { 35 | const button = h("button", { onClick: this.onClick }, "update"); 36 | const p = h("p", {}, "count:" + this.count); 37 | 38 | return h("div", {}, [button, p]); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import App from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#root"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../dist/tiny-vue3.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 | 8 | export default { 9 | name: "App", 10 | setup() {}, 11 | 12 | render() { 13 | return h("div", { tId: 1 }, [ 14 | h("p", {}, "主页"), 15 | // 老的是 array 新的是 text 16 | // h(ArrayToText), 17 | // 老的是 text 新的是 text 18 | // h(TextToText), 19 | // 老的是 text 新的是 array 20 | // h(TextToArray) 21 | // 老的是 array 新的是 array 22 | h(ArrayToArray), 23 | ]); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 array 3 | 4 | import { ref, h } from "../../dist/tiny-vue3.esm.js"; 5 | 6 | // 1. 左侧的对比 7 | // (a b) c 8 | // (a b) d e 9 | // const prevChildren = [ 10 | // h("p", { key: "A" }, "A"), 11 | // h("p", { key: "B" }, "B"), 12 | // h("p", { key: "C" }, "C"), 13 | // ]; 14 | // const nextChildren = [ 15 | // h("p", { key: "A" }, "A"), 16 | // h("p", { key: "B" }, "B"), 17 | // h("p", { key: "D" }, "D"), 18 | // h("p", { key: "E" }, "E"), 19 | // ]; 20 | 21 | // 2. 右侧的对比 22 | // a (b c) 23 | // d e (b c) 24 | // const prevChildren = [ 25 | // h("p", { key: "A" }, "A"), 26 | // h("p", { key: "B" }, "B"), 27 | // h("p", { key: "C" }, "C"), 28 | // ]; 29 | // const nextChildren = [ 30 | // h("p", { key: "D" }, "D"), 31 | // h("p", { key: "E" }, "E"), 32 | // h("p", { key: "B" }, "B"), 33 | // h("p", { key: "C" }, "C"), 34 | // ]; 35 | 36 | // 3. 新的比老的长 37 | // 创建新的 38 | // 左侧 39 | // (a b) 40 | // (a b) c 41 | // i = 2, e1 = 1, e2 = 2 42 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 43 | // const nextChildren = [ 44 | // h("p", { key: "A" }, "A"), 45 | // h("p", { key: "B" }, "B"), 46 | // h("p", { key: "C" }, "C"), 47 | // h("p", { key: "D" }, "D"), 48 | // ]; 49 | 50 | // 右侧 51 | // (a b) 52 | // c (a b) 53 | // i = 0, e1 = -1, e2 = 0 54 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 55 | // const nextChildren = [ 56 | // h("p", { key: "C" }, "C"), 57 | // h("p", { key: "D" }, "D"), 58 | // h("p", { key: "A" }, "A"), 59 | // h("p", { key: "B" }, "B"), 60 | // ]; 61 | 62 | // 4. 老的比新的长 63 | // 删除老的 64 | // 左侧 65 | // (a b) c 66 | // (a b) 67 | // i = 2, e1 = 2, e2 = 1 68 | // const prevChildren = [ 69 | // h("p", { key: "A" }, "A"), 70 | // h("p", { key: "B" }, "B"), 71 | // h("p", { key: "C" }, "C"), 72 | // ]; 73 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 74 | 75 | // 右侧 76 | // a (b c) 77 | // (b c) 78 | // i = 0, e1 = 0, e2 = -1 79 | 80 | // const prevChildren = [ 81 | // h("p", { key: "A" }, "A"), 82 | // h("p", { key: "B" }, "B"), 83 | // h("p", { key: "C" }, "C"), 84 | // ]; 85 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; 86 | 87 | // 5. 对比中间的部分 88 | // 删除老的 (在老的里面存在,新的里面不存在) 89 | // 5.1 90 | // a,b,(c,d),f,g 91 | // a,b,(e,c),f,g 92 | // D 节点在新的里面是没有的 - 需要删除掉 93 | // C 节点 props 也发生了变化 94 | 95 | // const prevChildren = [ 96 | // h("p", { key: "A" }, "A"), 97 | // h("p", { key: "B" }, "B"), 98 | // h("p", { key: "C", id: "c-prev" }, "C"), 99 | // h("p", { key: "D" }, "D"), 100 | // h("p", { key: "F" }, "F"), 101 | // h("p", { key: "G" }, "G"), 102 | // ]; 103 | 104 | // const nextChildren = [ 105 | // h("p", { key: "A" }, "A"), 106 | // h("p", { key: "B" }, "B"), 107 | // h("p", { key: "E" }, "E"), 108 | // h("p", { key: "C", id:"c-next" }, "C"), 109 | // h("p", { key: "F" }, "F"), 110 | // h("p", { key: "G" }, "G"), 111 | // ]; 112 | 113 | // 5.1.1 114 | // a,b,(c,e,d),f,g 115 | // a,b,(e,c),f,g 116 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 117 | // const prevChildren = [ 118 | // h("p", { key: "A" }, "A"), 119 | // h("p", { key: "B" }, "B"), 120 | // h("p", { key: "C", id: "c-prev" }, "C"), 121 | // h("p", { key: "E" }, "E"), 122 | // h("p", { key: "D" }, "D"), 123 | // h("p", { key: "F" }, "F"), 124 | // h("p", { key: "G" }, "G"), 125 | // ]; 126 | 127 | // const nextChildren = [ 128 | // h("p", { key: "A" }, "A"), 129 | // h("p", { key: "B" }, "B"), 130 | // h("p", { key: "E" }, "E"), 131 | // h("p", { key: "C", id:"c-next" }, "C"), 132 | // h("p", { key: "F" }, "F"), 133 | // h("p", { key: "G" }, "G"), 134 | // ]; 135 | 136 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 137 | 138 | // 2.1 139 | // a,b,(c,d,e),f,g 140 | // a,b,(e,c,d),f,g 141 | // 最长子序列: [1,2] 142 | 143 | // const prevChildren = [ 144 | // h("p", { key: "A" }, "A"), 145 | // h("p", { key: "B" }, "B"), 146 | // h("p", { key: "C" }, "C"), 147 | // h("p", { key: "D" }, "D"), 148 | // h("p", { key: "E" }, "E"), 149 | // h("p", { key: "F" }, "F"), 150 | // h("p", { key: "G" }, "G"), 151 | // ]; 152 | 153 | // const nextChildren = [ 154 | // h("p", { key: "A" }, "A"), 155 | // h("p", { key: "B" }, "B"), 156 | // h("p", { key: "E" }, "E"), 157 | // h("p", { key: "C" }, "C"), 158 | // h("p", { key: "D" }, "D"), 159 | // h("p", { key: "F" }, "F"), 160 | // h("p", { key: "G" }, "G"), 161 | // ]; 162 | 163 | // 3. 创建新的节点 164 | // a,b,(c,e),f,g 165 | // a,b,(e,c,d),f,g 166 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 167 | // const prevChildren = [ 168 | // h("p", { key: "A" }, "A"), 169 | // h("p", { key: "B" }, "B"), 170 | // h("p", { key: "C" }, "C"), 171 | // h("p", { key: "E" }, "E"), 172 | // h("p", { key: "F" }, "F"), 173 | // h("p", { key: "G" }, "G"), 174 | // ]; 175 | 176 | // const nextChildren = [ 177 | // h("p", { key: "A" }, "A"), 178 | // h("p", { key: "B" }, "B"), 179 | // h("p", { key: "E" }, "E"), 180 | // h("p", { key: "C" }, "C"), 181 | // h("p", { key: "D" }, "D"), 182 | // h("p", { key: "F" }, "F"), 183 | // h("p", { key: "G" }, "G"), 184 | // ]; 185 | 186 | // 综合例子 187 | // a,b,(c,d,e,z),f,g 188 | // a,b,(d,c,y,e),f,g 189 | 190 | // const prevChildren = [ 191 | // h("p", { key: "A" }, "A"), 192 | // h("p", { key: "B" }, "B"), 193 | // h("p", { key: "C" }, "C"), 194 | // h("p", { key: "D" }, "D"), 195 | // h("p", { key: "E" }, "E"), 196 | // h("p", { key: "Z" }, "Z"), 197 | // h("p", { key: "F" }, "F"), 198 | // h("p", { key: "G" }, "G"), 199 | // ]; 200 | 201 | // const nextChildren = [ 202 | // h("p", { key: "A" }, "A"), 203 | // h("p", { key: "B" }, "B"), 204 | // h("p", { key: "D" }, "D"), 205 | // h("p", { key: "C" }, "C"), 206 | // h("p", { key: "Y" }, "Y"), 207 | // h("p", { key: "E" }, "E"), 208 | // h("p", { key: "F" }, "F"), 209 | // h("p", { key: "G" }, "G"), 210 | // ]; 211 | 212 | // fix c 节点应该是 move 而不是删除之后重新创建的 213 | const prevChildren = [ 214 | h("p", { key: "A" }, "A"), 215 | h("p", {}, "C"), 216 | h("p", { key: "B" }, "B"), 217 | h("p", { key: "D" }, "D"), 218 | ]; 219 | 220 | const nextChildren = [ 221 | h("p", { key: "A" }, "A"), 222 | h("p", { key: "B" }, "B"), 223 | h("p", {}, "C"), 224 | h("p", { key: "D" }, "D"), 225 | ]; 226 | 227 | export default { 228 | name: "ArrayToArray", 229 | setup() { 230 | const isChange = ref(false); 231 | window.isChange = isChange; 232 | 233 | return { 234 | isChange, 235 | }; 236 | }, 237 | render() { 238 | const self = this; 239 | 240 | return self.isChange === true 241 | ? h("div", {}, nextChildren) 242 | : h("div", {}, prevChildren); 243 | }, 244 | }; 245 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 text 3 | 4 | import { ref, h } from "../../dist/tiny-vue3.esm.js"; 5 | const nextChildren = "newChildren"; 6 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")]; 7 | 8 | export default { 9 | name: "ArrayToText", 10 | setup() { 11 | const isChange = ref(false); 12 | window.isChange = isChange; 13 | 14 | return { 15 | isChange, 16 | }; 17 | }, 18 | render() { 19 | const self = this; 20 | 21 | return self.isChange === true 22 | ? h("div", {}, nextChildren) 23 | : h("div", {}, prevChildren); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | // 新的是 array 2 | // 老的是 text 3 | import { ref, h } from "../../dist/tiny-vue3.esm.js"; 4 | 5 | const prevChildren = "oldChild"; 6 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")]; 7 | 8 | export default { 9 | name: "TextToArray", 10 | setup() { 11 | const isChange = ref(false); 12 | window.isChange = isChange; 13 | 14 | return { 15 | isChange, 16 | }; 17 | }, 18 | render() { 19 | const self = this; 20 | 21 | return self.isChange === true 22 | ? h("div", {}, nextChildren) 23 | : h("div", {}, prevChildren); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | // 新的是 text 2 | // 老的是 text 3 | import { ref, h } from "../../dist/tiny-vue3.esm.js"; 4 | 5 | const prevChildren = "oldChild"; 6 | const nextChildren = "newChild"; 7 | 8 | export default { 9 | name: "TextToText", 10 | setup() { 11 | const isChange = ref(false); 12 | window.isChange = isChange; 13 | 14 | return { 15 | isChange, 16 | }; 17 | }, 18 | render() { 19 | const self = this; 20 | 21 | return self.isChange === true 22 | ? h("div", {}, nextChildren) 23 | : h("div", {}, prevChildren); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import App from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#root"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /packages/vue/examples/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../dist/tiny-vue3.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | 6 | setup() { 7 | const count = ref(0); 8 | 9 | const onClick = () => { 10 | count.value++; 11 | }; 12 | 13 | const props = ref({ 14 | foo: "foo", 15 | bar: "bar", 16 | }); 17 | const onChangePropsDemo1 = () => { 18 | props.value.foo = "new-foo"; 19 | }; 20 | 21 | const onChangePropsDemo2 = () => { 22 | props.value.foo = undefined; 23 | }; 24 | 25 | const onChangePropsDemo3 = () => { 26 | props.value = { 27 | foo: "foo", 28 | }; 29 | }; 30 | 31 | return { 32 | count, 33 | onClick, 34 | onChangePropsDemo1, 35 | onChangePropsDemo2, 36 | onChangePropsDemo3, 37 | props, 38 | }; 39 | }, 40 | render() { 41 | return h( 42 | "div", 43 | { 44 | id: "root", 45 | ...this.props, 46 | }, 47 | [ 48 | h("div", {}, "count:" + this.count), 49 | h( 50 | "button", 51 | { 52 | onClick: this.onClick, 53 | }, 54 | "click" 55 | ), 56 | h( 57 | "button", 58 | { 59 | onClick: this.onChangePropsDemo1, 60 | }, 61 | "changeProps - 值改变了 - 修改" 62 | ), 63 | 64 | h( 65 | "button", 66 | { 67 | onClick: this.onChangePropsDemo2, 68 | }, 69 | "changeProps - 值变成了 undefined - 删除" 70 | ), 71 | 72 | h( 73 | "button", 74 | { 75 | onClick: this.onChangePropsDemo3, 76 | }, 77 | "changeProps - key 在新的里面没有了 - 删除" 78 | ), 79 | ] 80 | ); 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /packages/vue/examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/examples/update/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../dist/tiny-vue3.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | 6 | createApp(App).mount(rootContainer); 7 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-vue3", 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 | "@tiny-vue3/compiler-core": "workspace:^1.0.0", 14 | "@tiny-vue3/runtime-dom": "workspace:^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@tiny-vue3/runtime-dom"; 2 | import { baseCompile } from "@tiny-vue3/compiler-core"; 3 | import * as runtimeDom from "@tiny-vue3/runtime-dom"; 4 | import { registerRuntimeCompiler } from "@tiny-vue3/runtime-dom"; 5 | 6 | function compileToFunction(template) { 7 | const { code } = baseCompile(template); 8 | const render = new Function("Vue", code)(runtimeDom); 9 | 10 | return render; 11 | } 12 | 13 | registerRuntimeCompiler(compileToFunction); 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "./packages/vue/src/index.ts", 5 | output: [ 6 | { 7 | format: "cjs", 8 | file: "packages/vue/dist/tiny-vue3.cjs.js", 9 | }, 10 | { 11 | format: "es", 12 | file: "packages/vue/dist/tiny-vue3.esm.js", 13 | }, 14 | ], 15 | 16 | plugins: [typescript()], 17 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["DOM", "ES6", "ES2016"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | "types": ["jest"], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 70 | "paths": { 71 | "@tiny-vue3/*": [ 72 | "./packages/*/src" 73 | ] 74 | } 75 | }, 76 | "include": [ 77 | "packages/*/src", 78 | "packages/*/__tests__" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import path from "path"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | resolve: { 9 | alias: [ 10 | { 11 | find: /@guide-mini-vue\/(\w*)/, 12 | replacement: path.resolve(__dirname, "packages") + "/$1/src", 13 | }, 14 | ], 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------