├── .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 | *
20 | *
21 | *
22 | * 123
23 | *
24 | *
25 | *
26 | */
27 | // const foo = h(Foo, {}, h('p', {}, '123'));
28 | /**
29 | * 场景 2
30 | *
31 | *
32 | *
33 | * 123
34 | * 123
35 | * 123
36 | *
37 | *
38 | *
39 | */
40 | /**
41 | * 场景 3 具名插槽
42 | *
43 | *
44 | *
45 | *
46 | * header
47 | *
48 | * 123
49 | *
50 | * footer
51 | *
52 | *
53 | *
54 | *
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 | *
68 | *
69 | *
70 | *
71 | * header {{age}}
72 | *
73 | * 123
74 | *
75 | * footer
76 | *
77 | *
78 | *
79 | *
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 | ); // --> hello, {{msg}}
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('')
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("")) {
78 | for (let i = ancestors.length -1; i >= 0 ; i--) {
79 | const tag = ancestors[i].tag
80 | if (startsWithEndTagOpen(s, tag)) {
81 | return true
82 | }
83 | }
84 | }
85 | // if (parentTag && s.startsWith(`${parentTag}>`)) {
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('') && source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
140 | }
141 |
142 | function parserTag(context: any, type: TagType) {
143 | const reg = /^<\/?([a-z]*)/i;
144 | const match: any = reg.exec(context.source);
145 | const tag = match[1];
146 |
147 | advanceBy(context, match[0].length)
148 | advanceBy(context, 1)
149 |
150 | if (type === TagType.End) return
151 |
152 | return {
153 | type: NodeTypes.ELEMENT,
154 | tag
155 | };
156 | }
157 |
158 | /**
159 | * 解析Text节点
160 | *
161 | * @param {*} context
162 | * @return {*}
163 | */
164 | function parseText(context: any) {
165 | let endIndex = context.source.length;
166 | let endToken = ["{{", "<"];
167 |
168 | for (let i = 0; i < endToken.length; i++) {
169 | const index = context.source.indexOf(endToken[i]);
170 | if (index !== -1 && endIndex > 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 |
--------------------------------------------------------------------------------