├── .gitignore ├── README.md ├── babel.config.js ├── drawio ├── createRender.drawio └── 双端对比.drawio ├── example ├── apiInject │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlots │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentUpdater │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── createRender │ ├── App.js │ ├── index.html │ └── main.js ├── currentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── helloWorld │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── nextTicker │ ├── App.js │ ├── index.html │ └── main.js ├── patchChildren │ ├── App.js │ ├── ArrayToArray.js │ ├── ArrayToText.js │ ├── TextToArray.js │ ├── TextToText.js │ ├── index.html │ └── main.js └── update │ ├── App.js │ ├── index.html │ └── main.js ├── getSequence.ts ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── compiler-core │ ├── __tests__ │ │ ├── parse.sepc.ts │ │ └── transform.spec.ts │ └── src │ │ ├── ast.ts │ │ ├── parse.ts │ │ └── transform.ts ├── index.ts ├── reactivity │ ├── __tests__ │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts │ ├── baseHandler.ts │ ├── computed.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── tests │ │ └── reactive.spec.ts ├── runtime-core │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── componentUpdateUtils.ts │ ├── createApp.ts │ ├── h.ts │ ├── helper │ │ └── renderSlots.ts │ ├── index.ts │ ├── renderer.ts │ ├── scheduler.ts │ └── vnode.ts ├── runtime-dom │ └── index.ts └── shared │ ├── SharpeFlags.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nimi vue 2 | 3 | ## test 4 | 5 | ```bash 6 | yarn test 7 | ``` 8 | 9 | ## build to js 10 | 11 | ```bash 12 | yarn 13 | yarn build 14 | ``` 15 | 16 | 17 | ## 过一遍 视频与实现视频中demo 18 | 19 | ``` 20 | // TODO: 实现双端对比场景 21 | ``` 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | "@babel/preset-typescript", 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /drawio/createRender.drawio: -------------------------------------------------------------------------------- 1 | 1VlLc9s2EP41mCQHe/gWeCRpKTkkM5mxp02PkAiJbEmCA0KWlF/fBQiKTzXqWJXZg2VgsaAW3377AIXsKD9+5qRMvrGYZsgy4iOyn5BlmY5jwz8pOdUS19WCHU9jrdQKntOfVAsNLd2nMa16ioKxTKRlX7hhRUE3oicjnLNDX23Lsv63lmRHR4LnDcnG0t/TWCS1FFuLVv6Fpruk+WbT8+uVnDTK+iRVQmJ26IjsJbIjzpioR/kxopkEr8Gl3re6sHo2jNNCXLNBW/xKsr0+m7ZLnJrD0hjOrqeMi4TtWEGyZSsNOdsXMZVPNGDW6nxlrAShCcI/qRAn7UiyFwxEicgzvTq2Wh+kYnu+0Xa42tGE76jW8mqRtLCzTZ/0M2U5FfwECpxmRKSvfe8RTYLdWa/FCQYaqmnY3BFsfF+INKcPG8bpCMI+QIckFfS5JOpgB4iQK8F4pVzQ4z8eVK9aWNNLx5etp4eWrGbDwKRDVM94OzSmNwtKAXj89EPuf3Sb6R/6cWrydOzNTnp2FRW9MRXNxa25qLd+ZylYcnatbQ9c6wx8Vhuldw3cdjbjKk+OHQmgxJRTjpYuCgPk27Njurm4I9XP5naoLpGJEFgFg2CBcKgGKxT6coBXCIOOh3xQAx2MwhXyQzkITIQjNVigwFY6S+QbI4Tnko4bwneDoAH6/gnZnChkE67AEnUM6C6Q76AA14RWYHvKRfBpS+eMUM8yaC7o+2Rv60pO41tw2p4EEgMvHY1T8CTxC0OEQeLIQegpiSnHoAzElWCvFPyR2oVlDMglAwVeK7GMFSe7XGJleSSXWBbrqlQ4Gi/0OCVeZvTChojlJSuoTnxzDJrGa72gsd4raBprOr4uidgkyqtYORPiAZxvSvcGWGczDK5+apPYnELFwfcMlXEXOJVzPBkDMutjCZtErs76voIZqmj4WwE3pPly1prgrPNunLUmQB+m9V7NlRA7aimQGcvyMjhEuOYw2gkFQj+RfOdsQ6sKuSFy4fHGzKuD59yT8vO4L/6nzX3D7S7fbfPWfL/Q3Tt95/rDNvV23b3tTwSSih9/pZIWhmL9vF+/cErbWm4Z59g6r0FFAHVVEYJI9rEzbWvPrDF7nGkpdGvWXN0Oa+8/GI+GoZ/9RiItvAuF8RdECjgnp45aKRWqy9/jDrIRdgbvm36h7/bfT8GgtmCwuzGHbbcVfSvzG1d1mJ8Dt0SnfRzWCLV+sfGc1I6SNIuBMrMqFti4Y7Gwx9fjLy/fvs4KkHOCvQsgU5fUce8CadSQ/bfsXVZqqc6nRiefQhpdqGvXpRcx//d8ak+87nVvXYSvdty40484JYIGZfkR/j49qpD/+AFZNinLD59G7hDyLttDj9Mq/UnWSkGiojMtaNeNpx1KxKsafLmBZOmugHFGt/JRkvPphmSBFgvptbCC4EmL3Yty4YMDkm2aZRHLGFeG2Nvt1tpspKbg7C/aWYm9ted6twkzdzFI8xNRhieizPr3UQbT9ueSuhq0PzrZy78B 2 | -------------------------------------------------------------------------------- /drawio/双端对比.drawio: -------------------------------------------------------------------------------- 1 | 7Vldb5swFP01ltqHVoCxgceQ0u5hkyp10rZHJ7iEieDMcdKkv352bALE9ENdCFK6l8g+99rY5xzf8AHgeL6542Qx+8ZSWgDPSTcA3gDPQ9CVvwrYagCjQAMZz1MNuTXwkD9TAzoGXeUpXbYSBWOFyBdtcMrKkk5FCyOcs6d22iMr2lddkIxawMOUFDb6I0/FTKOhF9T4F5pns+rKLo50ZE6qZLOT5Yyk7KkBwQTAMWdM6NZ8M6aF4q7iRY+7fSG6XxinpXjPAF8PWJNiZfZm1iW21WZpKvduuoyLGctYSYqkRmPOVmVK1YyO7NU5XxlbSNCV4G8qxNYISVaCSWgm5oWJ2qs2G1myFZ+adZiVCcIzarKghtQKG8PMTu8om1PBtzKB04KIfN1WjxgTZPu8mifZMFR104aHYEmSw7c/1fhrH+2BXxK4cq6dCFbIzcZcQ/e2zd495bncLeUG/Cjz6NjM74aOOCfbRsKC5aVYNma+V4BMMIUEes61WYmpJC6Gbb8fjgjfGCEbehW1D/bbeZc1PPtEJQiEtyAcgyQAoxiMbhUif+MIJFg1Il81QghCpBpRpMBDhwm6EW1DcLrMn8lkl6DENHTJbBQDdCMRZaKl9pMaQIo8K2W7oI9qqjXlIpdVbWRgoYwYLxdkmpfZ950rr/zXTKImoJtXDWCiXoAOea+K+VNdQSMDzRrFs6qU/3Jco6GPqwMPj6vX22mF9mkNh6qTrjsw89C3mcenpL5y+em5h52FKJYlCIPEB3ECRkFHITqvsgPhYdmJsFV2cE9lx4VD1x0cNN0vzQ/d3syPOszvDVZ40MDUI88qPBidlHt/KO7RS3dAMfw0hQeiIQtPaClwQWR/cqmeiYGHC8XGhMtWplp1MFWGOS8lXMdp6eCF9n1nlw7wGH8AzgeFOCsBgrcFQH0JYD+NKYovJor/yw4BqkB/AnC9w1MqgNwBFeh6w6RpT/N1xXsFye2XlhY6JC/UjDbgjnnemLrDA++c/qx84aMBfYFf9MUj272xqWnGf1asClxp6kYyIVxs6lglEXUb0umZ+tdugKJaPVkb6SD0Lem8vh5rgv/SHVM6bJ+63qSz7wyPIl3+OZWLelNOduuPQPotdP0lDSZ/AQ== 2 | -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | provide, 4 | inject 5 | } from '../../lib/guide-mini-vue.esm.js' 6 | 7 | 8 | window.self = null; 9 | 10 | const Provider = { 11 | name: 'Provider', 12 | setup() { 13 | provide('foo', 'fooVal'); 14 | provide('bar', 'barVal'); 15 | }, 16 | render() { 17 | return h('div', {}, [ 18 | h('p', {}, "Provider Demo"), 19 | h(ProviderTwo) 20 | ]) 21 | }, 22 | } 23 | 24 | const ProviderTwo = { 25 | name: 'ProviderTwo', 26 | setup() { 27 | provide('foo', 'fooTwo') 28 | 29 | const foo = inject('foo') 30 | 31 | return { 32 | foo 33 | } 34 | }, 35 | render() { 36 | return h('div', {}, [ 37 | h('p', {}, "Provider Two Demo: " + this.foo), 38 | h(Consumer) 39 | ]) 40 | }, 41 | } 42 | 43 | const Consumer = { 44 | name: 'Consumer', 45 | setup() { 46 | const foo = inject('foo'); 47 | const bar = inject('bar'); 48 | // const baz = inject('baz', 'baz Default'); 49 | const baz = inject('baz', () => 'baz Default'); 50 | return { 51 | foo, 52 | bar, 53 | baz 54 | } 55 | }, 56 | render() { 57 | return h('div', {}, 'Consumer: -' + this.foo + ' - ' + this.bar + ' --- ' + this.baz) 58 | } 59 | } 60 | 61 | export default { 62 | name: 'App', 63 | setup() {}, 64 | render() { 65 | return h('div', {}, [ 66 | h('p', {}, 'api inject'), 67 | h(Provider) 68 | ]) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/apiInject/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | getCurrentInstance 4 | } from "../../lib/guide-mini-vue.esm.js" 5 | 6 | export const Foo = { 7 | // props is Readonly 8 | setup() { 9 | 10 | const instance = getCurrentInstance(); 11 | console.log('Foo: ', instance); 12 | 13 | return {} 14 | }, 15 | render() { 16 | return h('div', {}, 'foo') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/apiInject/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import App from './App.js' 5 | 6 | const rootContainer = document.querySelector('#app') 7 | createApp(App).mount(rootContainer) // ('#app') 8 | -------------------------------------------------------------------------------- /example/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | createTextNode 4 | } from "../../lib/guide-mini-vue.esm.js" 5 | import { 6 | Foo 7 | } from './Foo.js' 8 | 9 | export const App = { 10 | setup() { 11 | return { 12 | msg: 'fei miao' 13 | } 14 | }, 15 | render() { 16 | const app = h('div', {}, 'App init content'); 17 | /** 18 | * 场景 1 19 | * 26 | */ 27 | // const foo = h(Foo, {}, h('p', {}, '123')); 28 | /** 29 | * 场景 2 30 | * 39 | */ 40 | /** 41 | * 场景 3 具名插槽 42 | * 55 | */ 56 | // const foo = h(Foo, {}, [ 57 | // h('p', {}, 'header'), 58 | // h('p', {}, '456a'), 59 | // h('p', {}, 'footer') 60 | // ]); 61 | // const foo = h(Foo, {}, { 62 | // header: h('p', {}, 'header'), 63 | // footer: h('p', {}, 'footer'), 64 | // }); 65 | /** 66 | * 场景 3 作用域插槽 67 | * 80 | */ 81 | // const foo = h(Foo, {}, { 82 | // header: ({ 83 | // age 84 | // }) => h('p', {}, 'header' + age), 85 | // footer: () => h('p', {}, 'footer'), 86 | // }); 87 | /** 88 | * Slot 放入 Fragment 元素,与 textNode元素 89 | */ 90 | const foo = h(Foo, {}, { 91 | header: ({ 92 | age 93 | }) => [h('p', {}, 'header' + age), createTextNode('你好')], 94 | footer: () => h('p', {}, 'footer'), 95 | }); 96 | 97 | return h('div', {}, [app, foo]) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/componentSlots/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | renderSlots 4 | } from "../../lib/guide-mini-vue.esm.js" 5 | 6 | export const Foo = { 7 | // props is Readonly 8 | setup() { 9 | return {} 10 | }, 11 | render() { 12 | const foo = h('p', {}, 'foo-init component conent'); 13 | const age = 18; 14 | 15 | // this.$slots 16 | return h('div', {}, [ 17 | renderSlots(this.$slots, 'header', { 18 | age 19 | }), 20 | foo, 21 | renderSlots(this.$slots, 'footer'), 22 | ]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | // const rootContainer = document.querySelector('#app') 9 | createApp(App).mount('#app') // ('#app') 10 | -------------------------------------------------------------------------------- /example/componentUpdater/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/guide-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 | const changeChildProps = () => { 13 | msg.value = '456' 14 | } 15 | 16 | const changeCount = () => { 17 | count.value++; 18 | } 19 | 20 | return { msg, changeChildProps, changeCount, count } 21 | }, 22 | render() { 23 | return h('div', {}, [ 24 | h('div', {}, '你好'), 25 | h('button', { 26 | onClick: this.changeChildProps, 27 | }, 'change child props'), 28 | h(Child, { msg: this.msg }), 29 | h('button', { 30 | onCLick: this.changeCount 31 | }, 'change count'), 32 | h('p', {}, 'count: ' + this.count), 33 | ]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/componentUpdater/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js" 2 | 3 | export default { 4 | name: 'Child', 5 | setup(props, { emit }) { }, 6 | render(proxy) { 7 | return h('div', {}, [h('div', {}, 'child-props-msg: ' + this.$props.msg)]) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/componentUpdater/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | update 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/componentUpdater/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | 6 | createApp(App).mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/createRender/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h 3 | } from "../../lib/guide-mini-vue.esm.js" 4 | 5 | export const App = { 6 | setup() { 7 | return { 8 | x: 100, 9 | y: 100, 10 | } 11 | }, 12 | render() { 13 | return h('rect', { 14 | x: this.x, 15 | y: this.y 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/createRender/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/createRender/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRender 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | const game = new PIXI.Application({ 9 | width: 500, 10 | height: 500 11 | }) 12 | 13 | document.body.append(game.view) 14 | 15 | const renderer = createRender({ 16 | createElement(type) { 17 | if (type === 'rect') { 18 | const rect = new PIXI.Graphics(); 19 | rect.beginFill(0xff0000); 20 | rect.drawRect(0, 0, 100, 100); 21 | rect.endFill(); 22 | 23 | return rect; 24 | } 25 | }, 26 | patchProp(el, key, value) { 27 | el[key] = value 28 | }, 29 | insert(el, parent) { 30 | parent.addChild(el) 31 | }, 32 | }) 33 | 34 | 35 | renderer.createApp(App).mount(game.stage); 36 | // const rootContainer = document.querySelector('#app') 37 | // createApp(App).mount(rootContainer) // ('#app') 38 | -------------------------------------------------------------------------------- /example/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | getCurrentInstance 4 | } from '../../lib/guide-mini-vue.esm.js' 5 | import { 6 | Foo 7 | } from './Foo.js'; 8 | 9 | window.self = null; 10 | 11 | 12 | export const App = { 13 | name: 'App', 14 | render() { 15 | return h('div', {}, [ 16 | h('p', {}, "currentInstance Demo"), 17 | h(Foo) 18 | ]) 19 | }, 20 | setup() { 21 | const instance = getCurrentInstance(); 22 | console.log('APP: ', instance); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | getCurrentInstance 4 | } from "../../lib/guide-mini-vue.esm.js" 5 | 6 | export const Foo = { 7 | // props is Readonly 8 | setup() { 9 | 10 | const instance = getCurrentInstance(); 11 | console.log('Foo: ', instance); 12 | 13 | return {} 14 | }, 15 | render() { 16 | return h('div', {}, 'foo') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | const rootContainer = document.querySelector('#app') 9 | createApp(App).mount(rootContainer) // ('#app') 10 | -------------------------------------------------------------------------------- /example/helloWorld/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from '../../lib/guide-mini-vue.esm.js' 5 | import { 6 | Foo 7 | } from './Foo.js'; 8 | 9 | window.self = null; 10 | 11 | 12 | export const App = { 13 | // 主要是模拟一个 .vue 文件的基本实现内容 14 | render() { 15 | window.self = this; 16 | return h( 17 | 'div', { 18 | id: 'root', 19 | class: ['red', 'hard'], 20 | onClick: this.onClick 21 | }, 22 | [ 23 | h('div', { 24 | onClick: () => console.log('click'), 25 | }, 'hi, ' + this.msg), 26 | h(Foo, { 27 | count: 1, 28 | onAdd(a, b) { 29 | console.log('add', a, b) 30 | }, 31 | onAddFoo(a, b) { 32 | console.log('addFoo', a, b) 33 | } 34 | }) 35 | ] 36 | // 'hello, ' + this.msg 37 | // [ 38 | // h('p', { 39 | // class: 'red' 40 | // }, 'sec'), 41 | // h('p', { 42 | // class: 'blue' 43 | // }, [h('p', { 44 | // class: 'blue' 45 | // }, [h('p', { 46 | // class: 'blue' 47 | // }, 'thr')])]) 48 | // ] 49 | ); // --> 50 | }, 51 | setup() { 52 | 53 | const userState = ref({ 54 | name: 'Spider Man 🕷', 55 | age: 18, 56 | sex: 1, 57 | departments: ['公司', '以部门'] 58 | }) 59 | 60 | const onClick = () => { 61 | console.log(userState.value.age = 19) 62 | } 63 | 64 | return { 65 | userState, 66 | onClick, 67 | msg: 'vue view' 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/helloWorld/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h 3 | } from "../../lib/guide-mini-vue.esm.js" 4 | 5 | export const Foo = { 6 | // props is Readonly 7 | setup(props, { 8 | emit 9 | }) { 10 | const emitAdd = () => { 11 | emit('add', 1, 2) 12 | emit('add-foo', 1, 2) 13 | } 14 | 15 | return { 16 | emitAdd 17 | } 18 | }, 19 | render() { 20 | return h('button', { 21 | onClick: this.emitAdd, 22 | }, 'foo: ' + this.count) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/helloWorld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/helloWorld/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | const rootContainer = document.querySelector('#app') 9 | createApp(App).mount(rootContainer) // ('#app') 10 | -------------------------------------------------------------------------------- /example/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, h, ref, nextTick } from '../../lib/guide-mini-vue.esm.js' 2 | 3 | export default { 4 | name: 'App', 5 | setup() { 6 | const count = ref(1); 7 | const instance = getCurrentInstance(); 8 | function onClick() { 9 | for (let i = 0; i < 100; i++) { 10 | console.log('update'); 11 | count.value = i 12 | } 13 | console.log(instance); 14 | nextTick(() => { 15 | console.log(instance); 16 | }) 17 | 18 | // console.log(instance); 19 | } 20 | 21 | return { 22 | count, onClick 23 | } 24 | }, 25 | render() { 26 | const button = h('button', { onClick: this.onClick, }, 'update') 27 | const p = h('p', {}, 'count:' + this.count) 28 | 29 | return h('div', {}, [button, p]) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import App from './App.js' 5 | 6 | const rootContainer = document.querySelector('#app') 7 | createApp(App).mount(rootContainer) // ('#app') 8 | -------------------------------------------------------------------------------- /example/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | reactive 5 | } from "../../lib/guide-mini-vue.esm.js" 6 | 7 | import ArrayToText from './ArrayToText.js' 8 | import TextToText from './TextToText.js' 9 | import TextToArray from './TextToArray.js' 10 | import ArrayToArray from './ArrayToArray.js' 11 | 12 | export const App = { 13 | name: 'App', 14 | setup() { }, 15 | render() { 16 | return h('div', { 17 | tId: 1, 18 | class: 'parent-component' 19 | }, [ 20 | h('p', {}, '主页'), 21 | // array --> text 22 | // h(ArrayToText), 23 | // text --> text 24 | // h(TextToText), 25 | // text --> array 26 | // h(TextToArray), 27 | // array --> array 28 | h(ArrayToArray), 29 | ]) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from '../../lib/guide-mini-vue.esm.js' 5 | 6 | // 左侧对比 7 | // (a b) c 8 | // (a b) d e 9 | // const prevChildren = [ 10 | // h('div', { key: 'A', }, 'A'), 11 | // h('div', { key: 'B', }, 'B'), 12 | // h('div', { key: 'C', }, 'C'), 13 | // ] 14 | // const nextChildren = [ 15 | // h('div', { key: 'A', }, 'A'), 16 | // h('div', { key: 'B', }, 'B'), 17 | // h('div', { key: 'D', }, 'D'), 18 | // h('div', { key: 'E', }, 'E'), 19 | // ] 20 | 21 | // 右侧对比 22 | // a (b c) 23 | // d e (b c) 24 | // const prevChildren = [ 25 | // h('div', { key: 'A', }, 'A'), 26 | // h('div', { key: 'B', }, 'B'), 27 | // h('div', { key: 'C', }, 'C'), 28 | // ] 29 | // const nextChildren = [ 30 | // h('div', { key: 'D', }, 'D'), 31 | // h('div', { key: 'E', }, 'E'), 32 | // h('div', { key: 'B', }, 'B'), 33 | // h('div', { key: 'C', }, 'C'), 34 | // ] 35 | 36 | // 左侧对比 37 | // (a b) c 38 | // (a b) 39 | // const prevChildren = [ 40 | // h('div', { key: 'A', }, 'A'), 41 | // h('div', { key: 'B', }, 'B'), 42 | // ] 43 | // const nextChildren = [ 44 | // h('div', { key: 'D', }, 'D'), 45 | // h('div', { key: 'C', }, 'C'), 46 | // h('div', { key: 'A', }, 'A'), 47 | // h('div', { key: 'B', }, 'B'), 48 | // ] 49 | 50 | // 右侧对比 51 | // a (b c) 52 | // (b c) 53 | // const prevChildren = [ 54 | // h('div', { key: 'A', }, 'A'), 55 | // h('div', { key: 'B', }, 'B'), 56 | // h('div', { key: 'C', }, 'C'), 57 | // ] 58 | // const nextChildren = [ 59 | // h('div', { key: 'B', }, 'B'), 60 | // h('div', { key: 'C', }, 'C'), 61 | // ] 62 | 63 | 64 | // const prevChildren = [ 65 | // h('div', { key: 'A', }, 'A'), 66 | // h('div', { key: 'B', }, 'B'), 67 | // h('div', { key: 'C', }, 'C'), 68 | // h('div', { key: 'D', }, 'D'), 69 | // h('div', { key: 'F', }, 'F'), 70 | // h('div', { key: 'G', }, 'G'), 71 | // ] 72 | // const nextChildren = [ 73 | // h('div', { key: 'A', }, 'A'), 74 | // h('div', { key: 'B', }, 'B'), 75 | // h('div', { key: 'E', }, 'E'), 76 | // h('div', { key: 'C', nextChild: 'CC' }, 'C'), 77 | // h('div', { key: 'F', }, 'F'), 78 | // h('div', { key: 'G', }, 'G'), 79 | // ] 80 | 81 | // const prevChildren = [ 82 | // h('div', { key: 'A', }, 'A'), 83 | // h('div', { key: 'B', }, 'B'), 84 | // h('div', { key: 'C', }, 'C'), 85 | // h('div', { key: 'E', }, 'E'), 86 | // h('div', { key: 'D', }, 'D'), 87 | // h('div', { key: 'F', }, 'F'), 88 | // h('div', { key: 'G', }, 'G'), 89 | // ] 90 | // const nextChildren = [ 91 | // h('div', { key: 'A', }, 'A'), 92 | // h('div', { key: 'B', }, 'B'), 93 | // h('div', { key: 'E', }, 'E'), 94 | // h('div', { key: 'C', nextChild: 'CC' }, 'C'), 95 | // h('div', { key: 'F', }, 'F'), 96 | // h('div', { key: 'G', }, 'G'), 97 | // ] 98 | 99 | // const prevChildren = [ 100 | // h('div', { key: 'A', }, 'A'), 101 | // h('div', { key: 'B', }, 'B'), 102 | // h('div', { key: 'C', }, 'C'), 103 | // h('div', { key: 'D', }, 'D'), 104 | // h('div', { key: 'E', }, 'E'), 105 | // h('div', { key: 'F', }, 'F'), 106 | // h('div', { key: 'G', }, 'G'), 107 | // ] 108 | // const nextChildren = [ 109 | // h('div', { key: 'A', }, 'A'), 110 | // h('div', { key: 'B', }, 'B'), 111 | // h('div', { key: 'E', }, 'E'), 112 | // h('div', { key: 'C', nextChild: 'CC' }, 'C'), 113 | // h('div', { key: 'D', }, 'D'), 114 | // h('div', { key: 'F', }, 'F'), 115 | // h('div', { key: 'G', }, 'G'), 116 | // ] 117 | 118 | /* 最长子序列 */ 119 | // const prevChildren = [ 120 | // h('div', { key: 'A', }, 'A'), 121 | // h('div', { key: 'B', }, 'B'), 122 | // h('div', { key: 'C', }, 'C'), 123 | // // h('div', { key: 'D', }, 'D'), 124 | // h('div', { key: 'E', }, 'E'), 125 | // h('div', { key: 'F', }, 'F'), 126 | // h('div', { key: 'G', }, 'G'), 127 | // ] 128 | // const nextChildren = [ 129 | // h('div', { key: 'A', }, 'A'), 130 | // h('div', { key: 'B', }, 'B'), 131 | // h('div', { key: 'E', }, 'E'), 132 | // h('div', { key: 'C', nextChild: 'CC' }, 'C'), 133 | // h('div', { key: 'D', }, 'D'), 134 | // h('div', { key: 'F', }, 'F'), 135 | // h('div', { key: 'G', }, 'G'), 136 | // ] 137 | 138 | // const prevChildren = [ 139 | // h('div', { key: 'A', }, 'A'), 140 | // h('div', { key: 'B', }, 'B'), 141 | // h('div', { key: 'C', }, 'C'), 142 | // h('div', { key: 'D', }, 'D'), 143 | // h('div', { key: 'E', }, 'E'), 144 | // h('div', { key: 'Z', }, 'Z'), 145 | // h('div', { key: 'F', }, 'F'), 146 | // h('div', { key: 'G', }, 'G'), 147 | // ] 148 | // const nextChildren = [ 149 | // h('div', { key: 'A', }, 'A'), 150 | // h('div', { key: 'B', }, 'B'), 151 | // h('div', { key: 'D', }, 'D'), 152 | // h('div', { key: 'C', nextChild: 'CC' }, 'C'), 153 | // h('div', { key: 'Y', }, 'Y'), 154 | // h('div', { key: 'E', }, 'E'), 155 | // h('div', { key: 'F', }, 'F'), 156 | // h('div', { key: 'G', }, 'G'), 157 | // ] 158 | 159 | const prevChildren = [ 160 | h('div', { key: 'A', }, 'A'), 161 | h('div', { key: 'C', }, 'C'), 162 | h('div', { key: 'B', }, 'B'), 163 | h('div', { key: 'D', }, 'D'), 164 | ] 165 | const nextChildren = [ 166 | h('div', { key: 'A', }, 'A'), 167 | h('div', { key: 'B', }, 'B'), 168 | h('div', { key: 'C', }, 'C'), 169 | h('div', { key: 'D', }, 'D'), 170 | ] 171 | 172 | export default { 173 | name: 'ArrayToText', 174 | setup() { 175 | const isChange = ref(false) 176 | window.isChange = isChange 177 | 178 | return { 179 | isChange, 180 | } 181 | }, 182 | render() { 183 | const self = this 184 | return self.isChange === true ? h('div', {}, nextChildren) : h('div', {}, prevChildren) 185 | }, 186 | } 187 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from "../../lib/guide-mini-vue.esm.js"; 5 | 6 | const nextChildren = 'newChildren'; 7 | const prevChildren = [ 8 | h('div', { 9 | class: 'child-component' 10 | }, "A"), 11 | h('div', { 12 | class: 'child-component' 13 | }, "B") 14 | ] 15 | 16 | export default { 17 | name: 'ArrayToText', 18 | setup() { 19 | const isChange = ref(false); 20 | window.isChange = isChange; 21 | 22 | return { 23 | isChange 24 | } 25 | }, 26 | render() { 27 | const self = this; 28 | return self.isChange === true ? 29 | h('div', {}, nextChildren) : 30 | h('div', {}, prevChildren) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from "../../lib/guide-mini-vue.esm.js"; 5 | 6 | const prevChildren = 'newChildren'; 7 | const nextChildren = [ 8 | h('div', {}, "A"), 9 | h('div', {}, "B") 10 | ] 11 | 12 | export default { 13 | name: 'ArrayToText', 14 | setup() { 15 | const isChange = ref(false); 16 | window.isChange = isChange; 17 | 18 | return { 19 | isChange 20 | } 21 | }, 22 | render() { 23 | const self = this; 24 | return self.isChange === true ? 25 | h('div', {}, nextChildren) : 26 | h('div', {}, prevChildren) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from "../../lib/guide-mini-vue.esm.js"; 5 | 6 | const prevChildren = 'oldChildren' 7 | const nextChildren = 'newChildren'; 8 | 9 | export default { 10 | name: 'ArrayToText', 11 | setup() { 12 | const isChange = ref(false); 13 | window.isChange = isChange; 14 | 15 | return { 16 | isChange 17 | } 18 | }, 19 | render() { 20 | const self = this; 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 | 8 | 更新 props children 9 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | const rootContainer = document.querySelector('#app') 9 | createApp(App).mount(rootContainer) // ('#app') 10 | -------------------------------------------------------------------------------- /example/update/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref 4 | } from "../../lib/guide-mini-vue.esm.js" 5 | 6 | export const App = { 7 | name: 'App', 8 | setup() { 9 | const count = ref(0); 10 | const onClick = () => { 11 | count.value++; 12 | } 13 | 14 | const props = ref({ 15 | foo: 'foo', 16 | bar: 'bar' 17 | }) 18 | const onChangePropsDemo1 = () => { 19 | props.value.foo = 'new-foo'; 20 | } 21 | const onChangePropsDemo2 = () => { 22 | props.value.foo = undefined 23 | } 24 | const onChangePropsDemo3 = () => { 25 | props.value = { 26 | foo: 'foo' 27 | } 28 | } 29 | 30 | return { 31 | count, 32 | props, 33 | onClick, 34 | onChangePropsDemo1, 35 | onChangePropsDemo2, 36 | onChangePropsDemo3, 37 | } 38 | }, 39 | render() { 40 | return h('div', { 41 | id: 'root', 42 | ...this.props, 43 | }, [ 44 | h('div', {}, 'count: ' + this.count), 45 | h('button', { 46 | onClick: this.onClick 47 | }, 'click'), 48 | h( 49 | 'button', { 50 | onClick: this.onChangePropsDemo1 51 | }, 52 | 'changeProps - 值改变了 - 修改' 53 | ), 54 | h( 55 | 'button', { 56 | onClick: this.onChangePropsDemo2 57 | }, 58 | 'changeProps - 值为Undefined' 59 | ), 60 | h( 61 | 'button', { 62 | onClick: this.onChangePropsDemo3 63 | }, 64 | 'changeProps - onChangePropsDemo3' 65 | ) 66 | ]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | view vue 9 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/update/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp 3 | } from '../../lib/guide-mini-vue.esm.js' 4 | import { 5 | App 6 | } from './App.js' 7 | 8 | const rootContainer = document.querySelector('#app') 9 | createApp(App).mount(rootContainer) // ('#app') 10 | -------------------------------------------------------------------------------- /getSequence.ts: -------------------------------------------------------------------------------- 1 | function getSequence(arr) { 2 | const p = arr.slice() 3 | const result = [0] 4 | let i, j, u, v, c 5 | const len = arr.length 6 | for (i = 0; i < len; i++) { 7 | const arrI = arr[i] 8 | if (arrI !== 0) { 9 | j = result[result.length - 1] 10 | if (arr[j] < arrI) { 11 | p[i] = j 12 | result.push(i) 13 | continue 14 | } 15 | u = 0 16 | v = result.length - 1 17 | while (u < v) { 18 | c = (u + v) >> 1 19 | if (arr[result[c]] < arrI) { 20 | u = c + 1 21 | } else { 22 | v = c 23 | } 24 | } 25 | if (arrI < arr[result[u]]) { 26 | if (u > 0) { 27 | p[i] = result[u - 1] 28 | } 29 | result[u] = i 30 | } 31 | } 32 | } 33 | u = result.length 34 | v = result[u - 1] 35 | while (u-- > 0) { 36 | result[u] = v 37 | v = p[v] 38 | } 39 | return result 40 | } 41 | 42 | const r = getSequence([4, 2, 3, 1, 5]) 43 | 44 | console.log(r) 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "view-vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "packageManager": "pnpm", 6 | "main": "lib/guide-mini-vue.cjs.js", 7 | "module": "lib/guide-mini-vue.esm.js", 8 | "scripts": { 9 | "test": "jest", 10 | "build": "rollup -c rollup.config.js" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.17.8", 16 | "@babel/preset-env": "^7.16.11", 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.3" 24 | }, 25 | "dependencies": { 26 | "@babel/preset-typescript": "^7.16.7" 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 | // cjs 8 | { 9 | format: 'cjs', 10 | file: pkg.main 11 | }, 12 | { 13 | format: 'es', 14 | file: pkg.module 15 | } 16 | ], 17 | plugins: [ 18 | typescript() 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/compiler-core/__tests__/parse.sepc.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../src/ast" 2 | import { baseParse } from "../src/parse" 3 | 4 | describe('happy path', () => { 5 | describe('interpolation', () => { 6 | test('simple interpolation', () => { 7 | const ast = baseParse("{{ message }}") 8 | 9 | expect(ast.children[0]).toStrictEqual({ 10 | type: NodeTypes.INTERPOLATION, 11 | content: { 12 | type: NodeTypes.SIMPLE_EXPRESSION, 13 | content: 'message' 14 | } 15 | }) 16 | }) 17 | }) 18 | 19 | describe('element', () => { 20 | test('simple element', () => { 21 | const ast = baseParse("
") 22 | 23 | expect(ast.children[0]).toStrictEqual({ 24 | type: NodeTypes.ELEMENT, 25 | tag: 'div', 26 | children: [] 27 | }) 28 | }) 29 | }) 30 | 31 | describe('text', () => { 32 | test('simple text', () => { 33 | const ast = baseParse("foo") 34 | 35 | expect(ast.children[0]).toStrictEqual({ 36 | type: NodeTypes.TEXT, 37 | content: 'foo' 38 | }) 39 | }) 40 | }) 41 | 42 | test('hello world', () => { 43 | const ast = baseParse('
hi,{{message}}
'); 44 | const ast2 = baseParse('

hi,{{message}}

'); 45 | 46 | expect(ast.children[0]).toStrictEqual({ 47 | type: NodeTypes.ELEMENT, 48 | tag: 'div', 49 | children: [ 50 | { 51 | type: NodeTypes.TEXT, 52 | content: 'hi,' 53 | }, 54 | { 55 | type: NodeTypes.INTERPOLATION, 56 | content: { 57 | type: NodeTypes.SIMPLE_EXPRESSION, 58 | content: 'message' 59 | } 60 | } 61 | ] 62 | }) 63 | 64 | expect(ast2.children[0]).toStrictEqual({ 65 | type: NodeTypes.ELEMENT, 66 | tag: 'p', 67 | children: [ 68 | { 69 | type: NodeTypes.TEXT, 70 | content: 'hi,' 71 | }, 72 | { 73 | type: NodeTypes.INTERPOLATION, 74 | content: { 75 | type: NodeTypes.SIMPLE_EXPRESSION, 76 | content: 'message' 77 | } 78 | } 79 | ] 80 | }) 81 | }) 82 | 83 | test('Nested element ', () => { 84 | const ast = baseParse('

hi,

{{message}}
') 85 | 86 | expect(ast.children[0]).toStrictEqual({ 87 | type: NodeTypes.ELEMENT, 88 | tag: 'div', 89 | children: [ 90 | { 91 | type: NodeTypes.ELEMENT, 92 | tag: 'p', 93 | children: [ 94 | { 95 | type: NodeTypes.TEXT, 96 | content: 'hi,' 97 | }, 98 | ] 99 | }, 100 | { 101 | type: NodeTypes.INTERPOLATION, 102 | content: { 103 | type: NodeTypes.SIMPLE_EXPRESSION, 104 | content: 'message' 105 | } 106 | } 107 | ] 108 | }) 109 | }) 110 | 111 | test('should throw error when lack end tag', () => { 112 | expect(() => { 113 | baseParse('
') 114 | }).toThrow(`缺少Element close code`); 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /src/compiler-core/__tests__/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast'; 2 | import { baseParse } from '../src/parse'; 3 | import {transform} from '../src/transform'; 4 | describe('transform', () => { 5 | test('happy path', () => { 6 | const ast = baseParse('
hi, {{message}}
'); 7 | const plugin = (node ) => { 8 | if(node.type === NodeTypes.TEXT) { 9 | node.content = node.content + 'mini-vue' 10 | } 11 | } 12 | 13 | transform(ast, { 14 | nodeTransforms: [plugin] 15 | }); 16 | 17 | const nodeText = ast.children[0].children[0].content 18 | 19 | expect(nodeText).toBe('hi, mini-vue') 20 | }) 21 | 22 | }) 23 | -------------------------------------------------------------------------------- /src/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | export const enum NodeTypes { 2 | INTERPOLATION, 3 | SIMPLE_EXPRESSION, 4 | ELEMENT, 5 | TEXT 6 | } 7 | -------------------------------------------------------------------------------- /src/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | 3 | const enum TagType { 4 | Start, End 5 | } 6 | 7 | export function baseParse(content: string) { 8 | const context = createParserContext(content); 9 | 10 | return createRoot(parserChildren(context, [])); 11 | } 12 | 13 | /** 14 | * 将数据格式化后统一处理 15 | * 16 | * @param {string} content 17 | * @return { source: string } 18 | */ 19 | function createParserContext(content: string) { 20 | return { 21 | source: content 22 | } 23 | } 24 | 25 | function createRoot(children) { 26 | return { 27 | children 28 | } 29 | } 30 | 31 | /** 32 | * 更新未解析的模板内容,大致过程如下 33 | * 34 | * 模板内容
hi, {{ message }}
35 | * 36 | * 37 | * 38 | * @param {*} context 39 | * @param {number} length 40 | */ 41 | function advanceBy(context: any, length: number) { 42 | context.source = context.source.slice(length); 43 | } 44 | 45 | /** 46 | * 解析模板入口 47 | * 48 | * @param {*} context 49 | * @return {*} 50 | */ 51 | function parserChildren(context, ancestors) { 52 | const nodes: any = []; 53 | while (!isEnd(context, ancestors)) { 54 | let node; 55 | const s = context.source; 56 | 57 | if (s.startsWith('{{')) { 58 | node = parserInterPolation(context); 59 | } else if (s[0] === '<' && /[a-z]/i.test(s[1])) { 60 | node = parserElement(context, ancestors) 61 | } 62 | 63 | if (!node) { 64 | node = parseText(context) 65 | } 66 | 67 | nodes.push(node); 68 | } 69 | 70 | return nodes 71 | } 72 | 73 | function isEnd(context, ancestors) { 74 | // 1. 存在结束标签 75 | const s = context.source 76 | 77 | if (s.startsWith("= 0 ; i--) { 79 | const tag = ancestors[i].tag 80 | if (startsWithEndTagOpen(s, tag)) { 81 | return true 82 | } 83 | } 84 | } 85 | // if (parentTag && s.startsWith(``)) { 86 | // return true 87 | // } 88 | // 2. source有值 89 | return !s 90 | 91 | } 92 | 93 | /** 94 | * 解析数据节点 95 | * 96 | * @param {*} context 97 | * @return {*} 98 | */ 99 | function parserInterPolation(context) { 100 | const openDelimiter = "{{"; 101 | const closeDelimiter = "}}"; 102 | const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length); 103 | 104 | advanceBy(context, openDelimiter.length) 105 | 106 | const rawContentLength = closeIndex - openDelimiter.length; 107 | const rawContent = parseTextData(context, rawContentLength) 108 | const content = rawContent.trim() 109 | advanceBy(context, closeDelimiter.length) 110 | return { 111 | type: NodeTypes.INTERPOLATION, // 'interpolation', 112 | content: { 113 | type: NodeTypes.SIMPLE_EXPRESSION, 114 | content: content 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * 解析DOM 121 | * 122 | * @param {*} context 123 | * @return {*} 124 | */ 125 | function parserElement(context: any, ancestors) { 126 | const element: any = parserTag(context, TagType.Start); 127 | ancestors.push(element); 128 | element.children = parserChildren(context, ancestors); 129 | ancestors.pop(); 130 | if (startsWithEndTagOpen(context.source, element.tag)) { 131 | parserTag(context, TagType.End); 132 | } else { 133 | throw new Error(`缺少Element close code`) 134 | } 135 | return element; 136 | } 137 | 138 | function startsWithEndTagOpen(source, tag) { 139 | return source.startsWith(' index) { 171 | 172 | endIndex = index 173 | } 174 | } 175 | 176 | 177 | 178 | const content = parseTextData(context, endIndex); 179 | 180 | return { 181 | type: NodeTypes.TEXT, 182 | content 183 | } 184 | } 185 | 186 | 187 | /** 188 | * 获取文本信息,推进解π析长度 189 | * 190 | * @param {*} context 191 | * @param {*} length 192 | * @return {*} 193 | */ 194 | function parseTextData(context, length) { 195 | const content = context.source.slice(0, length); 196 | 197 | advanceBy(context, length) 198 | 199 | return content 200 | } 201 | 202 | 203 | -------------------------------------------------------------------------------- /src/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | 3 | export function transform(root, options) { 4 | 5 | const context = createTransformContext(root, options) 6 | // 1. 深度优先搜索 7 | traverseNode(root, context) 8 | // 2. 修改 content text 9 | } 10 | 11 | function createTransformContext(root: any, options: any) { 12 | const context = { 13 | root, 14 | nodeTransforms: options.nodeTransforms || [] 15 | } 16 | 17 | return context 18 | } 19 | 20 | function traverseNode(node: any,context) { 21 | 22 | const nodeTransforms = context.nodeTransforms 23 | 24 | for (let i = 0; i < nodeTransforms.length; i++) { 25 | const transform = nodeTransforms[i]; 26 | transform(node); 27 | } 28 | 29 | console.log(node) 30 | 31 | // if(node.type === NodeTypes.TEXT) { 32 | // node.content = node.content + 'mini-vue' 33 | // } 34 | 35 | traverseChildren(node, context); 36 | } 37 | 38 | 39 | function traverseChildren(node: any, context: any) { 40 | const children = node.children; 41 | 42 | if (children) { 43 | for (let i = 0; i < children.length; i++) { 44 | const node = children[i]; 45 | traverseNode(node, context); 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // project output 2 | export * from './runtime-dom' 3 | export * from './reactivity' 4 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../computed"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("reactivity/computed", () => { 5 | it("happy path", () => { 6 | const value = reactive({ 7 | foo: 1, 8 | }); 9 | const cValue = computed(() => { 10 | return value.foo; 11 | }); 12 | 13 | expect(cValue.value).toBe(1); 14 | // value.foo = 2; 15 | // expect(cValue.value).toBe(2); 16 | }); 17 | 18 | it("should computed lazily", () => { 19 | const value = reactive({ 20 | foo: 1, 21 | }); 22 | const getter = jest.fn(() => { 23 | return value.foo; 24 | }); 25 | const cValue = computed(getter); 26 | 27 | // lazy 28 | expect(getter).not.toHaveBeenCalled(); 29 | 30 | expect(cValue.value).toBe(1); 31 | expect(getter).toHaveBeenCalledTimes(1); 32 | 33 | cValue.value; 34 | expect(getter).toHaveBeenCalledTimes(1); 35 | 36 | // // // should not computed again 37 | value.foo = 2; 38 | expect(getter).toHaveBeenCalledTimes(1); 39 | 40 | // // // now it should compute 41 | expect(cValue.value).toBe(2); 42 | expect(getter).toHaveBeenCalledTimes(2); 43 | 44 | cValue.value; 45 | expect(getter).toBeCalledTimes(2); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive"; 2 | import { effect, stop } from "../effect"; 3 | 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 10, 8 | }); 9 | let nextAge; 10 | effect(() => { 11 | nextAge = user.age + 1; 12 | }); 13 | expect(nextAge).toBe(11); 14 | }); 15 | 16 | it("should ruturn runner when call effect", () => { 17 | let foo = 10; 18 | const runner = effect(() => { 19 | foo++; 20 | return "foo"; 21 | }); 22 | expect(foo).toBe(11); 23 | const r = runner(); 24 | expect(foo).toBe(12); 25 | expect(r).toBe("foo"); 26 | }); 27 | 28 | it("scheduler", () => { 29 | let dummy; 30 | let run: any; 31 | const scheduler = jest.fn(() => { 32 | run = runner; 33 | }); 34 | const obj = reactive({ foo: 1 }); 35 | const runner = effect( 36 | () => { 37 | dummy = obj.foo; 38 | }, 39 | { 40 | scheduler, 41 | } 42 | ); 43 | expect(scheduler).not.toHaveBeenCalled(); 44 | expect(dummy).toBe(1); 45 | obj.foo++; 46 | expect(scheduler).toHaveBeenCalledTimes(1); 47 | expect(dummy).toBe(1); 48 | run(); 49 | expect(dummy).toBe(2); 50 | }); 51 | 52 | it("stop", () => { 53 | let dummy; 54 | const obj = reactive({ prop: 1 }); 55 | const runner = effect(() => { 56 | dummy = obj.prop; 57 | }); 58 | obj.prop = 2; 59 | expect(dummy).toBe(2); 60 | stop(runner); 61 | // obj.prop = 3; 62 | obj.prop++; 63 | expect(dummy).toBe(2); 64 | runner(); 65 | expect(dummy).toBe(3); 66 | }); 67 | 68 | it("onStop", () => { 69 | const obj = reactive({ foo: 1 }); 70 | const onStop = jest.fn(); 71 | let dummy; 72 | const runner = effect( 73 | () => { 74 | dummy = obj.foo; 75 | }, 76 | { 77 | onStop, 78 | } 79 | ); 80 | stop(runner); 81 | expect(onStop).toBeCalledTimes(1); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive, isReactive, isProxy } from "../reactive"; 2 | 3 | describe("reactive", () => { 4 | it("happy path", () => { 5 | const original = { foo: 1 }; 6 | const observed = reactive(original); 7 | expect(observed).not.toBe(original); 8 | expect(observed.foo).toBe(1); 9 | expect(isReactive(observed)).toBe(true); 10 | expect(isReactive(original)).toBe(false); 11 | 12 | expect(isProxy(observed)).toBe(true); 13 | expect(isProxy(original)).toBe(false); 14 | }); 15 | 16 | it("nested reactive", () => { 17 | const original = { 18 | nested: { 19 | foo: 1, 20 | }, 21 | array: [ 22 | { 23 | bar: 2, 24 | }, 25 | ], 26 | }; 27 | const observed = reactive(original); 28 | expect(isReactive(observed.nested)).toBe(true); 29 | expect(isReactive(observed.array)).toBe(true); 30 | expect(isReactive(observed.array[0])).toBe(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, readonly, isProxy } from "../reactive"; 2 | 3 | describe("readonly", () => { 4 | it("should make wrapped values readonly", () => { 5 | const original = { foo: 1, bar: { baz: 2, array: [{ baa: 3 }] } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | expect(isReadonly(wrapped)).toBe(true); 9 | expect(isReadonly(original)).toBe(false); 10 | 11 | expect(isReadonly(wrapped.bar)).toBe(true); 12 | expect(isReadonly(wrapped.bar.array)).toBe(true); 13 | expect(isReadonly(original.bar)).toBe(false); 14 | 15 | expect(isProxy(wrapped)).toBe(true); 16 | 17 | expect(wrapped.foo).toBe(1); 18 | }); 19 | 20 | it("warn then call set", () => { 21 | console.warn = jest.fn(); 22 | 23 | const user = readonly({ 24 | age: 10, 25 | }); 26 | user.age = 11; 27 | expect(console.warn).toBeCalled(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | import { ref, isRef, unRef, proxyRefs } from "../ref"; 4 | 5 | describe("ref", () => { 6 | it("happy path", () => { 7 | const a = ref(1); 8 | expect(a.value).toBe(1); 9 | }); 10 | 11 | it("should be reactive", () => { 12 | const a = ref(1); 13 | let dummy; 14 | let calls = 0; 15 | 16 | effect(() => { 17 | calls++; 18 | dummy = a.value; 19 | }); 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 | 27 | // same value should not trigger 28 | a.value = 2; 29 | expect(calls).toBe(2); 30 | expect(dummy).toBe(2); 31 | }); 32 | 33 | it("should make nested properties reactive", () => { 34 | const a = ref({ 35 | count: 1, 36 | }); 37 | let dummy; 38 | effect(() => { 39 | dummy = a.value.count; 40 | }); 41 | 42 | expect(dummy).toBe(1); 43 | a.value.count = 2; 44 | expect(dummy).toBe(2); 45 | }); 46 | 47 | 48 | 49 | it("Object Value Change", () => { 50 | const a = ref({ 51 | foo: 'foo', 52 | bar: 'bar' 53 | }); 54 | 55 | let dummy; 56 | effect(() => { 57 | dummy = a.value.foo 58 | }); 59 | 60 | expect(dummy).toBe('foo'); 61 | a.value.foo = 'new-foo'; 62 | expect(dummy).toBe('new-foo'); 63 | }); 64 | 65 | // isRef unRef 66 | it("isRef", () => { 67 | const a = ref(1); 68 | const b = 1; 69 | const user = reactive({ age: 1 }); 70 | 71 | expect(isRef(a)).toBe(true); 72 | expect(isRef(b)).toBe(false); 73 | expect(isRef(user)).toBe(false); 74 | }); 75 | 76 | it("unRef", () => { 77 | const a = ref(1); 78 | const b = 1; 79 | 80 | expect(unRef(a)).toBe(1); 81 | expect(unRef(b)).toBe(1); 82 | }); 83 | 84 | it("proxyRefs", () => { 85 | const user = { 86 | age: ref(1), 87 | name: "zhangsan", 88 | }; 89 | const proxyUser = proxyRefs(user); 90 | expect(user.age.value).toBe(1); 91 | expect(proxyUser.age).toBe(1); 92 | expect(proxyUser.name).toBe("zhangsan"); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../reactive"; 2 | 3 | describe("shallowReadonly", () => { 4 | it("should not make non-reactive properties reactive", () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }); 6 | expect(isReadonly(props)).toBe(true); 7 | expect(isReadonly(props.n)).toBe(false); 8 | }); 9 | 10 | it("warn in shallowReadonly", () => { 11 | console.warn = jest.fn(); 12 | 13 | const user = shallowReadonly({ 14 | age: 10, 15 | }); 16 | user.age = 11; 17 | expect(console.warn).toBeCalled(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/reactivity/baseHandler.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "../shared/index"; 2 | import { tigger, track } from "./effect"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | 5 | const get = createGetter(); 6 | const readonlyGet = createGetter(true); 7 | const shallowReadonlyGet = createGetter(true, true); 8 | 9 | const set = createSetter(); 10 | 11 | function createGetter(isReadonly = false, shallow = false) { 12 | return function get(target, key) { 13 | if (key === ReactiveFlags.IS_REACTIVE) { 14 | console.log('调用了 isReactive API') 15 | return !isReadonly; 16 | } else if (key === ReactiveFlags.IS_READONLY) { 17 | console.log('调用了 isReadonly API') 18 | return isReadonly; 19 | } 20 | console.log('设置getter') 21 | const res = Reflect.get(target, key); 22 | 23 | if (shallow) { 24 | console.log('浅响应对象') 25 | return res; 26 | } 27 | 28 | if (isObject(res)) { 29 | console.log(isReadonly ? '是一个只读的响应式对象' : '是一个响应式对象') 30 | return isReadonly ? readonly(res) : reactive(res); 31 | } 32 | 33 | if (!isReadonly) { 34 | console.log('%c开始依赖收集', 'color:green;') 35 | track(target, key); 36 | } 37 | return res; 38 | }; 39 | } 40 | 41 | function createSetter() { 42 | return function set(target, key, value) { 43 | const res = Reflect.set(target, key, value); 44 | tigger(target, key); 45 | return res; 46 | }; 47 | } 48 | 49 | export const mutableHanlders = { 50 | get, 51 | set, 52 | }; 53 | 54 | export const readonlyHanlders = { 55 | get: readonlyGet, 56 | set(target, key, value) { 57 | console.warn(`key:${key} 不可被set`); 58 | return true; 59 | }, 60 | }; 61 | 62 | export const shallowOnlyHandlers = extend({}, readonlyHanlders, { 63 | get: shallowReadonlyGet, 64 | }); 65 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _getter: any; 5 | private _dirty: boolean = true; 6 | private _value: any; 7 | private _effect: any; 8 | constructor(getter) { 9 | this._getter = getter; 10 | this._effect = new ReactiveEffect(getter, () => { 11 | if (!this._dirty) { 12 | this._dirty = true; 13 | } 14 | }); 15 | } 16 | get value() { 17 | // 当依赖项的响应式对象发生改变时候 收集这个依赖 18 | if (this._dirty) { 19 | this._dirty = false; 20 | this._value = this._effect.run(); 21 | } 22 | 23 | return this._value; 24 | } 25 | } 26 | 27 | export function computed(getter: () => void) { 28 | return new ComputedRefImpl(getter); 29 | } 30 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared/index"; 2 | let activeEffect; 3 | let shouldTrack; 4 | export class ReactiveEffect { 5 | private _fn: any; 6 | deps: any = []; 7 | active = true; 8 | onStop?: () => void; 9 | constructor(fn, public scheduler?) { 10 | this._fn = fn; 11 | } 12 | run() { 13 | if (!this.active) { 14 | return this._fn(); 15 | } 16 | shouldTrack = true; 17 | activeEffect = this; 18 | 19 | const result = this._fn(); 20 | 21 | shouldTrack = false; 22 | 23 | return result; 24 | } 25 | stop() { 26 | if (this.active) { 27 | cleanUpEffect(this); 28 | if (this.onStop) { 29 | this.onStop(); 30 | } 31 | this.active = false; 32 | } 33 | } 34 | } 35 | 36 | function cleanUpEffect(effect) { 37 | effect.deps.forEach((dep) => { 38 | dep.delete(effect); 39 | }); 40 | effect.deps.length = 0; 41 | } 42 | 43 | /////////////////////////////////收集依赖///////////////////////////////// 44 | let targetMap = new Map(); 45 | console.log( 46 | `%c初始依赖集合`, 47 | 'color: blue', 48 | targetMap, // Console Message 49 | ); 50 | export function track(target, key) { 51 | if (!isTracking()) return; 52 | 53 | let depsMap = targetMap.get(target); 54 | // 初始化 depsMap 55 | if (!depsMap) { 56 | depsMap = new Map(); 57 | targetMap.set(target, depsMap); 58 | console.log('初始化DepsMap') 59 | } 60 | let dep = depsMap.get(key); 61 | // 初始化dep 62 | if (!dep) { 63 | dep = new Set(); 64 | depsMap.set(key, dep); 65 | console.log('初始化Dep') 66 | } 67 | trackEffect(dep); 68 | } 69 | 70 | export function trackEffect(dep) { 71 | console.log('收集Dep') 72 | if (dep.has(activeEffect)) { 73 | console.log('已经存在Dep, 跳过收集') 74 | 75 | return 76 | }; 77 | 78 | dep.add(activeEffect); 79 | activeEffect.deps.push(dep); 80 | console.log('收集完成', targetMap) 81 | } 82 | 83 | export function isTracking() { 84 | console.log('%c是否需要收集', 'color: green', shouldTrack && activeEffect !== undefined) 85 | return shouldTrack && activeEffect !== undefined; 86 | } 87 | 88 | /////////////////////////////////执行依赖///////////////////////////////// 89 | export function tigger(target, key) { 90 | console.log('%c执行依赖集合', 'color:#00a0ea') 91 | let depsMap = targetMap.get(target); 92 | let dep = depsMap.get(key); 93 | 94 | triggerEffect(dep); 95 | } 96 | 97 | export function triggerEffect(dep) { 98 | // console.log('触发依赖') 99 | for (const effect of dep) { 100 | if (effect.scheduler) { 101 | effect.scheduler(); 102 | } else { 103 | effect.run(); 104 | } 105 | } 106 | } 107 | 108 | export function effect(fn, options: any = {}) { 109 | // fn() 110 | 111 | const _effect = new ReactiveEffect(fn, options.scheduler); 112 | extend(_effect, options); 113 | _effect.run(); 114 | 115 | const runner: any = _effect.run.bind(_effect); 116 | runner.effect = _effect; 117 | return runner; 118 | } 119 | 120 | export function stop(runner) { 121 | runner.effect.stop(); 122 | } 123 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./reactive"; 2 | export * from "./effect"; 3 | export * from "./ref"; 4 | export * from "./computed"; 5 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "../shared/index"; 2 | import { 3 | mutableHanlders, 4 | readonlyHanlders, 5 | shallowOnlyHandlers, 6 | } from "./baseHandler"; 7 | 8 | export const enum ReactiveFlags { 9 | IS_REACTIVE = "__v_isReactive", 10 | IS_READONLY = "__v_isReadonly", 11 | } 12 | 13 | export function reactive(raw) { 14 | console.log('传入的参数', raw) 15 | return createReactiveObject(raw, mutableHanlders); 16 | } 17 | 18 | export function readonly(raw) { 19 | return createReactiveObject(raw, readonlyHanlders); 20 | } 21 | 22 | export function shallowReadonly(raw) { 23 | return createReactiveObject(raw, shallowOnlyHandlers); 24 | } 25 | 26 | /////////////////////////////////创建响应式对象开始///////////////////////////////// 27 | function createReactiveObject(target: any, baseHandler) { 28 | if (!isObject(target)) { 29 | console.warn('target'.concat(target).concat('必须是一个对象')) 30 | return 31 | } 32 | 33 | return new Proxy(target, baseHandler); 34 | } 35 | 36 | export function isReactive(value) { 37 | return !!value[ReactiveFlags.IS_REACTIVE]; 38 | } 39 | 40 | export function isReadonly(value) { 41 | return !!value[ReactiveFlags.IS_READONLY]; 42 | } 43 | 44 | export function isProxy(value) { 45 | return isReactive(value) || isReadonly(value); 46 | } 47 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "../shared/index"; 2 | import { triggerEffect, trackEffect, isTracking } from "./effect"; 3 | import { reactive } from "./reactive"; 4 | 5 | class ResImpl { 6 | private _value: any; 7 | private _rawValue: any; 8 | public dep: any; 9 | public __v_isRef = true; 10 | constructor(value) { 11 | // 1. value 是否是对象 12 | this._rawValue = value; 13 | this._value = convert(value); 14 | this.dep = new Set(); 15 | } 16 | 17 | get value() { 18 | trackRefValue(this); 19 | 20 | return this._value; 21 | } 22 | set value(newValue) { 23 | if (!hasChanged(newValue, this._rawValue)) return; 24 | this._rawValue = newValue; 25 | this._value = convert(newValue); 26 | triggerEffect(this.dep); 27 | } 28 | } 29 | 30 | function convert(value) { 31 | return isObject(value) ? reactive(value) : value; 32 | } 33 | 34 | export function ref(value) { 35 | return new ResImpl(value); 36 | } 37 | 38 | export function trackRefValue(ref) { 39 | if (isTracking()) { 40 | trackEffect(ref.dep); 41 | } 42 | } 43 | 44 | /** 判断是否是一个Ref */ 45 | export function isRef(ref) { 46 | return !!ref.__v_isRef; 47 | } 48 | 49 | /** 直接取Ref值,不需要使用Ref.value */ 50 | export function unRef(ref) { 51 | if (isRef(ref)) { 52 | return ref.value; 53 | } else { 54 | return ref; 55 | } 56 | } 57 | 58 | // 场景 59 | // setup(() => { return { ref } }) 60 | // Template {{ref}} 而不是 {{ref.value}} 61 | export function proxyRefs(objectWithRefs) { 62 | return new Proxy(objectWithRefs, { 63 | get(target, key) { 64 | return unRef(Reflect.get(target, key)); 65 | }, 66 | set(target, key, value) { 67 | if (isRef(target[key]) && !isRef(value)) { 68 | return (target[key] = value); 69 | } else { 70 | return Reflect.set(target, key, value); 71 | } 72 | }, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive" 2 | 3 | describe('API reactive 使用', () => { 4 | 5 | it('查看基本流程', () => { 6 | const userState = reactive({ 7 | name: 'Spider Man 🕷', 8 | age: 18, 9 | sex: 1, 10 | departments: ['公司', '以部门'] 11 | }) 12 | 13 | expect(userState.name).toBe('Spider Man 🕷'); 14 | }) 15 | 16 | 17 | }) 18 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | // # 与 react Context 相似 都是用来跨组件传参. 4 | 5 | export function provide(key, value) { 6 | const currentIstance: any = getCurrentInstance(); 7 | // instance.providers 8 | if (currentIstance) { 9 | let { provides } = currentIstance; 10 | const parentProvides = currentIstance.parent.provides; 11 | 12 | // init 13 | if (provides === parentProvides) { 14 | provides = currentIstance.provides = Object.create(parentProvides); 15 | } 16 | 17 | 18 | provides[key] = value; 19 | } 20 | } 21 | 22 | export function inject(key, defaultValue) { 23 | const currentIstance: any = getCurrentInstance(); 24 | if (currentIstance) { 25 | const parentProvides = currentIstance.parent.provides; 26 | 27 | if (key in parentProvides) { 28 | return parentProvides[key] 29 | } else if (defaultValue) { 30 | if (typeof defaultValue === 'function') { 31 | return defaultValue() 32 | } 33 | return defaultValue; 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs } from "../reactivity"; 2 | import { shallowReadonly } from "../reactivity/reactive"; 3 | import { emit } from "./componentEmit"; 4 | import { initProps } from "./componentProps"; 5 | import { publicInstanceProxyHandlers } from "./componentPublicInstance"; 6 | import { initSlots } from "./componentSlots"; 7 | 8 | export function createComponentInstance(vnode, parent) { 9 | const component: any = { 10 | vnode, 11 | type: vnode.type, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | provides: parent ? parent.provides : {}, 16 | parent, 17 | isMounted: false, 18 | subTree: {}, 19 | emit: () => { }, 20 | } 21 | 22 | component.emit = emit.bind(null, component); 23 | 24 | return component; 25 | } 26 | 27 | export function setupComponent(instance) { 28 | initProps(instance, instance.vnode.props) 29 | initSlots(instance, instance.vnode.children) 30 | setupStatefulComponent(instance) 31 | } 32 | 33 | 34 | function setupStatefulComponent(instance: any) { 35 | 36 | const Component = instance.type; 37 | 38 | // ctx 39 | instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers) 40 | 41 | const { setup } = Component; 42 | 43 | if (setup) { 44 | setCurrentInstance(instance) 45 | // function | object 46 | const setupResult = setup(shallowReadonly(instance.props), { 47 | emit: instance.emit 48 | }); 49 | 50 | handleSetupResult(instance, setupResult) 51 | 52 | } 53 | 54 | } 55 | 56 | function handleSetupResult(instance: any, setupResult: any) { 57 | 58 | if (typeof setupResult === 'object') { 59 | instance.setupState = proxyRefs(setupResult); 60 | } 61 | 62 | finishComponentSetup(instance); 63 | } 64 | 65 | function finishComponentSetup(instance: any) { 66 | const Component = instance.type; 67 | // TODO: 1.0.0 必须写Render 68 | instance.render = Component.render 69 | } 70 | 71 | let currentInstance = null; 72 | export function getCurrentInstance() { 73 | return currentInstance 74 | } 75 | 76 | export function setCurrentInstance(instance) { 77 | currentInstance = instance; 78 | } 79 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "../shared/index"; 2 | 3 | export function emit(instance, event, ...args) { 4 | const { props } = instance; 5 | const handlerName = toHandlerKey(camelize(event)) 6 | const handler = props[handlerName]; 7 | 8 | handler && handler(...args) 9 | } 10 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | instance.props = rawProps || {}; 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "../shared/index" 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | $props: (i) => i.props 7 | } 8 | 9 | export const publicInstanceProxyHandlers = { 10 | get({ _: instance }, key) { 11 | const { setupState, props } = instance 12 | 13 | if (hasOwn(setupState, key)) { 14 | return setupState[key] 15 | } else if (hasOwn(props, key)) { 16 | return props[key] 17 | 18 | } 19 | 20 | const publicGetter = publicPropertiesMap[key]; 21 | if (publicGetter) { 22 | return publicGetter(instance) 23 | } 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { shapeFlags } from "../shared/SharpeFlags"; 2 | 3 | export function initSlots(instance, children) { 4 | 5 | const { vnode } = instance 6 | 7 | if (vnode.shapeFlag & shapeFlags.SLOT_CHILDREN) { 8 | normalizeObjectSlots(children, instance) 9 | } 10 | } 11 | 12 | function normalizeObjectSlots(children: any, instance: any) { 13 | const slots = {}; 14 | for (const key in children) { 15 | const value = children[key]; 16 | slots[key] = (props) => normalizeSlotValue(value(props)) 17 | } 18 | instance.slots = slots 19 | } 20 | 21 | function normalizeSlotValue(value) { 22 | return Array.isArray(value) ? value : [value] 23 | } 24 | -------------------------------------------------------------------------------- /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 (nextProps[key] !== prevProps[key]) { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /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: function (rootContainer) { 7 | let rootDom = rootContainer; 8 | // rootComponent -> vnode 9 | // 全都基于vnode进行下一步处理 10 | if (typeof rootContainer === 'string') { 11 | const root = document.createElement('div'); 12 | root.setAttribute('id', rootContainer); 13 | document.body.childNodes[0].before(root); 14 | rootDom = root 15 | } 16 | 17 | const vnode = createVNode(rootComponent) 18 | render(vnode, rootDom) 19 | } 20 | } 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function h(type, props?, children?) { 4 | return createVNode(type, props, children) 5 | } 6 | -------------------------------------------------------------------------------- /src/runtime-core/helper/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, name, props) { 4 | 5 | const slot = slots[name] 6 | if (slot) { 7 | if (typeof slot === 'function') { 8 | return createVNode(Fragment, {}, slot(props)) 9 | } 10 | } 11 | 12 | } `` 13 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { h } from './h' 3 | export { renderSlots } from './helper/renderSlots' 4 | export { createTextNode } from './vnode' 5 | export { getCurrentInstance } from './component' 6 | export { provide, inject } from './apiInject' 7 | export { createRender } from './renderer' 8 | export { nextTick } from './scheduler' 9 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { createAppAPI } from "./createApp"; 2 | import { shapeFlags } from "../shared/SharpeFlags"; 3 | import { Fragment, Text } from "./vnode"; 4 | import { createComponentInstance, setupComponent } from "./component"; 5 | import { effect } from "../reactivity"; 6 | import { EMPTY_OBJ } from "../shared"; 7 | import { shouldUpdateComponent } from './componentUpdateUtils' 8 | import { queueJobs } from "./scheduler"; 9 | 10 | // 使用闭包进行封装 11 | export function createRender(options) { 12 | 13 | const { 14 | createElement: hostCreateElement, 15 | patchProp: hostPatchProp, 16 | insert: hostInsert, 17 | remove: hostRemove, 18 | setElementText: hostSetElementText 19 | } = options; 20 | 21 | function render(vnode, container) { 22 | patch(null, vnode, container, null, null); 23 | } 24 | 25 | function patch(n1, n2, container, parentComponent, anchor) { 26 | // shapeFlags 27 | // debugger; 28 | // 分析组件,将组件强化后输出 29 | // 区分 Components 类型与 Element 类型 30 | const { type, shapeFlag } = n2; 31 | 32 | switch (type) { 33 | case Fragment: 34 | processFragment(n1, n2, container, parentComponent, anchor); 35 | break; 36 | case Text: 37 | processText(n1, n2, container) 38 | break; 39 | default: 40 | if (shapeFlag & shapeFlags.ELEMENT) { 41 | processElement(n1, n2, container, parentComponent, anchor) 42 | } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 43 | processComponent(n1, n2, container, parentComponent, anchor) 44 | } 45 | } 46 | } 47 | 48 | function processText(n1, n2, container) { 49 | const { children } = n2; 50 | const textNode = (n2.el = document.createTextNode(children)); 51 | container.append(textNode) 52 | } 53 | 54 | function processFragment(n1, n2: any, container, parentComponent, anchor) { 55 | mountChildren(n2.children, container, parentComponent, anchor) 56 | } 57 | 58 | function processElement(n1, n2, container, parentComponent, anchor) { 59 | if (!n1) { 60 | mountElement(n2, container, parentComponent, anchor) 61 | } else { 62 | patchElement(n1, n2, container, parentComponent, anchor) 63 | } 64 | } 65 | 66 | function patchElement(n1, n2, container, parentComponent, anchor) { 67 | console.log('patchElement') 68 | 69 | const oldProps = n1.props || EMPTY_OBJ; 70 | const newProps = n2.props || EMPTY_OBJ; 71 | 72 | const el = (n2.el = n1.el) 73 | patchChildren(n1, n2, el, parentComponent, anchor); 74 | patchProps(el, oldProps, newProps) 75 | } 76 | 77 | function patchChildren(n1: any, n2: any, container, parentComponent, anchor) { 78 | const prevShapeFlag = n1.shapeFlag; 79 | const c1 = n1.children; 80 | const { shapeFlag } = n2; 81 | const c2 = n2.children; 82 | 83 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 84 | if (prevShapeFlag & shapeFlags.ARRAY_CHILDREN) { 85 | unmountChildren(n1.children); 86 | } 87 | if (c1 !== c2) { 88 | hostSetElementText(container, c2) 89 | } 90 | } else { 91 | if (prevShapeFlag & shapeFlags.TEXT_CHILDREN) { 92 | hostSetElementText(container, '') 93 | mountChildren(c2, container, parentComponent, anchor) 94 | } else { 95 | // array diff array 96 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 97 | } 98 | } 99 | } 100 | 101 | function patchKeyedChildren(c1: any, c2: any, container, parentComponent, parentAnchor) { 102 | const l2 = c2.length; 103 | 104 | let i = 0; 105 | let e1 = c1.length - 1; 106 | let e2 = l2 - 1; 107 | 108 | function isSomeVNodeType(n1, n2) { 109 | // type key 110 | return n1.type === n2.type && n1.key === n2.key 111 | } 112 | 113 | // 左侧对比 114 | while (i <= e1 && i <= e2) { 115 | const n1 = c1[i]; 116 | const n2 = c2[i]; 117 | 118 | if (isSomeVNodeType(n1, n2)) { 119 | patch(n1, n2, container, parentComponent, parentAnchor); 120 | } else { 121 | break; 122 | } 123 | 124 | i++ 125 | } 126 | 127 | // 右侧对比 128 | while (i <= e1 && i <= e2) { 129 | const n1 = c1[e1]; 130 | const n2 = c2[e2]; 131 | 132 | if (isSomeVNodeType(n1, n2)) { 133 | patch(n1, n2, container, parentComponent, parentAnchor); 134 | } else { 135 | break; 136 | } 137 | // 左侧移动 138 | e1--; 139 | e2--; 140 | } 141 | 142 | // 新元素比旧元素多 -> 创建新的 143 | if (i > e1) { 144 | if (i <= e2) { 145 | const nextPos = e2 + 1; 146 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 147 | while (i <= e2) { 148 | patch(null, c2[i], container, parentComponent, anchor) 149 | i++; 150 | } 151 | } 152 | } else if (i > e2) { 153 | // 旧元素比新元素多 -> 删除旧的 154 | while (i <= e1) { 155 | hostRemove(c1[i].el) 156 | i++ 157 | } 158 | } else { 159 | // 中间逻辑对比 160 | let s1 = i; 161 | let s2 = i; 162 | 163 | const toBePatched = e2 - s2 + 1; 164 | let patched = 0; 165 | const keyToNewIndexMap = new Map(); 166 | const newIndexToOldIndexMap = new Array(toBePatched); 167 | 168 | let moved = false; 169 | let maxNewIndexSoFar = 0; 170 | 171 | for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0; 172 | 173 | for (let i = s2; i <= e2; i++) { 174 | const nextChild = c2[i]; 175 | keyToNewIndexMap.set(nextChild.key, i) 176 | } 177 | 178 | for (let i = s1; i <= e1; i++) { 179 | const prevChild = c1[i]; 180 | 181 | if (patched > toBePatched) { 182 | hostRemove(prevChild.el) 183 | continue; 184 | } 185 | 186 | let newIndex; 187 | // null undefined 188 | if (prevChild.key !== null) { 189 | newIndex = keyToNewIndexMap.get(prevChild.key); 190 | } else { 191 | for (let j = s2; j <= e2; j++) { 192 | if (isSomeVNodeType(prevChild, c2[j])) { 193 | newIndex = j; 194 | 195 | break; 196 | } 197 | } 198 | } 199 | // 新的子组件不存在该元素 200 | if (newIndex === undefined) { 201 | hostRemove(prevChild.el) 202 | } else { 203 | 204 | if (newIndex >= maxNewIndexSoFar) { 205 | maxNewIndexSoFar = newIndex 206 | } else { 207 | moved = true 208 | } 209 | 210 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 211 | patch(prevChild, c2[newIndex], container, parentComponent, null) 212 | patched++ 213 | } 214 | } 215 | 216 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [] 217 | let j = increasingNewIndexSequence.length - 1; 218 | for (let i = toBePatched - 1; i >= 0; i--) { 219 | 220 | const nextIndex = i + s2; 221 | const nextChild = c2[nextIndex]; 222 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 223 | 224 | if (newIndexToOldIndexMap[i] === 0) { 225 | patch(null, nextChild, container, parentComponent, anchor) 226 | } 227 | 228 | if (moved) { 229 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 230 | console.log('移动') 231 | hostInsert(nextChild.el, container, anchor) 232 | } else { 233 | j-- 234 | } 235 | } 236 | 237 | 238 | 239 | } 240 | 241 | } 242 | } 243 | 244 | function unmountChildren(children) { 245 | for (let i = 0; i < children.length; i++) { 246 | const el = children[i].el; 247 | hostRemove(el); 248 | } 249 | } 250 | 251 | function patchProps(el, oldProps: any, newProps: any) { 252 | 253 | if (oldProps !== newProps) { 254 | for (const key in newProps) { 255 | if (Object.prototype.hasOwnProperty.call(newProps, key)) { 256 | const prevProps = oldProps[key]; 257 | const nextProps = newProps[key]; 258 | 259 | if (prevProps !== nextProps) { 260 | hostPatchProp(el, key, prevProps, nextProps) 261 | } 262 | } 263 | } 264 | 265 | if (oldProps !== EMPTY_OBJ) { 266 | for (const key in oldProps) { 267 | if (!(key in newProps)) { 268 | hostPatchProp(el, key, oldProps[key], null) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | 276 | 277 | function mountElement(vnode, container, parentComponent, anchor) { 278 | // 默认是针对DOM进行操作。 279 | // 可封装针对 Canvas 进行封装,因为实现过程基本相似 280 | // DOM过程 -> document.createElement -> setTextContent -> setAttribute -> body.append() 281 | // Canvas -> new Element() -> setProps -> append 282 | const { children, shapeFlag, props } = vnode 283 | const el = (vnode.el = hostCreateElement(vnode.type)); 284 | // // string array 285 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 286 | el.textContent = children; // 'hi mini vue'; 287 | } else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) { 288 | mountChildren(vnode.children, el, parentComponent, anchor) 289 | } 290 | 291 | for (const key in props) { 292 | const val = props[key]; 293 | 294 | hostPatchProp(el, key, null, val) 295 | } 296 | 297 | hostInsert(el, container, anchor); 298 | } 299 | 300 | function mountChildren(children, container, parentComponent, anchor) { 301 | children.forEach(v => { 302 | patch(null, v, container, parentComponent, anchor) 303 | }) 304 | } 305 | 306 | function processComponent(n1, n2, container: any, parentComponent, anchor) { 307 | // 不存在此组件才进行mount, 存在则进行update 308 | if (!n1) { 309 | mountComponent(n2, container, parentComponent, anchor) 310 | } else { 311 | updateComponent(n1, n2) 312 | } 313 | } 314 | 315 | function updateComponent(n1, n2) { 316 | const instance = (n2.component = n1.component) 317 | // 优化更新 318 | if (shouldUpdateComponent(n1, n2)) { 319 | instance.next = n2 320 | // 调用返回的runnner 进行更新 321 | instance.update() 322 | } else { 323 | // 不需要更新的组件,需要重置 324 | n2.el = n1.el; 325 | instance.vnode = n2 326 | } 327 | } 328 | 329 | function mountComponent(initialVNode: any, container: any, parentComponent, anchor) { 330 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 331 | 332 | setupComponent(instance) 333 | setupRenderEffect(instance, initialVNode, container, anchor); 334 | } 335 | 336 | 337 | function setupRenderEffect(instance: any, initialVNode, container, anchor) { 338 | // Effect会返回Renner 利用这个方法,重新执行更新逻辑 339 | instance.update = effect(() => { 340 | // 抽离出更新逻辑 341 | if (!instance.isMounted) { 342 | const { proxy } = instance; 343 | const subTree = (instance.subTree = instance.render.call(proxy)); 344 | 345 | patch(null, subTree, container, instance, anchor) 346 | 347 | initialVNode.el = subTree.el 348 | instance.isMounted = true 349 | } else { 350 | console.log('update') 351 | 352 | const { proxy, next, vnode } = instance; 353 | if (next) { 354 | next.el = vnode.el 355 | 356 | updateComponentPreRender(instance, next) 357 | } 358 | 359 | const subTree = instance.render.call(proxy); 360 | const prevSubTree = instance.subTree; 361 | instance.subTree = subTree; 362 | 363 | patch(prevSubTree, subTree, container, instance, anchor) 364 | } 365 | }, { 366 | scheduler() { 367 | console.log('update - scheduler') 368 | queueJobs(instance.update) 369 | } 370 | }) 371 | } 372 | 373 | return { 374 | createApp: createAppAPI(render) 375 | } 376 | } 377 | 378 | function updateComponentPreRender(instance, nextVnode) { 379 | instance.vnode = nextVnode; 380 | instance.next = null; 381 | 382 | instance.props = nextVnode.props; 383 | } 384 | 385 | // 最长递增子序列 386 | function getSequence(arr) { 387 | const p = arr.slice() 388 | const result = [0] 389 | let i, j, u, v, c 390 | const len = arr.length 391 | for (i = 0; i < len; i++) { 392 | const arrI = arr[i] 393 | if (arrI !== 0) { 394 | j = result[result.length - 1] 395 | if (arr[j] < arrI) { 396 | p[i] = j 397 | result.push(i) 398 | continue 399 | } 400 | u = 0 401 | v = result.length - 1 402 | while (u < v) { 403 | c = (u + v) >> 1 404 | if (arr[result[c]] < arrI) { 405 | u = c + 1 406 | } else { 407 | v = c 408 | } 409 | } 410 | if (arrI < arr[result[u]]) { 411 | if (u > 0) { 412 | p[i] = result[u - 1] 413 | } 414 | result[u] = i 415 | } 416 | } 417 | } 418 | u = result.length 419 | v = result[u - 1] 420 | while (u-- > 0) { 421 | result[u] = v 422 | v = p[v] 423 | } 424 | return result 425 | } 426 | 427 | const r = getSequence([4, 2, 3, 1, 5]) 428 | 429 | console.log(r) 430 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any = []; 2 | const p = Promise.resolve(); 3 | let isFlushPending = 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 (isFlushPending) return; 19 | isFlushPending = true; 20 | 21 | nextTick(flushJobs); 22 | 23 | Promise.resolve().then(() => { 24 | flushJobs(); 25 | }) 26 | } 27 | 28 | function flushJobs() { 29 | isFlushPending = false; 30 | let job; 31 | while (job = queue.shift()) { 32 | job && job() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { shapeFlags } from "../shared/SharpeFlags" 2 | 3 | export const Fragment = Symbol('Fragment') 4 | export const Text = Symbol('Text') 5 | 6 | export function createVNode(type, props?, children?) { 7 | const vnode = { 8 | type, 9 | props, 10 | children, 11 | key: props && props.key, 12 | shapeFlag: getShapeFlag(type), 13 | component: null, 14 | next: null, 15 | el: null, 16 | } 17 | 18 | if (typeof children === 'string') { 19 | vnode.shapeFlag |= shapeFlags.TEXT_CHILDREN 20 | } else if (Array.isArray(children)) { 21 | vnode.shapeFlag |= shapeFlags.ARRAY_CHILDREN 22 | } 23 | 24 | // 组件 + children Object 25 | if (vnode.shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 26 | if (typeof children === 'object') { 27 | vnode.shapeFlag |= shapeFlags.SLOT_CHILDREN 28 | } 29 | } 30 | 31 | return vnode 32 | } 33 | 34 | export function createTextNode(text: string) { 35 | return createVNode(Text, {}, text) 36 | } 37 | 38 | 39 | function getShapeFlag(type) { 40 | return typeof type === 'string' ? shapeFlags.ELEMENT : shapeFlags.STATEFUL_COMPONENT 41 | } 42 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRender } from '../runtime-core' 2 | 3 | // NOTE: 将DOM层创建过程从Core中抽离出来 4 | function createElement(type) { 5 | return document.createElement(type) 6 | } 7 | 8 | function patchProp(el, key, prevVal, nextVal) { 9 | const isOn = (key: string) => /^on[A-Z]/.test(key); 10 | if (isOn(key)) { 11 | const event = key.slice(2).toLowerCase(); 12 | el.addEventListener(event, nextVal) 13 | } else { 14 | if (nextVal === undefined || nextVal === null) { 15 | el.removeAttribute(key, nextVal) 16 | } else { 17 | el.setAttribute(key, nextVal) 18 | } 19 | } 20 | } 21 | 22 | function insert(child, parent, anchor) { 23 | // parent.append(child) 24 | parent.insertBefore(child, anchor || null) 25 | } 26 | 27 | 28 | function remove(child) { 29 | const parent = child.parentNode; 30 | console.log(parent) 31 | if (parent) { 32 | console.log(child) 33 | parent.removeChild(child) 34 | } 35 | } 36 | 37 | function setElementText(el, text) { 38 | el.textContent = text 39 | } 40 | 41 | const renderer: any = createRender({ 42 | createElement, 43 | patchProp, 44 | insert, 45 | remove, 46 | setElementText 47 | }) 48 | 49 | 50 | export function createApp(...arg) { 51 | return renderer.createApp(...arg); 52 | } 53 | 54 | export * from '../runtime-core'; 55 | -------------------------------------------------------------------------------- /src/shared/SharpeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum shapeFlags { 2 | ELEMENT = 1, // 0001 3 | STATEFUL_COMPONENT = 1 << 1, // 0010 4 | TEXT_CHILDREN = 1 << 2, // 0100 5 | ARRAY_CHILDREN = 1 << 3, // 1000 6 | SLOT_CHILDREN = 1 << 4, 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | 3 | export function isObject(val) { 4 | return val !== null && typeof val === "object"; 5 | } 6 | 7 | export const hasChanged = (newValue, val) => !Object.is(newValue, val); 8 | 9 | export const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key) 10 | 11 | export const camelize = (str: string) => { 12 | return str.replace(/-(\w)/g, (_, c: string) => { return c ? c.toUpperCase() : '' }) 13 | } 14 | 15 | export const capitalize = (str: string) => { 16 | return str.charAt(0).toUpperCase() + str.slice(1); 17 | } 18 | 19 | export const toHandlerKey = (str: string) => { 20 | return str ? 'on' + capitalize(str) : '' 21 | } 22 | 23 | export const EMPTY_OBJ = {}; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | "lib": [ 14 | "DOM", 15 | "ES6" 16 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 23 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | /* Modules */ 27 | "module": "esnext" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": [ 35 | "jest" 36 | ] /* Specify type package names to be included without being referenced in a source file. */, 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files */ 39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 40 | /* JavaScript Support */ 41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "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. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 74 | /* Type Checking */ 75 | "strict": true /* Enable all strict type-checking options. */, 76 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 77 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 78 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 79 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 80 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 81 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 82 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 83 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 84 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 85 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 86 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 87 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 88 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 89 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 90 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 91 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 92 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 93 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 94 | /* Completeness */ 95 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 96 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 97 | } 98 | } 99 | --------------------------------------------------------------------------------