├── .gitignore ├── .vscode └── launch.json ├── README.md ├── babel.config.js ├── examples ├── apiInject │ ├── App.js │ └── index.html ├── componentEmit │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlot │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── currentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── helloworld │ ├── App.js │ ├── Foo.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 ├── lib ├── guide-mini-vue.cjs.js └── guide-mini-vue.esm.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index.ts ├── reactivity │ ├── baseHandler.ts │ ├── computed.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── tests │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── createApp.ts │ ├── h.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── index.ts │ ├── renderer.ts │ └── vnode.ts ├── runtime-dom │ └── index.ts └── shared │ ├── ShapeFlags.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand", 13 | "--watchAll=false" 14 | ], 15 | "cwd": "${workspaceFolder}", 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "disableOptimisticBPs": true, 19 | "program": "${workspaceFolder}/node_modules/.bin/jest", 20 | "windows": { 21 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 响应式原理 2 | 3 | 不管是 vue2 还是 vue3,都还存在着响应式原理。虽然功能还是这个功能,但是其实现的方式却是不一样了。 4 | 5 | ### 什么是响应式原理 6 | 7 | 响应式原理本质就是观察者模式(定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新)的实现。对应到 vue 中,被观察的对象就是响应式对象,观察者就是(渲染 watcher,用户 watcher,computed watcher)。 8 | 9 | ### 实现响应式原理的思路 10 | 11 | 将我们的响应式对象做层代理,当响应式对象进行读操作时,对当前活动的观察者(很重要)进行依赖收集。当响应式对象进行写操作时,会对收集的观察者进行派发更新 12 | 13 | ### 本项目中 reactivity 模块 14 | 15 | - src/reactivity/\*(功能实现) 16 | - src/reactivity/tests(测试用例) 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /examples/apiInject/App.js: -------------------------------------------------------------------------------- 1 | // 组件 provide 和 inject 功能 2 | import { h, provide, inject } from "../../lib/guide-mini-vue.esm.js"; 3 | 4 | const Provider = { 5 | name: "Provider", 6 | setup() { 7 | provide("foo", "fooVal"); 8 | provide("bar", "barVal"); 9 | }, 10 | render() { 11 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]); 12 | }, 13 | }; 14 | 15 | const ProviderTwo = { 16 | name: "ProviderTwo", 17 | setup() { 18 | provide("foo", "fooTwo"); 19 | const foo = inject("foo"); 20 | 21 | return { 22 | foo, 23 | }; 24 | }, 25 | render() { 26 | return h("div", {}, [ 27 | h("p", {}, `ProviderTwo foo:${this.foo}`), 28 | h(Consumer), 29 | ]); 30 | }, 31 | }; 32 | 33 | const Consumer = { 34 | name: "Consumer", 35 | setup() { 36 | const foo = inject("foo"); 37 | const bar = inject("bar"); 38 | // const baz = inject("baz", "bazDefault"); 39 | const baz = inject("baz", () => "bazDefault"); 40 | 41 | return { 42 | foo, 43 | bar, 44 | baz, 45 | }; 46 | }, 47 | 48 | render() { 49 | return h("div", {}, `Consumer: - ${this.foo} - ${this.bar}-${this.baz}`); 50 | }, 51 | }; 52 | 53 | export default { 54 | name: "App", 55 | setup() {}, 56 | render() { 57 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /examples/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | // emit 8 | return h("div", {}, [ 9 | h("div", {}, "App"), 10 | h(Foo, { 11 | onAdd(a, b) { 12 | console.log("onAdd", a, b); 13 | }, 14 | onAddFoo() { 15 | console.log("onAddFoo"); 16 | }, 17 | }), 18 | ]); 19 | }, 20 | 21 | setup() { 22 | return {}; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /examples/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.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"); 9 | }; 10 | 11 | return { 12 | emitAdd, 13 | }; 14 | }, 15 | render() { 16 | const btn = h( 17 | "button", 18 | { 19 | onClick: this.emitAdd, 20 | }, 21 | "emitAdd" 22 | ); 23 | 24 | const foo = h("p", {}, "foo"); 25 | return h("div", {}, [foo, btn]); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /examples/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | // Fragment 以及 Text 5 | export const App = { 6 | name: "App", 7 | render() { 8 | const app = h("div", {}, "App"); 9 | // object key 10 | const foo = h( 11 | Foo, 12 | {}, 13 | { 14 | header: ({ age }) => [ 15 | h("p", {}, "header" + age), 16 | createTextVNode("你好呀"), 17 | ], 18 | footer: () => h("p", {}, "footer"), 19 | } 20 | ); 21 | // 数组 vnode 22 | // const foo = h(Foo, {}, h("p", {}, "123")); 23 | return h("div", {}, [app, foo]); 24 | }, 25 | 26 | setup() { 27 | return {}; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /examples/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | setup() { 5 | return {}; 6 | }, 7 | render() { 8 | const foo = h("p", {}, "foo"); 9 | 10 | // Foo .vnode. children 11 | console.log(this.$slots); 12 | // children -> vnode 13 | // 14 | // renderSlots 15 | // 具名插槽 16 | // 1. 获取到要渲染的元素 1 17 | // 2. 要获取到渲染的位置 18 | // 作用域插槽 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 | -------------------------------------------------------------------------------- /examples/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /examples/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | return h("div", {}, [h("p", {}, "currentInstance demo"), h(Foo)]); 8 | }, 9 | 10 | setup() { 11 | const instance = getCurrentInstance(); 12 | console.log("App:", instance); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | name:"Foo", 5 | setup() { 6 | const instance = getCurrentInstance(); 7 | console.log("Foo:", instance); 8 | return {}; 9 | }, 10 | render() { 11 | return h("div", {}, "foo"); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /examples/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null; 5 | export const App = { 6 | // 必须要写 render 7 | name: "App", 8 | render() { 9 | window.self = this; 10 | // ui 11 | return h( 12 | "div", 13 | { 14 | id: "root", 15 | class: ["red", "hard"], 16 | onClick() { 17 | console.log("click"); 18 | }, 19 | onMousedown() { 20 | console.log("mousedown"); 21 | }, 22 | }, 23 | [ 24 | h("div", {}, "hi," + this.msg), 25 | h(Foo, { 26 | count: 1, 27 | }), 28 | ] 29 | // "hi, " + this.msg 30 | // string 31 | // "hi, mini-vue" 32 | // Array 33 | // [h("p", { class:"red"}, "hi"), h("p", {class:"blue"}, "mini-vue")] 34 | ); 35 | }, 36 | 37 | setup() { 38 | return { 39 | msg: "mini-vue-haha", 40 | }; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /examples/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props) { 5 | // props.count 6 | console.log(props); 7 | 8 | // 3. 9 | // shallow readonly 10 | props.count++; 11 | console.log(props); 12 | }, 13 | render() { 14 | return h("div", {}, "foo: " + this.count); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /examples/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | import ArrayToText from "./ArrayToText.js"; 4 | import TextToText from "./TextToText.js"; 5 | import TextToArray from "./TextToArray.js"; 6 | import ArrayToArray from "./ArrayToArray.js"; 7 | 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 | -------------------------------------------------------------------------------- /examples/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 array 3 | 4 | import { ref, h } from "../../lib/guide-mini-vue.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: "A" }, "A"), 58 | // h("p", { key: "B" }, "B"), 59 | // ]; 60 | 61 | // 4. 老的比新的长 62 | // 删除老的 63 | // 左侧 64 | // (a b) c 65 | // (a b) 66 | // i = 2, e1 = 2, e2 = 1 67 | // const prevChildren = [ 68 | // h("p", { key: "A" }, "A"), 69 | // h("p", { key: "B" }, "B"), 70 | // h("p", { key: "C" }, "C"), 71 | // ]; 72 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 73 | 74 | // 右侧 75 | // a (b c) 76 | // (b c) 77 | // i = 0, e1 = 0, e2 = -1 78 | 79 | // const prevChildren = [ 80 | // h("p", { key: "A" }, "A"), 81 | // h("p", { key: "B" }, "B"), 82 | // h("p", { key: "C" }, "C"), 83 | // ]; 84 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; 85 | 86 | // 5. 对比中间的部分 87 | // 删除老的 (在老的里面存在,新的里面不存在) 88 | // 5.1 89 | // a,b,(c,d),f,g 90 | // a,b,(e,c),f,g 91 | // D 节点在新的里面是没有的 - 需要删除掉 92 | // C 节点 props 也发生了变化 93 | 94 | // const prevChildren = [ 95 | // h("p", { key: "A" }, "A"), 96 | // h("p", { key: "B" }, "B"), 97 | // h("p", { key: "C", id: "c-prev" }, "C"), 98 | // h("p", { key: "D" }, "D"), 99 | // h("p", { key: "F" }, "F"), 100 | // h("p", { key: "G" }, "G"), 101 | // ]; 102 | 103 | // const nextChildren = [ 104 | // h("p", { key: "A" }, "A"), 105 | // h("p", { key: "B" }, "B"), 106 | // h("p", { key: "E" }, "E"), 107 | // h("p", { key: "C", id:"c-next" }, "C"), 108 | // h("p", { key: "F" }, "F"), 109 | // h("p", { key: "G" }, "G"), 110 | // ]; 111 | 112 | // 5.1.1 113 | // a,b,(c,e,d),f,g 114 | // a,b,(e,c),f,g 115 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 116 | // const prevChildren = [ 117 | // h("p", { key: "A" }, "A"), 118 | // h("p", { key: "B" }, "B"), 119 | // h("p", { key: "C", id: "c-prev" }, "C"), 120 | // h("p", { key: "E" }, "E"), 121 | // h("p", { key: "D" }, "D"), 122 | // h("p", { key: "F" }, "F"), 123 | // h("p", { key: "G" }, "G"), 124 | // ]; 125 | 126 | // const nextChildren = [ 127 | // h("p", { key: "A" }, "A"), 128 | // h("p", { key: "B" }, "B"), 129 | // h("p", { key: "E" }, "E"), 130 | // h("p", { key: "C", id:"c-next" }, "C"), 131 | // h("p", { key: "F" }, "F"), 132 | // h("p", { key: "G" }, "G"), 133 | // ]; 134 | 135 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 136 | 137 | // 2.1 138 | // a,b,(c,d,e),f,g 139 | // a,b,(e,c,d),f,g 140 | // 最长子序列: [1,2] 141 | 142 | // const prevChildren = [ 143 | // h("p", { key: "A" }, "A"), 144 | // h("p", { key: "B" }, "B"), 145 | // h("p", { key: "C" }, "C"), 146 | // h("p", { key: "D" }, "D"), 147 | // h("p", { key: "E" }, "E"), 148 | // h("p", { key: "F" }, "F"), 149 | // h("p", { key: "G" }, "G"), 150 | // ]; 151 | 152 | // const nextChildren = [ 153 | // h("p", { key: "A" }, "A"), 154 | // h("p", { key: "B" }, "B"), 155 | // h("p", { key: "E" }, "E"), 156 | // h("p", { key: "C" }, "C"), 157 | // h("p", { key: "D" }, "D"), 158 | // h("p", { key: "F" }, "F"), 159 | // h("p", { key: "G" }, "G"), 160 | // ]; 161 | 162 | // 3. 创建新的节点 163 | // a,b,(c,e),f,g 164 | // a,b,(e,c,d),f,g 165 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 166 | // const prevChildren = [ 167 | // h("p", { key: "A" }, "A"), 168 | // h("p", { key: "B" }, "B"), 169 | // h("p", { key: "C" }, "C"), 170 | // h("p", { key: "E" }, "E"), 171 | // h("p", { key: "F" }, "F"), 172 | // h("p", { key: "G" }, "G"), 173 | // ]; 174 | 175 | // const nextChildren = [ 176 | // h("p", { key: "A" }, "A"), 177 | // h("p", { key: "B" }, "B"), 178 | // h("p", { key: "E" }, "E"), 179 | // h("p", { key: "C" }, "C"), 180 | // h("p", { key: "D" }, "D"), 181 | // h("p", { key: "F" }, "F"), 182 | // h("p", { key: "G" }, "G"), 183 | // ]; 184 | 185 | // 综合例子 186 | // a,b,(c,d,e,z),f,g 187 | // a,b,(d,c,y,e),f,g 188 | const prevChildren = [ 189 | h("p", { key: "A" }, "A"), 190 | h("p", { key: "B" }, "B"), 191 | h("p", { key: "C" }, "C"), 192 | h("p", { key: "D" }, "D"), 193 | h("p", { key: "E" }, "E"), 194 | h("p", { key: "Z" }, "Z"), 195 | h("p", { key: "F" }, "F"), 196 | h("p", { key: "G" }, "G"), 197 | ]; 198 | 199 | const nextChildren = [ 200 | h("p", { key: "A" }, "A"), 201 | h("p", { key: "B" }, "B"), 202 | h("p", { key: "D" }, "D"), 203 | h("p", { key: "C" }, "C"), 204 | h("p", { key: "Y" }, "Y"), 205 | h("p", { key: "E" }, "E"), 206 | h("p", { key: "F" }, "F"), 207 | h("p", { key: "G" }, "G"), 208 | ]; 209 | 210 | export default { 211 | name: "ArrayToArray", 212 | setup() { 213 | const isChange = ref(false); 214 | window.isChange = isChange; 215 | 216 | return { 217 | isChange, 218 | }; 219 | }, 220 | render() { 221 | const self = this; 222 | 223 | return self.isChange === true 224 | ? h("div", {}, nextChildren) 225 | : h("div", {}, prevChildren); 226 | }, 227 | }; 228 | -------------------------------------------------------------------------------- /examples/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 text 3 | 4 | import { ref, h } from "../../lib/guide-mini-vue.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 | -------------------------------------------------------------------------------- /examples/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | // 新的是 array 2 | // 老的是 text 3 | import { ref, h } from "../../lib/guide-mini-vue.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 | -------------------------------------------------------------------------------- /examples/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | // 新的是 text 2 | // 老的是 text 3 | import { ref, h } from "../../lib/guide-mini-vue.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 | -------------------------------------------------------------------------------- /examples/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import App from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#root"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /examples/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/guide-mini-vue.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 | -------------------------------------------------------------------------------- /examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/update/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /lib/guide-mini-vue.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 | el: null, 13 | shapeFlag: getShapeType(type), 14 | key: props && props.key, 15 | }; 16 | if (typeof children === "string") { 17 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 18 | } 19 | else if (Array.isArray(children)) { 20 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 21 | } 22 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 23 | if (typeof children === "object") { 24 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 25 | } 26 | } 27 | return vnode; 28 | } 29 | function createTextVNode(text) { 30 | return createVNode(Text, {}, text); 31 | } 32 | function getShapeType(type) { 33 | return typeof type === "string" 34 | ? 1 /* ELEMENT */ 35 | : 2 /* STATEFUL_COMPONENT */; 36 | } 37 | 38 | function h(type, props, children) { 39 | return createVNode(type, props, children); 40 | } 41 | 42 | function renderSlots(slots, name, props) { 43 | let slot = slots[name]; 44 | if (slot) { 45 | if (typeof slot === "function") { 46 | return createVNode(Fragment, {}, slot(props)); 47 | } 48 | } 49 | } 50 | 51 | const extend = Object.assign; 52 | const isObject = (value) => { 53 | return value !== null && typeof value === "object"; 54 | }; 55 | const hasChanged = (val, newValue) => { 56 | return !Object.is(val, newValue); 57 | }; 58 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 59 | const camelize = (str) => { 60 | return str.replace(/-(\w)/g, (_, c) => { 61 | return c ? c.toUpperCase() : ""; 62 | }); 63 | }; 64 | const capitalize = (str) => { 65 | return str.charAt(0).toUpperCase() + str.slice(1); 66 | }; 67 | const toHandlerKey = (str) => { 68 | return str ? "on" + capitalize(str) : ""; 69 | }; 70 | 71 | const publicPropertiesMap = { 72 | $el: (i) => i.vnode.el, 73 | $slots: (i) => i.slots, 74 | }; 75 | const PublicInstanceProxyHandlers = { 76 | get({ _: instance }, key) { 77 | // setupState 78 | const { setupState, props } = instance; 79 | if (hasOwn(setupState, key)) { 80 | return setupState[key]; 81 | } 82 | else if (hasOwn(props, key)) { 83 | return props[key]; 84 | } 85 | const publicGetter = publicPropertiesMap[key]; 86 | if (publicGetter) { 87 | return publicGetter(instance); 88 | } 89 | }, 90 | }; 91 | 92 | function initProps(intstance, rawProps) { 93 | intstance.props = rawProps || {}; 94 | } 95 | 96 | function initSlots(instance, children) { 97 | const { vnode } = instance; 98 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 99 | normalizeObjectSlots(children, instance.slots); 100 | } 101 | } 102 | function normalizeObjectSlots(children, slots) { 103 | for (let key in children) { 104 | let value = children[key]; 105 | slots[key] = (props) => normalizeSlotValue(value(props)); 106 | } 107 | } 108 | function normalizeSlotValue(value) { 109 | return Array.isArray(value) ? value : [value]; 110 | } 111 | 112 | let activeEffect; 113 | let shouldTrack = false; 114 | class ReactiveEffect { 115 | constructor(fn, scheduler) { 116 | this.deps = []; 117 | this.active = true; 118 | this._fn = fn; 119 | this.scheduler = scheduler; 120 | } 121 | run() { 122 | if (!this.active) { 123 | return this._fn(); 124 | } 125 | // 应该收集 126 | shouldTrack = true; 127 | activeEffect = this; 128 | const r = this._fn(); 129 | // 重置 130 | shouldTrack = false; 131 | return r; 132 | } 133 | stop() { 134 | if (this.active) { 135 | cleanupEffect(this); 136 | if (this.onStop) { 137 | this.onStop(); 138 | } 139 | this.active = false; 140 | } 141 | } 142 | } 143 | function cleanupEffect(effect) { 144 | effect.deps.forEach((dep) => { 145 | dep.delete(effect); 146 | }); 147 | // 把 effect.deps 清空 148 | effect.deps.length = 0; 149 | } 150 | const targetMap = new Map(); 151 | function track(target, key) { 152 | if (!isTracking()) 153 | return; 154 | // target -> key -> dep 155 | let depsMap = targetMap.get(target); 156 | if (!depsMap) { 157 | depsMap = new Map(); 158 | targetMap.set(target, depsMap); 159 | } 160 | let dep = depsMap.get(key); 161 | if (!dep) { 162 | dep = new Set(); 163 | depsMap.set(key, dep); 164 | } 165 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 166 | if (dep.has(activeEffect)) 167 | return; 168 | trackEffects(dep); 169 | } 170 | function trackEffects(dep) { 171 | dep.add(activeEffect); 172 | activeEffect.deps.push(dep); 173 | } 174 | function isTracking() { 175 | return shouldTrack && activeEffect !== undefined; 176 | } 177 | function trigger(target, key) { 178 | let depsMap = targetMap.get(target); 179 | let dep = depsMap.get(key); 180 | triggerEffects(dep); 181 | } 182 | function triggerEffects(dep) { 183 | for (const effect of dep) { 184 | if (effect.scheduler) { 185 | effect.scheduler(); 186 | } 187 | else { 188 | effect.run(); 189 | } 190 | } 191 | } 192 | // 返回一个runner函数 193 | function effect(fn, options = {}) { 194 | // fn 195 | const _effect = new ReactiveEffect(fn, options.scheduler); 196 | extend(_effect, options); 197 | _effect.run(); // 依赖收集的入口 198 | const runner = _effect.run.bind(_effect); 199 | runner.effect = _effect; 200 | return runner; 201 | } 202 | 203 | const get = createGetter(); 204 | const set = createSetter(); 205 | const readonlyGet = createGetter(true); 206 | const shallowReadonlyGet = createGetter(true, true); 207 | function createGetter(isReadonly = false, shallow = false) { 208 | return function get(target, key) { 209 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 210 | return !isReadonly; 211 | } 212 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 213 | return isReadonly; 214 | } 215 | const res = Reflect.get(target, key); 216 | if (shallow) { 217 | return res; 218 | } 219 | if (isObject(res)) { 220 | return isReadonly ? readonly(res) : reactive(res); 221 | } 222 | if (!isReadonly) { 223 | track(target, key); 224 | } 225 | return res; 226 | }; 227 | } 228 | function createSetter() { 229 | return function set(target, key, value) { 230 | const res = Reflect.set(target, key, value); 231 | trigger(target, key); 232 | return res; 233 | }; 234 | } 235 | const mutableHandlers = { 236 | get, 237 | set, 238 | }; 239 | const readonlyHandlers = { 240 | get: readonlyGet, 241 | set(target, key) { 242 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 243 | return true; 244 | }, 245 | }; 246 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 247 | get: shallowReadonlyGet, 248 | }); 249 | 250 | function reactive(raw) { 251 | return createReactiveObject(raw, mutableHandlers); 252 | } 253 | function readonly(raw) { 254 | return createReactiveObject(raw, readonlyHandlers); 255 | } 256 | function shallowReadonly(raw) { 257 | return createReactiveObject(raw, shallowReadonlyHandlers); 258 | } 259 | function createReactiveObject(target, baseHandles) { 260 | return new Proxy(target, baseHandles); 261 | } 262 | 263 | function emit(instance, event, ...args) { 264 | const { props } = instance; 265 | const handlerName = toHandlerKey(camelize(event)); 266 | const handler = props[handlerName]; 267 | handler && handler(...args); 268 | } 269 | 270 | class RefImpl { 271 | constructor(value) { 272 | this._v_isRef = true; 273 | this._rawValue = value; // 保存原始值 274 | this._value = convert(value); // 如果是对象,则转化成响应式对象 275 | this.dep = new Set(); 276 | } 277 | get value() { 278 | trackRefValue(this); 279 | return this._value; 280 | } 281 | set value(newVal) { 282 | // 如果值没有改变,那么没必要重复触发 283 | if (hasChanged(newVal, this._rawValue)) { 284 | this._rawValue = newVal; 285 | this._value = convert(newVal); 286 | triggerEffects(this.dep); 287 | } 288 | } 289 | } 290 | function ref(value) { 291 | return new RefImpl(value); 292 | } 293 | function convert(value) { 294 | return isObject(value) ? reactive(value) : value; 295 | } 296 | function trackRefValue(ref) { 297 | if (isTracking()) { 298 | trackEffects(ref.dep); 299 | } 300 | } 301 | function isRef(ref) { 302 | return !!ref._v_isRef; 303 | } 304 | function unRef(ref) { 305 | return isRef(ref) ? ref.value : ref; 306 | } 307 | function proxyRefs(objectWithRefs) { 308 | return new Proxy(objectWithRefs, { 309 | get(target, key) { 310 | return unRef(Reflect.get(target, key)); 311 | }, 312 | set(target, key, value) { 313 | if (isRef(target[key]) && !isRef(value)) { 314 | return (target[key].value = value); 315 | } 316 | else { 317 | return Reflect.set(target, key, value); 318 | } 319 | }, 320 | }); 321 | } 322 | 323 | function createComponentInstance(vnode, parent) { 324 | const component = { 325 | vnode, 326 | type: vnode.type, 327 | setupState: {}, 328 | props: {}, 329 | slots: {}, 330 | provides: parent ? parent.provides : {}, 331 | parent, 332 | isMounted: false, 333 | emit: () => { }, 334 | }; 335 | component.emit = emit.bind(null, component); 336 | return component; 337 | } 338 | function setupComponent(instance) { 339 | // TODO 340 | initProps(instance, instance.vnode.props); 341 | initSlots(instance, instance.vnode.children); 342 | setupStatefulComponent(instance); 343 | } 344 | function setupStatefulComponent(instance) { 345 | const Component = instance.type; 346 | const { setup } = Component; 347 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 348 | if (setup) { 349 | setCurrentInstance(instance); 350 | const setupResult = setup(shallowReadonly(instance.props), { 351 | emit: instance.emit, 352 | }); 353 | setCurrentInstance(null); 354 | handleSetupResult(instance, setupResult); 355 | } 356 | } 357 | function handleSetupResult(instance, setupResult) { 358 | if (typeof setupResult === "object") { 359 | instance.setupState = proxyRefs(setupResult); 360 | } 361 | finishComponentSetup(instance); 362 | } 363 | // 最终肯定是要返回一个render函数 364 | function finishComponentSetup(instance) { 365 | const Component = instance.type; 366 | if (Component.render) { 367 | instance.render = Component.render; 368 | } 369 | } 370 | let currentInstance = null; 371 | function getCurrentInstance() { 372 | return currentInstance; 373 | } 374 | function setCurrentInstance(instance) { 375 | currentInstance = instance; 376 | } 377 | 378 | function provide(key, value) { 379 | const currentInstance = getCurrentInstance(); 380 | if (currentInstance) { 381 | let { provides } = currentInstance; 382 | const parentProvides = currentInstance.parent.provides; 383 | if (parentProvides === provides) { 384 | provides = currentInstance.provides = Object.create(parentProvides); 385 | } 386 | provides[key] = value; 387 | } 388 | } 389 | function inject(key, defaultValue) { 390 | const currentInstance = getCurrentInstance(); 391 | if (currentInstance) { 392 | const parentProvides = currentInstance.parent.provides; 393 | if (key in parentProvides) { 394 | return parentProvides[key]; 395 | } 396 | else if (defaultValue) { 397 | if (typeof defaultValue === "function") { 398 | return defaultValue(); 399 | } 400 | return defaultValue; 401 | } 402 | } 403 | } 404 | 405 | function createAppAPI(render) { 406 | return function createApp(rootComponent) { 407 | return { 408 | mount(rootContainer) { 409 | const vnode = createVNode(rootComponent); 410 | render(vnode, rootContainer); 411 | }, 412 | }; 413 | }; 414 | } 415 | 416 | function createRenderer(options) { 417 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 418 | function render(vnode, container) { 419 | patch(null, vnode, container, null, null); 420 | } 421 | function patch(n1, n2, container, parentComponent, anchor) { 422 | const { type, shapeFlag } = n2; 423 | switch (type) { 424 | case Fragment: 425 | processFragment(n1, n2, container, parentComponent, anchor); 426 | break; 427 | case Text: 428 | processText(n1, n2, container); 429 | break; 430 | default: 431 | if (shapeFlag & 1 /* ELEMENT */) { 432 | processElement(n1, n2, container, parentComponent, anchor); 433 | } 434 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 435 | processComponent(n1, n2, container, parentComponent, anchor); 436 | } 437 | break; 438 | } 439 | } 440 | function processText(n1, n2, container) { 441 | const { children } = n2; 442 | const textVNode = (n2.el = document.createTextNode(children)); 443 | container.append(textVNode); 444 | } 445 | function processFragment(n1, n2, container, parentComponent, anchor) { 446 | mountChildren(n2.children, container, parentComponent, anchor); 447 | } 448 | function processElement(n1, n2, container, parentComponent, anchor) { 449 | if (!n1) { 450 | mountElement(n2, container, parentComponent, anchor); 451 | } 452 | else { 453 | patchElement(n1, n2, container, parentComponent, anchor); 454 | } 455 | } 456 | function patchElement(n1, n2, container, parentComponent, anchor) { 457 | console.log("patchElement"); 458 | console.log("n1", n1); 459 | console.log("n2", n2); 460 | const oldProps = n1.props; 461 | const newProps = n2.props; 462 | const el = (n2.el = n1.el); 463 | patchChildren(n1, n2, el, parentComponent, anchor); 464 | patchProps(el, oldProps, newProps); 465 | } 466 | function patchChildren(n1, n2, container, parentComponent, anchor) { 467 | const prevShapeFlag = n1.shapeFlag; 468 | const shapeFlag = n2.shapeFlag; 469 | const c1 = n1.children; 470 | const c2 = n2.children; 471 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 472 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 473 | unmountChildren(c1); 474 | } 475 | if (c1 !== c2) { 476 | hostSetElementText(container, c2); 477 | } 478 | } 479 | else { 480 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 481 | hostSetElementText(container, ""); 482 | mountChildren(c2, container, parentComponent, anchor); 483 | } 484 | else { 485 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 486 | } 487 | } 488 | } 489 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 490 | const l2 = c2.length; 491 | let i = 0; 492 | let e1 = c1.length - 1; 493 | let e2 = l2 - 1; 494 | function isSomeVNodeType(n1, n2) { 495 | return n1.type === n2.type && n1.key === n2.key; 496 | } 497 | // 左侧进行对比相同节点 498 | while (i <= e1 && i <= e2) { 499 | let n1 = c1[i]; 500 | let n2 = c2[i]; 501 | if (isSomeVNodeType(n1, n2)) { 502 | patch(n1, n2, container, parentComponent, parentAnchor); 503 | } 504 | else { 505 | break; 506 | } 507 | i++; 508 | } 509 | // 右侧进行对比相同节点 510 | while (i <= e1 && i <= e2) { 511 | let n1 = c1[e1]; 512 | let n2 = c2[e2]; 513 | if (isSomeVNodeType(n1, n2)) { 514 | patch(n1, n2, container, parentComponent, parentAnchor); 515 | } 516 | else { 517 | break; 518 | } 519 | e1--; 520 | e2--; 521 | } 522 | if (i > e1) { 523 | if (i <= e2) { 524 | // 旧节点结束,新节点没有结束的情况 525 | const nextPos = e2 + 1; 526 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 527 | while (i <= e2) { 528 | patch(null, c2[i], container, parentComponent, anchor); 529 | i++; 530 | } 531 | } 532 | } 533 | else if (i > e2) { 534 | // 新节点结束,旧节点未结束的情况 535 | while (i <= e1) { 536 | hostRemove(c1[i].el); 537 | i++; 538 | } 539 | } 540 | else { 541 | // 中间对比 542 | let s1 = i; 543 | let s2 = i; 544 | let patched = 0; 545 | let moved = false; 546 | let maxNewIndexSoFar = 0; 547 | const toBePatched = e2 - s2 + 1; 548 | const keyToNewIndexMap = new Map(); 549 | const newIndexToOldIndexMap = new Array(toBePatched); 550 | for (let i = 0; i < toBePatched; i++) 551 | newIndexToOldIndexMap[i] = 0; 552 | // 建立旧节点key映射到新节点index的hash表 553 | for (let i = s2; i <= e2; i++) { 554 | const nextChild = c2[i]; 555 | keyToNewIndexMap.set(nextChild.key, i); 556 | } 557 | for (let i = s1; i <= e1; i++) { 558 | const prevChild = c1[i]; 559 | if (patched >= toBePatched) { 560 | hostRemove(prevChild.el); 561 | continue; 562 | } 563 | // 哈希表中找是否存在的值 564 | let newIndex; 565 | if (prevChild.key != null) { 566 | newIndex = keyToNewIndexMap.get(prevChild.key); 567 | } 568 | else { 569 | for (let j = s2; j < e2; j++) { 570 | if (isSomeVNodeType(prevChild, s2[j])) { 571 | newIndex = j; 572 | break; 573 | } 574 | } 575 | } 576 | // 如果哈希表中没有找到,则进行删除操作 577 | if (newIndex == null) { 578 | hostRemove(prevChild.el); 579 | } 580 | else { 581 | // 如果在哈希表中找到 582 | if (newIndex >= maxNewIndexSoFar) { 583 | maxNewIndexSoFar = newIndex; 584 | } 585 | else { 586 | moved = true; 587 | } 588 | // 建立新节点index(这个index只针对中间的节点序列)到旧节点index的映射关系 589 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 590 | patch(prevChild, c2[newIndex], container, parentComponent, null); 591 | patched++; 592 | } 593 | } 594 | const increasingNewIndexSequence = moved 595 | ? getSequence(newIndexToOldIndexMap) 596 | : []; 597 | let j = increasingNewIndexSequence.length - 1; // 生成一个最长的递增子序列 598 | for (let i = toBePatched - 1; i >= 0; i--) { 599 | const nextIndex = i + s2; 600 | const nextChild = c2[nextIndex]; 601 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 602 | if (newIndexToOldIndexMap[i] === 0) { 603 | // 新节点没有在旧节点中找到,给了个标识,直接在这里进行插入 604 | patch(null, nextChild, container, parentComponent, anchor); 605 | } 606 | else if (moved) { 607 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 608 | // 这时对顺序进行调整 609 | hostInsert(nextChild.el, container, anchor); 610 | } 611 | else { 612 | j--; 613 | } 614 | } 615 | } 616 | } 617 | } 618 | function unmountChildren(children) { 619 | for (let i = 0; i < children.length; i++) { 620 | const el = children[i].el; 621 | hostRemove(el); 622 | } 623 | } 624 | function patchProps(el, oldProps, newProps) { 625 | if (oldProps !== newProps) { 626 | for (const key in newProps) { 627 | const prevProp = oldProps[key]; 628 | const nextProp = newProps[key]; 629 | if (prevProp !== nextProp) { 630 | hostPatchProp(el, key, prevProp, nextProp); 631 | } 632 | } 633 | if (JSON.stringify(oldProps) !== "{}") { 634 | for (const key in oldProps) { 635 | if (!(key in newProps)) { 636 | hostPatchProp(el, key, oldProps[key], null); 637 | } 638 | } 639 | } 640 | } 641 | } 642 | function processComponent(n1, n2, container, parentComponent, anchor) { 643 | mountComponent(n2, container, parentComponent, anchor); 644 | } 645 | function mountElement(vnode, container, parentComponent, anchor) { 646 | // createElement 647 | const el = (vnode.el = hostCreateElement(vnode.type)); 648 | const { children, shapeFlag } = vnode; 649 | // children 650 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 651 | el.textContent = children; 652 | } 653 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 654 | mountChildren(vnode.children, el, parentComponent, anchor); 655 | } 656 | // props 657 | const { props } = vnode; 658 | for (const key in props) { 659 | hostPatchProp(el, key, null, props[key]); 660 | } 661 | hostInsert(el, container, anchor); 662 | } 663 | function mountChildren(children, container, parentComponent, anchor) { 664 | children.forEach((v) => { 665 | patch(null, v, container, parentComponent, anchor); 666 | }); 667 | } 668 | function mountComponent(initialVNode, container, parentComponent, anchor) { 669 | const instance = createComponentInstance(initialVNode, parentComponent); 670 | setupComponent(instance); 671 | setupRenderEffect(instance, initialVNode, container, anchor); 672 | } 673 | function setupRenderEffect(instance, initialVNode, container, anchor) { 674 | effect(() => { 675 | if (!instance.isMounted) { 676 | console.log("init"); 677 | const { proxy } = instance; 678 | const subTree = (instance.subTree = instance.render.call(proxy)); 679 | patch(null, subTree, container, instance, anchor); 680 | initialVNode.el = subTree.el; 681 | instance.isMounted = true; 682 | } 683 | else { 684 | console.log("update"); 685 | const { proxy } = instance; 686 | const subTree = instance.render.call(proxy); 687 | const prevSubTree = instance.subTree; 688 | instance.subTree = subTree; 689 | patch(prevSubTree, subTree, container, instance, anchor); 690 | } 691 | }); 692 | } 693 | return { 694 | createApp: createAppAPI(render), 695 | }; 696 | } 697 | function getSequence(arr) { 698 | const p = arr.slice(); 699 | const result = [0]; 700 | let i, j, u, v, c; 701 | const len = arr.length; 702 | for (i = 0; i < len; i++) { 703 | const arrI = arr[i]; 704 | if (arrI !== 0) { 705 | j = result[result.length - 1]; 706 | if (arr[j] < arrI) { 707 | p[i] = j; 708 | result.push(i); 709 | continue; 710 | } 711 | u = 0; 712 | v = result.length - 1; 713 | while (u < v) { 714 | c = (u + v) >> 1; 715 | if (arr[result[c]] < arrI) { 716 | u = c + 1; 717 | } 718 | else { 719 | v = c; 720 | } 721 | } 722 | if (arrI < arr[result[u]]) { 723 | if (u > 0) { 724 | p[i] = result[u - 1]; 725 | } 726 | result[u] = i; 727 | } 728 | } 729 | } 730 | u = result.length; 731 | v = result[u - 1]; 732 | while (u-- > 0) { 733 | result[u] = v; 734 | v = p[v]; 735 | } 736 | return result; 737 | } 738 | 739 | function createElement(type) { 740 | return document.createElement(type); 741 | } 742 | function patchProp(el, key, oldValue, nextValue) { 743 | const isOn = (key) => /^on[A-Z]/.test(key); 744 | if (isOn(key)) { 745 | const event = key.slice(2).toLowerCase(); 746 | el.addEventListener(event, nextValue); 747 | } 748 | else { 749 | if (nextValue == null) { 750 | el.removeAttribute(key); 751 | } 752 | else { 753 | el.setAttribute(key, nextValue); 754 | } 755 | } 756 | } 757 | function insert(child, parent, anchor) { 758 | parent.insertBefore(child, anchor || null); 759 | } 760 | function remove(child) { 761 | const parent = child.parentNode; 762 | if (parent) { 763 | parent.removeChild(child); 764 | } 765 | } 766 | function setElementText(el, text) { 767 | el.textContent = text; 768 | } 769 | const renderer = createRenderer({ 770 | createElement, 771 | patchProp, 772 | insert, 773 | remove, 774 | setElementText, 775 | }); 776 | function createApp(...args) { 777 | return renderer.createApp(...args); 778 | } 779 | 780 | exports.createApp = createApp; 781 | exports.createRenderer = createRenderer; 782 | exports.createTextVNode = createTextVNode; 783 | exports.getCurrentInstance = getCurrentInstance; 784 | exports.h = h; 785 | exports.inject = inject; 786 | exports.provide = provide; 787 | exports.proxyRefs = proxyRefs; 788 | exports.ref = ref; 789 | exports.renderSlots = renderSlots; 790 | -------------------------------------------------------------------------------- /lib/guide-mini-vue.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 | el: null, 9 | shapeFlag: getShapeType(type), 10 | key: props && props.key, 11 | }; 12 | if (typeof children === "string") { 13 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 14 | } 15 | else if (Array.isArray(children)) { 16 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 17 | } 18 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 19 | if (typeof children === "object") { 20 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 21 | } 22 | } 23 | return vnode; 24 | } 25 | function createTextVNode(text) { 26 | return createVNode(Text, {}, text); 27 | } 28 | function getShapeType(type) { 29 | return typeof type === "string" 30 | ? 1 /* ELEMENT */ 31 | : 2 /* STATEFUL_COMPONENT */; 32 | } 33 | 34 | function h(type, props, children) { 35 | return createVNode(type, props, children); 36 | } 37 | 38 | function renderSlots(slots, name, props) { 39 | let slot = slots[name]; 40 | if (slot) { 41 | if (typeof slot === "function") { 42 | return createVNode(Fragment, {}, slot(props)); 43 | } 44 | } 45 | } 46 | 47 | const extend = Object.assign; 48 | const isObject = (value) => { 49 | return value !== null && typeof value === "object"; 50 | }; 51 | const hasChanged = (val, newValue) => { 52 | return !Object.is(val, newValue); 53 | }; 54 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 55 | const camelize = (str) => { 56 | return str.replace(/-(\w)/g, (_, c) => { 57 | return c ? c.toUpperCase() : ""; 58 | }); 59 | }; 60 | const capitalize = (str) => { 61 | return str.charAt(0).toUpperCase() + str.slice(1); 62 | }; 63 | const toHandlerKey = (str) => { 64 | return str ? "on" + capitalize(str) : ""; 65 | }; 66 | 67 | const publicPropertiesMap = { 68 | $el: (i) => i.vnode.el, 69 | $slots: (i) => i.slots, 70 | }; 71 | const PublicInstanceProxyHandlers = { 72 | get({ _: instance }, key) { 73 | // setupState 74 | const { setupState, props } = instance; 75 | if (hasOwn(setupState, key)) { 76 | return setupState[key]; 77 | } 78 | else if (hasOwn(props, key)) { 79 | return props[key]; 80 | } 81 | const publicGetter = publicPropertiesMap[key]; 82 | if (publicGetter) { 83 | return publicGetter(instance); 84 | } 85 | }, 86 | }; 87 | 88 | function initProps(intstance, rawProps) { 89 | intstance.props = rawProps || {}; 90 | } 91 | 92 | function initSlots(instance, children) { 93 | const { vnode } = instance; 94 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 95 | normalizeObjectSlots(children, instance.slots); 96 | } 97 | } 98 | function normalizeObjectSlots(children, slots) { 99 | for (let key in children) { 100 | let value = children[key]; 101 | slots[key] = (props) => normalizeSlotValue(value(props)); 102 | } 103 | } 104 | function normalizeSlotValue(value) { 105 | return Array.isArray(value) ? value : [value]; 106 | } 107 | 108 | let activeEffect; 109 | let shouldTrack = false; 110 | class ReactiveEffect { 111 | constructor(fn, scheduler) { 112 | this.deps = []; 113 | this.active = true; 114 | this._fn = fn; 115 | this.scheduler = scheduler; 116 | } 117 | run() { 118 | if (!this.active) { 119 | return this._fn(); 120 | } 121 | // 应该收集 122 | shouldTrack = true; 123 | activeEffect = this; 124 | const r = this._fn(); 125 | // 重置 126 | shouldTrack = false; 127 | return r; 128 | } 129 | stop() { 130 | if (this.active) { 131 | cleanupEffect(this); 132 | if (this.onStop) { 133 | this.onStop(); 134 | } 135 | this.active = false; 136 | } 137 | } 138 | } 139 | function cleanupEffect(effect) { 140 | effect.deps.forEach((dep) => { 141 | dep.delete(effect); 142 | }); 143 | // 把 effect.deps 清空 144 | effect.deps.length = 0; 145 | } 146 | const targetMap = new Map(); 147 | function track(target, key) { 148 | if (!isTracking()) 149 | return; 150 | // target -> key -> dep 151 | let depsMap = targetMap.get(target); 152 | if (!depsMap) { 153 | depsMap = new Map(); 154 | targetMap.set(target, depsMap); 155 | } 156 | let dep = depsMap.get(key); 157 | if (!dep) { 158 | dep = new Set(); 159 | depsMap.set(key, dep); 160 | } 161 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 162 | if (dep.has(activeEffect)) 163 | return; 164 | trackEffects(dep); 165 | } 166 | function trackEffects(dep) { 167 | dep.add(activeEffect); 168 | activeEffect.deps.push(dep); 169 | } 170 | function isTracking() { 171 | return shouldTrack && activeEffect !== undefined; 172 | } 173 | function trigger(target, key) { 174 | let depsMap = targetMap.get(target); 175 | let dep = depsMap.get(key); 176 | triggerEffects(dep); 177 | } 178 | function triggerEffects(dep) { 179 | for (const effect of dep) { 180 | if (effect.scheduler) { 181 | effect.scheduler(); 182 | } 183 | else { 184 | effect.run(); 185 | } 186 | } 187 | } 188 | // 返回一个runner函数 189 | function effect(fn, options = {}) { 190 | // fn 191 | const _effect = new ReactiveEffect(fn, options.scheduler); 192 | extend(_effect, options); 193 | _effect.run(); // 依赖收集的入口 194 | const runner = _effect.run.bind(_effect); 195 | runner.effect = _effect; 196 | return runner; 197 | } 198 | 199 | const get = createGetter(); 200 | const set = createSetter(); 201 | const readonlyGet = createGetter(true); 202 | const shallowReadonlyGet = createGetter(true, true); 203 | function createGetter(isReadonly = false, shallow = false) { 204 | return function get(target, key) { 205 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 206 | return !isReadonly; 207 | } 208 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 209 | return isReadonly; 210 | } 211 | const res = Reflect.get(target, key); 212 | if (shallow) { 213 | return res; 214 | } 215 | if (isObject(res)) { 216 | return isReadonly ? readonly(res) : reactive(res); 217 | } 218 | if (!isReadonly) { 219 | track(target, key); 220 | } 221 | return res; 222 | }; 223 | } 224 | function createSetter() { 225 | return function set(target, key, value) { 226 | const res = Reflect.set(target, key, value); 227 | trigger(target, key); 228 | return res; 229 | }; 230 | } 231 | const mutableHandlers = { 232 | get, 233 | set, 234 | }; 235 | const readonlyHandlers = { 236 | get: readonlyGet, 237 | set(target, key) { 238 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 239 | return true; 240 | }, 241 | }; 242 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 243 | get: shallowReadonlyGet, 244 | }); 245 | 246 | function reactive(raw) { 247 | return createReactiveObject(raw, mutableHandlers); 248 | } 249 | function readonly(raw) { 250 | return createReactiveObject(raw, readonlyHandlers); 251 | } 252 | function shallowReadonly(raw) { 253 | return createReactiveObject(raw, shallowReadonlyHandlers); 254 | } 255 | function createReactiveObject(target, baseHandles) { 256 | return new Proxy(target, baseHandles); 257 | } 258 | 259 | function emit(instance, event, ...args) { 260 | const { props } = instance; 261 | const handlerName = toHandlerKey(camelize(event)); 262 | const handler = props[handlerName]; 263 | handler && handler(...args); 264 | } 265 | 266 | class RefImpl { 267 | constructor(value) { 268 | this._v_isRef = true; 269 | this._rawValue = value; // 保存原始值 270 | this._value = convert(value); // 如果是对象,则转化成响应式对象 271 | this.dep = new Set(); 272 | } 273 | get value() { 274 | trackRefValue(this); 275 | return this._value; 276 | } 277 | set value(newVal) { 278 | // 如果值没有改变,那么没必要重复触发 279 | if (hasChanged(newVal, this._rawValue)) { 280 | this._rawValue = newVal; 281 | this._value = convert(newVal); 282 | triggerEffects(this.dep); 283 | } 284 | } 285 | } 286 | function ref(value) { 287 | return new RefImpl(value); 288 | } 289 | function convert(value) { 290 | return isObject(value) ? reactive(value) : value; 291 | } 292 | function trackRefValue(ref) { 293 | if (isTracking()) { 294 | trackEffects(ref.dep); 295 | } 296 | } 297 | function isRef(ref) { 298 | return !!ref._v_isRef; 299 | } 300 | function unRef(ref) { 301 | return isRef(ref) ? ref.value : ref; 302 | } 303 | function proxyRefs(objectWithRefs) { 304 | return new Proxy(objectWithRefs, { 305 | get(target, key) { 306 | return unRef(Reflect.get(target, key)); 307 | }, 308 | set(target, key, value) { 309 | if (isRef(target[key]) && !isRef(value)) { 310 | return (target[key].value = value); 311 | } 312 | else { 313 | return Reflect.set(target, key, value); 314 | } 315 | }, 316 | }); 317 | } 318 | 319 | function createComponentInstance(vnode, parent) { 320 | const component = { 321 | vnode, 322 | type: vnode.type, 323 | setupState: {}, 324 | props: {}, 325 | slots: {}, 326 | provides: parent ? parent.provides : {}, 327 | parent, 328 | isMounted: false, 329 | emit: () => { }, 330 | }; 331 | component.emit = emit.bind(null, component); 332 | return component; 333 | } 334 | function setupComponent(instance) { 335 | // TODO 336 | initProps(instance, instance.vnode.props); 337 | initSlots(instance, instance.vnode.children); 338 | setupStatefulComponent(instance); 339 | } 340 | function setupStatefulComponent(instance) { 341 | const Component = instance.type; 342 | const { setup } = Component; 343 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 344 | if (setup) { 345 | setCurrentInstance(instance); 346 | const setupResult = setup(shallowReadonly(instance.props), { 347 | emit: instance.emit, 348 | }); 349 | setCurrentInstance(null); 350 | handleSetupResult(instance, setupResult); 351 | } 352 | } 353 | function handleSetupResult(instance, setupResult) { 354 | if (typeof setupResult === "object") { 355 | instance.setupState = proxyRefs(setupResult); 356 | } 357 | finishComponentSetup(instance); 358 | } 359 | // 最终肯定是要返回一个render函数 360 | function finishComponentSetup(instance) { 361 | const Component = instance.type; 362 | if (Component.render) { 363 | instance.render = Component.render; 364 | } 365 | } 366 | let currentInstance = null; 367 | function getCurrentInstance() { 368 | return currentInstance; 369 | } 370 | function setCurrentInstance(instance) { 371 | currentInstance = instance; 372 | } 373 | 374 | function provide(key, value) { 375 | const currentInstance = getCurrentInstance(); 376 | if (currentInstance) { 377 | let { provides } = currentInstance; 378 | const parentProvides = currentInstance.parent.provides; 379 | if (parentProvides === provides) { 380 | provides = currentInstance.provides = Object.create(parentProvides); 381 | } 382 | provides[key] = value; 383 | } 384 | } 385 | function inject(key, defaultValue) { 386 | const currentInstance = getCurrentInstance(); 387 | if (currentInstance) { 388 | const parentProvides = currentInstance.parent.provides; 389 | if (key in parentProvides) { 390 | return parentProvides[key]; 391 | } 392 | else if (defaultValue) { 393 | if (typeof defaultValue === "function") { 394 | return defaultValue(); 395 | } 396 | return defaultValue; 397 | } 398 | } 399 | } 400 | 401 | function createAppAPI(render) { 402 | return function createApp(rootComponent) { 403 | return { 404 | mount(rootContainer) { 405 | const vnode = createVNode(rootComponent); 406 | render(vnode, rootContainer); 407 | }, 408 | }; 409 | }; 410 | } 411 | 412 | function createRenderer(options) { 413 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 414 | function render(vnode, container) { 415 | patch(null, vnode, container, null, null); 416 | } 417 | function patch(n1, n2, container, parentComponent, anchor) { 418 | const { type, shapeFlag } = n2; 419 | switch (type) { 420 | case Fragment: 421 | processFragment(n1, n2, container, parentComponent, anchor); 422 | break; 423 | case Text: 424 | processText(n1, n2, container); 425 | break; 426 | default: 427 | if (shapeFlag & 1 /* ELEMENT */) { 428 | processElement(n1, n2, container, parentComponent, anchor); 429 | } 430 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 431 | processComponent(n1, n2, container, parentComponent, anchor); 432 | } 433 | break; 434 | } 435 | } 436 | function processText(n1, n2, container) { 437 | const { children } = n2; 438 | const textVNode = (n2.el = document.createTextNode(children)); 439 | container.append(textVNode); 440 | } 441 | function processFragment(n1, n2, container, parentComponent, anchor) { 442 | mountChildren(n2.children, container, parentComponent, anchor); 443 | } 444 | function processElement(n1, n2, container, parentComponent, anchor) { 445 | if (!n1) { 446 | mountElement(n2, container, parentComponent, anchor); 447 | } 448 | else { 449 | patchElement(n1, n2, container, parentComponent, anchor); 450 | } 451 | } 452 | function patchElement(n1, n2, container, parentComponent, anchor) { 453 | console.log("patchElement"); 454 | console.log("n1", n1); 455 | console.log("n2", n2); 456 | const oldProps = n1.props; 457 | const newProps = n2.props; 458 | const el = (n2.el = n1.el); 459 | patchChildren(n1, n2, el, parentComponent, anchor); 460 | patchProps(el, oldProps, newProps); 461 | } 462 | function patchChildren(n1, n2, container, parentComponent, anchor) { 463 | const prevShapeFlag = n1.shapeFlag; 464 | const shapeFlag = n2.shapeFlag; 465 | const c1 = n1.children; 466 | const c2 = n2.children; 467 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 468 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 469 | unmountChildren(c1); 470 | } 471 | if (c1 !== c2) { 472 | hostSetElementText(container, c2); 473 | } 474 | } 475 | else { 476 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 477 | hostSetElementText(container, ""); 478 | mountChildren(c2, container, parentComponent, anchor); 479 | } 480 | else { 481 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 482 | } 483 | } 484 | } 485 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 486 | const l2 = c2.length; 487 | let i = 0; 488 | let e1 = c1.length - 1; 489 | let e2 = l2 - 1; 490 | function isSomeVNodeType(n1, n2) { 491 | return n1.type === n2.type && n1.key === n2.key; 492 | } 493 | // 左侧进行对比相同节点 494 | while (i <= e1 && i <= e2) { 495 | let n1 = c1[i]; 496 | let n2 = c2[i]; 497 | if (isSomeVNodeType(n1, n2)) { 498 | patch(n1, n2, container, parentComponent, parentAnchor); 499 | } 500 | else { 501 | break; 502 | } 503 | i++; 504 | } 505 | // 右侧进行对比相同节点 506 | while (i <= e1 && i <= e2) { 507 | let n1 = c1[e1]; 508 | let n2 = c2[e2]; 509 | if (isSomeVNodeType(n1, n2)) { 510 | patch(n1, n2, container, parentComponent, parentAnchor); 511 | } 512 | else { 513 | break; 514 | } 515 | e1--; 516 | e2--; 517 | } 518 | if (i > e1) { 519 | if (i <= e2) { 520 | // 旧节点结束,新节点没有结束的情况 521 | const nextPos = e2 + 1; 522 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 523 | while (i <= e2) { 524 | patch(null, c2[i], container, parentComponent, anchor); 525 | i++; 526 | } 527 | } 528 | } 529 | else if (i > e2) { 530 | // 新节点结束,旧节点未结束的情况 531 | while (i <= e1) { 532 | hostRemove(c1[i].el); 533 | i++; 534 | } 535 | } 536 | else { 537 | // 中间对比 538 | let s1 = i; 539 | let s2 = i; 540 | let patched = 0; 541 | let moved = false; 542 | let maxNewIndexSoFar = 0; 543 | const toBePatched = e2 - s2 + 1; 544 | const keyToNewIndexMap = new Map(); 545 | const newIndexToOldIndexMap = new Array(toBePatched); 546 | for (let i = 0; i < toBePatched; i++) 547 | newIndexToOldIndexMap[i] = 0; 548 | // 建立旧节点key映射到新节点index的hash表 549 | for (let i = s2; i <= e2; i++) { 550 | const nextChild = c2[i]; 551 | keyToNewIndexMap.set(nextChild.key, i); 552 | } 553 | for (let i = s1; i <= e1; i++) { 554 | const prevChild = c1[i]; 555 | if (patched >= toBePatched) { 556 | hostRemove(prevChild.el); 557 | continue; 558 | } 559 | // 哈希表中找是否存在的值 560 | let newIndex; 561 | if (prevChild.key != null) { 562 | newIndex = keyToNewIndexMap.get(prevChild.key); 563 | } 564 | else { 565 | for (let j = s2; j < e2; j++) { 566 | if (isSomeVNodeType(prevChild, s2[j])) { 567 | newIndex = j; 568 | break; 569 | } 570 | } 571 | } 572 | // 如果哈希表中没有找到,则进行删除操作 573 | if (newIndex == null) { 574 | hostRemove(prevChild.el); 575 | } 576 | else { 577 | // 如果在哈希表中找到 578 | if (newIndex >= maxNewIndexSoFar) { 579 | maxNewIndexSoFar = newIndex; 580 | } 581 | else { 582 | moved = true; 583 | } 584 | // 建立新节点index(这个index只针对中间的节点序列)到旧节点index的映射关系 585 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 586 | patch(prevChild, c2[newIndex], container, parentComponent, null); 587 | patched++; 588 | } 589 | } 590 | const increasingNewIndexSequence = moved 591 | ? getSequence(newIndexToOldIndexMap) 592 | : []; 593 | let j = increasingNewIndexSequence.length - 1; // 生成一个最长的递增子序列 594 | for (let i = toBePatched - 1; i >= 0; i--) { 595 | const nextIndex = i + s2; 596 | const nextChild = c2[nextIndex]; 597 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 598 | if (newIndexToOldIndexMap[i] === 0) { 599 | // 新节点没有在旧节点中找到,给了个标识,直接在这里进行插入 600 | patch(null, nextChild, container, parentComponent, anchor); 601 | } 602 | else if (moved) { 603 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 604 | // 这时对顺序进行调整 605 | hostInsert(nextChild.el, container, anchor); 606 | } 607 | else { 608 | j--; 609 | } 610 | } 611 | } 612 | } 613 | } 614 | function unmountChildren(children) { 615 | for (let i = 0; i < children.length; i++) { 616 | const el = children[i].el; 617 | hostRemove(el); 618 | } 619 | } 620 | function patchProps(el, oldProps, newProps) { 621 | if (oldProps !== newProps) { 622 | for (const key in newProps) { 623 | const prevProp = oldProps[key]; 624 | const nextProp = newProps[key]; 625 | if (prevProp !== nextProp) { 626 | hostPatchProp(el, key, prevProp, nextProp); 627 | } 628 | } 629 | if (JSON.stringify(oldProps) !== "{}") { 630 | for (const key in oldProps) { 631 | if (!(key in newProps)) { 632 | hostPatchProp(el, key, oldProps[key], null); 633 | } 634 | } 635 | } 636 | } 637 | } 638 | function processComponent(n1, n2, container, parentComponent, anchor) { 639 | mountComponent(n2, container, parentComponent, anchor); 640 | } 641 | function mountElement(vnode, container, parentComponent, anchor) { 642 | // createElement 643 | const el = (vnode.el = hostCreateElement(vnode.type)); 644 | const { children, shapeFlag } = vnode; 645 | // children 646 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 647 | el.textContent = children; 648 | } 649 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 650 | mountChildren(vnode.children, el, parentComponent, anchor); 651 | } 652 | // props 653 | const { props } = vnode; 654 | for (const key in props) { 655 | hostPatchProp(el, key, null, props[key]); 656 | } 657 | hostInsert(el, container, anchor); 658 | } 659 | function mountChildren(children, container, parentComponent, anchor) { 660 | children.forEach((v) => { 661 | patch(null, v, container, parentComponent, anchor); 662 | }); 663 | } 664 | function mountComponent(initialVNode, container, parentComponent, anchor) { 665 | const instance = createComponentInstance(initialVNode, parentComponent); 666 | setupComponent(instance); 667 | setupRenderEffect(instance, initialVNode, container, anchor); 668 | } 669 | function setupRenderEffect(instance, initialVNode, container, anchor) { 670 | effect(() => { 671 | if (!instance.isMounted) { 672 | console.log("init"); 673 | const { proxy } = instance; 674 | const subTree = (instance.subTree = instance.render.call(proxy)); 675 | patch(null, subTree, container, instance, anchor); 676 | initialVNode.el = subTree.el; 677 | instance.isMounted = true; 678 | } 679 | else { 680 | console.log("update"); 681 | const { proxy } = instance; 682 | const subTree = instance.render.call(proxy); 683 | const prevSubTree = instance.subTree; 684 | instance.subTree = subTree; 685 | patch(prevSubTree, subTree, container, instance, anchor); 686 | } 687 | }); 688 | } 689 | return { 690 | createApp: createAppAPI(render), 691 | }; 692 | } 693 | function getSequence(arr) { 694 | const p = arr.slice(); 695 | const result = [0]; 696 | let i, j, u, v, c; 697 | const len = arr.length; 698 | for (i = 0; i < len; i++) { 699 | const arrI = arr[i]; 700 | if (arrI !== 0) { 701 | j = result[result.length - 1]; 702 | if (arr[j] < arrI) { 703 | p[i] = j; 704 | result.push(i); 705 | continue; 706 | } 707 | u = 0; 708 | v = result.length - 1; 709 | while (u < v) { 710 | c = (u + v) >> 1; 711 | if (arr[result[c]] < arrI) { 712 | u = c + 1; 713 | } 714 | else { 715 | v = c; 716 | } 717 | } 718 | if (arrI < arr[result[u]]) { 719 | if (u > 0) { 720 | p[i] = result[u - 1]; 721 | } 722 | result[u] = i; 723 | } 724 | } 725 | } 726 | u = result.length; 727 | v = result[u - 1]; 728 | while (u-- > 0) { 729 | result[u] = v; 730 | v = p[v]; 731 | } 732 | return result; 733 | } 734 | 735 | function createElement(type) { 736 | return document.createElement(type); 737 | } 738 | function patchProp(el, key, oldValue, nextValue) { 739 | const isOn = (key) => /^on[A-Z]/.test(key); 740 | if (isOn(key)) { 741 | const event = key.slice(2).toLowerCase(); 742 | el.addEventListener(event, nextValue); 743 | } 744 | else { 745 | if (nextValue == null) { 746 | el.removeAttribute(key); 747 | } 748 | else { 749 | el.setAttribute(key, nextValue); 750 | } 751 | } 752 | } 753 | function insert(child, parent, anchor) { 754 | parent.insertBefore(child, anchor || null); 755 | } 756 | function remove(child) { 757 | const parent = child.parentNode; 758 | if (parent) { 759 | parent.removeChild(child); 760 | } 761 | } 762 | function setElementText(el, text) { 763 | el.textContent = text; 764 | } 765 | const renderer = createRenderer({ 766 | createElement, 767 | patchProp, 768 | insert, 769 | remove, 770 | setElementText, 771 | }); 772 | function createApp(...args) { 773 | return renderer.createApp(...args); 774 | } 775 | 776 | export { createApp, createRenderer, createTextVNode, getCurrentInstance, h, inject, provide, proxyRefs, ref, renderSlots }; 777 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guide-mini-vue", 3 | "version": "1.0.0", 4 | "main": "lib/guide-mini-vue.cjs.js", 5 | "module": "lib/guide-mini-vue.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js --watch" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.15.5", 13 | "@babel/preset-env": "^7.15.4", 14 | "@babel/preset-typescript": "^7.15.0", 15 | "@types/jest": "^27.0.1", 16 | "babel-jest": "^27.1.0", 17 | "jest": "^27.1.0", 18 | "rollup": "^2.61.1", 19 | "tslib": "^2.3.1", 20 | "typescript": "^4.4.2" 21 | }, 22 | "dependencies": { 23 | "@rollup/plugin-typescript": "^8.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import pkg from "./package.json"; 3 | 4 | export default { 5 | input: "./src/index.ts", 6 | output: [ 7 | { 8 | format: "cjs", 9 | file: pkg.main, 10 | }, 11 | { 12 | format: "es", 13 | file: pkg.module, 14 | }, 15 | ], 16 | plugins: [typescript()], 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // mini-vue 出口 2 | export * from "./runtime-dom"; 3 | export * from "./reactivity"; 4 | -------------------------------------------------------------------------------- /src/reactivity/baseHandler.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "../shared"; 2 | import { track, trigger } from "./effect"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | 5 | function createGetter(isReadonly = false, shallow = false) { 6 | return function get(target, key) { 7 | if (key === ReactiveFlags.IS_REACTIVE) { 8 | return !isReadonly; 9 | } else if (key === ReactiveFlags.IS_READONLY) { 10 | return isReadonly; 11 | } 12 | 13 | const res = Reflect.get(target, key); 14 | 15 | if (shallow) { 16 | return res; 17 | } 18 | 19 | if (isObject(res)) { 20 | return isReadonly ? readonly(res) : reactive(res); 21 | } 22 | // 如果不是只读,需要进行依赖收集 23 | if (!isReadonly) { 24 | track(target, key); 25 | } 26 | return res; 27 | }; 28 | } 29 | 30 | function createSetter() { 31 | return function set(target, key, value) { 32 | const res = Reflect.set(target, key, value); 33 | 34 | trigger(target, key); 35 | return res; 36 | }; 37 | } 38 | 39 | const get = createGetter(); 40 | const set = createSetter(); 41 | const readonlyGet = createGetter(true); 42 | const shallowReadonlyGet = createGetter(true, true); 43 | 44 | export const mutableHandlers = { 45 | get, 46 | set, 47 | }; 48 | 49 | export const readonlyHandlers = { 50 | get: readonlyGet, 51 | set(target, key) { 52 | console.warn( 53 | `key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, 54 | target 55 | ); 56 | 57 | return true; 58 | }, 59 | }; 60 | 61 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 62 | get: shallowReadonlyGet, 63 | }); 64 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _dirty: boolean; 5 | private _value: any; 6 | private _effect: any; 7 | 8 | constructor(getter) { 9 | this._dirty = true; 10 | // 派发更新时没有立即进行依赖收集 11 | // 而是打开了一个开关,等下次读取时再重新进行依赖收集 12 | this._effect = new ReactiveEffect(getter, () => { 13 | if (!this._dirty) { 14 | this._dirty = true; 15 | } 16 | }); 17 | } 18 | get value() { 19 | // 如果没有更新时,那么只是拿取缓存值 20 | if (this._dirty) { 21 | this._dirty = false; 22 | this._value = this._effect.run(); // 进行依赖收集 23 | } 24 | return this._value; // 拿取缓存 25 | } 26 | } 27 | 28 | // 初始化时调用一次,之后如果没有发生更新,那么拿的就是缓存的值 29 | // 如果更新了,不会立即触发getter函数,而是等到下一次读取值的时候再进行触发 30 | export function computed(getter) { 31 | return new ComputedRefImpl(getter); 32 | } 33 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared"; 2 | 3 | let activeEffect; 4 | let shouldTrack = false; 5 | export class ReactiveEffect { 6 | private _fn: any; 7 | deps = []; 8 | active = true; 9 | onStop?: () => void; 10 | public scheduler: Function | undefined; 11 | constructor(fn, scheduler?: Function) { 12 | this._fn = fn; 13 | this.scheduler = scheduler; 14 | } 15 | run() { 16 | if (!this.active) { 17 | return this._fn(); 18 | } 19 | 20 | // 打开收集依赖的开关 21 | shouldTrack = true; 22 | // 指向当前活跃的观察者 23 | activeEffect = this; 24 | // 这个过程会涉及到对响应式数据操作 25 | const r = this._fn(); 26 | 27 | // 重置 28 | shouldTrack = false; 29 | 30 | return r; 31 | } 32 | stop() { 33 | if (this.active) { 34 | cleanupEffect(this); 35 | if (this.onStop) { 36 | this.onStop(); 37 | } 38 | this.active = false; 39 | } 40 | } 41 | } 42 | 43 | function cleanupEffect(effect) { 44 | effect.deps.forEach((dep: any) => { 45 | dep.delete(effect); 46 | }); 47 | 48 | // 把 effect.deps 清空 49 | effect.deps.length = 0; 50 | } 51 | 52 | const targetMap = new Map(); 53 | 54 | // 依赖收集实现的原理 55 | export function track(target, key) { 56 | if (!isTracking()) return; 57 | // target -> key -> dep 58 | let depsMap = targetMap.get(target); 59 | if (!depsMap) { 60 | depsMap = new Map(); 61 | targetMap.set(target, depsMap); 62 | } 63 | 64 | let dep = depsMap.get(key); 65 | if (!dep) { 66 | dep = new Set(); 67 | depsMap.set(key, dep); 68 | } 69 | 70 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 71 | if (dep.has(activeEffect)) return; 72 | trackEffects(dep); 73 | } 74 | 75 | export function trackEffects(dep) { 76 | dep.add(activeEffect); 77 | activeEffect.deps.push(dep); 78 | } 79 | 80 | export function isTracking() { 81 | return shouldTrack && activeEffect !== undefined; 82 | } 83 | 84 | export function trigger(target, key) { 85 | let depsMap = targetMap.get(target); 86 | let dep = depsMap.get(key); 87 | 88 | triggerEffects(dep); 89 | } 90 | 91 | export function triggerEffects(dep) { 92 | for (const effect of dep) { 93 | if (effect.scheduler) { 94 | effect.scheduler(); 95 | } else { 96 | effect.run(); 97 | } 98 | } 99 | } 100 | 101 | // 返回一个runner函数 102 | export function effect(fn, options: any = {}) { 103 | // fn 104 | const _effect = new ReactiveEffect(fn, options.scheduler); 105 | extend(_effect, options); 106 | // 依赖收集的入口 107 | _effect.run(); 108 | 109 | const runner: any = _effect.run.bind(_effect); 110 | runner.effect = _effect; 111 | 112 | return runner; 113 | } 114 | 115 | export function stop(runner) { 116 | runner.effect.stop(); 117 | } 118 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, proxyRefs } from "./ref"; 2 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mutableHandlers, 3 | readonlyHandlers, 4 | shallowReadonlyHandlers, 5 | } from "./baseHandler"; 6 | 7 | export const enum ReactiveFlags { 8 | IS_REACTIVE = "__v_isReactive", 9 | IS_READONLY = "__v_isReadonly", 10 | } 11 | 12 | export function reactive(raw) { 13 | return createReactiveObject(raw, mutableHandlers); 14 | } 15 | 16 | export function readonly(raw) { 17 | return createReactiveObject(raw, readonlyHandlers); 18 | } 19 | 20 | export function shallowReadonly(raw) { 21 | return createReactiveObject(raw, shallowReadonlyHandlers); 22 | } 23 | 24 | export function isReactive(value) { 25 | return !!value[ReactiveFlags.IS_REACTIVE]; 26 | } 27 | 28 | export function isReadonly(value) { 29 | return !!value[ReactiveFlags.IS_READONLY]; 30 | } 31 | 32 | export function isProxy(value) { 33 | return isReactive(value) || isReadonly(value); 34 | } 35 | 36 | function createReactiveObject(target, baseHandles) { 37 | return new Proxy(target, baseHandles); 38 | } 39 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "../shared"; 2 | import { trackEffects, triggerEffects, isTracking } from "./effect"; 3 | import { reactive } from "./reactive"; 4 | 5 | class RefImpl { 6 | private _value: any; 7 | public dep: any; 8 | private _rawValue: any; 9 | public _v_isRef = true; 10 | constructor(value) { 11 | this._rawValue = value; // 保存原始值 12 | this._value = convert(value); // 如果是对象,则转化成响应式对象 13 | this.dep = new Set(); 14 | } 15 | get value() { 16 | trackRefValue(this); 17 | return this._value; 18 | } 19 | set value(newVal) { 20 | // 如果值没有改变,那么没必要重复触发 21 | if (hasChanged(newVal, this._rawValue)) { 22 | this._rawValue = newVal; 23 | this._value = convert(newVal); 24 | triggerEffects(this.dep); 25 | } 26 | } 27 | } 28 | 29 | export function ref(value) { 30 | return new RefImpl(value); 31 | } 32 | 33 | function convert(value) { 34 | return isObject(value) ? reactive(value) : value; 35 | } 36 | 37 | function trackRefValue(ref) { 38 | if (isTracking()) { 39 | trackEffects(ref.dep); 40 | } 41 | } 42 | 43 | export function isRef(ref) { 44 | return !!ref._v_isRef; 45 | } 46 | 47 | export function unRef(ref) { 48 | return isRef(ref) ? ref.value : ref; 49 | } 50 | 51 | export function proxyRefs(objectWithRefs) { 52 | return new Proxy(objectWithRefs, { 53 | get(target, key) { 54 | return unRef(Reflect.get(target, key)); 55 | }, 56 | set(target, key, value) { 57 | if (isRef(target[key]) && !isRef(value)) { 58 | return (target[key].value = value); 59 | } else { 60 | return Reflect.set(target, key, value); 61 | } 62 | }, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/reactivity/tests/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../computed"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("computed", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 1, 8 | }); 9 | 10 | const age = computed(() => { 11 | return user.age; 12 | }); 13 | 14 | expect(age.value).toBe(1); 15 | }); 16 | 17 | it("should compute lazily", () => { 18 | const value = reactive({ 19 | foo: 1, 20 | }); 21 | const getter = jest.fn(() => { 22 | return value.foo; 23 | }); 24 | const cValue = computed(getter); 25 | 26 | // lazy 27 | expect(getter).not.toHaveBeenCalled(); 28 | 29 | expect(cValue.value).toBe(1); 30 | expect(getter).toHaveBeenCalledTimes(1); 31 | 32 | // should not compute again 33 | cValue.value; // get 34 | expect(getter).toHaveBeenCalledTimes(1); 35 | 36 | // should not compute until needed 37 | value.foo = 2; 38 | expect(getter).toHaveBeenCalledTimes(1); 39 | 40 | // now it should compute 41 | expect(cValue.value).toBe(2); 42 | expect(getter).toHaveBeenCalledTimes(2); 43 | 44 | // should not compute again 45 | cValue.value; 46 | expect(getter).toHaveBeenCalledTimes(2); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/reactivity/tests/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect, stop } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 10, 8 | }); 9 | 10 | let nextAge; 11 | effect(() => { 12 | nextAge = user.age + 1; 13 | }); 14 | 15 | expect(nextAge).toBe(11); 16 | 17 | // update 18 | user.age++; 19 | expect(nextAge).toBe(12); 20 | }); 21 | 22 | it("should return runner when call effect", () => { 23 | // 当调用 runner 的时候可以重新执行 effect.run 24 | // runner 的返回值就是用户给的 fn 的返回值 25 | let foo = 0; 26 | const runner = effect(() => { 27 | foo++; 28 | return foo; 29 | }); 30 | 31 | expect(foo).toBe(1); 32 | runner(); 33 | expect(foo).toBe(2); 34 | expect(runner()).toBe(3); 35 | }); 36 | 37 | it("scheduler", () => { 38 | let dummy; 39 | let run: any; 40 | const scheduler = jest.fn(() => { 41 | run = runner; 42 | }); 43 | const obj = reactive({ foo: 1 }); 44 | const runner = effect( 45 | () => { 46 | dummy = obj.foo; 47 | }, 48 | { scheduler } 49 | ); 50 | expect(scheduler).not.toHaveBeenCalled(); 51 | expect(dummy).toBe(1); 52 | // should be called on first trigger 53 | obj.foo++; 54 | expect(scheduler).toHaveBeenCalledTimes(1); 55 | // // should not run yet 56 | expect(dummy).toBe(1); 57 | // // manually run 58 | run(); 59 | // // should have run 60 | expect(dummy).toBe(2); 61 | }); 62 | 63 | it("stop", () => { 64 | let dummy; 65 | const obj = reactive({ prop: 1 }); 66 | const runner = effect(() => { 67 | dummy = obj.prop; 68 | }); 69 | obj.prop = 2; 70 | expect(dummy).toBe(2); 71 | stop(runner); 72 | // obj.prop = 3; 73 | obj.prop++; 74 | expect(dummy).toBe(2); 75 | 76 | // stopped effect should still be manually callable 77 | runner(); 78 | expect(dummy).toBe(3); 79 | }); 80 | 81 | it("onStop", () => { 82 | const obj = reactive({ 83 | foo: 1, 84 | }); 85 | const onStop = jest.fn(); 86 | let dummy; 87 | const runner = effect( 88 | () => { 89 | dummy = obj.foo; 90 | }, 91 | { 92 | onStop, 93 | } 94 | ); 95 | 96 | stop(runner); 97 | expect(onStop).toBeCalledTimes(1); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive, isProxy } from "../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 | -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly } from "../reactive"; 2 | 3 | describe("readonly", () => { 4 | it("should make nested values readonly", () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | expect(isReadonly(wrapped)).toBe(true); 9 | expect(isReadonly(original)).toBe(false); 10 | expect(isReadonly(wrapped.bar)).toBe(true); 11 | expect(isReadonly(original.bar)).toBe(false); 12 | expect(isProxy(wrapped)).toBe(true); 13 | 14 | expect(wrapped.foo).toBe(1); 15 | }); 16 | 17 | it("should call console.warn when set", () => { 18 | console.warn = jest.fn(); 19 | const user = readonly({ 20 | age: 10, 21 | }); 22 | 23 | user.age = 11; 24 | expect(console.warn).toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/reactivity/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | import { isRef, ref, unRef, proxyRefs } from "../ref"; 4 | describe("ref", () => { 5 | it("happy path", () => { 6 | const a = ref(1); 7 | expect(a.value).toBe(1); 8 | }); 9 | 10 | it("should be reactive", () => { 11 | const a = 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 = 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 | it("proxyRefs", () => { 59 | const user = { 60 | age: ref(10), 61 | name: "xiaohong", 62 | }; 63 | 64 | const proxyUser = proxyRefs(user); 65 | expect(user.age.value).toBe(10); 66 | expect(proxyUser.age).toBe(10); 67 | expect(proxyUser.name).toBe("xiaohong"); 68 | 69 | proxyUser.age = 20; 70 | 71 | expect(proxyUser.age).toBe(20); 72 | expect(user.age.value).toBe(20); 73 | 74 | proxyUser.age = ref(10); 75 | expect(proxyUser.age).toBe(10); 76 | expect(user.age.value).toBe(10); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../reactive"; 2 | 3 | describe("shallowReadonly", () => { 4 | test("should not make non-reactive properties reactive", () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }); 6 | expect(isReadonly(props)).toBe(true); 7 | expect(isReadonly(props.n)).toBe(false); 8 | }); 9 | 10 | it("should call console.warn when set", () => { 11 | console.warn = jest.fn(); 12 | const user = shallowReadonly({ 13 | age: 10, 14 | }); 15 | 16 | user.age = 11; 17 | expect(console.warn).toHaveBeenCalled(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | export function provide(key, value) { 4 | const currentInstance: any = getCurrentInstance(); 5 | if (currentInstance) { 6 | let { provides } = currentInstance; 7 | const parentProvides = currentInstance.parent.provides; 8 | 9 | if (parentProvides === provides) { 10 | provides = currentInstance.provides = Object.create(parentProvides); 11 | } 12 | 13 | provides[key] = value; 14 | } 15 | } 16 | 17 | export function inject(key, defaultValue) { 18 | const currentInstance: any = getCurrentInstance(); 19 | if (currentInstance) { 20 | const parentProvides = currentInstance.parent.provides; 21 | if (key in parentProvides) { 22 | return parentProvides[key]; 23 | } else if (defaultValue) { 24 | if (typeof defaultValue === "function") { 25 | return defaultValue(); 26 | } 27 | return defaultValue; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 2 | import { initProps } from "./componentProps"; 3 | import { initSlots } from "./componentSlots"; 4 | import { shallowReadonly } from "../reactivity/reactive"; 5 | import { emit } from "./componentEmit"; 6 | import { proxyRefs } from "../reactivity"; 7 | 8 | export function createComponentInstance(vnode: any, parent: any) { 9 | const component = { 10 | vnode, 11 | type: vnode.type, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | provides: parent ? parent.provides : {}, 16 | parent, 17 | isMounted: false, 18 | emit: () => {}, 19 | }; 20 | component.emit = emit.bind(null, component) as any; 21 | return component; 22 | } 23 | 24 | export function setupComponent(instance) { 25 | // TODO 26 | initProps(instance, instance.vnode.props); 27 | initSlots(instance, instance.vnode.children); 28 | setupStatefulComponent(instance); 29 | } 30 | 31 | function setupStatefulComponent(instance: any) { 32 | const Component = instance.type; 33 | const { setup } = Component; 34 | 35 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 36 | 37 | if (setup) { 38 | setCurrentInstance(instance); 39 | const setupResult = setup(shallowReadonly(instance.props), { 40 | emit: instance.emit, 41 | }); 42 | setCurrentInstance(null); 43 | handleSetupResult(instance, setupResult); 44 | } 45 | } 46 | 47 | function handleSetupResult(instance, setupResult: any) { 48 | if (typeof setupResult === "object") { 49 | instance.setupState = proxyRefs(setupResult); 50 | } 51 | 52 | finishComponentSetup(instance); 53 | } 54 | 55 | // 最终肯定是要返回一个render函数 56 | function finishComponentSetup(instance: any) { 57 | const Component = instance.type; 58 | 59 | if (Component.render) { 60 | instance.render = Component.render; 61 | } 62 | } 63 | 64 | let currentInstance = null; 65 | 66 | export function getCurrentInstance() { 67 | return currentInstance; 68 | } 69 | 70 | export function setCurrentInstance(instance) { 71 | currentInstance = instance; 72 | } 73 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "../shared/index"; 2 | 3 | export function emit(instance, event, ...args) { 4 | const { props } = instance; 5 | const handlerName = toHandlerKey(camelize(event)); 6 | const handler = props[handlerName]; 7 | handler && handler(...args); 8 | } 9 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(intstance, rawProps) { 2 | intstance.props = rawProps || {}; 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "../shared/index"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | }; 7 | 8 | export const PublicInstanceProxyHandlers = { 9 | get({ _: instance }, key) { 10 | // setupState 11 | const { setupState, props } = instance; 12 | if (hasOwn(setupState, key)) { 13 | return setupState[key]; 14 | } else if (hasOwn(props, key)) { 15 | return props[key]; 16 | } 17 | 18 | const publicGetter = publicPropertiesMap[key]; 19 | if (publicGetter) { 20 | return publicGetter(instance); 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/ShapeFlags"; 2 | 3 | export function initSlots(instance, children) { 4 | const { vnode } = instance; 5 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 6 | normalizeObjectSlots(children, instance.slots); 7 | } 8 | } 9 | 10 | function normalizeObjectSlots(children, slots) { 11 | for (let key in children) { 12 | let value = children[key]; 13 | slots[key] = (props) => normalizeSlotValue(value(props)); 14 | } 15 | } 16 | 17 | function normalizeSlotValue(value) { 18 | return Array.isArray(value) ? value : [value]; 19 | } 20 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function createAppAPI(render) { 4 | return function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | const vnode = createVNode(rootComponent); 8 | render(vnode, rootContainer); 9 | }, 10 | }; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function h(type, props?, children?) { 4 | return createVNode(type, props, children); 5 | } 6 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, name, props) { 4 | let slot = slots[name]; 5 | if (slot) { 6 | if (typeof slot === "function") { 7 | return createVNode(Fragment, {}, slot(props)); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from "./h"; 2 | export { renderSlots } from "./helpers/renderSlots"; 3 | export { createTextVNode } from "./vnode"; 4 | export { getCurrentInstance } from "./component"; 5 | export { inject, provide } from "./apiInject"; 6 | export { createRenderer } from "./renderer"; 7 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { createComponentInstance, setupComponent } from "./component"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | import { Fragment, Text } from "./vnode"; 4 | import { createAppAPI } from "./createApp"; 5 | import { effect } from "../reactivity/effect"; 6 | 7 | export function createRenderer(options) { 8 | const { 9 | createElement: hostCreateElement, 10 | patchProp: hostPatchProp, 11 | insert: hostInsert, 12 | remove: hostRemove, 13 | setElementText: hostSetElementText, 14 | } = options; 15 | 16 | function render(vnode, container) { 17 | patch(null, vnode, container, null, null); 18 | } 19 | 20 | function patch(n1, n2, container, parentComponent, anchor) { 21 | const { type, shapeFlag } = n2; 22 | 23 | switch (type) { 24 | case Fragment: 25 | processFragment(n1, n2, container, parentComponent, anchor); 26 | break; 27 | case Text: 28 | processText(n1, n2, container); 29 | break; 30 | default: 31 | if (shapeFlag & ShapeFlags.ELEMENT) { 32 | processElement(n1, n2, container, parentComponent, anchor); 33 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 34 | processComponent(n1, n2, container, parentComponent, anchor); 35 | } 36 | break; 37 | } 38 | } 39 | 40 | function processText(n1, n2, container) { 41 | const { children } = n2; 42 | const textVNode = (n2.el = document.createTextNode(children)); 43 | container.append(textVNode); 44 | } 45 | 46 | function processFragment(n1, n2, container, parentComponent, anchor) { 47 | mountChildren(n2.children, container, parentComponent, anchor); 48 | } 49 | 50 | function processElement(n1, n2, container, parentComponent, anchor) { 51 | if (!n1) { 52 | mountElement(n2, container, parentComponent, anchor); 53 | } else { 54 | patchElement(n1, n2, container, parentComponent, anchor); 55 | } 56 | } 57 | 58 | function patchElement(n1, n2, container, parentComponent, anchor) { 59 | console.log("patchElement"); 60 | console.log("n1", n1); 61 | console.log("n2", n2); 62 | 63 | const oldProps = n1.props; 64 | const newProps = n2.props; 65 | const el = (n2.el = n1.el); 66 | patchChildren(n1, n2, el, parentComponent, anchor); 67 | patchProps(el, oldProps, newProps); 68 | } 69 | 70 | function patchChildren(n1, n2, container, parentComponent, anchor) { 71 | const prevShapeFlag = n1.shapeFlag; 72 | const shapeFlag = n2.shapeFlag; 73 | const c1 = n1.children; 74 | const c2 = n2.children; 75 | 76 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 77 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 78 | unmountChildren(c1); 79 | } 80 | if (c1 !== c2) { 81 | hostSetElementText(container, c2); 82 | } 83 | } else { 84 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 85 | hostSetElementText(container, ""); 86 | mountChildren(c2, container, parentComponent, anchor); 87 | } else { 88 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 89 | } 90 | } 91 | } 92 | 93 | function patchKeyedChildren( 94 | c1, 95 | c2, 96 | container, 97 | parentComponent, 98 | parentAnchor 99 | ) { 100 | const l2 = c2.length; 101 | let i = 0; 102 | let e1 = c1.length - 1; 103 | let e2 = l2 - 1; 104 | function isSomeVNodeType(n1, n2) { 105 | return n1.type === n2.type && n1.key === n2.key; 106 | } 107 | 108 | // 左侧进行对比相同节点 109 | while (i <= e1 && i <= e2) { 110 | let n1 = c1[i]; 111 | let n2 = c2[i]; 112 | if (isSomeVNodeType(n1, n2)) { 113 | patch(n1, n2, container, parentComponent, parentAnchor); 114 | } else { 115 | break; 116 | } 117 | i++; 118 | } 119 | 120 | // 右侧进行对比相同节点 121 | while (i <= e1 && i <= e2) { 122 | let n1 = c1[e1]; 123 | let n2 = c2[e2]; 124 | if (isSomeVNodeType(n1, n2)) { 125 | patch(n1, n2, container, parentComponent, parentAnchor); 126 | } else { 127 | break; 128 | } 129 | e1--; 130 | e2--; 131 | } 132 | 133 | if (i > e1) { 134 | if (i <= e2) { 135 | // 旧节点结束,新节点没有结束的情况 136 | const nextPos = e2 + 1; 137 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 138 | while (i <= e2) { 139 | patch(null, c2[i], container, parentComponent, anchor); 140 | i++; 141 | } 142 | } 143 | } else if (i > e2) { 144 | // 新节点结束,旧节点未结束的情况 145 | while (i <= e1) { 146 | hostRemove(c1[i].el); 147 | i++; 148 | } 149 | } else { 150 | // 中间对比 151 | let s1 = i; 152 | let s2 = i; 153 | let patched = 0; 154 | let moved = false; 155 | let maxNewIndexSoFar = 0; 156 | const toBePatched = e2 - s2 + 1; 157 | const keyToNewIndexMap = new Map(); 158 | const newIndexToOldIndexMap = new Array(toBePatched); 159 | 160 | for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0; 161 | 162 | // 建立旧节点key映射到新节点index的hash表 163 | for (let i = s2; i <= e2; i++) { 164 | const nextChild = c2[i]; 165 | keyToNewIndexMap.set(nextChild.key, i); 166 | } 167 | 168 | for (let i = s1; i <= e1; i++) { 169 | const prevChild = c1[i]; 170 | 171 | if (patched >= toBePatched) { 172 | hostRemove(prevChild.el); 173 | continue; 174 | } 175 | 176 | // 哈希表中找是否存在的值 177 | let newIndex; 178 | if (prevChild.key != null) { 179 | newIndex = keyToNewIndexMap.get(prevChild.key); 180 | } else { 181 | for (let j = s2; j < e2; j++) { 182 | if (isSomeVNodeType(prevChild, s2[j])) { 183 | newIndex = j; 184 | break; 185 | } 186 | } 187 | } 188 | // 如果哈希表中没有找到,则进行删除操作 189 | if (newIndex == null) { 190 | hostRemove(prevChild.el); 191 | } else { 192 | // 如果在哈希表中找到 193 | if (newIndex >= maxNewIndexSoFar) { 194 | maxNewIndexSoFar = newIndex; 195 | } else { 196 | moved = true; 197 | } 198 | // 建立新节点index(这个index只针对中间的节点序列)到旧节点index的映射关系 199 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 200 | patch(prevChild, c2[newIndex], container, parentComponent, null); 201 | patched++; 202 | } 203 | } 204 | 205 | const increasingNewIndexSequence = moved 206 | ? getSequence(newIndexToOldIndexMap) 207 | : []; 208 | let j = increasingNewIndexSequence.length - 1; // 生成一个最长的递增子序列 209 | for (let i = toBePatched - 1; i >= 0; i--) { 210 | const nextIndex = i + s2; 211 | const nextChild = c2[nextIndex]; 212 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 213 | if (newIndexToOldIndexMap[i] === 0) { 214 | // 新节点没有在旧节点中找到,给了个标识,直接在这里进行插入 215 | patch(null, nextChild, container, parentComponent, anchor); 216 | } else if (moved) { 217 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 218 | // 这时对顺序进行调整 219 | hostInsert(nextChild.el, container, anchor); 220 | } else { 221 | j--; 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | function unmountChildren(children) { 229 | for (let i = 0; i < children.length; i++) { 230 | const el = children[i].el; 231 | hostRemove(el); 232 | } 233 | } 234 | 235 | function patchProps(el, oldProps, newProps) { 236 | if (oldProps !== newProps) { 237 | for (const key in newProps) { 238 | const prevProp = oldProps[key]; 239 | const nextProp = newProps[key]; 240 | 241 | if (prevProp !== nextProp) { 242 | hostPatchProp(el, key, prevProp, nextProp); 243 | } 244 | } 245 | 246 | if (JSON.stringify(oldProps) !== "{}") { 247 | for (const key in oldProps) { 248 | if (!(key in newProps)) { 249 | hostPatchProp(el, key, oldProps[key], null); 250 | } 251 | } 252 | } 253 | } 254 | } 255 | 256 | function processComponent( 257 | n1: any, 258 | n2: any, 259 | container: any, 260 | parentComponent: any, 261 | anchor: any 262 | ) { 263 | mountComponent(n2, container, parentComponent, anchor); 264 | } 265 | 266 | function mountElement(vnode, container, parentComponent, anchor) { 267 | // createElement 268 | const el = (vnode.el = hostCreateElement(vnode.type)); 269 | 270 | const { children, shapeFlag } = vnode; 271 | // children 272 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 273 | el.textContent = children; 274 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 275 | mountChildren(vnode.children, el, parentComponent, anchor); 276 | } 277 | 278 | // props 279 | const { props } = vnode; 280 | for (const key in props) { 281 | hostPatchProp(el, key, null, props[key]); 282 | } 283 | 284 | hostInsert(el, container, anchor); 285 | } 286 | 287 | function mountChildren(children, container, parentComponent, anchor) { 288 | children.forEach((v) => { 289 | patch(null, v, container, parentComponent, anchor); 290 | }); 291 | } 292 | 293 | function mountComponent( 294 | initialVNode: any, 295 | container: any, 296 | parentComponent: any, 297 | anchor: any 298 | ) { 299 | const instance = createComponentInstance(initialVNode, parentComponent); 300 | 301 | setupComponent(instance); 302 | setupRenderEffect(instance, initialVNode, container, anchor); 303 | } 304 | 305 | function setupRenderEffect(instance: any, initialVNode, container, anchor) { 306 | effect(() => { 307 | if (!instance.isMounted) { 308 | console.log("init"); 309 | const { proxy } = instance; 310 | const subTree = (instance.subTree = instance.render.call(proxy)); 311 | 312 | patch(null, subTree, container, instance, anchor); 313 | 314 | initialVNode.el = subTree.el; 315 | 316 | instance.isMounted = true; 317 | } else { 318 | console.log("update"); 319 | const { proxy } = instance; 320 | const subTree = instance.render.call(proxy); 321 | const prevSubTree = instance.subTree; 322 | instance.subTree = subTree; 323 | 324 | patch(prevSubTree, subTree, container, instance, anchor); 325 | } 326 | }); 327 | } 328 | 329 | return { 330 | createApp: createAppAPI(render), 331 | }; 332 | } 333 | 334 | function getSequence(arr) { 335 | const p = arr.slice(); 336 | const result = [0]; 337 | let i, j, u, v, c; 338 | const len = arr.length; 339 | for (i = 0; i < len; i++) { 340 | const arrI = arr[i]; 341 | if (arrI !== 0) { 342 | j = result[result.length - 1]; 343 | if (arr[j] < arrI) { 344 | p[i] = j; 345 | result.push(i); 346 | continue; 347 | } 348 | u = 0; 349 | v = result.length - 1; 350 | while (u < v) { 351 | c = (u + v) >> 1; 352 | if (arr[result[c]] < arrI) { 353 | u = c + 1; 354 | } else { 355 | v = c; 356 | } 357 | } 358 | if (arrI < arr[result[u]]) { 359 | if (u > 0) { 360 | p[i] = result[u - 1]; 361 | } 362 | result[u] = i; 363 | } 364 | } 365 | } 366 | u = result.length; 367 | v = result[u - 1]; 368 | while (u-- > 0) { 369 | result[u] = v; 370 | v = p[v]; 371 | } 372 | return result; 373 | } 374 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/ShapeFlags"; 2 | 3 | export const Fragment = Symbol("Fragment"); 4 | export const Text = Symbol("Text"); 5 | 6 | export function createVNode(type, props?, children?) { 7 | const vnode = { 8 | type, 9 | props, 10 | children, 11 | el: null, 12 | shapeFlag: getShapeType(type), 13 | key: props && props.key, 14 | }; 15 | if (typeof children === "string") { 16 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 17 | } else if (Array.isArray(children)) { 18 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 19 | } 20 | 21 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 22 | if (typeof children === "object") { 23 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 24 | } 25 | } 26 | return vnode; 27 | } 28 | 29 | export function createTextVNode(text: string) { 30 | return createVNode(Text, {}, text); 31 | } 32 | 33 | function getShapeType(type) { 34 | return typeof type === "string" 35 | ? ShapeFlags.ELEMENT 36 | : ShapeFlags.STATEFUL_COMPONENT; 37 | } 38 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../runtime-core"; 2 | 3 | function createElement(type) { 4 | return document.createElement(type); 5 | } 6 | 7 | function patchProp(el, key, oldValue, nextValue) { 8 | const isOn = (key: string) => /^on[A-Z]/.test(key); 9 | if (isOn(key)) { 10 | const event = key.slice(2).toLowerCase(); 11 | el.addEventListener(event, nextValue); 12 | } else { 13 | if (nextValue == null) { 14 | el.removeAttribute(key); 15 | } else { 16 | el.setAttribute(key, nextValue); 17 | } 18 | } 19 | } 20 | 21 | function insert(child, parent, anchor) { 22 | parent.insertBefore(child, anchor || null); 23 | } 24 | 25 | function remove(child) { 26 | const parent = child.parentNode; 27 | if (parent) { 28 | parent.removeChild(child); 29 | } 30 | } 31 | 32 | function setElementText(el, text) { 33 | el.textContent = text; 34 | } 35 | 36 | const renderer: any = createRenderer({ 37 | createElement, 38 | patchProp, 39 | insert, 40 | remove, 41 | setElementText, 42 | }); 43 | 44 | export function createApp(...args) { 45 | return renderer.createApp(...args); 46 | } 47 | 48 | export * from "../runtime-core"; 49 | -------------------------------------------------------------------------------- /src/shared/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum ShapeFlags { 2 | ELEMENT = 1, // 0001 3 | STATEFUL_COMPONENT = 1 << 1, // 0010 4 | TEXT_CHILDREN = 1 << 2, // 0100 5 | ARRAY_CHILDREN = 1 << 3, // 1000 6 | SLOT_CHILDREN = 1 << 4, 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | 3 | export const isObject = (value) => { 4 | return value !== null && typeof value === "object"; 5 | }; 6 | 7 | export const hasChanged = (val, newValue) => { 8 | return !Object.is(val, newValue); 9 | }; 10 | 11 | export const hasOwn = (val, key) => 12 | Object.prototype.hasOwnProperty.call(val, key); 13 | 14 | export const camelize = (str: string) => { 15 | return str.replace(/-(\w)/g, (_, c: string) => { 16 | return c ? c.toUpperCase() : ""; 17 | }); 18 | }; 19 | 20 | const capitalize = (str: string) => { 21 | return str.charAt(0).toUpperCase() + str.slice(1); 22 | }; 23 | 24 | export const toHandlerKey = (str: string) => { 25 | return str ? "on" + capitalize(str) : ""; 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "es6" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 25 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | 29 | /* Modules */ 30 | "module": "esnext" /* Specify what module code is generated. */, 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 37 | "types": [ 38 | "jest" 39 | ] /* Specify type package names to be included without being referenced in a source file. */, 40 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files */ 42 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 55 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | 73 | /* Interop Constraints */ 74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 76 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 79 | 80 | /* Type Checking */ 81 | "strict": true /* Enable all strict type-checking options. */, 82 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 83 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 88 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 89 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 91 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 92 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 96 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | } 105 | } 106 | --------------------------------------------------------------------------------