├── .gitignore ├── README.md ├── babel.config.js ├── example ├── apiInject │ ├── App.js │ └── index.html ├── componentEmit.js │ ├── App.js │ ├── Child.js │ └── index.html ├── componentUpdate │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── customRenderer │ ├── App.js │ ├── game.js │ ├── index.html │ ├── main.js │ └── renderer.js ├── getCurrentInstance │ ├── App.js │ └── index.html ├── helloworld │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── nextTicker │ ├── App.js │ ├── index.html │ └── main.js ├── patchChildren │ ├── App.js │ ├── ArrayToArray.js │ ├── ArrayToText.js │ ├── TextToArray.js │ ├── TextToText.js │ ├── index.html │ └── main.js └── slotsComponent │ ├── App.js │ ├── Child.js │ └── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index.ts ├── reactivity │ ├── index.ts │ ├── src │ │ ├── baseHandlers.ts │ │ ├── computed.ts │ │ ├── dep.ts │ │ ├── effective.ts │ │ ├── reactive.ts │ │ ├── ref.ts │ │ ├── scheduler.ts │ │ └── watch.ts │ └── tests │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ ├── shallowReactive.spec.ts │ │ ├── shallowReadonly.spec.ts │ │ └── watch.spec.ts ├── runtime-core │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── componentUpdateUtils.ts │ ├── createApp.ts │ ├── h.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── index.ts │ ├── render.ts │ ├── scheduler.ts │ └── vnode.ts ├── runtime-dom │ └── index.ts └── shared │ ├── ShapeFlags.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # my-mini-vue3 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | // 组件 provide 和 inject 功能 2 | import { 3 | h, 4 | provide, 5 | inject, 6 | } from "../../lib/mini-vue.esm.js"; 7 | 8 | const ProviderOne = { 9 | setup() { 10 | provide("foo", "foo"); 11 | provide("bar", "bar"); 12 | }, 13 | render() { 14 | return h(ProviderTwo); 15 | } 16 | }; 17 | 18 | const ProviderTwo = { 19 | setup() { 20 | // override parent value 21 | provide("foo", "fooOverride"); 22 | provide("baz", "baz"); 23 | const foo = inject("foo"); 24 | // 这里获取的 foo 的值应该是 "foo" 25 | // 这个组件的子组件获取的 foo ,才应该是 fooOverride 26 | if (foo !== "foo") { 27 | throw new Error("Foo should equal to foo"); 28 | } 29 | return { 30 | foo 31 | } 32 | }, 33 | render() { 34 | return h("div", {}, [h("div", {}, `${this.foo}`), h(Consumer)]); 35 | } 36 | }; 37 | 38 | const Consumer = { 39 | setup() { 40 | const foo = inject("foo"); 41 | const bar = inject("bar"); 42 | const baz = inject("baz"); 43 | return { 44 | foo, 45 | bar, 46 | baz 47 | } 48 | }, 49 | render() { 50 | return h("div", {}, `${this.foo}-${this.bar}-${this.baz}`); 51 | } 52 | }; 53 | 54 | export default { 55 | name: "App", 56 | setup() { 57 | 58 | }, 59 | render() { 60 | return h("div", {}, [h("p", {}, "apiInject"), h(ProviderOne)]); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/componentEmit.js/App.js: -------------------------------------------------------------------------------- 1 | // 组件 emit 逻辑 demo 2 | // click emit 发出 change, 可以触发 App 组件内定义好的侦听函数 3 | // 允许接收多个参数 4 | import { h } from "../../lib/mini-vue.esm.js"; 5 | import Child from "./Child.js"; 6 | 7 | export default { 8 | name: "App", 9 | setup() {}, 10 | 11 | render() { 12 | return h("div", {}, [ 13 | h("div", {}, "你好"), 14 | h(Child, { 15 | msg: "your name is child", 16 | onChange(a, b) { 17 | console.log("---------------change------------------"); 18 | console.log(a, b); 19 | }, 20 | onChangePageName(a, b) { 21 | console.log("---------------change-page-name------------------"); 22 | console.log(a, b); 23 | }, 24 | }), 25 | ]); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /example/componentEmit.js/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/mini-vue.esm.js"; 2 | export default { 3 | name: "Child", 4 | setup(props, { emit }) { 5 | emit("change", "aaaaa", "bbbbbb"); 6 | // 支持多个 - 7 | emit("change-page-name", "start", "game"); 8 | }, 9 | render() { 10 | return h("div", {}, "child"); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /example/componentEmit.js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, onMounted } from "../../lib/mini-vue.esm.js"; 2 | import Child from "./Child.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | setup() { 7 | const msg = ref("123"); 8 | const count = ref(1); 9 | 10 | window.msg = msg; 11 | 12 | onMounted(() => { 13 | console.log("App —————— onMounted 1"); 14 | }) 15 | 16 | onMounted(() => { 17 | console.log("App —————— onMounted 2"); 18 | }) 19 | 20 | const changeChildProps = () => { 21 | msg.value = "456"; 22 | }; 23 | 24 | const changeCount = () => { 25 | count.value++; 26 | }; 27 | 28 | return { msg, changeChildProps, changeCount, count }; 29 | }, 30 | 31 | beforeCreate() { 32 | console.log("App —————— beforeCreate"); 33 | }, 34 | created() { 35 | console.log("App —————— created"); 36 | }, 37 | beforeMount() { 38 | console.log("App —————— beforeMount"); 39 | }, 40 | mounted() { 41 | console.log("App —————— mounted"); 42 | }, 43 | beforeUpdate() { 44 | console.log("App —————— beforeUpdate"); 45 | }, 46 | updated() { 47 | console.log("App —————— updated"); 48 | }, 49 | 50 | render() { 51 | return h("div", {}, [ 52 | h("div", {}, "你好"), 53 | h( 54 | "button", 55 | { 56 | onClick: this.changeChildProps, 57 | }, 58 | "change child props" 59 | ), 60 | h(Child, { 61 | msg: this.msg, 62 | }), 63 | h( 64 | "button", 65 | { 66 | onClick: this.changeCount, 67 | }, 68 | "change self count" 69 | ), 70 | h("p", {}, "count: " + this.count), 71 | ]); 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /example/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/mini-vue.esm.js"; 2 | export default { 3 | name: "Child", 4 | setup(props, { emit }) {}, 5 | beforeCreate() { 6 | console.log("Child —————— beforeCreate"); 7 | }, 8 | created() { 9 | console.log("Child —————— created"); 10 | }, 11 | beforeMount() { 12 | console.log("Child —————— beforeMount"); 13 | }, 14 | mounted() { 15 | console.log("Child —————— mounted"); 16 | }, 17 | beforeUpdate() { 18 | console.log("Child —————— beforeUpdate"); 19 | }, 20 | updated() { 21 | console.log("Child —————— updated"); 22 | }, 23 | render(proxy) { 24 | return h("div", {}, [h("div", {}, "child - props - msg: " + this.$props.msg)]); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /example/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /example/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/mini-vue.esm.js"; 2 | 3 | export default { 4 | name: "App", 5 | setup() { 6 | return { 7 | x: 100, 8 | y: 100, 9 | }; 10 | }, 11 | 12 | render() { 13 | return h("rect", { x: this.x, y: this.y }); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /example/customRenderer/game.js: -------------------------------------------------------------------------------- 1 | export const game = new PIXI.Application({ 2 | width: 500, 3 | height: 500, 4 | }); 5 | 6 | document.body.append(game.view); 7 | 8 | export function createRootContainer() { 9 | return game.stage; 10 | } 11 | -------------------------------------------------------------------------------- /example/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import App from "./App.js"; 2 | import { createApp } from "./renderer.js"; 3 | import { createRootContainer } from "./game.js"; 4 | 5 | createApp(App).mount(createRootContainer()); 6 | -------------------------------------------------------------------------------- /example/customRenderer/renderer.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../../lib/mini-vue.esm.js"; 2 | 3 | // 给基于 pixi.js 的渲染函数 4 | const renderer = createRenderer({ 5 | createElement(type) { 6 | const rect = new PIXI.Graphics(); 7 | rect.beginFill(0xff0000); 8 | rect.drawRect(0, 0, 100, 100); 9 | rect.endFill(); 10 | 11 | return rect; 12 | }, 13 | 14 | patchProp(el, key, prevValue, nextValue) { 15 | el[key] = nextValue; 16 | }, 17 | 18 | insert(el, parent) { 19 | parent.addChild(el); 20 | }, 21 | }); 22 | 23 | export function createApp(rootComponent) { 24 | return renderer.createApp(rootComponent); 25 | } 26 | -------------------------------------------------------------------------------- /example/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | // 可以在 setup 中使用 getCurrentInstance 获取组件实例对象 2 | import { h, getCurrentInstance } from "../../lib/mini-vue.esm.js"; 3 | 4 | export default { 5 | name: "App", 6 | setup() { 7 | console.log(getCurrentInstance()); 8 | }, 9 | render() { 10 | return h("div", {}, [h("p", {}, "getCurrentInstance")]); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /example/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/mini-vue.esm.js" 2 | import { Foo } from "./Foo.js" 3 | 4 | window.self = null; 5 | // 这里直接 render 和写template一样,template 需要经过编译转换成 render 函数 6 | export const App = { 7 | render() { 8 | window.self = this; 9 | return h( 10 | "div", 11 | { 12 | id: "root", 13 | onClick() { 14 | console.log("click"); 15 | }, 16 | onMousedown() { 17 | console.log("mousedown"); 18 | } 19 | }, 20 | [ 21 | h("div", { 22 | class: [{blue: true}, "red", "12 34"], 23 | }, "hi, " + this.msg), 24 | h(Foo, { 25 | count: 1, 26 | }) 27 | ] 28 | ); 29 | }, 30 | setup() { 31 | // composition api 32 | return { 33 | msg: "mini-vue" 34 | } 35 | }, 36 | } -------------------------------------------------------------------------------- /example/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/mini-vue.esm.js" 2 | 3 | export const Foo = { 4 | setup(props) { 5 | // props.count 6 | // shallow readonly 7 | props.count++; 8 | console.log(props); 9 | }, 10 | render() { 11 | return h( 12 | "div", 13 | {}, 14 | "foo: " + this.count 15 | ); 16 | }, 17 | } -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/mini-vue.esm.js" 2 | import { App } from "./App.js" 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | console.log(rootContainer) 6 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | getCurrentInstance, 5 | nextTick, 6 | } from "../../lib/mini-vue.esm.js"; 7 | 8 | export default { 9 | name: "App", 10 | setup() { 11 | const count = ref(1); 12 | const instance = getCurrentInstance(); 13 | 14 | function onClick() { 15 | for (let i = 0; i < 100; i++) { 16 | console.log("update"); 17 | count.value = i; 18 | } 19 | 20 | debugger; 21 | console.log(instance); 22 | nextTick(() => { 23 | console.log(instance); 24 | }); 25 | 26 | // await nextTick() 27 | // console.log(instance) 28 | } 29 | 30 | return { 31 | onClick, 32 | count, 33 | }; 34 | }, 35 | render() { 36 | const button = h("button", { onClick: this.onClick }, "update"); 37 | const p = h("p", {}, "count:" + this.count); 38 | 39 | return h("div", {}, [button, p]); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /example/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/mini-vue.esm.js"; 2 | import App from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#root"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /example/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/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 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 array 3 | 4 | import { ref, h } from "../../lib/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 | 189 | // const prevChildren = [ 190 | // h("p", { key: "A" }, "A"), 191 | // h("p", { key: "B" }, "B"), 192 | // h("p", { key: "C" }, "C"), 193 | // h("p", { key: "D" }, "D"), 194 | // h("p", { key: "E" }, "E"), 195 | // h("p", { key: "Z" }, "Z"), 196 | // h("p", { key: "F" }, "F"), 197 | // h("p", { key: "G" }, "G"), 198 | // ]; 199 | 200 | // const nextChildren = [ 201 | // h("p", { key: "A" }, "A"), 202 | // h("p", { key: "B" }, "B"), 203 | // h("p", { key: "D" }, "D"), 204 | // h("p", { key: "C" }, "C"), 205 | // h("p", { key: "Y" }, "Y"), 206 | // h("p", { key: "E" }, "E"), 207 | // h("p", { key: "F" }, "F"), 208 | // h("p", { key: "G" }, "G"), 209 | // ]; 210 | 211 | // fix c 节点应该是 move 而不是删除之后重新创建的 212 | const prevChildren = [ 213 | h("p", { key: "A" }, "A"), 214 | h("p", {}, "C"), 215 | h("p", { key: "B" }, "B"), 216 | h("p", { key: "D" }, "D"), 217 | ]; 218 | 219 | const nextChildren = [ 220 | h("p", { key: "A" }, "A"), 221 | h("p", { key: "B" }, "B"), 222 | h("p", {}, "C"), 223 | h("p", { key: "D" }, "D"), 224 | ]; 225 | 226 | export default { 227 | name: "ArrayToArray", 228 | setup() { 229 | const isChange = ref(false); 230 | window.isChange = isChange; 231 | 232 | return { 233 | isChange, 234 | }; 235 | }, 236 | render() { 237 | const self = this; 238 | 239 | return self.isChange === true 240 | ? h("div", {}, nextChildren) 241 | : h("div", {}, prevChildren); 242 | }, 243 | }; 244 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 text 3 | 4 | import { ref, h } from "../../lib/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 | -------------------------------------------------------------------------------- /example/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | // 新的是 array 2 | // 老的是 text 3 | import { ref, h } from "../../lib/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 | -------------------------------------------------------------------------------- /example/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | // 新的是 text 2 | // 老的是 text 3 | import { ref, h } from "../../lib/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 | -------------------------------------------------------------------------------- /example/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/mini-vue.esm.js"; 2 | import App from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#root"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /example/slotsComponent/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode, createCommentVNode } from "../../lib/mini-vue.esm.js"; 2 | import Child from "./Child.js"; 3 | 4 | export default { 5 | name: "App", 6 | setup() {}, 7 | 8 | render() { 9 | return h("div", {}, [ 10 | h("div", {}, "你好"), 11 | h( 12 | Child, 13 | { 14 | msg: "your name is child", 15 | }, 16 | { 17 | default: ({ age }) => [ 18 | h("p", {}, "我是通过 slot 渲染出来的第一个元素 "), 19 | h("p", {}, "我是通过 slot 渲染出来的第二个元素"), 20 | h("p", {}, `我可以接收到 age: ${age}`), 21 | createTextVNode("hello"), 22 | createCommentVNode("我是注释"), 23 | ], 24 | } 25 | ), 26 | ]); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /example/slotsComponent/Child.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/mini-vue.esm.js"; 2 | export default { 3 | name: "Child", 4 | setup(props, context) {}, 5 | render() { 6 | console.log(this.$slots) 7 | return h("div", {}, [ 8 | h("div", {}, "child"), 9 | // renderSlot 会返回一个 vnode 10 | // 其本质和 h 是一样的 11 | // 第三个参数给出数据 12 | // 具名插槽 13 | // 作用域插槽 14 | renderSlots(this.$slots, "default", { 15 | age: 16, 16 | }), 17 | ]); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/slotsComponent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-mini-vue3", 3 | "version": "1.0.0", 4 | "main": "lib/mini-vue.cjs.js", 5 | "module": "lib/mini-vue.esm.js", 6 | "repository": "https://github.com/dannykbsoul/my-mini-vue3.git", 7 | "author": "moises.zhou ", 8 | "license": "MIT", 9 | "scripts": { 10 | "test": "jest", 11 | "build": "rollup -c rollup.config.js" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.17.8", 15 | "@babel/preset-env": "^7.16.11", 16 | "@babel/preset-typescript": "^7.16.7", 17 | "@rollup/plugin-typescript": "^8.3.1", 18 | "@types/jest": "^27.4.1", 19 | "babel-jest": "^27.5.1", 20 | "jest": "^27.5.1", 21 | "rollup": "^2.70.1", 22 | "tslib": "^2.3.1", 23 | "typescript": "^4.6.2" 24 | }, 25 | "dependencies": { 26 | "pixi.js": "^6.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from "./package.json" 2 | import typescript from "@rollup/plugin-typescript" 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: [ 17 | typescript() 18 | ] 19 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runtime-dom" 2 | export * from "./reactivity" -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, proxyRefs } from "./src/ref"; 2 | export { reactive } from "./src/reactive"; -------------------------------------------------------------------------------- /src/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject, isSymbol } from "../../shared"; 2 | import { enableTracking, pauseTracking, track, trigger } from "./effective"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | import { isRef } from "./ref"; 5 | 6 | // 对于一些内置的 symbol 类型,不希望收集依赖 7 | const builtInSymbols = new Set( 8 | Object.getOwnPropertyNames(Symbol) 9 | .map((key) => (Symbol as any)[key]) 10 | ); 11 | 12 | const get = createGetter(); 13 | const set = createSetter(); 14 | const shallowGet = createGetter(false, true); 15 | const readonlyGet = createGetter(true); 16 | const shallowReadonlyGet = createGetter(true, true); 17 | 18 | const arrayInstrumentations = createArrayInstrumentations(); 19 | 20 | function createArrayInstrumentations() { 21 | const instrumentations = {}; 22 | ["includes", "indexOf", "lastIndexOf"].forEach((key) => { 23 | const originMethod = Array.prototype[key]; 24 | instrumentations[key] = function (...args: any) { 25 | let res = originMethod.apply(this, args); 26 | 27 | if (!res) { 28 | res = originMethod.apply((this as any).raw, args); 29 | } 30 | return res; 31 | }; 32 | }); 33 | ["push", "pop", "shift", "unshift", "splice"].forEach((key) => { 34 | const originMethod = Array.prototype[key]; 35 | instrumentations[key] = function (...args: any) { 36 | // 暂停 track 37 | pauseTracking(); 38 | let res = originMethod.apply(this, args); 39 | // 恢复 track 40 | enableTracking(); 41 | return res; 42 | }; 43 | }); 44 | return instrumentations; 45 | } 46 | 47 | export const ITERATE_KEY = Symbol(); 48 | export const enum TriggerType { 49 | SET = "SET", 50 | ADD = "ADD", 51 | DEL = "DEL", 52 | } 53 | 54 | function createGetter(isReadonly = false, isShallow = false) { 55 | return function get(target, key, receiver) { 56 | if (key === ReactiveFlags.IS_REACTIVE) { 57 | return !isReadonly; 58 | } else if (key === ReactiveFlags.IS_READONLY) { 59 | return isReadonly; 60 | } else if (key === ReactiveFlags.IS_SHALLOW) { 61 | return isShallow; 62 | } 63 | // 代理对象可以通过 raw 属性访问到原始数据 64 | if (key === "raw") { 65 | return target; 66 | } 67 | 68 | if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) { 69 | return Reflect.get(arrayInstrumentations, key, receiver); 70 | } 71 | // key 是内置 symbol 类型,不希望收集依赖 72 | if (!isReadonly && !(isSymbol(key) && builtInSymbols.has(key))) { 73 | // 只读的情况 不需要依赖收集 74 | track(target, key); 75 | } 76 | const res = Reflect.get(target, key); 77 | if (isShallow) return res; 78 | // 深响应 79 | if (isObject(res)) { 80 | return isReadonly ? readonly(res) : reactive(res); 81 | } 82 | return res; 83 | }; 84 | } 85 | 86 | function createSetter() { 87 | return function set(target, key, value, reveiver) { 88 | const type = isArray(target) && isIntegerKey(key) 89 | ? Number(key) < target.length 90 | ? TriggerType.SET 91 | : TriggerType.ADD 92 | : hasOwn(target, key) 93 | ? TriggerType.SET 94 | : TriggerType.ADD; 95 | const oldValue = Reflect.get(target, key); 96 | if (isRef(oldValue) && !isRef(value)) { 97 | oldValue.value = value; 98 | } 99 | const res = Reflect.set(target, key, value); 100 | // reveiver 就是 target 的代理对象,此时才会去考虑触发 101 | if (reveiver.raw === target) { 102 | hasChanged(oldValue, value) && trigger(target, key, type, value); 103 | } 104 | return res; 105 | }; 106 | } 107 | 108 | function deleteProperty(target, key) { 109 | // 检查被操作的属性是否是对象自己的属性 110 | const hadKey = hasOwn(target, key); 111 | const result = Reflect.deleteProperty(target, key); 112 | // 只有当被删除的属性是对象自己的属性 并且 删除成功的时候,才会去触发更新 113 | if (hadKey && result) { 114 | trigger(target, key, TriggerType.DEL); 115 | } 116 | return result; 117 | } 118 | 119 | // key in obj 类似的操作 120 | function has(target, key) { 121 | track(target, key); 122 | return Reflect.has(target, key); 123 | } 124 | 125 | // for ... in 类似的操作 126 | // 对象属性新增、删除才需要触发 127 | // for ... in 操作对于数组也是可以遍历,哪些操作会影响到 for ... in的遍历 128 | // 1. 添加新元素 arr[100] = 1; 129 | // 2. 修改数组长度 arr.length = 0; 130 | // 无论是新增还是修改数组长度,本质都是 length 的变化,所以将 length 与副作用函数绑定在一起 131 | // 将来 length 变化的时候去触发相应的副作用函数 132 | function ownKeys(target) { 133 | track(target, isArray(target) ? "length" : ITERATE_KEY); 134 | return Reflect.ownKeys(target); 135 | } 136 | 137 | 138 | export const mutableHandlers = { 139 | get, 140 | set, 141 | deleteProperty, 142 | has, 143 | ownKeys, 144 | }; 145 | 146 | export const shallowReactiveHandlers = extend({}, mutableHandlers, { 147 | get: shallowGet, 148 | }); 149 | 150 | export const readonlyHandlers = { 151 | get: readonlyGet, 152 | set: function set(target, key, value) { 153 | console.warn(`Set operation on key "${key}" failed: target is readonly.`); 154 | return true; 155 | }, 156 | }; 157 | 158 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 159 | get: shallowReadonlyGet, 160 | }); -------------------------------------------------------------------------------- /src/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { createDep } from "./dep"; 2 | import { ReactiveEffect, triggerEffects } from "./effective"; 3 | import { trackRefValue } from "./ref"; 4 | 5 | class ComputedRefImpl { 6 | public dep: any; 7 | private _getter: any; 8 | private _dirty = true; 9 | private _value: any; 10 | private _effect: any; 11 | constructor(getter) { 12 | this._getter = getter; 13 | this.dep = new Set(); 14 | // 巧妙的运用了 effect 的 scheduler,当 getter 依赖的响应式对象发生 set 的时候 15 | // 此时会调用 scheduler,将 _dirty 置为 true,当下次调用 .value 的时候,会去执行 this._effect.run(); 16 | // 此时 _dirty 为 false,当接下来没有响应式对象发生 set,调用 .value,直接返回 this._value,实现缓存 17 | this._effect = new ReactiveEffect(getter, () => { 18 | if (!this._dirty) { 19 | this._dirty = true; 20 | triggerEffects(createDep(this.dep)); 21 | } 22 | }); 23 | } 24 | get value() { 25 | trackRefValue(this); 26 | if (this._dirty) { 27 | this._dirty = false; 28 | this._value = this._effect.run(); 29 | } 30 | return this._value; 31 | } 32 | } 33 | 34 | export function computed(getter) { 35 | return new ComputedRefImpl(getter); 36 | } -------------------------------------------------------------------------------- /src/reactivity/src/dep.ts: -------------------------------------------------------------------------------- 1 | export const createDep = (effects?) => { 2 | const dep = new Set(effects); 3 | return dep; 4 | }; 5 | -------------------------------------------------------------------------------- /src/reactivity/src/effective.ts: -------------------------------------------------------------------------------- 1 | import { extend, isArray } from "../../shared"; 2 | import { ITERATE_KEY, TriggerType } from "./baseHandlers"; 3 | import { createDep } from "./dep"; 4 | 5 | let activeEffect; 6 | let shouldTrack; 7 | // 当嵌套 effect 存在时,activeEffect就不够用了,因为当嵌套的 activeEffect 激活的时候就会覆盖上一个 activeEffect 8 | // 此时内层的 activeEffect执行完,需要找到外层的 activeEffect,所以需要一个 stack 来存储 9 | const effectStack: unknown[] = []; 10 | export class ReactiveEffect { 11 | private _fn: any; 12 | deps = []; 13 | active = true; 14 | onStop?: () => void; 15 | public scheduler: Function | undefined; 16 | constructor(fn, scheduler?: Function) { 17 | this._fn = fn; 18 | this.scheduler = scheduler; 19 | } 20 | run() { 21 | // 每次都需要清除一次依赖再去收集依赖,用于分支切换 22 | cleanupEffect(this); 23 | if (!this.active) { 24 | return this._fn(); 25 | } 26 | shouldTrack = true; 27 | activeEffect && effectStack.push(activeEffect); 28 | activeEffect = this; 29 | const result = this._fn(); 30 | // 执行完当前的 effect,需要还原之前的 effect 31 | activeEffect = effectStack.pop(); 32 | // activeEffect 为空代表没有要执行的 effect,此时 shouldTrack 关闭 33 | !activeEffect && (shouldTrack = false); 34 | return result; 35 | } 36 | stop() { 37 | // 确保反复执行 stop 只清空一次 38 | if (this.active) { 39 | cleanupEffect(this); 40 | this.onStop && this.onStop(); 41 | this.active = false; 42 | } 43 | } 44 | } 45 | 46 | function cleanupEffect(effect) { 47 | const { deps } = effect; 48 | if (deps.length) { 49 | for (let i = 0; i < deps.length; i++) { 50 | deps[i].delete(effect); 51 | } 52 | deps.length = 0; 53 | } 54 | effect.deps.length = 0; 55 | } 56 | 57 | // why use WeakMap? 58 | // 因为 targetMap 都是对象作为键名,但是对键名所引用的对象是弱引用,不会影响垃圾回收机制 59 | // 只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。 60 | // 也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 61 | // 应用场景 62 | // WeakMap 经常用于存储那些只有当 key 所引用的对象存在时才有价值的信息,例如这里的 targetMap 对应的 target, 63 | // 如果 target 对象没有任何的引用了,说明用户侧不再需要它了,此时如果用 Map 的话,即使用户侧的代码对 target 没有任何引用, 64 | // 这个 target 也不会被回收,最后可能导致内存溢出 65 | const targetMap = new WeakMap(); 66 | export function track(target, key) { 67 | // 没有 activeEffect,也就没有必要进行依赖收集 68 | if (!isTracking()) return; 69 | // target -> key -> deps(effect) 70 | // 这样的映射关系可以精确的实现响应式 71 | // 取出 depsMap,数据类型是 Map: key -> deps(effect) 72 | let depsMap = targetMap.get(target); 73 | if (!depsMap) { 74 | targetMap.set(target, (depsMap = new Map())); 75 | } 76 | // 根据 key 取出 deps,数据类型是 Set 77 | let dep = depsMap.get(key); 78 | if (!dep) { 79 | depsMap.set(key, (dep = new Set())); 80 | } 81 | trackEffects(dep); 82 | } 83 | 84 | export function trackEffects(dep) { 85 | if (dep.has(activeEffect)) return; 86 | dep.add(activeEffect); 87 | // activeEffect中去记录dep 88 | activeEffect.deps.push(dep); 89 | } 90 | 91 | export function isTracking() { 92 | return shouldTrack && activeEffect; 93 | } 94 | 95 | export function pauseTracking() { 96 | shouldTrack = false; 97 | } 98 | 99 | export function enableTracking() { 100 | shouldTrack = true; 101 | } 102 | 103 | export function trigger(target, key, type, value?) { 104 | const depsMap = targetMap.get(target); 105 | if (!depsMap) return; 106 | // const deps = depsMap.get(key) || []; 107 | let deps: any = []; 108 | // 如果新增了 key,需要触发之前 for in 收集到的 effect 109 | // delete 会影响 for ... in的遍历结果,所以也需要触发它的依赖 110 | const iterateDeps = 111 | (type === TriggerType.ADD || TriggerType.DEL ? depsMap.get(ITERATE_KEY) : []) || []; 112 | // 数组 set length,会触发 length 的effect,以及修改 length 导致部分数组元素的变更 113 | // 比如 arr = [1, 2, 3],此时设置arr.length = 1; 会导致arr[1]、arr[2]这两个元素发生变更,即要触发相应的副作用函数 114 | // 同样 如果数组情况下是 ADD 的情况,则同样需要触发 length 的副作用函数 115 | if (isArray(target)) { 116 | // length 触发会影响到数组元素的变更 117 | if (key === "length") { 118 | depsMap.forEach((dep, key) => { 119 | if (key === "length" || Number(key) >= value) { 120 | deps.push(...dep); 121 | } 122 | }) 123 | } 124 | // 新增了元素也需要触发 length 绑定的副作用函数 125 | if (type === TriggerType.ADD) { 126 | deps.push(...(depsMap.get("length")) || []); 127 | } 128 | if (type === TriggerType.SET) { 129 | deps.push(...(depsMap.get(key) || [])); 130 | } 131 | } else { 132 | deps.push(...(depsMap.get(key)||[])); 133 | } 134 | // deps执行前进行保存,防止陷入死循环 135 | triggerEffects(createDep([...deps, ...iterateDeps])); 136 | } 137 | 138 | export function triggerEffects(deps) { 139 | for (const effect of deps) { 140 | // 当前执行的 activeEffect 与trigger触发的 effect 相同,则不触发执行 141 | if (effect === activeEffect) return; 142 | if (effect.scheduler) { 143 | effect.scheduler(); 144 | } else { 145 | effect.run(); 146 | } 147 | } 148 | } 149 | 150 | export function stop(runner) { 151 | runner.effect.stop(); 152 | } 153 | 154 | export function effect(fn, options: any = {}) { 155 | const _effect = new ReactiveEffect(fn, options.scheduler); 156 | extend(_effect, options); 157 | !options.lazy && _effect.run(); 158 | const runner: any = _effect.run.bind(_effect); 159 | runner.effect = _effect; 160 | return runner; 161 | } -------------------------------------------------------------------------------- /src/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { toRawType } from "../../shared"; 2 | import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers } from "./baseHandlers"; 3 | 4 | export const enum ReactiveFlags { 5 | IS_REACTIVE = "__v_isReactive", 6 | IS_READONLY = "__v_isReadonly", 7 | IS_SHALLOW = '__v_isShallow', 8 | } 9 | 10 | export const reactiveMap = new WeakMap(); 11 | export const shallowReactiveMap = new WeakMap(); 12 | 13 | const enum TargetType { 14 | INVALID = 0, 15 | COMMON = 1, 16 | COLLECTION = 2, 17 | } 18 | 19 | function targetTypeMap(rawType: string) { 20 | switch (rawType) { 21 | case "Object": 22 | case "Array": 23 | return TargetType.COMMON; 24 | case "Map": 25 | case "Set": 26 | case "WeakMap": 27 | case "WeakSet": 28 | return TargetType.COLLECTION; 29 | default: 30 | return TargetType.INVALID; 31 | } 32 | } 33 | 34 | function getTargetType(value) { 35 | return targetTypeMap(toRawType(value)); 36 | } 37 | 38 | export function reactive(raw) { 39 | // 优先通过原始对象寻找之前创建的代理对象 40 | const existProxy = reactiveMap.get(raw); 41 | if (existProxy) return existProxy; 42 | // 防止影响到内层的 reactive,如果已经是转换过的,直接返回 43 | if (isProxy(raw)) return raw; 44 | const targetType = getTargetType(raw); 45 | if (targetType === TargetType.INVALID) { 46 | return raw; 47 | } 48 | const proxy = createActiveObject( 49 | raw, 50 | targetType === TargetType.COLLECTION 51 | ? null 52 | : mutableHandlers 53 | ); 54 | reactiveMap.set(raw, proxy); 55 | return proxy; 56 | } 57 | 58 | export function shallowReactive(raw) { 59 | const existProxy = shallowReactiveMap.get(raw); 60 | if (existProxy) return existProxy; 61 | const proxy = createActiveObject(raw, shallowReactiveHandlers); 62 | shallowReactiveMap.set(raw, proxy); 63 | return proxy; 64 | } 65 | 66 | export function readonly(raw) { 67 | return createActiveObject(raw, readonlyHandlers); 68 | } 69 | 70 | export function shallowReadonly(raw) { 71 | return createActiveObject(raw, shallowReadonlyHandlers); 72 | } 73 | 74 | function createActiveObject(raw, baseHandlers) { 75 | return new Proxy(raw, baseHandlers); 76 | } 77 | 78 | // if value not wrapped will return undefined,so we need !! operator 79 | export function isReactive(value) { 80 | return !!value[ReactiveFlags.IS_REACTIVE]; 81 | } 82 | 83 | export function isReadonly(value) { 84 | return !!value[ReactiveFlags.IS_READONLY]; 85 | } 86 | 87 | export function isShallow(value) { 88 | return !!value[ReactiveFlags.IS_SHALLOW]; 89 | } 90 | 91 | export function isProxy(value) { 92 | return isReactive(value) || isReadonly(value); 93 | } -------------------------------------------------------------------------------- /src/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isArray, isObject } from "../../shared"; 2 | import { createDep } from "./dep"; 3 | import { isTracking, trackEffects, triggerEffects } from "./effective"; 4 | import { isProxy, reactive } from "./reactive"; 5 | 6 | class RefImpl { 7 | // ref只有一个值,只会对应一个dep 8 | public dep: any; 9 | public __v_isRef = true; 10 | private _value: any; 11 | private _rawValue: any; 12 | constructor(value, isShallow = false) { 13 | this._rawValue = value; 14 | // 如果 value 是 object,则需要用 reactive 包裹 value 15 | this._value = isShallow ? value : convert(value); 16 | this.dep = new Set(); 17 | } 18 | get value() { 19 | trackRefValue(this); 20 | return this._value; 21 | } 22 | set value(newValue) { 23 | // 新旧值对比的时候需要考虑到value是对象的时候,其实我们用reactive去包装了 24 | // 所以对比需要用包装前的值和现有的值做比较 25 | if (hasChanged(this._rawValue, newValue)) { 26 | // 先修改值 再执行trigger 27 | this._rawValue = newValue; 28 | this._value = convert(newValue); 29 | triggerRefValue(this); 30 | } 31 | } 32 | } 33 | 34 | class ObjectRefImpl { 35 | public readonly __v_isRef = true; 36 | 37 | constructor( 38 | private readonly _object, 39 | private readonly _key, 40 | private readonly _defaultValue 41 | ) {} 42 | 43 | get value() { 44 | const val = this._object[this._key]; 45 | return val === undefined ? this._defaultValue : val; 46 | } 47 | 48 | set value(newVal) { 49 | this._object[this._key] = newVal; 50 | } 51 | } 52 | 53 | export function trackRefValue(ref) { 54 | isTracking() && trackEffects(ref.dep || (ref.dep = createDep())); 55 | } 56 | 57 | export function triggerRefValue(ref) { 58 | triggerEffects(createDep(ref.dep)); 59 | } 60 | 61 | function convert(value) { 62 | return isObject(value) ? reactive(value) : value; 63 | } 64 | 65 | export function ref(value?: unknown) { 66 | return new RefImpl(value); 67 | } 68 | 69 | export function shallowRef(value?: unknown) { 70 | return new RefImpl(value, true); 71 | } 72 | 73 | export function toRef(target, key, defaultValue?) { 74 | const val = target[key]; 75 | return isRef(val) 76 | ? val 77 | : new ObjectRefImpl(target, key, defaultValue); 78 | } 79 | 80 | export function toRefs(target) { 81 | if (!isProxy(target)) { 82 | console.warn( 83 | `toRefs() expects a reactive object but received a plain one.` 84 | ); 85 | } 86 | const ret: any = isArray(target) ? new Array(target.length) : {}; 87 | for (const key in target) { 88 | ret[key] = toRef(target, key); 89 | } 90 | return ret; 91 | } 92 | 93 | class CustomRef { 94 | // ref只有一个值,只会对应一个dep 95 | public dep: any; 96 | public __v_isRef = true; 97 | private _get: any; 98 | private _set: any; 99 | constructor(func) { 100 | const { get, set } = func( 101 | () => trackRefValue(this), 102 | () => triggerRefValue(this) 103 | ); 104 | this._get = get; 105 | this._set = set; 106 | } 107 | get value() { 108 | return this._get(); 109 | } 110 | set value(newVal) { 111 | this._set(newVal); 112 | } 113 | } 114 | 115 | export function customRef(func) { 116 | return new CustomRef(func); 117 | } 118 | 119 | export function isRef(ref) { 120 | return !!(ref && ref.__v_isRef); 121 | } 122 | 123 | export function unref(ref) { 124 | return isRef(ref) ? ref.value : ref; 125 | } 126 | 127 | export function proxyRefs(objectWithRefs) { 128 | return new Proxy(objectWithRefs, { 129 | get(target, key) { 130 | return unref(Reflect.get(target, key)); 131 | }, 132 | set(target, key, newVal) { 133 | // 传过来的新值如果不是 ref 则需要.value替换 134 | // 如果是 ref 则需要整体替换 135 | if (isRef(Reflect.get(target, key)) && !isRef(newVal)) { 136 | return Reflect.get(target, key).value = newVal; 137 | } 138 | return Reflect.set(target, key, newVal); 139 | }, 140 | }); 141 | } -------------------------------------------------------------------------------- /src/reactivity/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "./effective"; 2 | import { reactive } from "./reactive"; 3 | 4 | export function scheduler() { 5 | const jobQueue = new Set(); 6 | const p = Promise.resolve(); 7 | 8 | let isFlushing = false; 9 | function flushJob() { 10 | if (isFlushing) return; 11 | isFlushing = true; 12 | p.then(() => { 13 | jobQueue.forEach((job: any) => job()); 14 | }).finally(() => { 15 | isFlushing = false; 16 | }) 17 | } 18 | 19 | const obj = reactive({ 20 | foo: 1 21 | }) 22 | effect(() => { 23 | console.log(obj.foo); 24 | }, { 25 | scheduler(fn) { 26 | jobQueue.add(fn); 27 | flushJob(); 28 | } 29 | }) 30 | obj.foo++; 31 | obj.foo++; 32 | // 最终只会输出 1 3 33 | // scheduler 帮我们很好的实现不包含过渡状态的操作 34 | // 这个操作类似于 vue 中连续多次修改响应式数据但只会触发一次更新 35 | } -------------------------------------------------------------------------------- /src/reactivity/src/watch.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject } from "../../shared"; 2 | import { effect } from "./effective"; 3 | 4 | // source 为 watch 的响应式对象 or 函数 5 | // 函数可以指定该 watch 依赖哪些响应式数据 6 | export function watch(source, cb, options?: any) { 7 | let getter; 8 | if (isFunction(source)) { 9 | getter = source; 10 | } else { 11 | getter = () => traverse(source); 12 | } 13 | 14 | let oldValue, newValue; 15 | 16 | // 用来存储用户注册的过期回调 17 | let cleanup; 18 | 19 | function onInvalidate(fn) { 20 | // 将过期 cb 存储到 cleanup 中 21 | cleanup = fn; 22 | } 23 | 24 | const job = () => { 25 | // 在 scheduler 中重新执行 effect,得到 newValue 26 | newValue = effectFn(); 27 | // 在调用当前函数之前,先调用上一个 cb 传过来过期函数 28 | if (cleanup) cleanup(); 29 | cb(newValue, oldValue, onInvalidate); 30 | // 更新 oldValue 31 | oldValue = newValue; 32 | }; 33 | // 懒执行 这样第一次 effectFn() 拿到 oldValue 34 | const effectFn = effect(getter, { 35 | lazy: true, 36 | scheduler: () => { 37 | // adjust the callback's flush timing 38 | // 默认值 为 pre,if you attempt to access the DOM inside a watcher callback, the DOM will be in the state before Vue has applied any updates. 39 | // flush 为 post,access the DOM in a watcher callback after Vue has updated it 40 | if (options && options.flush === "post") { 41 | const p = Promise.resolve(); 42 | p.then(job); 43 | } else { 44 | // 类似于 sync 的实现机制,pre 的方式暂时没办法模拟 45 | job(); 46 | } 47 | }, 48 | }); 49 | // immdiate 立即触发回调函数 50 | if (options && options.immdiate) { 51 | job(); 52 | } else { 53 | oldValue = effectFn(); 54 | } 55 | } 56 | 57 | // 对 source 递归的进行调用,即依赖收集,如果 source 有相关变化则会执行 cb 58 | // seen 用来记录 traverse 读取过了,避免循环引用导致的死循环 59 | // 暂时只考虑 object 的情况 60 | function traverse(value, seen = new Set()) { 61 | if (!isObject(value) || seen.has(value)) return; 62 | seen.add(value); 63 | for (const k in value) { 64 | traverse(value[k], seen); 65 | } 66 | return value; 67 | } -------------------------------------------------------------------------------- /src/reactivity/tests/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../src/computed"; 2 | import { effect } from "../src/effective"; 3 | import { reactive } from "../src/reactive"; 4 | 5 | describe("reactivity/computed", () => { 6 | // 和 ref 类似,但具有缓存功能 7 | it("happy path", () => { 8 | const user = reactive({ 9 | age: 1 10 | }) 11 | 12 | const age = computed(() => { 13 | return user.age 14 | }) 15 | 16 | expect(age.value).toBe(1); 17 | }) 18 | it("should return updated value", () => { 19 | const value = reactive({ 20 | foo: 1, 21 | }); 22 | const cValue = computed(() => value.foo); 23 | expect(cValue.value).toBe(1); 24 | value.foo = 2; 25 | expect(cValue.value).toBe(2); 26 | }); 27 | 28 | it("should compute lazily", () => { 29 | const value = reactive({ 30 | foo: 1 31 | }); 32 | const getter = jest.fn(() => value.foo); 33 | // 当调用 cValue 的 value 属性,才会去执行getter,并且如果依赖的值没有改变,只会执行一次 34 | const cValue = computed(getter); 35 | 36 | // lazy 37 | expect(getter).not.toHaveBeenCalled(); 38 | 39 | expect(cValue.value).toBe(1); 40 | expect(getter).toHaveBeenCalledTimes(1); 41 | 42 | // should not compute again 43 | cValue.value; 44 | expect(getter).toHaveBeenCalledTimes(1); 45 | 46 | // should not compute until needed 47 | value.foo = 2; 48 | expect(getter).toHaveBeenCalledTimes(1); 49 | 50 | // now it should compute 51 | expect(cValue.value).toBe(2); 52 | expect(getter).toHaveBeenCalledTimes(2); 53 | 54 | // should not compute again 55 | cValue.value; 56 | expect(getter).toHaveBeenCalledTimes(2); 57 | }); 58 | 59 | // 需要 computed 在 effect中生效 60 | // 当 61 | it("should trigger effect", () => { 62 | const value = reactive({}); 63 | const cValue = computed(() => value.foo); 64 | let dummy; 65 | effect(() => { 66 | dummy = cValue.value; 67 | }); 68 | expect(dummy).toBe(undefined); 69 | value.foo = 1; 70 | expect(dummy).toBe(1); 71 | }); 72 | 73 | // it("should work when chained", () => { 74 | // const value = reactive({ foo: 0 }); 75 | // const c1 = computed(() => value.foo); 76 | // const c2 = computed(() => c1.value + 1); 77 | // expect(c2.value).toBe(1); 78 | // expect(c1.value).toBe(0); 79 | // value.foo++; 80 | // expect(c2.value).toBe(2); 81 | // expect(c1.value).toBe(1); 82 | // }); 83 | 84 | // it("should trigger effect when chained", () => { 85 | // const value = reactive({ foo: 0 }); 86 | // const getter1 = jest.fn(() => value.foo); 87 | // const getter2 = jest.fn(() => { 88 | // return c1.value + 1; 89 | // }); 90 | // const c1 = computed(getter1); 91 | // const c2 = computed(getter2); 92 | 93 | // let dummy; 94 | // effect(() => { 95 | // dummy = c2.value; 96 | // }); 97 | // expect(dummy).toBe(1); 98 | // expect(getter1).toHaveBeenCalledTimes(1); 99 | // expect(getter2).toHaveBeenCalledTimes(1); 100 | // value.foo++; 101 | // expect(dummy).toBe(2); 102 | // // should not result in duplicate calls 103 | // expect(getter1).toHaveBeenCalledTimes(2); 104 | // expect(getter2).toHaveBeenCalledTimes(2); 105 | // }); 106 | 107 | // it("should trigger effect when chained (mixed invocations)", () => { 108 | // const value = reactive({ foo: 0 }); 109 | // const getter1 = jest.fn(() => value.foo); 110 | // const getter2 = jest.fn(() => { 111 | // return c1.value + 1; 112 | // }); 113 | // const c1 = computed(getter1); 114 | // const c2 = computed(getter2); 115 | 116 | // let dummy; 117 | // effect(() => { 118 | // dummy = c1.value + c2.value; 119 | // }); 120 | // expect(dummy).toBe(1); 121 | 122 | // expect(getter1).toHaveBeenCalledTimes(1); 123 | // expect(getter2).toHaveBeenCalledTimes(1); 124 | // value.foo++; 125 | // expect(dummy).toBe(3); 126 | // // should not result in duplicate calls 127 | // expect(getter1).toHaveBeenCalledTimes(2); 128 | // expect(getter2).toHaveBeenCalledTimes(2); 129 | // }); 130 | 131 | // it("should no longer update when stopped", () => { 132 | // const value = reactive<{ foo?: number }>({}); 133 | // const cValue = computed(() => value.foo); 134 | // let dummy; 135 | // effect(() => { 136 | // dummy = cValue.value; 137 | // }); 138 | // expect(dummy).toBe(undefined); 139 | // value.foo = 1; 140 | // expect(dummy).toBe(1); 141 | // cValue.effect.stop(); 142 | // value.foo = 2; 143 | // expect(dummy).toBe(1); 144 | // }); 145 | 146 | // it("should support setter", () => { 147 | // const n = ref(1); 148 | // const plusOne = computed({ 149 | // get: () => n.value + 1, 150 | // set: (val) => { 151 | // n.value = val - 1; 152 | // }, 153 | // }); 154 | 155 | // expect(plusOne.value).toBe(2); 156 | // n.value++; 157 | // expect(plusOne.value).toBe(3); 158 | 159 | // plusOne.value = 0; 160 | // expect(n.value).toBe(-1); 161 | // }); 162 | 163 | // it("should trigger effect w/ setter", () => { 164 | // const n = ref(1); 165 | // const plusOne = computed({ 166 | // get: () => n.value + 1, 167 | // set: (val) => { 168 | // n.value = val - 1; 169 | // }, 170 | // }); 171 | 172 | // let dummy; 173 | // effect(() => { 174 | // dummy = n.value; 175 | // }); 176 | // expect(dummy).toBe(1); 177 | 178 | // plusOne.value = 0; 179 | // expect(dummy).toBe(-1); 180 | // }); 181 | 182 | // it("should warn if trying to set a readonly computed", () => { 183 | // const n = ref(1); 184 | // const plusOne = computed(() => n.value + 1); 185 | // (plusOne as WritableComputedRef).value++; // Type cast to prevent TS from preventing the error 186 | 187 | // expect( 188 | // "Write operation failed: computed value is readonly" 189 | // ).toHaveBeenWarnedLast(); 190 | // }); 191 | 192 | // it("should be readonly", () => { 193 | // let a = { a: 1 }; 194 | // const x = computed(() => a); 195 | // expect(isReadonly(x)).toBe(true); 196 | // expect(isReadonly(x.value)).toBe(false); 197 | // expect(isReadonly(x.value.a)).toBe(false); 198 | // const z = computed({ 199 | // get() { 200 | // return a; 201 | // }, 202 | // set(v) { 203 | // a = v; 204 | // }, 205 | // }); 206 | // expect(isReadonly(z)).toBe(false); 207 | // expect(isReadonly(z.value.a)).toBe(false); 208 | // }); 209 | 210 | // it("should expose value when stopped", () => { 211 | // const x = computed(() => 1); 212 | // x.effect.stop(); 213 | // expect(x.value).toBe(1); 214 | // }); 215 | 216 | // it("debug: onTrack", () => { 217 | // let events: DebuggerEvent[] = []; 218 | // const onTrack = jest.fn((e: DebuggerEvent) => { 219 | // events.push(e); 220 | // }); 221 | // const obj = reactive({ foo: 1, bar: 2 }); 222 | // const c = computed(() => (obj.foo, "bar" in obj, Object.keys(obj)), { 223 | // onTrack, 224 | // }); 225 | // expect(c.value).toEqual(["foo", "bar"]); 226 | // expect(onTrack).toHaveBeenCalledTimes(3); 227 | // expect(events).toEqual([ 228 | // { 229 | // effect: c.effect, 230 | // target: toRaw(obj), 231 | // type: TrackOpTypes.GET, 232 | // key: "foo", 233 | // }, 234 | // { 235 | // effect: c.effect, 236 | // target: toRaw(obj), 237 | // type: TrackOpTypes.HAS, 238 | // key: "bar", 239 | // }, 240 | // { 241 | // effect: c.effect, 242 | // target: toRaw(obj), 243 | // type: TrackOpTypes.ITERATE, 244 | // key: ITERATE_KEY, 245 | // }, 246 | // ]); 247 | // }); 248 | 249 | // it("debug: onTrigger", () => { 250 | // let events: DebuggerEvent[] = []; 251 | // const onTrigger = jest.fn((e: DebuggerEvent) => { 252 | // events.push(e); 253 | // }); 254 | // const obj = reactive({ foo: 1 }); 255 | // const c = computed(() => obj.foo, { onTrigger }); 256 | 257 | // // computed won't trigger compute until accessed 258 | // c.value; 259 | 260 | // obj.foo++; 261 | // expect(c.value).toBe(2); 262 | // expect(onTrigger).toHaveBeenCalledTimes(1); 263 | // expect(events[0]).toEqual({ 264 | // effect: c.effect, 265 | // target: toRaw(obj), 266 | // type: TriggerOpTypes.SET, 267 | // key: "foo", 268 | // oldValue: 1, 269 | // newValue: 2, 270 | // }); 271 | 272 | // // @ts-ignore 273 | // delete obj.foo; 274 | // expect(c.value).toBeUndefined(); 275 | // expect(onTrigger).toHaveBeenCalledTimes(2); 276 | // expect(events[1]).toEqual({ 277 | // effect: c.effect, 278 | // target: toRaw(obj), 279 | // type: TriggerOpTypes.DELETE, 280 | // key: "foo", 281 | // oldValue: 2, 282 | // }); 283 | // }); 284 | }); 285 | -------------------------------------------------------------------------------- /src/reactivity/tests/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../src/reactive"; 2 | import { effect, stop } from "../src/effective"; 3 | 4 | describe("reactivity/effect", () => { 5 | it("should run the passed function once (wrapped by a effect)", () => { 6 | const fnSpy = jest.fn(() => {}); 7 | effect(fnSpy); 8 | expect(fnSpy).toHaveBeenCalledTimes(1); 9 | }); 10 | it("should observe basic properties", () => { 11 | let dummy; 12 | const counter = reactive({ num: 0 }); 13 | effect(() => (dummy = counter.num)); 14 | expect(dummy).toBe(0); 15 | counter.num = 7; 16 | expect(dummy).toBe(7); 17 | }); 18 | 19 | it("should return runner when call effect", () => { 20 | let dummy = 10; 21 | const runner = effect(() => { 22 | dummy++; 23 | return "foo"; 24 | }); 25 | expect(dummy).toBe(11); 26 | const r = runner(); 27 | expect(dummy).toBe(12); 28 | expect(r).toBe("foo"); 29 | }); 30 | 31 | it("should observe multiple properties", () => { 32 | let dummy; 33 | const counter = reactive({ num1: 0, num2: 0 }); 34 | effect(() => (dummy = counter.num1 + counter.num1 + counter.num2)); 35 | 36 | expect(dummy).toBe(0); 37 | counter.num1 = counter.num2 = 7; 38 | expect(dummy).toBe(21); 39 | }); 40 | 41 | it("should handle multiple effects", () => { 42 | let dummy1, dummy2; 43 | const counter = reactive({ num: 0 }); 44 | effect(() => (dummy1 = counter.num)); 45 | effect(() => (dummy2 = counter.num)); 46 | 47 | expect(dummy1).toBe(0); 48 | expect(dummy2).toBe(0); 49 | counter.num++; 50 | expect(dummy1).toBe(1); 51 | expect(dummy2).toBe(1); 52 | }); 53 | 54 | it("should observe nested properties", () => { 55 | let dummy; 56 | const counter = reactive({ nested: { num: 0 } }); 57 | effect(() => (dummy = counter.nested.num)); 58 | 59 | expect(dummy).toBe(0); 60 | counter.nested.num = 8; 61 | expect(dummy).toBe(8); 62 | }); 63 | 64 | it("should observe delete operations", () => { 65 | let dummy; 66 | const obj = reactive({ prop: "value" }); 67 | effect(() => (dummy = obj.prop)); 68 | 69 | expect(dummy).toBe("value"); 70 | // @ts-ignore 71 | delete obj.prop; 72 | expect(dummy).toBe(undefined); 73 | }); 74 | 75 | it("should observe has operations", () => { 76 | let dummy; 77 | const obj = reactive({ prop: "value" }); 78 | effect(() => (dummy = "prop" in obj)); 79 | 80 | expect(dummy).toBe(true); 81 | // @ts-ignore 82 | delete obj.prop; 83 | expect(dummy).toBe(false); 84 | obj.prop = 12; 85 | expect(dummy).toBe(true); 86 | }); 87 | 88 | it("should observe properties on the prototype chain", () => { 89 | let dummy; 90 | const counter = reactive({ num: 0 }); 91 | const parentCounter = reactive({ num: 2 }); 92 | Object.setPrototypeOf(counter, parentCounter); 93 | effect(() => (dummy = counter.num)); 94 | 95 | expect(dummy).toBe(0); 96 | // @ts-ignore 97 | delete counter.num; 98 | expect(dummy).toBe(2); 99 | parentCounter.num = 4; 100 | expect(dummy).toBe(4); 101 | counter.num = 3; 102 | expect(dummy).toBe(3); 103 | }); 104 | 105 | it("should observe has operations on the prototype chain", () => { 106 | let dummy; 107 | const counter = reactive({ num: 0 }); 108 | const parentCounter = reactive({ num: 2 }); 109 | Object.setPrototypeOf(counter, parentCounter); 110 | effect(() => (dummy = "num" in counter)); 111 | 112 | expect(dummy).toBe(true); 113 | // @ts-ignore 114 | delete counter.num; 115 | expect(dummy).toBe(true); 116 | // @ts-ignore 117 | delete parentCounter.num; 118 | expect(dummy).toBe(false); 119 | counter.num = 3; 120 | expect(dummy).toBe(true); 121 | }); 122 | 123 | // obj.prop = 4; 会导致副作用函数执行两次 124 | // effect(() => (dummy = obj.prop)); 125 | // 1. 读取 obj 的 prop 属性,此时会执行 obj 的 get 方法,此时obj.prop与副作用函数有了响应式联系 126 | // 2. 当 obj 中 发现找不到 prop 属性,会去它的原型上找,此时触发 parent 的 get 方法,此时 parent.prop与副作用函数有了响应式联系 127 | // 3. 当我们 obj.prop = 4; 设置的时候,首先会去触发 obj 的 set 方法,然后触发依赖的副作用函数执行 128 | // 4. 由于 set 的时候发现找不到 prop 属性,则去它的原型上 set,即触发 parent 的 set 方法,然后触发依赖的副作用函数执行 129 | // 5. 所以一次设置会导致副作用函数重复的执行两次 130 | 131 | // 优化 132 | // 通过 reveiver 来判断当前 set 触发 是自己的行为还是他人的行为 133 | // 这里 obj get 的时候,target 是 {}, receiver 是 obj 134 | // 通过 obj get 触发从而间接触发 parent get 的时候,target 是 proto,reveiver 是 obj 135 | it("should observe inherited property accessors", () => { 136 | let dummy, parentDummy, hiddenValue: any; 137 | const obj = reactive({}); 138 | const parent = reactive({ 139 | set prop(value) { 140 | hiddenValue = value; 141 | }, 142 | get prop() { 143 | return hiddenValue; 144 | }, 145 | }); 146 | Object.setPrototypeOf(obj, parent); 147 | effect(() => (dummy = obj.prop)); 148 | effect(() => (parentDummy = parent.prop)); 149 | 150 | expect(dummy).toBe(undefined); 151 | expect(parentDummy).toBe(undefined); 152 | obj.prop = 4; 153 | expect(dummy).toBe(4); 154 | // this doesn't work, should it? 155 | // expect(parentDummy).toBe(4) 156 | parent.prop = 2; 157 | expect(dummy).toBe(2); 158 | expect(parentDummy).toBe(2); 159 | }); 160 | 161 | it("should observe function call chains", () => { 162 | let dummy; 163 | const counter = reactive({ num: 0 }); 164 | effect(() => (dummy = getNum())); 165 | 166 | function getNum() { 167 | return counter.num; 168 | } 169 | 170 | expect(dummy).toBe(0); 171 | counter.num = 2; 172 | expect(dummy).toBe(2); 173 | }); 174 | 175 | // 对数组的读取操作 176 | // 1. arr[0] 177 | // 2. arr.length 178 | // 3. arr 当作对象,使用 for ... in 循环遍历 179 | // 4. for ... of遍历数组 180 | // 5. 数组的原型方法 concat/join/every/some/find/findIndex/includes以及其他不改变原数组的原型方法 181 | 182 | // 对数组的设置操作 183 | // 1. arr[1] = 3 184 | // 2. arr.length = 0 185 | // 3. 栈方法push/pop/shift/unshift 186 | // 4. 修改原数组的原型方法 splice/fill/sort 187 | it("should observe iteration", () => { 188 | let dummy; 189 | const list = reactive(["Hello"]); 190 | effect(() => (dummy = list.join(" "))); 191 | 192 | expect(dummy).toBe("Hello"); 193 | list.push("World!"); 194 | expect(dummy).toBe("Hello World!"); 195 | list.shift(); 196 | expect(dummy).toBe("World!"); 197 | }); 198 | 199 | it("should observe implicit array length changes", () => { 200 | let dummy; 201 | const list = reactive(["Hello"]); 202 | effect(() => (dummy = list.join(" "))); 203 | 204 | expect(dummy).toBe("Hello"); 205 | list[1] = "World!"; 206 | expect(dummy).toBe("Hello World!"); 207 | list[3] = "Hello!"; 208 | expect(dummy).toBe("Hello World! Hello!"); 209 | }); 210 | 211 | // 由于 reactive 具有深响应的特性 212 | // get arr[0] 的时候,发现 arr[0] 是 object,则会继续调用 reactive 进行包装 213 | // arr.includes 内部也会调用 get arr[0],这样又会调用 reactive 进行包装 214 | // 两次 reactive 返回的对象不一致,所以我们考虑维护一个 reactiveMap,用来维护原始对象 raw 和 proxy 对象的map 215 | it("avoid create reactive multiply", () => { 216 | const obj = {}; 217 | const arr = reactive([obj]); 218 | expect(arr.includes(arr[0])).toBe(true); 219 | // 我们希望这也是 true,所以我们需要拦截 includes 方法,当在响应式对象中没找到符合的 220 | // 则去 raw 上找是否符合 221 | expect(arr.includes(obj)).toBe(true); 222 | }); 223 | 224 | it("should observe sparse array mutations", () => { 225 | let dummy; 226 | const list = reactive([]); 227 | list[1] = "World!"; 228 | effect(() => (dummy = list.join(" "))); 229 | 230 | expect(dummy).toBe(" World!"); 231 | list[0] = "Hello"; 232 | expect(dummy).toBe("Hello World!"); 233 | list.pop(); 234 | expect(dummy).toBe("Hello"); 235 | }); 236 | 237 | it("should observe enumeration", () => { 238 | let dummy = 0; 239 | const numbers = reactive({ num1: 3 }); 240 | effect(() => { 241 | dummy = 0; 242 | for (let key in numbers) { 243 | dummy += numbers[key]; 244 | } 245 | }); 246 | 247 | expect(dummy).toBe(3); 248 | numbers.num2 = 4; 249 | expect(dummy).toBe(7); 250 | delete numbers.num1; 251 | expect(dummy).toBe(4); 252 | }); 253 | 254 | it("should observe symbol keyed properties", () => { 255 | const key = Symbol("symbol keyed prop"); 256 | let dummy, hasDummy; 257 | const obj = reactive({ [key]: "value" }); 258 | effect(() => (dummy = obj[key])); 259 | effect(() => (hasDummy = key in obj)); 260 | 261 | expect(dummy).toBe("value"); 262 | expect(hasDummy).toBe(true); 263 | obj[key] = "newValue"; 264 | expect(dummy).toBe("newValue"); 265 | // @ts-ignore 266 | delete obj[key]; 267 | expect(dummy).toBe(undefined); 268 | expect(hasDummy).toBe(false); 269 | }); 270 | 271 | it("should not observe well-known symbol keyed properties", () => { 272 | const key = Symbol.isConcatSpreadable; 273 | let dummy; 274 | const array: any = reactive([]); 275 | effect(() => (dummy = array[key])); 276 | 277 | expect(array[key]).toBe(undefined); 278 | expect(dummy).toBe(undefined); 279 | array[key] = true; 280 | expect(array[key]).toBe(true); 281 | expect(dummy).toBe(undefined); 282 | }); 283 | 284 | it("should observe function valued properties", () => { 285 | const oldFunc = () => {}; 286 | const newFunc = () => {}; 287 | 288 | let dummy; 289 | const obj = reactive({ func: oldFunc }); 290 | effect(() => (dummy = obj.func)); 291 | 292 | expect(dummy).toBe(oldFunc); 293 | obj.func = newFunc; 294 | expect(dummy).toBe(newFunc); 295 | }); 296 | 297 | // it("should observe chained getters relying on this", () => { 298 | // const obj = reactive({ 299 | // a: 1, 300 | // get b() { 301 | // return this.a; 302 | // }, 303 | // }); 304 | 305 | // let dummy; 306 | // effect(() => (dummy = obj.b)); 307 | // expect(dummy).toBe(1); 308 | // obj.a++; 309 | // expect(dummy).toBe(2); 310 | // }); 311 | 312 | // it("should observe methods relying on this", () => { 313 | // const obj = reactive({ 314 | // a: 1, 315 | // b() { 316 | // return this.a; 317 | // }, 318 | // }); 319 | 320 | // let dummy; 321 | // effect(() => (dummy = obj.b())); 322 | // expect(dummy).toBe(1); 323 | // obj.a++; 324 | // expect(dummy).toBe(2); 325 | // }); 326 | 327 | // it("should not observe set operations without a value change", () => { 328 | // let hasDummy, getDummy; 329 | // const obj = reactive({ prop: "value" }); 330 | 331 | // const getSpy = jest.fn(() => (getDummy = obj.prop)); 332 | // const hasSpy = jest.fn(() => (hasDummy = "prop" in obj)); 333 | // effect(getSpy); 334 | // effect(hasSpy); 335 | 336 | // expect(getDummy).toBe("value"); 337 | // expect(hasDummy).toBe(true); 338 | // obj.prop = "value"; 339 | // expect(getSpy).toHaveBeenCalledTimes(1); 340 | // expect(hasSpy).toHaveBeenCalledTimes(1); 341 | // expect(getDummy).toBe("value"); 342 | // expect(hasDummy).toBe(true); 343 | // }); 344 | 345 | // it("should not observe raw mutations", () => { 346 | // let dummy; 347 | // const obj = reactive<{ prop?: string }>({}); 348 | // effect(() => (dummy = toRaw(obj).prop)); 349 | 350 | // expect(dummy).toBe(undefined); 351 | // obj.prop = "value"; 352 | // expect(dummy).toBe(undefined); 353 | // }); 354 | 355 | // it("should not be triggered by raw mutations", () => { 356 | // let dummy; 357 | // const obj = reactive<{ prop?: string }>({}); 358 | // effect(() => (dummy = obj.prop)); 359 | 360 | // expect(dummy).toBe(undefined); 361 | // toRaw(obj).prop = "value"; 362 | // expect(dummy).toBe(undefined); 363 | // }); 364 | 365 | // it("should not be triggered by inherited raw setters", () => { 366 | // let dummy, parentDummy, hiddenValue: any; 367 | // const obj = reactive<{ prop?: number }>({}); 368 | // const parent = reactive({ 369 | // set prop(value) { 370 | // hiddenValue = value; 371 | // }, 372 | // get prop() { 373 | // return hiddenValue; 374 | // }, 375 | // }); 376 | // Object.setPrototypeOf(obj, parent); 377 | // effect(() => (dummy = obj.prop)); 378 | // effect(() => (parentDummy = parent.prop)); 379 | 380 | // expect(dummy).toBe(undefined); 381 | // expect(parentDummy).toBe(undefined); 382 | // toRaw(obj).prop = 4; 383 | // expect(dummy).toBe(undefined); 384 | // expect(parentDummy).toBe(undefined); 385 | // }); 386 | 387 | // it("should avoid implicit infinite recursive loops with itself", () => { 388 | // const counter = reactive({ num: 0 }); 389 | 390 | // const counterSpy = jest.fn(() => counter.num++); 391 | // effect(counterSpy); 392 | // expect(counter.num).toBe(1); 393 | // expect(counterSpy).toHaveBeenCalledTimes(1); 394 | // counter.num = 4; 395 | // expect(counter.num).toBe(5); 396 | // expect(counterSpy).toHaveBeenCalledTimes(2); 397 | // }); 398 | 399 | // effect 里面嵌套了诸如 push 等栈操作 400 | // push 既有读取 length 的操作,也有设置 length 的操作 401 | // 所以当有两个副作用函数在一起 402 | // 第一个 副作用函数执行完毕后,会与 length 属性建立响应式联系 403 | // 当第二个副作用函数执行 push,导致 length 变化,从而第一个副作用函数也执行 404 | // 第一个执行,length 变化,又会导致第二个副作用函数执行 405 | // 。。。 406 | 407 | // 考虑 push 的时候停止 track,因为 push 的本意是修改数组,而不是读取操作,push 的时候加上 shouldTrack 状态 408 | it("should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift", () => { 409 | (["push", "unshift"] as const).forEach((key) => { 410 | const arr = reactive([]); 411 | const counterSpy1 = jest.fn(() => (arr[key] as any)(1)); 412 | const counterSpy2 = jest.fn(() => (arr[key] as any)(2)); 413 | effect(counterSpy1); 414 | effect(counterSpy2); 415 | expect(arr.length).toBe(2); 416 | expect(counterSpy1).toHaveBeenCalledTimes(1); 417 | expect(counterSpy2).toHaveBeenCalledTimes(1); 418 | }); 419 | (["pop", "shift"] as const).forEach((key) => { 420 | const arr = reactive([1, 2, 3, 4]); 421 | const counterSpy1 = jest.fn(() => (arr[key] as any)()); 422 | const counterSpy2 = jest.fn(() => (arr[key] as any)()); 423 | effect(counterSpy1); 424 | effect(counterSpy2); 425 | expect(arr.length).toBe(2); 426 | expect(counterSpy1).toHaveBeenCalledTimes(1); 427 | expect(counterSpy2).toHaveBeenCalledTimes(1); 428 | }); 429 | }); 430 | 431 | // it("should allow explicitly recursive raw function loops", () => { 432 | // const counter = reactive({ num: 0 }); 433 | // const numSpy = jest.fn(() => { 434 | // counter.num++; 435 | // if (counter.num < 10) { 436 | // numSpy(); 437 | // } 438 | // }); 439 | // effect(numSpy); 440 | // expect(counter.num).toEqual(10); 441 | // expect(numSpy).toHaveBeenCalledTimes(10); 442 | // }); 443 | 444 | // it("should avoid infinite loops with other effects", () => { 445 | // const nums = reactive({ num1: 0, num2: 1 }); 446 | 447 | // const spy1 = jest.fn(() => (nums.num1 = nums.num2)); 448 | // const spy2 = jest.fn(() => (nums.num2 = nums.num1)); 449 | // effect(spy1); 450 | // effect(spy2); 451 | // expect(nums.num1).toBe(1); 452 | // expect(nums.num2).toBe(1); 453 | // expect(spy1).toHaveBeenCalledTimes(1); 454 | // expect(spy2).toHaveBeenCalledTimes(1); 455 | // nums.num2 = 4; 456 | // expect(nums.num1).toBe(4); 457 | // expect(nums.num2).toBe(4); 458 | // expect(spy1).toHaveBeenCalledTimes(2); 459 | // expect(spy2).toHaveBeenCalledTimes(2); 460 | // nums.num1 = 10; 461 | // expect(nums.num1).toBe(10); 462 | // expect(nums.num2).toBe(10); 463 | // expect(spy1).toHaveBeenCalledTimes(3); 464 | // expect(spy2).toHaveBeenCalledTimes(3); 465 | // }); 466 | 467 | // it("should return a new reactive version of the function", () => { 468 | // function greet() { 469 | // return "Hello World"; 470 | // } 471 | // const effect1 = effect(greet); 472 | // const effect2 = effect(greet); 473 | // expect(typeof effect1).toBe("function"); 474 | // expect(typeof effect2).toBe("function"); 475 | // expect(effect1).not.toBe(greet); 476 | // expect(effect1).not.toBe(effect2); 477 | // }); 478 | 479 | // it("should discover new branches while running automatically", () => { 480 | // let dummy; 481 | // const obj = reactive({ prop: "value", run: false }); 482 | 483 | // const conditionalSpy = jest.fn(() => { 484 | // dummy = obj.run ? obj.prop : "other"; 485 | // }); 486 | // effect(conditionalSpy); 487 | 488 | // expect(dummy).toBe("other"); 489 | // expect(conditionalSpy).toHaveBeenCalledTimes(1); 490 | // obj.prop = "Hi"; 491 | // expect(dummy).toBe("other"); 492 | // expect(conditionalSpy).toHaveBeenCalledTimes(1); 493 | // obj.run = true; 494 | // expect(dummy).toBe("Hi"); 495 | // expect(conditionalSpy).toHaveBeenCalledTimes(2); 496 | // obj.prop = "World"; 497 | // expect(dummy).toBe("World"); 498 | // expect(conditionalSpy).toHaveBeenCalledTimes(3); 499 | // }); 500 | 501 | // it("should discover new branches when running manually", () => { 502 | // let dummy; 503 | // let run = false; 504 | // const obj = reactive({ prop: "value" }); 505 | // const runner = effect(() => { 506 | // dummy = run ? obj.prop : "other"; 507 | // }); 508 | 509 | // expect(dummy).toBe("other"); 510 | // runner(); 511 | // expect(dummy).toBe("other"); 512 | // run = true; 513 | // runner(); 514 | // expect(dummy).toBe("value"); 515 | // obj.prop = "World"; 516 | // expect(dummy).toBe("World"); 517 | // }); 518 | 519 | // 分支切换与cleanup 520 | it("should not be triggered by mutating a property, which is used in an inactive branch", () => { 521 | let dummy; 522 | const obj = reactive({ prop: "value", run: true }); 523 | 524 | const conditionalSpy = jest.fn(() => { 525 | dummy = obj.run ? obj.prop : "other"; 526 | }); 527 | effect(conditionalSpy); 528 | 529 | expect(dummy).toBe("value"); 530 | expect(conditionalSpy).toHaveBeenCalledTimes(1); 531 | // 此时 run 为 false,应该不再依赖 obj.prop,也就是后续 obj.prop 变动,effect也不会执行 532 | obj.run = false; 533 | expect(dummy).toBe("other"); 534 | expect(conditionalSpy).toHaveBeenCalledTimes(2); 535 | obj.prop = "value2"; 536 | expect(dummy).toBe("other"); 537 | expect(conditionalSpy).toHaveBeenCalledTimes(2); 538 | }); 539 | 540 | // obj.foo++ 拆分成 obj.foo = obj.foo + 1 541 | // 1. 首先读取 obj.foo,会触发 track 操作,将当前的 effect 收集起来 542 | // 2. 接着将 +1 后的 foo 赋值 给 obj.foo,此时会触发 trigger 操作,即把之前收集的 effect 拿出来执行 543 | // 3. 但其实上一个 effect 还在执行中,然后又把自己拿出来执行,就会导致无限递归的调用自己,造成栈溢出 544 | // 我们发现对 obj.foo 的读取和设置都在同一个 effect 中执行 545 | // 所以无论是 track 时候要收集的 effect 还是 trigger 时候要触发的 effect,都是 activeEffect 546 | // 基于此我们可以在 trigger 的时候增加 守卫条件 547 | it("should avoid infinite recursion", () => { 548 | let obj = reactive({ 549 | foo: 1, 550 | }); 551 | effect(() => { 552 | obj.foo++; 553 | }); 554 | expect(obj.foo).toBe(2); 555 | }); 556 | 557 | // 允许嵌套 effect 558 | // 应用场景: 559 | // vue 的渲染函数其实就是在一个 effect 中执行的,Bar是Foo的子组件 560 | // effect(() => { 561 | // Foo.render(); 562 | // effect(() => { 563 | // Bar.render(); 564 | // }) 565 | // }) 566 | it("should allow nested effects", () => { 567 | const nums = reactive({ num1: 0, num2: 1, num3: 2 }); 568 | const dummy: any = {}; 569 | 570 | const childSpy = jest.fn(() => (dummy.num1 = nums.num1)); 571 | const childeffect = effect(childSpy); 572 | const parentSpy = jest.fn(() => { 573 | dummy.num2 = nums.num2; 574 | childeffect(); 575 | dummy.num3 = nums.num3; 576 | }); 577 | effect(parentSpy); 578 | 579 | expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 }); 580 | expect(parentSpy).toHaveBeenCalledTimes(1); 581 | expect(childSpy).toHaveBeenCalledTimes(2); 582 | // this should only call the childeffect 583 | nums.num1 = 4; 584 | expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 }); 585 | expect(parentSpy).toHaveBeenCalledTimes(1); 586 | expect(childSpy).toHaveBeenCalledTimes(3); 587 | // this calls the parenteffect, which calls the childeffect once 588 | nums.num2 = 10; 589 | expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 }); 590 | expect(parentSpy).toHaveBeenCalledTimes(2); 591 | expect(childSpy).toHaveBeenCalledTimes(4); 592 | // this calls the parenteffect, which calls the childeffect once 593 | nums.num3 = 7; 594 | expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 }); 595 | expect(parentSpy).toHaveBeenCalledTimes(3); 596 | expect(childSpy).toHaveBeenCalledTimes(5); 597 | }); 598 | 599 | // 懒执行 effect 600 | it("lazy", () => { 601 | const obj = reactive({ foo: 1 }); 602 | let dummy; 603 | const runner = effect(() => (dummy = obj.foo), { lazy: true }); 604 | expect(dummy).toBe(undefined); 605 | 606 | expect(runner()).toBe(1); 607 | expect(dummy).toBe(1); 608 | obj.foo = 2; 609 | expect(dummy).toBe(2); 610 | }); 611 | 612 | // 可调度 613 | // 当 trigger 动作触发 effect 重新执行时,有能力决定 effect 执行的时机、次数以及方式 614 | it("scheduler", () => { 615 | // 通过 effect 的第二个参数给定 scheduler 616 | // 当 effect 第一次执行的时候还会执行 fn 617 | // 当 set 的时候,不会执行 fn,而是执行 scheduler 618 | // 如果执行 runner 的时候,会再次执行 fn 619 | let dummy; 620 | let run: any; 621 | const scheduler = jest.fn(() => { 622 | run = runner; 623 | }); 624 | const obj = reactive({ foo: 1 }); 625 | const runner = effect( 626 | () => { 627 | dummy = obj.foo; 628 | }, 629 | { scheduler } 630 | ); 631 | expect(scheduler).not.toHaveBeenCalled(); 632 | expect(dummy).toBe(1); 633 | // should be called on first trigger 634 | obj.foo++; 635 | expect(scheduler).toHaveBeenCalledTimes(1); 636 | // should not run yet 637 | expect(dummy).toBe(1); 638 | // manually run 639 | run(); 640 | // should have run 641 | expect(dummy).toBe(2); 642 | }); 643 | 644 | it("stop", () => { 645 | // 通过 stop(runner) 以后,通过 set 触发 fn 执行就不存在了 646 | // 但是仍然可以手动执行 runner 647 | // 所以我们考虑将当调用stop的时候将 effect 从 deps 中删除,这样我们 set 操作以后,就不会执行 effect 648 | // 之前是用 depsMap 记录了 ReactiveEffect,那么如何通过 ReactiveEffect 找到 deps 649 | // 通过双向记录的方式 650 | let dummy; 651 | const obj = reactive({ prop: 1 }); 652 | const runner = effect(() => { 653 | dummy = obj.prop; 654 | }); 655 | obj.prop = 2; 656 | expect(dummy).toBe(2); 657 | stop(runner); 658 | // obj.prop = 3; 659 | // obj.prop = obj.prop + 1; 660 | // 相比于 obj.prop = 3,++涉及到了get操作,这时候又会去收集依赖,之前 661 | // stop 的时候清除的依赖白做了,考虑给全局加个 shouldTrack 参数用来判断 662 | obj.prop++; 663 | expect(dummy).toBe(2); 664 | 665 | // stopped effect should still be manually callable 666 | runner(); 667 | expect(dummy).toBe(3); 668 | }); 669 | 670 | it("events: onStop", () => { 671 | const onStop = jest.fn(); 672 | const runner = effect(() => {}, { 673 | onStop, 674 | }); 675 | 676 | stop(runner); 677 | expect(onStop).toHaveBeenCalled(); 678 | }); 679 | }); 680 | -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive } from "../src/reactive" 2 | 3 | describe('reactivity/reactive', () => { 4 | test("Object", () => { 5 | const original = { foo: 1 }; 6 | const observed = reactive(original); 7 | expect(observed).not.toBe(original); 8 | expect(isReactive(observed)).toBe(true); 9 | expect(isReactive(original)).toBe(false); 10 | // get 11 | expect(observed.foo).toBe(1); 12 | // has 13 | expect("foo" in observed).toBe(true); 14 | // ownKeys 15 | expect(Object.keys(observed)).toEqual(["foo"]); 16 | }); 17 | 18 | test("proto", () => { 19 | const obj = {}; 20 | const reactiveObj = reactive(obj); 21 | expect(isReactive(reactiveObj)).toBe(true); 22 | // read prop of reactiveObject will cause reactiveObj[prop] to be reactive 23 | // @ts-ignore 24 | const prototype = reactiveObj["__proto__"]; 25 | const otherObj = { data: ["a"] }; 26 | expect(isReactive(otherObj)).toBe(false); 27 | const reactiveOther = reactive(otherObj); 28 | expect(isReactive(reactiveOther)).toBe(true); 29 | expect(reactiveOther.data[0]).toBe("a"); 30 | }); 31 | 32 | test("nested reactives", () => { 33 | const original = { 34 | nested: { 35 | foo: 1, 36 | }, 37 | array: [{ bar: 2 }], 38 | }; 39 | const observed = reactive(original); 40 | expect(isReactive(observed.nested)).toBe(true); 41 | expect(isReactive(observed.array)).toBe(true); 42 | expect(isReactive(observed.array[0])).toBe(true); 43 | }); 44 | 45 | // test("observing subtypes of IterableCollections(Map, Set)", () => { 46 | // // subtypes of Map 47 | // class CustomMap extends Map {} 48 | // const cmap = reactive(new CustomMap()); 49 | 50 | // expect(cmap instanceof Map).toBe(true); 51 | // expect(isReactive(cmap)).toBe(true); 52 | 53 | // cmap.set("key", {}); 54 | // expect(isReactive(cmap.get("key"))).toBe(true); 55 | 56 | // // subtypes of Set 57 | // class CustomSet extends Set {} 58 | // const cset = reactive(new CustomSet()); 59 | 60 | // expect(cset instanceof Set).toBe(true); 61 | // expect(isReactive(cset)).toBe(true); 62 | 63 | // let dummy; 64 | // effect(() => (dummy = cset.has("value"))); 65 | // expect(dummy).toBe(false); 66 | // cset.add("value"); 67 | // expect(dummy).toBe(true); 68 | // cset.delete("value"); 69 | // expect(dummy).toBe(false); 70 | // }); 71 | 72 | // test("observing subtypes of WeakCollections(WeakMap, WeakSet)", () => { 73 | // // subtypes of WeakMap 74 | // class CustomMap extends WeakMap {} 75 | // const cmap = reactive(new CustomMap()); 76 | 77 | // expect(cmap instanceof WeakMap).toBe(true); 78 | // expect(isReactive(cmap)).toBe(true); 79 | 80 | // const key = {}; 81 | // cmap.set(key, {}); 82 | // expect(isReactive(cmap.get(key))).toBe(true); 83 | 84 | // // subtypes of WeakSet 85 | // class CustomSet extends WeakSet {} 86 | // const cset = reactive(new CustomSet()); 87 | 88 | // expect(cset instanceof WeakSet).toBe(true); 89 | // expect(isReactive(cset)).toBe(true); 90 | 91 | // let dummy; 92 | // effect(() => (dummy = cset.has(key))); 93 | // expect(dummy).toBe(false); 94 | // cset.add(key); 95 | // expect(dummy).toBe(true); 96 | // cset.delete(key); 97 | // expect(dummy).toBe(false); 98 | // }); 99 | 100 | // test("observed value should proxy mutations to original (Object)", () => { 101 | // const original: any = { foo: 1 }; 102 | // const observed = reactive(original); 103 | // // set 104 | // observed.bar = 1; 105 | // expect(observed.bar).toBe(1); 106 | // expect(original.bar).toBe(1); 107 | // // delete 108 | // delete observed.foo; 109 | // expect("foo" in observed).toBe(false); 110 | // expect("foo" in original).toBe(false); 111 | // }); 112 | 113 | // test("original value change should reflect in observed value (Object)", () => { 114 | // const original: any = { foo: 1 }; 115 | // const observed = reactive(original); 116 | // // set 117 | // original.bar = 1; 118 | // expect(original.bar).toBe(1); 119 | // expect(observed.bar).toBe(1); 120 | // // delete 121 | // delete original.foo; 122 | // expect("foo" in original).toBe(false); 123 | // expect("foo" in observed).toBe(false); 124 | // }); 125 | 126 | // test("setting a property with an unobserved value should wrap with reactive", () => { 127 | // const observed = reactive<{ foo?: object }>({}); 128 | // const raw = {}; 129 | // observed.foo = raw; 130 | // expect(observed.foo).not.toBe(raw); 131 | // expect(isReactive(observed.foo)).toBe(true); 132 | // }); 133 | 134 | // test("observing already observed value should return same Proxy", () => { 135 | // const original = { foo: 1 }; 136 | // const observed = reactive(original); 137 | // const observed2 = reactive(observed); 138 | // expect(observed2).toBe(observed); 139 | // }); 140 | 141 | // test("observing the same value multiple times should return same Proxy", () => { 142 | // const original = { foo: 1 }; 143 | // const observed = reactive(original); 144 | // const observed2 = reactive(original); 145 | // expect(observed2).toBe(observed); 146 | // }); 147 | 148 | // test("should not pollute original object with Proxies", () => { 149 | // const original: any = { foo: 1 }; 150 | // const original2 = { bar: 2 }; 151 | // const observed = reactive(original); 152 | // const observed2 = reactive(original2); 153 | // observed.bar = observed2; 154 | // expect(observed.bar).toBe(observed2); 155 | // expect(original.bar).toBe(original2); 156 | // }); 157 | 158 | // test("toRaw", () => { 159 | // const original = { foo: 1 }; 160 | // const observed = reactive(original); 161 | // expect(toRaw(observed)).toBe(original); 162 | // expect(toRaw(original)).toBe(original); 163 | // }); 164 | 165 | // test("toRaw on object using reactive as prototype", () => { 166 | // const original = reactive({}); 167 | // const obj = Object.create(original); 168 | // const raw = toRaw(obj); 169 | // expect(raw).toBe(obj); 170 | // expect(raw).not.toBe(toRaw(original)); 171 | // }); 172 | 173 | // test("should not unwrap Ref", () => { 174 | // const observedNumberRef = reactive(ref(1)); 175 | // const observedObjectRef = reactive(ref({ foo: 1 })); 176 | 177 | // expect(isRef(observedNumberRef)).toBe(true); 178 | // expect(isRef(observedObjectRef)).toBe(true); 179 | // }); 180 | 181 | // test("should unwrap computed refs", () => { 182 | // // readonly 183 | // const a = computed(() => 1); 184 | // // writable 185 | // const b = computed({ 186 | // get: () => 1, 187 | // set: () => {}, 188 | // }); 189 | // const obj = reactive({ a, b }); 190 | // // check type 191 | // obj.a + 1; 192 | // obj.b + 1; 193 | // expect(typeof obj.a).toBe(`number`); 194 | // expect(typeof obj.b).toBe(`number`); 195 | // }); 196 | 197 | // test("should allow setting property from a ref to another ref", () => { 198 | // const foo = ref(0); 199 | // const bar = ref(1); 200 | // const observed = reactive({ a: foo }); 201 | // const dummy = computed(() => observed.a); 202 | // expect(dummy.value).toBe(0); 203 | 204 | // // @ts-ignore 205 | // observed.a = bar; 206 | // expect(dummy.value).toBe(1); 207 | 208 | // bar.value++; 209 | // expect(dummy.value).toBe(2); 210 | // }); 211 | 212 | // test("non-observable values", () => { 213 | // const assertValue = (value: any) => { 214 | // reactive(value); 215 | // expect( 216 | // `value cannot be made reactive: ${String(value)}` 217 | // ).toHaveBeenWarnedLast(); 218 | // }; 219 | 220 | // // number 221 | // assertValue(1); 222 | // // string 223 | // assertValue("foo"); 224 | // // boolean 225 | // assertValue(false); 226 | // // null 227 | // assertValue(null); 228 | // // undefined 229 | // assertValue(undefined); 230 | // // symbol 231 | // const s = Symbol(); 232 | // assertValue(s); 233 | 234 | // // built-ins should work and return same value 235 | // const p = Promise.resolve(); 236 | // expect(reactive(p)).toBe(p); 237 | // const r = new RegExp(""); 238 | // expect(reactive(r)).toBe(r); 239 | // const d = new Date(); 240 | // expect(reactive(d)).toBe(d); 241 | // }); 242 | 243 | // test("markRaw", () => { 244 | // const obj = reactive({ 245 | // foo: { a: 1 }, 246 | // bar: markRaw({ b: 2 }), 247 | // }); 248 | // expect(isReactive(obj.foo)).toBe(true); 249 | // expect(isReactive(obj.bar)).toBe(false); 250 | // }); 251 | 252 | // test("should not observe non-extensible objects", () => { 253 | // const obj = reactive({ 254 | // foo: Object.preventExtensions({ a: 1 }), 255 | // // sealed or frozen objects are considered non-extensible as well 256 | // bar: Object.freeze({ a: 1 }), 257 | // baz: Object.seal({ a: 1 }), 258 | // }); 259 | // expect(isReactive(obj.foo)).toBe(false); 260 | // expect(isReactive(obj.bar)).toBe(false); 261 | // expect(isReactive(obj.baz)).toBe(false); 262 | // }); 263 | 264 | // test("should not observe objects with __v_skip", () => { 265 | // const original = { 266 | // foo: 1, 267 | // __v_skip: true, 268 | // }; 269 | // const observed = reactive(original); 270 | // expect(isReactive(observed)).toBe(false); 271 | // }); 272 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, isReadonly, readonly } from "../src/reactive"; 2 | 3 | describe('reactivity/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(isProxy(wrapped)).toBe(true); 9 | expect(isReactive(wrapped)).toBe(false); 10 | expect(isReadonly(wrapped)).toBe(true); 11 | expect(isReactive(original)).toBe(false); 12 | expect(isReadonly(original)).toBe(false); 13 | expect(isReactive(wrapped.bar)).toBe(false); 14 | expect(isReadonly(wrapped.bar)).toBe(true); 15 | expect(isReactive(original.bar)).toBe(false); 16 | expect(isReadonly(original.bar)).toBe(false); 17 | // get 18 | expect(wrapped.foo).toBe(1); 19 | // has 20 | expect("foo" in wrapped).toBe(true); 21 | // ownKeys 22 | expect(Object.keys(wrapped)).toEqual(["foo", "bar"]); 23 | }); 24 | 25 | it("warn then call set", () => { 26 | console.warn = jest.fn(); 27 | const original = { foo: 1, bar: { baz: 2 } }; 28 | const wrapped = readonly(original); 29 | wrapped.foo = 2; 30 | expect(console.warn).toBeCalled(); 31 | }) 32 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../src/effective"; 2 | import { reactive } from "../src/reactive"; 3 | import { customRef, isRef, proxyRefs, ref, shallowRef, toRef, toRefs, unref } from "../src/ref"; 4 | 5 | describe("reactivity/ref", () => { 6 | it("should hold a value", () => { 7 | const a = ref(1); 8 | expect(a.value).toBe(1); 9 | a.value = 2; 10 | expect(a.value).toBe(2); 11 | }); 12 | 13 | it("should be reactive", () => { 14 | const a = ref(1); 15 | let dummy; 16 | let calls = 0; 17 | effect(() => { 18 | calls++; 19 | dummy = a.value; 20 | }); 21 | expect(calls).toBe(1); 22 | expect(dummy).toBe(1); 23 | a.value = 2; 24 | expect(calls).toBe(2); 25 | expect(dummy).toBe(2); 26 | // same value should not trigger 27 | a.value = 2; 28 | expect(calls).toBe(2); 29 | }); 30 | 31 | it("should make nested properties reactive", () => { 32 | const a = ref({ 33 | count: 1, 34 | }); 35 | let dummy; 36 | effect(() => { 37 | dummy = a.value.count; 38 | }); 39 | expect(dummy).toBe(1); 40 | a.value.count = 2; 41 | expect(dummy).toBe(2); 42 | }); 43 | 44 | // setup() 返回的对象值有ref类型的,但是我们实际用的时候没有再 .value 45 | it("proxyRefs", () => { 46 | const user = { 47 | age: ref(10), 48 | name: "zhou", 49 | }; 50 | const proxyUser = proxyRefs(user); 51 | expect(user.age.value).toBe(10); 52 | expect(proxyUser.age).toBe(10); 53 | expect(proxyUser.name).toBe("zhou"); 54 | 55 | proxyUser.age = 20; 56 | expect(user.age.value).toBe(20); 57 | expect(proxyUser.age).toBe(20); 58 | 59 | proxyUser.age = ref(10); 60 | expect(user.age.value).toBe(10); 61 | expect(proxyUser.age).toBe(10); 62 | }); 63 | 64 | it("should work without initial value", () => { 65 | const a = ref(); 66 | let dummy; 67 | effect(() => { 68 | dummy = a.value; 69 | }); 70 | expect(dummy).toBe(undefined); 71 | a.value = 2; 72 | expect(dummy).toBe(2); 73 | }); 74 | 75 | // it("should work like a normal property when nested in a reactive object", () => { 76 | // const a = ref(1); 77 | // const obj = reactive({ 78 | // a, 79 | // b: { 80 | // c: a, 81 | // }, 82 | // }); 83 | 84 | // let dummy1: number; 85 | // let dummy2: number; 86 | 87 | // effect(() => { 88 | // dummy1 = obj.a; 89 | // dummy2 = obj.b.c; 90 | // }); 91 | 92 | // const assertDummiesEqualTo = (val: number) => 93 | // [dummy1, dummy2].forEach((dummy) => expect(dummy).toBe(val)); 94 | 95 | // assertDummiesEqualTo(1); 96 | // a.value++; 97 | // assertDummiesEqualTo(2); 98 | // obj.a++; 99 | // assertDummiesEqualTo(3); 100 | // obj.b.c++; 101 | // assertDummiesEqualTo(4); 102 | // }); 103 | 104 | // it("should unwrap nested ref in types", () => { 105 | // const a = ref(0); 106 | // const b = ref(a); 107 | 108 | // expect(typeof (b.value + 1)).toBe("number"); 109 | // }); 110 | 111 | // it("should unwrap nested values in types", () => { 112 | // const a = { 113 | // b: ref(0), 114 | // }; 115 | 116 | // const c = ref(a); 117 | 118 | // expect(typeof (c.value.b + 1)).toBe("number"); 119 | // }); 120 | 121 | // it("should NOT unwrap ref types nested inside arrays", () => { 122 | // const arr = ref([1, ref(3)]).value; 123 | // expect(isRef(arr[0])).toBe(false); 124 | // expect(isRef(arr[1])).toBe(true); 125 | // expect((arr[1] as Ref).value).toBe(3); 126 | // }); 127 | 128 | // it("should unwrap ref types as props of arrays", () => { 129 | // const arr = [ref(0)]; 130 | // const symbolKey = Symbol(""); 131 | // arr["" as any] = ref(1); 132 | // arr[symbolKey as any] = ref(2); 133 | // const arrRef = ref(arr).value; 134 | // expect(isRef(arrRef[0])).toBe(true); 135 | // expect(isRef(arrRef["" as any])).toBe(false); 136 | // expect(isRef(arrRef[symbolKey as any])).toBe(false); 137 | // expect(arrRef["" as any]).toBe(1); 138 | // expect(arrRef[symbolKey as any]).toBe(2); 139 | // }); 140 | 141 | // it("should keep tuple types", () => { 142 | // const tuple: [number, string, { a: number }, () => number, Ref] = [ 143 | // 0, 144 | // "1", 145 | // { a: 1 }, 146 | // () => 0, 147 | // ref(0), 148 | // ]; 149 | // const tupleRef = ref(tuple); 150 | 151 | // tupleRef.value[0]++; 152 | // expect(tupleRef.value[0]).toBe(1); 153 | // tupleRef.value[1] += "1"; 154 | // expect(tupleRef.value[1]).toBe("11"); 155 | // tupleRef.value[2].a++; 156 | // expect(tupleRef.value[2].a).toBe(2); 157 | // expect(tupleRef.value[3]()).toBe(0); 158 | // tupleRef.value[4].value++; 159 | // expect(tupleRef.value[4].value).toBe(1); 160 | // }); 161 | 162 | // it("should keep symbols", () => { 163 | // const customSymbol = Symbol(); 164 | // const obj = { 165 | // [Symbol.asyncIterator]: ref(1), 166 | // [Symbol.hasInstance]: { a: ref("a") }, 167 | // [Symbol.isConcatSpreadable]: { b: ref(true) }, 168 | // [Symbol.iterator]: [ref(1)], 169 | // [Symbol.match]: new Set>(), 170 | // [Symbol.matchAll]: new Map>(), 171 | // [Symbol.replace]: { arr: [ref("a")] }, 172 | // [Symbol.search]: { set: new Set>() }, 173 | // [Symbol.species]: { map: new Map>() }, 174 | // [Symbol.split]: new WeakSet>(), 175 | // [Symbol.toPrimitive]: new WeakMap, string>(), 176 | // [Symbol.toStringTag]: { weakSet: new WeakSet>() }, 177 | // [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, 178 | // [customSymbol]: { arr: [ref(1)] }, 179 | // }; 180 | 181 | // const objRef = ref(obj); 182 | 183 | // const keys: (keyof typeof obj)[] = [ 184 | // Symbol.asyncIterator, 185 | // Symbol.hasInstance, 186 | // Symbol.isConcatSpreadable, 187 | // Symbol.iterator, 188 | // Symbol.match, 189 | // Symbol.matchAll, 190 | // Symbol.replace, 191 | // Symbol.search, 192 | // Symbol.species, 193 | // Symbol.split, 194 | // Symbol.toPrimitive, 195 | // Symbol.toStringTag, 196 | // Symbol.unscopables, 197 | // customSymbol, 198 | // ]; 199 | 200 | // keys.forEach((key) => { 201 | // expect(objRef.value[key]).toStrictEqual(obj[key]); 202 | // }); 203 | // }); 204 | 205 | test("unref", () => { 206 | expect(unref(1)).toBe(1); 207 | expect(unref(ref(1))).toBe(1); 208 | }); 209 | 210 | // test("shallowRef", () => { 211 | // const sref = shallowRef({ a: 1 }); 212 | // expect(isReactive(sref.value)).toBe(false); 213 | 214 | // let dummy; 215 | // effect(() => { 216 | // dummy = sref.value.a; 217 | // }); 218 | // expect(dummy).toBe(1); 219 | 220 | // sref.value = { a: 2 }; 221 | // expect(isReactive(sref.value)).toBe(false); 222 | // expect(dummy).toBe(2); 223 | // }); 224 | 225 | // test("shallowRef force trigger", () => { 226 | // const sref = shallowRef({ a: 1 }); 227 | // let dummy; 228 | // effect(() => { 229 | // dummy = sref.value.a; 230 | // }); 231 | // expect(dummy).toBe(1); 232 | 233 | // sref.value.a = 2; 234 | // expect(dummy).toBe(1); // should not trigger yet 235 | 236 | // // force trigger 237 | // triggerRef(sref); 238 | // expect(dummy).toBe(2); 239 | // }); 240 | 241 | // test("shallowRef isShallow", () => { 242 | // expect(isShallow(shallowRef({ a: 1 }))).toBe(true); 243 | // }); 244 | 245 | test("isRef", () => { 246 | expect(isRef(ref(1))).toBe(true); 247 | // expect(isRef(computed(() => 1))).toBe(true); 248 | 249 | expect(isRef(0)).toBe(false); 250 | expect(isRef(1)).toBe(false); 251 | // an object that looks like a ref isn't necessarily a ref 252 | expect(isRef({ value: 0 })).toBe(false); 253 | }); 254 | 255 | // toRef 主要用来解决响应丢失问题 256 | test("toRef", () => { 257 | const a = reactive({ 258 | x: 1, 259 | }); 260 | const x = toRef(a, "x"); 261 | expect(isRef(x)).toBe(true); 262 | expect(x.value).toBe(1); 263 | 264 | // source -> proxy 265 | a.x = 2; 266 | expect(x.value).toBe(2); 267 | 268 | // proxy -> source 269 | x.value = 3; 270 | expect(a.x).toBe(3); 271 | 272 | // reactivity 273 | let dummyX; 274 | effect(() => { 275 | dummyX = x.value; 276 | }); 277 | expect(dummyX).toBe(x.value); 278 | 279 | // mutating source should trigger effect using the proxy refs 280 | a.x = 4; 281 | expect(dummyX).toBe(4); 282 | 283 | // should keep ref 284 | const r = { x: ref(1) }; 285 | expect(toRef(r, "x")).toBe(r.x); 286 | }); 287 | 288 | test("toRef default value", () => { 289 | const a: { x: number | undefined } = { x: undefined }; 290 | const x = toRef(a, "x", 1); 291 | expect(x.value).toBe(1); 292 | 293 | a.x = 2; 294 | expect(x.value).toBe(2); 295 | 296 | a.x = undefined; 297 | expect(x.value).toBe(1); 298 | }); 299 | 300 | test("toRefs", () => { 301 | const a = reactive({ 302 | x: 1, 303 | y: 2, 304 | }); 305 | 306 | const { x, y } = toRefs(a); 307 | 308 | expect(isRef(x)).toBe(true); 309 | expect(isRef(y)).toBe(true); 310 | expect(x.value).toBe(1); 311 | expect(y.value).toBe(2); 312 | 313 | // source -> proxy 314 | a.x = 2; 315 | a.y = 3; 316 | expect(x.value).toBe(2); 317 | expect(y.value).toBe(3); 318 | 319 | // proxy -> source 320 | x.value = 3; 321 | y.value = 4; 322 | expect(a.x).toBe(3); 323 | expect(a.y).toBe(4); 324 | 325 | // reactivity 326 | let dummyX, dummyY; 327 | effect(() => { 328 | dummyX = x.value; 329 | dummyY = y.value; 330 | }); 331 | expect(dummyX).toBe(x.value); 332 | expect(dummyY).toBe(y.value); 333 | 334 | // mutating source should trigger effect using the proxy refs 335 | a.x = 4; 336 | a.y = 5; 337 | expect(dummyX).toBe(4); 338 | expect(dummyY).toBe(5); 339 | }); 340 | 341 | // test("toRefs should warn on plain object", () => { 342 | // toRefs({}); 343 | // expect(`toRefs() expects a reactive object`).toHaveBeenWarned(); 344 | // }); 345 | 346 | // test("toRefs should warn on plain array", () => { 347 | // toRefs([]); 348 | // expect(`toRefs() expects a reactive object`).toHaveBeenWarned(); 349 | // }); 350 | 351 | test("toRefs reactive array", () => { 352 | const arr = reactive(["a", "b", "c"]); 353 | const refs = toRefs(arr); 354 | 355 | expect(Array.isArray(refs)).toBe(true); 356 | 357 | refs[0].value = "1"; 358 | expect(arr[0]).toBe("1"); 359 | 360 | arr[1] = "2"; 361 | expect(refs[1].value).toBe("2"); 362 | }); 363 | 364 | test("customRef", () => { 365 | let value = 1; 366 | let _trigger: () => void; 367 | 368 | const custom = customRef((track, trigger) => ({ 369 | get() { 370 | track(); 371 | return value; 372 | }, 373 | set(newValue: number) { 374 | value = newValue; 375 | _trigger = trigger; 376 | }, 377 | })); 378 | 379 | expect(isRef(custom)).toBe(true); 380 | 381 | let dummy; 382 | effect(() => { 383 | dummy = custom.value; 384 | }); 385 | expect(dummy).toBe(1); 386 | 387 | custom.value = 2; 388 | // should not trigger yet 389 | expect(dummy).toBe(1); 390 | 391 | _trigger!(); 392 | expect(dummy).toBe(2); 393 | }); 394 | 395 | test("should not trigger when setting value to same proxy", () => { 396 | const obj = reactive({ count: 0 }); 397 | 398 | const a = ref(obj); 399 | const spy1 = jest.fn(() => a.value); 400 | 401 | effect(spy1); 402 | 403 | a.value = obj; 404 | expect(spy1).toBeCalledTimes(1); 405 | 406 | const b = shallowRef(obj); 407 | const spy2 = jest.fn(() => b.value); 408 | 409 | effect(spy2); 410 | 411 | b.value = obj; 412 | expect(spy2).toBeCalledTimes(1); 413 | }); 414 | }); 415 | -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../src/effective"; 2 | import { isReactive, isShallow, reactive, shallowReactive, shallowReadonly } from "../src/reactive"; 3 | 4 | describe("shallowReactive", () => { 5 | test("should not make non-reactive properties reactive", () => { 6 | const props = shallowReactive({ n: { foo: 1 } }); 7 | expect(isReactive(props.n)).toBe(false); 8 | }); 9 | 10 | test("should keep reactive properties reactive", () => { 11 | const props: any = shallowReactive({ n: reactive({ foo: 1 }) }); 12 | props.n = reactive({ foo: 2 }); 13 | expect(isReactive(props.n)).toBe(true); 14 | }); 15 | 16 | // #2843 17 | test("should allow shallow and normal reactive for same target", () => { 18 | const original = { foo: {} }; 19 | const shallowProxy = shallowReactive(original); 20 | const reactiveProxy = reactive(original); 21 | expect(shallowProxy).not.toBe(reactiveProxy); 22 | expect(isReactive(shallowProxy.foo)).toBe(false); 23 | expect(isReactive(reactiveProxy.foo)).toBe(true); 24 | }); 25 | 26 | test("isShallow", () => { 27 | expect(isShallow(shallowReactive({}))).toBe(true); 28 | expect(isShallow(shallowReadonly({}))).toBe(true); 29 | }); 30 | 31 | // #5271 32 | test("should respect shallow reactive nested inside reactive on reset", () => { 33 | const r = reactive({ foo: shallowReactive({ bar: {} }) }); 34 | expect(isShallow(r.foo)).toBe(true); 35 | expect(isReactive(r.foo.bar)).toBe(false); 36 | 37 | r.foo = shallowReactive({ bar: {} }); 38 | expect(isShallow(r.foo)).toBe(true); 39 | expect(isReactive(r.foo.bar)).toBe(false); 40 | }); 41 | 42 | test("should respect shallow/deep versions of same target on access", () => { 43 | const original = {}; 44 | const shallow = shallowReactive(original); 45 | const deep = reactive(original); 46 | const r = reactive({ shallow, deep }); 47 | expect(r.shallow).toBe(shallow); 48 | expect(r.deep).toBe(deep); 49 | }); 50 | 51 | // describe("collections", () => { 52 | // test("should be reactive", () => { 53 | // const shallowSet = shallowReactive(new Set()); 54 | // const a = {}; 55 | // let size; 56 | 57 | // effect(() => { 58 | // size = shallowSet.size; 59 | // }); 60 | 61 | // expect(size).toBe(0); 62 | 63 | // shallowSet.add(a); 64 | // expect(size).toBe(1); 65 | 66 | // shallowSet.delete(a); 67 | // expect(size).toBe(0); 68 | // }); 69 | 70 | // test("should not observe when iterating", () => { 71 | // const shallowSet = shallowReactive(new Set()); 72 | // const a = {}; 73 | // shallowSet.add(a); 74 | 75 | // const spreadA = [...shallowSet][0]; 76 | // expect(isReactive(spreadA)).toBe(false); 77 | // }); 78 | 79 | // test("should not get reactive entry", () => { 80 | // const shallowMap = shallowReactive(new Map()); 81 | // const a = {}; 82 | // const key = "a"; 83 | 84 | // shallowMap.set(key, a); 85 | 86 | // expect(isReactive(shallowMap.get(key))).toBe(false); 87 | // }); 88 | 89 | // test("should not get reactive on foreach", () => { 90 | // const shallowSet = shallowReactive(new Set()); 91 | // const a = {}; 92 | // shallowSet.add(a); 93 | 94 | // shallowSet.forEach((x) => expect(isReactive(x)).toBe(false)); 95 | // }); 96 | 97 | // // #1210 98 | // test("onTrack on called on objectSpread", () => { 99 | // const onTrackFn = jest.fn(); 100 | // const shallowSet = shallowReactive(new Set()); 101 | // let a; 102 | // effect( 103 | // () => { 104 | // a = Array.from(shallowSet); 105 | // }, 106 | // { 107 | // onTrack: onTrackFn, 108 | // } 109 | // ); 110 | 111 | // expect(a).toMatchObject([]); 112 | // expect(onTrackFn).toHaveBeenCalled(); 113 | // }); 114 | // }); 115 | 116 | // describe("array", () => { 117 | // test("should be reactive", () => { 118 | // const shallowArray = shallowReactive([]); 119 | // const a = {}; 120 | // let size; 121 | 122 | // effect(() => { 123 | // size = shallowArray.length; 124 | // }); 125 | 126 | // expect(size).toBe(0); 127 | 128 | // shallowArray.push(a); 129 | // expect(size).toBe(1); 130 | 131 | // shallowArray.pop(); 132 | // expect(size).toBe(0); 133 | // }); 134 | // test("should not observe when iterating", () => { 135 | // const shallowArray = shallowReactive([]); 136 | // const a = {}; 137 | // shallowArray.push(a); 138 | 139 | // const spreadA = [...shallowArray][0]; 140 | // expect(isReactive(spreadA)).toBe(false); 141 | // }); 142 | 143 | // test("onTrack on called on objectSpread", () => { 144 | // const onTrackFn = jest.fn(); 145 | // const shallowArray = shallowReactive([]); 146 | // let a; 147 | // effect( 148 | // () => { 149 | // a = Array.from(shallowArray); 150 | // }, 151 | // { 152 | // onTrack: onTrackFn, 153 | // } 154 | // ); 155 | 156 | // expect(a).toMatchObject([]); 157 | // expect(onTrackFn).toHaveBeenCalled(); 158 | // }); 159 | // }); 160 | }); 161 | -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, isReadonly, shallowReadonly } from "../src/reactive"; 2 | 3 | describe('reactivity/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 | expect(isReactive(props.n)).toBe(false); 9 | }); 10 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/watch.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../src/reactive" 2 | import { watch } from "../src/watch"; 3 | 4 | describe('api: watch', () => { 5 | it('happy path', () => { 6 | let dummy; 7 | const obj = reactive({ 8 | num: 1 9 | }) 10 | watch(obj, () => { 11 | dummy = obj.num; 12 | }) 13 | obj.num = 2; 14 | expect(dummy).toBe(2); 15 | }) 16 | 17 | it("support function", () => { 18 | let dummy; 19 | const obj = reactive({ 20 | num: 1, 21 | }); 22 | watch( 23 | () => obj.num, 24 | () => { 25 | dummy = obj.num; 26 | } 27 | ); 28 | obj.num = 2; 29 | expect(dummy).toBe(2); 30 | }); 31 | 32 | it("support expired effect", () => { 33 | let finalData; 34 | const obj = reactive({ 35 | foo: 1 36 | }) 37 | watch(obj, async (newValue, oldValue, onInvalid) => { 38 | let expired = false; 39 | onInvalid(() => { 40 | expired = true; 41 | }) 42 | const res = await fetch('...'); 43 | 44 | if (!expired) { 45 | finalData = res; 46 | } 47 | }) 48 | obj.foo++; 49 | setTimeout(() => { 50 | obj.foo++ 51 | }, 200); 52 | }); 53 | }) -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | export function provide(key, value) { 4 | // 存 5 | const currentInstance: any = getCurrentInstance(); 6 | 7 | if (currentInstance) { 8 | let { provides } = currentInstance; 9 | const parentProvides = currentInstance.parent.provides; 10 | // init 11 | // 如果当前组件 provides和父组件 provides 相同,则说明当前组件没有初始化 provides 12 | // 此时需要初始化 provides,我们需要将当前 provides 的原型指向父组件的 provides, 13 | // 这样在当前 provides 中设置属性,不会影响到父组件的同名属性 14 | // 这样的操作类似于 get 操作, set 的时候是在当前对象 set,get 的时候是顺着原型链查到的 15 | if (provides === parentProvides) { 16 | provides = currentInstance.provides = Object.create(parentProvides); 17 | } 18 | 19 | provides[key] = value; 20 | } 21 | } 22 | 23 | // 当在原型链上查找不到 key 的时候,会拿到默认值 defaultValue 24 | export function inject(key, defaultValue) { 25 | // 取 26 | const currentInstance: any = getCurrentInstance(); 27 | 28 | if (currentInstance) { 29 | const { parent } = currentInstance; 30 | const parentProvides = parent.provides; 31 | if (key in parentProvides) { 32 | return parentProvides[key]; 33 | } else if (defaultValue) { 34 | if (typeof defaultValue === "function") { 35 | return defaultValue(); 36 | } 37 | return defaultValue; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs } from "../reactivity"; 2 | import { shallowReadonly } from "../reactivity/src/reactive"; 3 | import { isFunction, isObject } from "../shared"; 4 | import { emit } from "./componentEmit"; 5 | import { initProps } from "./componentProps"; 6 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 7 | import { initSlots } from "./componentSlots"; 8 | 9 | export function createComponentInstance(vnode, parent) { 10 | const component = { 11 | vnode, 12 | type: vnode.type, 13 | next: null, 14 | setupState: {}, 15 | props: {}, 16 | slots: {}, 17 | // 给定当前的组件 provides,默认初始值指向父组件 provides, 18 | // 将来 inject 的时候可以沿着 provides 指向查找到对应的值 19 | provides: parent ? parent.provides : {}, 20 | parent, 21 | isMounted: false, 22 | subTree: {}, 23 | emit: () => {}, 24 | mounted: [], 25 | }; 26 | component.emit = emit.bind(null, component) as any; 27 | return component; 28 | } 29 | 30 | export function setupComponent(instance) { 31 | const { type = {} } = instance; 32 | const { beforeCreate } = type; 33 | // 调用 beforeCreate 钩子函数,Called when the instance is initialized. 34 | beforeCreate && beforeCreate(); 35 | // initProps 36 | initProps(instance, instance.vnode.props); 37 | // initSlots 38 | initSlots(instance, instance.vnode.children); 39 | setupStatefulComponent(instance); 40 | } 41 | 42 | function setupStatefulComponent(instance) { 43 | const Component = instance.type; 44 | 45 | // 组件代理对象 46 | instance.proxy = new Proxy({_: instance}, PublicInstanceProxyHandlers); 47 | 48 | const { setup } = Component; 49 | 50 | if (setup) { 51 | setCurrentInstance(instance); 52 | const setupResult = setup(shallowReadonly(instance.props), { 53 | emit: instance.emit 54 | }); 55 | setCurrentInstance(null); 56 | handleSetupResult(instance, setupResult); 57 | } 58 | } 59 | 60 | function handleSetupResult(instance, setupResult) { 61 | if (isObject(setupResult)) { 62 | instance.setupState = proxyRefs(setupResult); 63 | } else if (isFunction(setupResult)) { 64 | if (instance.type.render) console.warn("steup 函数返回渲染函数, render 选项将被忽略"); 65 | instance.type.render = setupResult; 66 | } 67 | 68 | finishComponentSetup(instance); 69 | } 70 | 71 | function finishComponentSetup(instance) { 72 | const Component = instance.type; 73 | 74 | instance.render = Component.render; 75 | } 76 | 77 | let currentInstance: any = null; 78 | export function getCurrentInstance() { 79 | return currentInstance; 80 | } 81 | 82 | // 封装成函数 方便调试 83 | // 这样 currentInstance 的改变只能通过这个函数,通过在这里打断点就能追踪到 currentInstance 的变化 84 | export function setCurrentInstance(instance) { 85 | currentInstance = instance; 86 | } 87 | 88 | export function onMounted(fn) { 89 | if (currentInstance) { 90 | currentInstance.mounted.push(fn); 91 | } else { 92 | console.error("onMounted 函数只能在 setup 中调用"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | // emit 这里需要将要执行的 event 从 instance 的 props 中找到对应要触发的函数 2 | // 但是实际应用的时候我们是直接 emit(event) 来触发,如何将 instance 传入呢? 3 | 4 | import { camelize, toHandlerKey } from "../shared"; 5 | 6 | // 通过 bind 的方式提前传入 instance,用户只需要传入 event 就能正常使用了 7 | export function emit(instance, event, ...args) { 8 | const { props } = instance; 9 | 10 | // TPP 11 | // 先去写一个特定的行为 -> 重构成通用的行为 12 | const handlerName = toHandlerKey(camelize(event)); 13 | const handler = props[handlerName]; 14 | handler && handler(...args); 15 | } -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | // attrs 3 | instance.props = rawProps || {}; 4 | } -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "../shared"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | $props: (i) => i.props, 7 | } 8 | 9 | export const PublicInstanceProxyHandlers = { 10 | get({_: instance}, key) { 11 | 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 | const publicGetter = publicPropertiesMap[key]; 18 | if (publicGetter) { 19 | return publicGetter(instance); 20 | } 21 | }, 22 | }; -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from "../shared"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | 4 | export function initSlots(instance, children) { 5 | const { vnode } = instance; 6 | // slot children 才会去处理 7 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 8 | normalizeObjectSlots(children, instance.slots); 9 | } 10 | } 11 | 12 | function normalizeObjectSlots(children, slots) { 13 | for (const key in children) { 14 | const value = children[key]; 15 | slots[key] = (props) => normalizeSlotValue(value(props)); 16 | } 17 | } 18 | 19 | function normalizeSlotValue(value) { 20 | return isArray(value) ? value : [value]; 21 | } -------------------------------------------------------------------------------- /src/runtime-core/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(prevVNode, nextVNode) { 2 | const { props: prevProps } = prevVNode; 3 | const { props: nextProps } = nextVNode; 4 | 5 | for (const key in nextProps) { 6 | if (prevProps[key] !== nextProps[key]) { 7 | return true; 8 | } 9 | } 10 | return false; 11 | } -------------------------------------------------------------------------------- /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 | // 先 vnode,后续的逻辑操作都会基于 vnode 做处理 8 | const vnode = createVnode(rootComponent); 9 | 10 | render(vnode, rootContainer); 11 | }, 12 | }; 13 | } 14 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVnode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, name, props) { 4 | const slot = slots[name]; 5 | 6 | if (slot) { 7 | if (typeof slot === "function") { 8 | return createVnode(Fragment, {}, slot(props)); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from "./h"; 2 | export { renderSlots } from "./helpers/renderSlots"; 3 | export { createTextVNode, createCommentVNode } from "./vnode"; 4 | export { getCurrentInstance, onMounted } from "./component"; 5 | export { provide, inject } from "./apiInject"; 6 | export { createRenderer } from "./render"; 7 | export { nextTick } from "./scheduler"; -------------------------------------------------------------------------------- /src/runtime-core/render.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../reactivity/src/effective"; 2 | import { EMPTY_OBJ } from "../shared"; 3 | import { ShapeFlags } from "../shared/ShapeFlags"; 4 | import { createComponentInstance, setupComponent } from "./component"; 5 | import { shouldUpdateComponent } from "./componentUpdateUtils"; 6 | import { createAppAPI } from "./createApp"; 7 | import { queueJobs } from "./scheduler"; 8 | import { Fragment, Text, Comment } from "./vnode"; 9 | 10 | // 这里需要 createRenderer,而不直接定义 render 函数,因为渲染器的内容非常广泛, 11 | // 而用来吧 vnode 渲染成真实 dom 的 render 函数只是其中一部分 12 | // 通过 options 传入与特定平台强依赖的配置项,意味着可以完成非浏览器环境下的渲染工作 13 | // 这样核心代码不再依赖平台特有的API,再通过支持个性化配置的能力来实现跨平台 14 | export function createRenderer(options) { 15 | 16 | const { 17 | createElement: hostCreateElement, 18 | patchProp: hostPatchProp, 19 | insert: hostInsert, 20 | remove: hostRemove, 21 | setElementText: hostSetElementText, 22 | createText: hostCreateText, 23 | setText: hostSetText, 24 | createComment: hostCreateComment, 25 | setComment: hostSetComment, 26 | } = options; 27 | 28 | function render(vnode, container) { 29 | // patch 30 | patch(null, vnode, container, null, null); 31 | } 32 | 33 | function patch(n1, n2, container, parentComponent, anchor) { 34 | const { type, shapeFlag } = n2; 35 | // Fragment 36 | // TODO 判断 vnode 是否一个 element 37 | switch (type) { 38 | case Fragment: 39 | processFragment(n1, n2, container, parentComponent, anchor); 40 | break; 41 | case Text: 42 | processText(n1, n2, container); 43 | break; 44 | case Comment: 45 | processComment(n1, n2, container); 46 | break; 47 | default: 48 | if (shapeFlag & ShapeFlags.ELEMENT) { 49 | processElement(n1, n2, container, parentComponent, anchor); 50 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 51 | processComponent(n1, n2, container, parentComponent, anchor); 52 | } 53 | break; 54 | } 55 | } 56 | 57 | function processFragment(n1, n2, container, parentComponent, anchor) { 58 | mountChildren(n2.children, container, parentComponent, anchor); 59 | } 60 | 61 | function processText(n1, n2, container) { 62 | if (!n1) { 63 | const { children } = n2; 64 | const textNode = (n2.el = hostCreateText(children)); 65 | hostInsert(textNode, container); 66 | } else { 67 | const textNode = (n2.el = n1.el); 68 | if (n2.children !== n1.children) { 69 | hostSetText(textNode, n2.chidlren); 70 | } 71 | } 72 | } 73 | 74 | function processComment(n1, n2, container) { 75 | if (!n1) { 76 | const { children } = n2; 77 | const commentNode = (n2.el = hostCreateComment(children)); 78 | hostInsert(commentNode, container); 79 | } else { 80 | const commentNode = (n2.el = n1.el); 81 | if (n2.children !== n1.children) { 82 | hostSetComment(commentNode, n2.chidlren); 83 | } 84 | } 85 | } 86 | 87 | function processComponent(n1, n2, container, parentComponent, anchor) { 88 | if (!n1) { 89 | mountComponent(n2, container, parentComponent, anchor); 90 | } else { 91 | updateComponent(n1, n2); 92 | } 93 | } 94 | 95 | function updateComponent(n1, n2) { 96 | const instance = (n2.component = n1.component); 97 | if (shouldUpdateComponent(n1, n2)) { 98 | instance.next = n2; 99 | instance.update(); 100 | } else { 101 | n2.el = n1.el; 102 | n2.vnode = n2; 103 | } 104 | } 105 | 106 | function processElement(n1, n2, container, parentComponent, anchor) { 107 | if (!n1) { 108 | mountElement(n2, container, parentComponent, anchor); 109 | } else { 110 | patchElement(n1, n2, container, parentComponent, anchor); 111 | } 112 | } 113 | 114 | function patchElement(n1, n2, container, parentComponent, anchor) { 115 | const oldProps = n1.props || EMPTY_OBJ; 116 | const newProps = n2.props || EMPTY_OBJ; 117 | 118 | const el = (n2.el = n1.el); 119 | patchProps(el, oldProps, newProps); 120 | patchChildren(el, n1, n2, parentComponent, anchor); 121 | } 122 | 123 | function patchChildren(container, n1, n2, parentComponent, anchor) { 124 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 125 | const { shapeFlag, children: c2 } = n2; 126 | 127 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 128 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 129 | // 1. 将老的 children 清空 130 | unmountChildren(c1); 131 | // 2. 设置新的 text 132 | hostSetElementText(container, c2); 133 | } else if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 134 | // 设置新的 text 135 | if (c1 !== c2) { 136 | hostSetElementText(container, c2); 137 | } 138 | } 139 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 140 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 141 | hostSetElementText(container, ""); 142 | mountChildren(c2, container, parentComponent, anchor); 143 | } else if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 144 | // array diff 145 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 146 | } 147 | } 148 | } 149 | 150 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 151 | let i = 0; 152 | let e1 = c1.length - 1; 153 | let e2 = c2.length - 1; 154 | 155 | function isSameVNodeType(n1, n2) { 156 | return n1.type === n2.type && n1.key === n2.key; 157 | } 158 | 159 | // 左侧 160 | while (i <= e1 && i <= e2) { 161 | const n1 = c1[i]; 162 | const n2 = c2[i]; 163 | 164 | if (isSameVNodeType(n1, n2)) { 165 | patch(n1, n2, container, parentComponent, parentAnchor); 166 | } else { 167 | break; 168 | } 169 | 170 | i++; 171 | } 172 | 173 | // 右侧 174 | while (i <= e1 && i <= e2) { 175 | const n1 = c1[e1]; 176 | const n2 = c2[e2]; 177 | 178 | if (isSameVNodeType(n1, n2)) { 179 | patch(n1, n2, container, parentComponent, parentAnchor); 180 | } else { 181 | break; 182 | } 183 | 184 | e1--; 185 | e2--; 186 | } 187 | 188 | // 新的比老的长 创建 189 | if (i > e1) { 190 | if (i <= e2) { 191 | const nextPos = e2 + 1; 192 | // anchor记录插入的位置节点 193 | const anchor = nextPos < c2.length ? c2[nextPos].el : null; 194 | while (i <= e2) { 195 | patch(null, c2[i], container, parentComponent, anchor); 196 | i++; 197 | } 198 | } 199 | } else if (i > e2) { 200 | // 老的比新的长 删除 201 | while (i <= e1) { 202 | hostRemove(c1[i].el); 203 | i++; 204 | } 205 | } else { 206 | // 乱序的部分 207 | let s1 = i; 208 | let s2 = i; 209 | 210 | const toBePatched = e2 - s2 + 1; 211 | let patched = 0; 212 | const keyToNewIndexMap = new Map(); 213 | const newIndexToOldIndexMap = new Array(toBePatched); 214 | let moved = false; 215 | let maxNewIndexSoFar = 0; 216 | // 初始化为 0,代表新的节点在老的节点中没有 217 | for (let i = 0;i < toBePatched; i++) { 218 | newIndexToOldIndexMap[i] = 0; 219 | } 220 | 221 | // 在新的 children 中构建映射表 222 | for (let i = s2; i <= e2; i++) { 223 | const nextChild = c2[i]; 224 | keyToNewIndexMap.set(nextChild.key, i); 225 | } 226 | 227 | // 在老的 children 中查看当前节点是不是在新的 children 中 228 | // 先通过 key 比对,如果没有 key,则直接通过遍历新的 children 查看是否有相同的节点类型 229 | for (let i = s1; i <= e1; i++) { 230 | const prevChild = c1[i]; 231 | 232 | // 当 patched >= toBePatched 的时候,说明后面的节点可以直接删除,不用去判断了 233 | if (patched >= toBePatched) { 234 | hostRemove(prevChild.el); 235 | continue; 236 | } 237 | 238 | let newIndex; 239 | if (prevChild.key != null) { 240 | newIndex = keyToNewIndexMap.get(prevChild.key); 241 | } else { 242 | for (let j = s2; j <= e2; j++) { 243 | if (isSameVNodeType(prevChild, c2[j])) { 244 | newIndex = j; 245 | break; 246 | } 247 | } 248 | } 249 | 250 | // 老的 children 存在新的 children 没有的,删除 251 | if (newIndex === undefined) { 252 | hostRemove(prevChild.el); 253 | } else { 254 | // 如果一直处于递增的状态,则说明目前是不需要移动的 255 | if (newIndex >= maxNewIndexSoFar) { 256 | maxNewIndexSoFar = newIndex; 257 | } else { 258 | moved = true; 259 | } 260 | 261 | newIndexToOldIndexMap[newIndex - s1] = i + 1; 262 | // 继续 patch 263 | patch(prevChild, c2[newIndex], container, parentComponent, null); 264 | patched++; 265 | } 266 | } 267 | 268 | // 需要移动的话,则需要算出最长递增子序列 269 | const increasingNewIndexSequence = moved 270 | ? getSequence(newIndexToOldIndexMap) 271 | : []; 272 | let j = increasingNewIndexSequence.length - 1; 273 | for (let i = toBePatched - 1; i >= 0; i--) { 274 | const nextIndex = i + s2; 275 | const nextChild = c2[nextIndex]; 276 | const anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null; 277 | if (newIndexToOldIndexMap[i] === 0) { // 新增 278 | patch(null, nextChild, container, parentComponent, anchor); 279 | } else if (moved) { //移动位置 280 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 281 | hostInsert(nextChild.el, container, anchor); 282 | } else { 283 | j--; 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | function unmountChildren(children) { 291 | for (let i = 0; i < children.length; i++) { 292 | const el = children[i].el; 293 | hostRemove(el); 294 | } 295 | } 296 | 297 | // 1. 属性前后值不一样,需要触发修改操作 298 | // 2. 属性修改后的值为 null || undefined,需要触发删除 prop 操作 299 | // 3. 属性在老prop 中有,新prop 中无,需触发删除操作 300 | function patchProps(el, oldProps, newProps) { 301 | if (oldProps !== newProps) { 302 | for (const key in newProps) { 303 | const prevProp = oldProps[key]; 304 | const nextProp = newProps[key]; 305 | 306 | if (prevProp !== nextProp) { 307 | hostPatchProp(el, key, prevProp, nextProp); 308 | } 309 | } 310 | 311 | if (oldProps !== EMPTY_OBJ) { 312 | for (const key in oldProps) { 313 | if (!(key in newProps)) { 314 | hostPatchProp(el, key, oldProps[key], null); 315 | } 316 | } 317 | } 318 | } 319 | } 320 | 321 | function mountComponent(initialVNode, container, parentComponent, anchor) { 322 | const instance = (initialVNode.component = createComponentInstance( 323 | initialVNode, 324 | parentComponent 325 | )); 326 | setupComponent(instance); 327 | const { type = {} } = instance; 328 | const { created } = type; 329 | // 调用 created 钩子函数,Called after the instance has finished processing all state-related options. 330 | // TODO 后期优化,created 需要将里面的 this 指向 data 331 | created && created(); 332 | setupRenderEffect(instance, container, initialVNode, anchor); 333 | } 334 | 335 | // 依赖于稳定的接口,而不是具体的实现,这就是自定义render 336 | function mountElement(vnode, container, parentComponent, anchor) { 337 | const { type, props, children, shapeFlag } = vnode; 338 | 339 | // 这里 el 是挂载到当前 element 类型的 vnode 上的,这样在 vnode 与真实 dom 元素之间就建立了联系 340 | const el = (vnode.el = hostCreateElement(type)); 341 | 342 | // string | array 343 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 344 | el.textContent = children; 345 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 346 | mountChildren(vnode.children, el, parentComponent, anchor); 347 | } 348 | 349 | // props 350 | for (const key in props) { 351 | const val = props[key]; 352 | hostPatchProp(el, key, null, val); 353 | } 354 | 355 | hostInsert(el, container, anchor); 356 | } 357 | 358 | function mountChildren(children, container, parentComponent, anchor) { 359 | children.forEach((v) => { 360 | patch(null, v, container, parentComponent, anchor); 361 | }); 362 | } 363 | 364 | function setupRenderEffect(instance, container, initialVNode, anchor) { 365 | // effect 做依赖收集,当内部有变更会重新执行 render 366 | instance.update = effect( 367 | () => { 368 | if (!instance.isMounted) { 369 | const { proxy, type = {} } = instance; 370 | const { beforeMount, mounted } = type; 371 | // 调用 beforeMount 钩子函数,Called right before the component is to be mounted. 372 | // TODO 后期优化,beforeMount 需要将里面的 this 指向 data 373 | beforeMount && beforeMount(); 374 | 375 | const subTree = (instance.subTree = instance.render.call(proxy)); 376 | instance.subTree = subTree; 377 | // vnode -> patch 378 | patch(null, subTree, container, instance, anchor); 379 | // vnode -> element -> mountElement 380 | // 此时所有的 subtree 处理完成,然后挂载到组件的 vnode 上 381 | initialVNode.el = subTree.el; 382 | 383 | instance.isMounted = true; 384 | 385 | // 调用 mounted 钩子函数,Called after the component has been mounted. 386 | // TODO 后期优化,mounted 需要将里面的 this 指向 data 387 | mounted && mounted(); 388 | 389 | // setup 内的生命周期函数 390 | // TODO 后期优化,mounted 需要将里面的 this 指向 data 391 | instance.mounted && instance.mounted.forEach((fn) => fn()); 392 | } else { 393 | const { proxy, vnode, next, type = {} } = instance; 394 | const { beforeUpdate, updated } = type; 395 | if (next) { 396 | next.el = vnode.el; 397 | updateComponentPreRender(instance, next); 398 | } 399 | const subTree = instance.render.call(proxy); 400 | const prevSubTree = instance.subTree; 401 | instance.subTree = subTree; 402 | 403 | // 调用 beforeUpdate 钩子函数,Called right before the component is about to update its DOM tree due to a reactive state change. 404 | // TODO beforeUpdate 需要将里面的 this 指向 data 405 | beforeUpdate && beforeUpdate(); 406 | 407 | patch(prevSubTree, subTree, container, instance, anchor); 408 | 409 | // 调用 updated 钩子函数,Called after the component has updated its DOM tree due to a reactive state change. 410 | // TODO updated 需要将里面的 this 指向 data 411 | updated && updated(); 412 | } 413 | }, 414 | { 415 | // 视图更新是异步的,如果想要得到视图的数据,则需要 nextTick 416 | scheduler() { 417 | queueJobs(instance.update); 418 | }, 419 | } 420 | ); 421 | } 422 | 423 | function updateComponentPreRender(instance, nextVNode) { 424 | instance.vnode = nextVNode; 425 | instance.next = null; 426 | instance.props = nextVNode.props; 427 | } 428 | 429 | return { 430 | createApp: createAppAPI(render), 431 | }; 432 | } 433 | 434 | // https://en.wikipedia.org/wiki/Longest_increasing_subsequence 435 | function getSequence(arr: number[]): number[] { 436 | const p = arr.slice() 437 | const result = [0] 438 | let i, j, u, v, c 439 | const len = arr.length 440 | for (i = 0; i < len; i++) { 441 | const arrI = arr[i] 442 | if (arrI !== 0) { 443 | j = result[result.length - 1] 444 | if (arr[j] < arrI) { 445 | p[i] = j 446 | result.push(i) 447 | continue 448 | } 449 | u = 0 450 | v = result.length - 1 451 | while (u < v) { 452 | c = (u + v) >> 1 453 | if (arr[result[c]] < arrI) { 454 | u = c + 1 455 | } else { 456 | v = c 457 | } 458 | } 459 | if (arrI < arr[result[u]]) { 460 | if (u > 0) { 461 | p[i] = result[u - 1] 462 | } 463 | result[u] = i 464 | } 465 | } 466 | } 467 | u = result.length 468 | v = result[u - 1] 469 | while (u-- > 0) { 470 | result[u] = v 471 | v = p[v] 472 | } 473 | return result 474 | } 475 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = []; 2 | const p = Promise.resolve(); 3 | let isFlushingPending = false; 4 | 5 | export function nextTick(fn) { 6 | return fn ? p.then(fn) : p; 7 | } 8 | 9 | export function queueJobs(job) { 10 | if (!queue.includes(job)) { 11 | queue.push(job); 12 | } 13 | 14 | queueFlush(); 15 | } 16 | 17 | function queueFlush() { 18 | if (isFlushingPending) return; 19 | isFlushingPending = true; 20 | 21 | nextTick(flushJobs); 22 | } 23 | 24 | function flushJobs() { 25 | isFlushingPending = false; 26 | let job; 27 | while ((job = queue.shift())) { 28 | job && job(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from "../shared"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | 4 | export const Fragment = Symbol("Fragment"); 5 | export const Text = Symbol("Text"); 6 | export const Comment = Symbol("Comment"); 7 | 8 | export function createVnode(type, props?, children?) { 9 | const vnode = { 10 | type, 11 | props, 12 | children, 13 | component: null, 14 | key: props && props.key, 15 | shapeFlag: getShapeFlag(type), 16 | el: null, 17 | }; 18 | 19 | if (typeof children === "string") { 20 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN; 21 | } else if (isArray(children)) { 22 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.ARRAY_CHILDREN; 23 | } 24 | 25 | // 组件类型 + children object 26 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 27 | if (typeof vnode.children === "object") { 28 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 29 | } 30 | } 31 | 32 | return vnode; 33 | } 34 | 35 | export function createTextVNode(text: string) { 36 | return createVnode(Text, {}, text); 37 | } 38 | 39 | export function createCommentVNode(text: string) { 40 | return createVnode(Comment, {}, text); 41 | } 42 | 43 | function getShapeFlag(type) { 44 | return typeof type === "string" 45 | ? ShapeFlags.ELEMENT 46 | : ShapeFlags.STATEFUL_COMPONENT; 47 | } -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../runtime-core"; 2 | import { isArray, isObject, isString } from "../shared"; 3 | 4 | function createElement(type) { 5 | return document.createElement(type); 6 | } 7 | 8 | // HTML attribute vs. DOM property 9 | // https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html 10 | // Attributes are defined by HTML. Properties are defined by the DOM (Document Object Model). 11 | // 1. A few HTML attributes have 1:1 mapping to properties. id is one example. 12 | // 2. Some HTML attributes don't have corresponding properties. colspan is one example. 13 | // 3. Some DOM properties don't have corresponding attributes. textContent is one example. 14 | // 4. Many HTML attributes appear to map to properties ... but not in the way you might think! 15 | // 对于第四点,只需要记住一个核心原则: 16 | // HTML attribute 的作用是设置与之对应的 DOM property 的初始值 17 | function patchProp(el, key, preVal, nextVal) { 18 | // 具体的 click => 通用的事件处理 19 | // on + Event name 20 | const isOn = (key) => /^on[A-Z]/.test(key); 21 | if (isOn(key)) { 22 | const name = key.slice(2).toLowerCase(); 23 | // 先移除之前添加的事件处理函数 24 | // preVal && el.removeEventListener(name, preVal); 25 | // 优化:如何避免每次更新我们都需要调用 removeEventListener 26 | 27 | // 伪造个虚拟的时间处理函数 invoker,并赋值到 el._vei 上 28 | const invokers = el._vei || (el._vei = {}); 29 | // 根据事件名获取 invoker 30 | let invoker = invokers[key]; 31 | if (nextVal) { 32 | if (!invoker) { 33 | // 没有 invoker,需要新建,并将 invoker 缓存到 el._vei 上 34 | invoker = el._vei[key] = (e) => { 35 | // e.timeStamp 是事件发生的时间 36 | // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数 37 | if (e.timeStamp < invoker.attached) return; 38 | 39 | // 对于同一个类型的事件而言,可能绑定了多个事件处理函数 40 | if (isArray(invoker.value)) { 41 | invoker.value.forEach((fn) => fn(e)); 42 | } else { 43 | // 当伪造的 invoker 执行的时候,会执行真正的时间处理函数 44 | invoker.value(e); 45 | } 46 | }; 47 | // 将真正的事件处理函数赋值给 invoker.value 48 | invoker.value = nextVal; 49 | // 添加 invoker.attached 属性,存储事件处理函数被绑定的时间 50 | invoker.attached = performance.now(); 51 | el.addEventListener(name, invoker); 52 | } else { 53 | // 如果 invoker 存在,意味着需要更新事件处理函数,我们只需要更新它的 value 即可,而不需要通过 removeEventListener 54 | invoker.value = nextVal; 55 | } 56 | } else if (invoker) { 57 | el.removeEventListener(name, invoker); 58 | } 59 | el.addEventListener(name, nextVal); 60 | } else { 61 | if (nextVal === undefined || nextVal === null) { 62 | el.removeAttribute(key); 63 | } else { 64 | // 使用 el.className 设置 class 性能最好 65 | if (key === "class" && nextVal) { 66 | el.className = normalizeClass(nextVal); 67 | } else if (shouldSetAsProps(el, key, nextVal)) { 68 | const type = typeof el[key]; 69 | // boolean 类型,并且赋值为空字符串,需赋值为 true 70 | // 比如说设置了 disabled 属性,此时转换为 VNode 的时候,disabled 为 '', el.disabled = '' 71 | // 赋值会矫正为 boolean 类型,即 el.disabled = false,这与本意违背 72 | if (type === "boolean" && nextVal === "") { 73 | el[key] = true; 74 | } else { 75 | el[key] = nextVal; 76 | } 77 | } else { 78 | // setAttribute 会将传入的 nextVal 字符串化,当设置 disabled 值为 false 79 | // el.setAttribute(disabled, 'false'),对于 el 来说,只要设置了 disabled 属性,默认为 true,所以我们需要优先设置 DOM properties 80 | el.setAttribute(key, nextVal); 81 | } 82 | } 83 | } 84 | } 85 | 86 | function shouldSetAsProps(el, key, value) { 87 | // 特殊处理 el.form 是只读的,只能通过 setAttribute 函数来设置它 88 | if (key === "form" && el.tagName === "INPUT") return false; 89 | // 判断 key 是否存在对应的 DOM properties 90 | return key in el; 91 | } 92 | 93 | // 对于 class 的不同处理 94 | // 1. 字符串 95 | // 2. 对象 96 | // 3. 数组 97 | function normalizeClass(value) { 98 | let res = ""; 99 | if (isString(value)) { 100 | res = value; 101 | } else if (isArray(value)) { 102 | for (const v of value) { 103 | const normalized = normalizeClass(v); 104 | if (normalized) res += normalized + " "; 105 | } 106 | } else if (isObject(value)) { 107 | for (const name in value) { 108 | if (value[name]) { 109 | res += name + " "; 110 | } 111 | } 112 | } 113 | return res.trim(); 114 | } 115 | 116 | function insert(child, parent, anchor) { 117 | // parent.append(child); 118 | parent.insertBefore(child, anchor || null); 119 | } 120 | 121 | function remove(child) { 122 | const parent = child.parentNode; 123 | if (parent) { 124 | parent.removeChild(child); 125 | } 126 | } 127 | 128 | function setElementText(el, text) { 129 | el.textContent = text; 130 | } 131 | 132 | function createText(text) { 133 | return document.createTextNode(text); 134 | } 135 | 136 | function setText(el, text) { 137 | el.nodeValue = text; 138 | } 139 | 140 | function createComment(text) { 141 | return document.createComment(text); 142 | } 143 | 144 | function setComment(el, text) { 145 | el.nodeValue = text; 146 | } 147 | 148 | const renderer: any = createRenderer({ 149 | createElement, 150 | patchProp, 151 | insert, 152 | remove, 153 | setElementText, 154 | createText, 155 | setText, 156 | createComment, 157 | setComment, 158 | }); 159 | 160 | export function createApp(...args) { 161 | return renderer.createApp(...args) 162 | } 163 | 164 | export * from "../runtime-core"; -------------------------------------------------------------------------------- /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, // 10000 7 | } 8 | 9 | // 位运算的方式 高效 10 | // 查找 & 11 | // 修改 | 12 | // 0001 element 13 | // 0010 stateful_component 14 | // 0100 text_children 15 | // 1000 array_children -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | 3 | export const isObject = (val) => { 4 | return val !== null && typeof val === "object"; 5 | } 6 | 7 | export const isFunction = (val) => { 8 | return typeof val === "function"; 9 | } 10 | 11 | export const isArray = (val) => Array.isArray(val); 12 | 13 | export const isString = (val) => typeof val === "string"; 14 | 15 | export const isSymbol = (val) => typeof val === "symbol"; 16 | 17 | export const isIntegerKey = (key) => 18 | isString(key) && 19 | key !== "NaN" && 20 | key[0] !== "-" && 21 | "" + parseInt(key, 10) === key; 22 | 23 | export const hasChanged = (oldVal, newVal) => { 24 | return !Object.is(oldVal, newVal); 25 | } 26 | 27 | export const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); 28 | 29 | export const objectToString = Object.prototype.toString; 30 | export const toTypeString = (value: unknown): string => 31 | objectToString.call(value); 32 | 33 | export const toRawType = (value: unknown): string => { 34 | // extract "RawType" from strings like "[object RawType]" 35 | return toTypeString(value).slice(8, -1); 36 | }; 37 | 38 | export const camelize = (str: string) => { 39 | return str.replace(/-(\w)/g, (_, c) => { 40 | return c ? c.toUpperCase() : ""; 41 | }); 42 | }; 43 | 44 | export const capitalize = (str: string) => { 45 | return str.charAt(0).toUpperCase() + str.slice(1); 46 | }; 47 | 48 | export const toHandlerKey = (str: string) => { 49 | return str ? "on" + capitalize(str) : ""; 50 | }; 51 | 52 | export const EMPTY_OBJ = {}; -------------------------------------------------------------------------------- /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 | "ES2018" 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": ["jest"], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files */ 40 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 46 | 47 | /* Emit */ 48 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 52 | // "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. */ 53 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 78 | 79 | /* Type Checking */ 80 | "strict": true, /* Enable all strict type-checking options. */ 81 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | --------------------------------------------------------------------------------