├── .gitignore
├── README.md
├── babel.config.js
├── example
├── apiInject
│ ├── app.js
│ ├── index.html
│ └── main.js
├── componentEmit
│ ├── Foo.js
│ ├── app.js
│ ├── index.html
│ └── main.js
├── componentSlots
│ ├── Foo.js
│ ├── app.js
│ ├── index.html
│ └── main.js
├── helloWorld
│ ├── Foo.js
│ ├── app.js
│ ├── index.html
│ └── main.js
├── nextTicker
│ ├── App.js
│ ├── index.html
│ └── main.js
├── updateChildren
│ ├── app.js
│ ├── arrayToArray.js
│ ├── arrayToText.js
│ ├── index.html
│ ├── main.js
│ ├── textToArray.js
│ └── textToText.js
├── updateComponent
│ ├── App.js
│ ├── Child.js
│ ├── index.html
│ └── main.js
└── updateElement
│ ├── app.js
│ ├── index.html
│ └── main.js
├── jest.config.ts
├── lib
├── guide-toy-vue3.cjs.js
├── guide-toy-vue3.cjs.js.map
├── guide-toy-vue3.esm.js
└── guide-toy-vue3.esm.js.map
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── index.ts
├── reactivity
│ ├── baseHandlers.ts
│ ├── computed.ts
│ ├── docs
│ │ ├── 1.summary.md
│ │ ├── 2.summary.md
│ │ ├── 3.summary.md
│ │ ├── 4.summary.md
│ │ └── 5.summary.md
│ ├── effect.ts
│ ├── index.ts
│ ├── reactive.ts
│ ├── ref.ts
│ └── test
│ │ ├── computed.spec.ts
│ │ ├── effect.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── ref.spec.ts
│ │ ├── shallowReactive.spec.ts
│ │ └── shallowReadonly.spec.ts
├── runtime-core
│ ├── apiCreateApp.ts
│ ├── apiInject.ts
│ ├── component.ts
│ ├── componentEmit.ts
│ ├── componentProps.ts
│ ├── componentPublicInstance.ts
│ ├── componentRenderUtils.ts
│ ├── componentSlots.ts
│ ├── docs
│ │ ├── 1.summary.md
│ │ ├── 2.summary.md
│ │ └── 3.summary.md
│ ├── h.ts
│ ├── helper
│ │ └── renderSlot.ts
│ ├── index.ts
│ ├── renderer.ts
│ ├── scheduler.ts
│ └── vnode.ts
├── runtime-dom
│ ├── docs
│ │ ├── 1.summary.md
│ │ ├── 2.summary.md
│ │ └── 3.summary.md
│ └── index.ts
└── shared
│ ├── index.ts
│ └── shapeFlags.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Toy-Vue3.x
2 |
3 | 手写 vue3 核心源码,理解其原理 by myself
4 |
5 | ## 🙌 目的
6 |
7 | 读懂 Vue3 源码可能是 2022 年要进大厂的必经之路了。
8 |
9 | 但是直接阅读源码的难度非常大,因为除了核心逻辑以外框架本身还要处理很多 edge case(边缘情况) 、错误处理、热更新等一系列工程问题。
10 |
11 | 为了学习源码,提高自己对 vue3 的理解,能在开发中规避一些不必要的 bug 以及迅速给出项目的优化方案,因此创建这个 repo。
12 |
13 | ## ✏ 吸取知识就要输出文章
14 |
15 | 掘金专栏
16 |
17 | [Vue3 核心原理代码解构](https://juejin.cn/user/493015847938664/columns)
18 |
19 | ## 🛠 功能清单
20 | reactivity部分
21 |
22 | - [x] 实现 effect & reactive 依赖收集和依赖触发
23 | - [x] 实现 effect 返回 runner
24 | - [x] 实现 effect 的 scheduler 功能
25 | - [x] 实现 effect 的 stop 功能
26 | - [x] 优化 stop 功能
27 | - [x] 实现 readonly 功能
28 | - [x] 实现 isReactive 和 isReadonly 功能
29 | - [x] readonly 和 reactive 嵌套对象功能
30 | - [x] 实现 shallowReadonly 功能
31 | - [x] 实现 shallowReactive 功能
32 | - [x] 实现 isProxy 功能
33 | - [x] 实现 isShallow 功能
34 | - [x] 实现 ref 功能
35 | - [x] 实现 isRef 和 unRef 功能
36 | - [x] 实现 proxyRefs 功能
37 | - [x] 实现 computed 计算属性功能
38 |
39 |
40 | runtime-core部分
41 |
42 | - [x] 实现初始化 component 主流程
43 | - [x] 实现初始化 element 主流程 (通过递归patch拆箱操作,最终都会走向mountElement这一步)
44 | - [x] 实现组件代理对象 (instance.proxy解决`render()`函数的this指向问题)
45 | - [x] 实现 shapeFlags (利用位运算 左移运算 对vnode添加标识,标识是什么类型:子级文本,子级数组,组件,HTML元素)
46 | - [x] 实现注册事件功能 (通过在vnode.props识别 props对象的key是以on开头并且后一个字母是大写来判断是否是事件)
47 | - [x] 实现组件 props 功能 (在render的h函数中可以用this访问到,并且是shallowReadonly)
48 | - [x] 实现组件 emit 功能 (获取组件的props并判断props的'on+事件名'是否是emit的第一个参数:事件名匹配,是的话就执行props的里面的事件)
49 | - [x] 实现组件 slots 功能
50 | - [x] 实现 Fragment 和 Text 类型节点
51 | - [x] 实现 getCurrentInstance
52 | - [x] 实现 provide-inject 功能
53 | - [X] 实现自定义渲染器 custom renderer
54 | - [X] 更新 element 流程搭建
55 | - [X] 更新 element 的 props
56 | - [X] 更新 element 的 children
57 | - [X] 更新 element 的双端对比 diff 算法
58 | - [X] 实现组件更新功能
59 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript'
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/example/apiInject/app.js:
--------------------------------------------------------------------------------
1 | import { h, provide, inject } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export const Provider = {
4 | name: 'Provider',
5 | setup(){
6 |
7 | provide('foo', 'fooVal')
8 | provide('bar', 'barVal')
9 | },
10 | render(){
11 | return h('div', {}, [
12 | h('p', {}, 'Provider'),
13 | h(ProviderTwo)
14 | ])
15 | }
16 | }
17 | const ProviderTwo = {
18 | name: 'ProviderTwo',
19 | setup(){
20 | provide('foo', 'fooTwo')
21 | provide('bar', 'barTwo')
22 | // 期望得到provider的foo---fooVal,实际上得到的是fooTwo
23 | const foo = inject('foo')
24 | const bar = inject('bar')
25 | return {
26 | foo,
27 | bar
28 | }
29 | },
30 | render(){
31 | return h('div', {}, [
32 | h('p', {}, `ProviderTwo-${this.foo}-${this.bar}`),
33 | h(Consumer)
34 | ])
35 | }
36 | }
37 | const Consumer = {
38 | name: 'Consumer',
39 | setup(){
40 | const fooVal = inject('foo')
41 | const barVal = inject('bar')
42 | return {
43 | fooVal,
44 | barVal
45 | }
46 | },
47 | render(){
48 | return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/example/apiInject/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 测试
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/example/apiInject/main.js:
--------------------------------------------------------------------------------
1 | import {Provider} from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(Provider).mount(rootContainer)
10 |
11 |
--------------------------------------------------------------------------------
/example/componentEmit/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export default {
4 | name: 'Foo',
5 | render() {
6 |
7 | return h('div', {}, [
8 | h('button', {
9 | onClick: this.onAdd
10 | }, '触发emit')
11 | ])
12 | },
13 | setup(props, {emit}) {
14 | // 1. 传入count
15 | console.log(props)
16 | // 3. shallow readonly
17 | props.count++
18 | console.log(props, props.count)
19 | function onAdd(){
20 | console.log('onAdd')
21 | emit('emitFooAddEvent', props.count)
22 | }
23 |
24 | return {
25 | onAdd
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/example/componentEmit/app.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../lib/guide-toy-vue3.esm.js'
2 | import Foo from './Foo.js'
3 |
4 |
5 | window.self = null
6 | export default {
7 | name: 'App',
8 | render() {
9 | // 为什么这里不直接写window.self = this.$el呢?因为h还没执行,元素还没mount上document,借助引用传递拿到值
10 | window.self = this
11 | return h('div', {
12 | id: 'root',
13 | class: ['flex', 'container-r'],
14 | onClick(){
15 | console.log('click event!')
16 | },
17 | onMouseDown(){
18 | console.log('mouse down!')
19 | }
20 | }, [
21 | h('p', {class: 'red'}, 'red'),
22 | h('p', {class: 'blue'}, 'blue'),
23 | // 在Foo的props中寻找有没有on + emitFooAddEvent这个函数,有就执行
24 | h(Foo, {
25 | count: 1,
26 | onEmitFooAddEvent: this.takeEmitEvent
27 | }, '')
28 | ])
29 | // this指向通过proxy
30 | return h('div', {
31 | id: 'root',
32 | class: ['flex', 'container']
33 | }, this.name)
34 | },
35 | setup() {
36 | function takeEmitEvent(count){
37 | console.log('app take in count number:', count)
38 | }
39 | // 返回对象或者h()渲染函数
40 | return {
41 | name: 'hi my app',
42 | takeEmitEvent
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/example/componentEmit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 测试
8 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/componentEmit/main.js:
--------------------------------------------------------------------------------
1 | import App from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(App).mount(rootContainer)
10 |
--------------------------------------------------------------------------------
/example/componentSlots/Foo.js:
--------------------------------------------------------------------------------
1 | import {
2 | h,
3 | renderSlot,
4 | createTextVNode
5 | } from '../../lib/guide-toy-vue3.esm.js'
6 |
7 | export default {
8 | name: 'Foo',
9 | render() {
10 | console.log('Foo--->', this.$slots)
11 | const foo = h('p', {}, '原本就在Foo里面的元素')
12 | return h('div', {}, [renderSlot(this.$slots, 'header', {age: 18}), foo, renderSlot(this.$slots, 'footer')])
13 | // return h('div', {}, [renderSlot(this.$slots, 'default'), foo])
14 | // return h('div', {}, [foo, renderSlot(this.$slots, 'default'), createTextVNode('aaa')])
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/componentSlots/app.js:
--------------------------------------------------------------------------------
1 | import {
2 | h
3 | } from '../../lib/guide-toy-vue3.esm.js'
4 | import Foo from './Foo.js'
5 |
6 |
7 | export default {
8 | name: 'App',
9 | render() {
10 | return h('div', {
11 | id: 'root',
12 | class: ['flex', 'container-r'],
13 | }, [
14 | h('p', {
15 | class: 'red'
16 | }, 'red'),
17 | h('p', {
18 | class: 'blue'
19 | }, this.name),
20 | // h(Foo, {}, [h('p', {}, '我是slot1'), h('p', {}, '我是slot1')])
21 | // h(Foo, {}, {
22 | // default: () => h('p', {}, '我是slot1')
23 | // })
24 | // 具名插槽
25 | // h(Foo, {}, {
26 | // header: h('p', {}, '我是header slot1'),
27 | // footer: h('p', {}, '我是footer slot1')
28 | // })
29 | // 作用域插槽
30 | h(Foo, {}, {
31 | header: ({age})=>h('p', {}, '我是header slot1' + age),
32 | footer: ()=>h('p', {}, '我是footer slot1')
33 | })
34 | ])
35 | },
36 | setup() {
37 | // 返回对象或者h()渲染函数
38 | return {
39 | name: 'hi my app',
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/example/componentSlots/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 测试
8 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/componentSlots/main.js:
--------------------------------------------------------------------------------
1 | import App from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(App).mount(rootContainer)
10 |
--------------------------------------------------------------------------------
/example/helloWorld/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export default {
4 | name: 'Foo',
5 | render() {
6 | // 2. 能在render中通过this访问到props
7 | return h('div', {}, 'foo: ' + this.count)
8 | },
9 | setup(props) {
10 | // 1. 传入count
11 | console.log(props)
12 | // 3. shallow readonly
13 | props.count++
14 | console.log(props)
15 | console.log('', getCurrentInstance());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/helloWorld/app.js:
--------------------------------------------------------------------------------
1 | import { h, getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js'
2 | import Foo from './Foo.js'
3 |
4 |
5 | window.self = null
6 | export default {
7 | name: 'App',
8 | render() {
9 | // 为什么这里不直接写window.self = this.$el呢?因为h还没执行,元素还没mount上document,借助引用传递拿到值
10 | window.self = this
11 | return h('div', {
12 | id: 'root',
13 | class: ['flex', 'container-r'],
14 | onClick(){
15 | console.log('click event!')
16 | },
17 | onMouseDown(){
18 | console.log('mouse down!')
19 | }
20 | }, [
21 | h('p', {class: 'red'}, 'red'),
22 | h('p', {class: 'blue'}, 'blue'),
23 | // 引入Foo组件
24 | h(Foo, {
25 | count: 1
26 | }, '')
27 | ])
28 | // this指向通过proxy
29 | // return h('div', {
30 | // id: 'root',
31 | // class: ['flex', 'container']
32 | // }, this.name)
33 | },
34 | setup() {
35 | console.log(getCurrentInstance());
36 | // 返回对象或者h()渲染函数
37 | return {
38 | name: 'hi my app'
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/helloWorld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 测试
8 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/helloWorld/main.js:
--------------------------------------------------------------------------------
1 | import App from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(App).mount(rootContainer)
10 |
--------------------------------------------------------------------------------
/example/nextTicker/App.js:
--------------------------------------------------------------------------------
1 |
2 | import { ref, h,getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js'
3 | export const App = {
4 | name: 'app',
5 | setup() {
6 | const instance = getCurrentInstance()
7 | const count = ref(0)
8 | const changeCount = () => {
9 |
10 | for(let i = 0;i<100;i++){
11 | console.log('update count');
12 | count.value = i
13 | }
14 | console.log('当前实例',instance)
15 | }
16 | return {
17 | changeCount,
18 | count
19 | }
20 | },
21 | render() {
22 | console.log(this);
23 | return h('div', {}, [
24 |
25 | h('button', {onClick: this.changeCount}, 'update'),
26 | h('p', {}, 'count:' + this.count)
27 | ])
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/example/nextTicker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 更新组件
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/nextTicker/main.js:
--------------------------------------------------------------------------------
1 | import { App } from './App.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 |
5 | const rootContainer = document.querySelector('#app')
6 |
7 | createApp(App).mount(rootContainer)
8 |
--------------------------------------------------------------------------------
/example/updateChildren/app.js:
--------------------------------------------------------------------------------
1 | import { ref, h, reactive } from '../../lib/guide-toy-vue3.esm.js'
2 | import textToText from './textToText.js'
3 | import textToArray from './textToArray.js'
4 | import arrayToText from './arrayToText.js'
5 | import arrayToArray from './arrayToArray.js'
6 | export const App = {
7 | name: 'app',
8 | setup() {},
9 | render() {
10 |
11 | // this.count进行依赖收集,
12 | return h('div', {}, [
13 | h('p',{},'主页'),
14 | // 旧的是文本 新的是文本
15 | // h(textToText),
16 | // 旧的是文本 新的是数组
17 | // h(textToArray),
18 | // 旧的是数组 新的是文本
19 | // h(arrayToText),
20 | // 旧的是数组 新的也是数组
21 | h(arrayToArray),
22 | ])
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/example/updateChildren/arrayToArray.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | // 1. 左侧的对比
4 | // (a b) c
5 | // (a b) d e
6 | // const prevChildren = [
7 | // h('p', { key: 'A' }, 'A'),
8 | // h('p', { key: 'B' }, 'B'),
9 | // h('p', { key: 'C' }, 'C'),
10 | // ]
11 | // const nextChildren = [
12 | // h('p', { key: 'A' }, 'A'),
13 | // h('p', { key: 'B' }, 'B'),
14 | // h('p', { key: 'D' }, 'D'),
15 | // h('p', { key: 'E' }, 'E'),
16 | // ]
17 |
18 | // 2. 右侧的对比
19 | // a (b c)
20 | // d e (b c)
21 | // const prevChildren = [
22 | // h("p", { key: "A" }, "A"),
23 | // h("p", { key: "B" }, "B"),
24 | // h("p", { key: "C" }, "C"),
25 | // ];
26 | // const nextChildren = [
27 | // h("p", { key: "D" }, "D"),
28 | // h("p", { key: "E" }, "E"),
29 | // h("p", { key: "B" }, "B"),
30 | // h("p", { key: "C" }, "C"),
31 | // ];
32 |
33 | // 3. 新的比老的长
34 | // 创建新的
35 | // 左侧
36 | // (a b)
37 | // (a b) c
38 | // i = 2, e1 = 1, e2 = 2
39 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
40 | // const nextChildren = [
41 | // h("p", { key: "A" }, "A"),
42 | // h("p", { key: "B" }, "B"),
43 | // h("p", { key: "C" }, "C"),
44 | // h("p", { key: "D" }, "D"),
45 | // ];
46 |
47 | // 右侧
48 | // (a b)
49 | // d c (a b)
50 | // i = 0, e1 = -1, e2 = 0
51 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
52 | // const nextChildren = [
53 | // h("p", { key: "D" }, "D"),
54 | // h("p", { key: "C" }, "C"),
55 | // h("p", { key: "A" }, "A"),
56 | // h("p", { key: "B" }, "B"),
57 | // ];
58 |
59 | // 4. 老的比新的长
60 | // 删除老的
61 | // 左侧
62 | // (a b) c
63 | // (a b)
64 | // i = 2, e1 = 2, e2 = 1
65 | // const prevChildren = [
66 | // h("p", { key: "A" }, "A"),
67 | // h("p", { key: "B" }, "B"),
68 | // h("p", { key: "C" }, "C"),
69 | // ];
70 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
71 |
72 | // 右侧
73 | // a (b c)
74 | // (b c)
75 | // i = 0, e1 = 0, e2 = -1
76 |
77 | // const prevChildren = [
78 | // h("p", { key: "A" }, "A"),
79 | // h("p", { key: "B" }, "B"),
80 | // h("p", { key: "C" }, "C"),
81 | // ];
82 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
83 |
84 | // 5. 对比中间的部分
85 | // 删除老的 (在老的里面存在,新的里面不存在)
86 | // 5.1
87 | // a,b,(c,d),f,g
88 | // a,b,(e,c),f,g
89 | // D 节点在新的里面是没有的 - 需要删除掉
90 | // C 节点 props 也发生了变化
91 |
92 | // const prevChildren = [
93 | // h("p", { key: "A" }, "A"),
94 | // h("p", { key: "B" }, "B"),
95 | // h("p", { key: "C", id: "c-prev" }, "C"),
96 | // h("p", { key: "D" }, "D"),
97 | // h("p", { key: "F" }, "F"),
98 | // h("p", { key: "G" }, "G"),
99 | // ];
100 |
101 | // const nextChildren = [
102 | // h("p", { key: "A" }, "A"),
103 | // h("p", { key: "B" }, "B"),
104 | // h("p", { key: "E" }, "E"),
105 | // h("p", { key: "C", id:"c-next" }, "C"),
106 | // h("p", { key: "F" }, "F"),
107 | // h("p", { key: "G" }, "G"),
108 | // ];
109 |
110 | // 5.1.1
111 | // a,b,(c,e,d),f,g
112 | // a,b,(e,c),f,g
113 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑)
114 | // const prevChildren = [
115 | // h("p", { key: "A" }, "A"),
116 | // h("p", { key: "B" }, "B"),
117 | // h("p", { key: "C", id: "c-prev" }, "C"),
118 | // h("p", { key: "E" }, "E"),
119 | // h("p", { key: "D" }, "D"),
120 | // h("p", { key: "F" }, "F"),
121 | // h("p", { key: "G" }, "G"),
122 | // ];
123 |
124 | // const nextChildren = [
125 | // h("p", { key: "A" }, "A"),
126 | // h("p", { key: "B" }, "B"),
127 | // h("p", { key: "E" }, "E"),
128 | // h("p", { key: "C", id:"c-next" }, "C"),
129 | // h("p", { key: "F" }, "F"),
130 | // h("p", { key: "G" }, "G"),
131 | // ];
132 |
133 | // 2 移动 (节点存在于新的和老的里面,但是位置变了)
134 |
135 | // 2.1
136 | // a,b,(c,d,e),f,g
137 | // a,b,(e,c,d),f,g
138 | // 最长子序列: [1,2]
139 |
140 | // const prevChildren = [
141 | // h("p", { key: "A" }, "A"),
142 | // h("p", { key: "B" }, "B"),
143 | // h("p", { key: "C" }, "C"),
144 | // h("p", { key: "D" }, "D"),
145 | // h("p", { key: "E" }, "E"),
146 | // h("p", { key: "F" }, "F"),
147 | // h("p", { key: "G" }, "G"),
148 | // ];
149 |
150 | // const nextChildren = [
151 | // h("p", { key: "A" }, "A"),
152 | // h("p", { key: "B" }, "B"),
153 | // h("p", { key: "E" }, "E"),
154 | // h("p", { key: "C" }, "C"),
155 | // h("p", { key: "D" }, "D"),
156 | // h("p", { key: "F" }, "F"),
157 | // h("p", { key: "G" }, "G"),
158 | // ];
159 |
160 | // 2.2
161 | // a,b,(c,d,e,z),f,g
162 | // a,b,(d,c,y,e),f,g
163 | // 最长子序列: [1,3]
164 | // y 新增
165 | // e 删除
166 | // d 移动
167 |
168 | const prevChildren = [
169 | h("p", { key: "A" }, "A"),
170 | h("p", { key: "B" }, "B"),
171 | h("p", { key: "C" }, "C"),
172 | h("p", { key: "D" }, "D"),
173 | h("p", { key: "E" }, "E"),
174 | h("p", { key: "Z" }, "Z"),
175 | h("p", { key: "F" }, "F"),
176 | h("p", { key: "G" }, "G"),
177 | ];
178 |
179 | const nextChildren = [
180 | h("p", { key: "A" }, "A"),
181 | h("p", { key: "B" }, "B"),
182 | h("p", { key: "D" }, "D"),
183 | h("p", { key: "C" }, "C"),
184 | h("p", { key: "Y" }, "Y"),
185 | h("p", { key: "E" }, "E"),
186 | h("p", { key: "F" }, "F"),
187 | h("p", { key: "G" }, "G"),
188 | ];
189 |
190 | // 3. 创建新的节点
191 | // a,b,(c,e),f,g
192 | // a,b,(e,c,d),f,g
193 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建
194 | // const prevChildren = [
195 | // h("p", { key: "A" }, "A"),
196 | // h("p", { key: "B" }, "B"),
197 | // h("p", { key: "C" }, "C"),
198 | // h("p", { key: "E" }, "E"),
199 | // h("p", { key: "F" }, "F"),
200 | // h("p", { key: "G" }, "G"),
201 | // ];
202 |
203 | // const nextChildren = [
204 | // h("p", { key: "A" }, "A"),
205 | // h("p", { key: "B" }, "B"),
206 | // h("p", { key: "E" }, "E"),
207 | // h("p", { key: "C" }, "C"),
208 | // h("p", { key: "D" }, "D"),
209 | // h("p", { key: "F" }, "F"),
210 | // h("p", { key: "G" }, "G"),
211 | // ];
212 |
213 | export default {
214 | name: 'PatchChildren',
215 | setup() {
216 | const isChange = ref(false)
217 | window.isChange = isChange
218 | const test = () => {
219 | isChange.value = !isChange.value
220 | }
221 | return {
222 | test,
223 | isChange,
224 | }
225 | },
226 | render() {
227 | // return h('div', {}, [
228 | // h(
229 | // 'button',
230 | // {
231 | // onClick: () => {
232 | // isChange.value = !isChange.value
233 | // },
234 | // },
235 | // '测试子组件之间的 patch 逻辑'
236 | // ),
237 | // h('children', {}, isChange.value === true ? nextChildren : prevChildren),
238 | // ])
239 | return isChange.value
240 | ? h('div', {}, nextChildren)
241 | : h('div', {}, prevChildren)
242 | },
243 | }
244 |
--------------------------------------------------------------------------------
/example/updateChildren/arrayToText.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export default {
4 | name: 'arrayToText',
5 | setup() {
6 | const isChange = ref(false)
7 | window.isChange = isChange
8 | return {
9 | isChange,
10 | }
11 | },
12 | render() {
13 | return this.isChange
14 | ? h('div', {}, 'new text')
15 | : h('div', {}, [h('div', {}, 'A'), h('div', {}, 'B')])
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/example/updateChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 更新子节点
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/updateChildren/main.js:
--------------------------------------------------------------------------------
1 | import { App } from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(App).mount(rootContainer)
10 |
--------------------------------------------------------------------------------
/example/updateChildren/textToArray.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export default {
4 | name: 'textToArray',
5 | setup() {
6 | const isChange = ref(false)
7 | window.isChange = isChange
8 | return {
9 | isChange,
10 | }
11 | },
12 | render() {
13 | return this.isChange
14 | ? h('div', {}, [h('p', {}, 'p标签'), h('span', {}, 'span标签')])
15 | : h('div', {}, 'old text')
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/example/updateChildren/textToText.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js'
2 |
3 | export default {
4 | name: 'textToText',
5 | setup(){
6 | const isChange = ref(false)
7 | window.isChange = isChange
8 | return {
9 | isChange,
10 | }
11 | },
12 | render(){
13 | return this.isChange ? h('div', {}, 'new text') : h('div', {}, 'old text')
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/updateComponent/App.js:
--------------------------------------------------------------------------------
1 | import Child from './Child.js'
2 | import { ref, h } from '../../lib/guide-toy-vue3.esm.js'
3 | export const App = {
4 | name: 'app',
5 | setup() {
6 | const msg = ref('hello')
7 | const count = ref(0)
8 | const changeMsg = () => {
9 | msg.value = 'world'
10 | }
11 | const changeCount = () => {
12 | count.value++
13 | }
14 | return {
15 | msg,
16 | count,
17 | changeMsg,
18 | changeCount
19 | }
20 | },
21 | render() {
22 | console.log(this);
23 | return h('div', {}, [
24 | h('div', {}, 'hello world'),
25 | h(Child, {msg: this.msg}),
26 | h('button', {onClick: this.changeMsg}, '改变msg'),
27 | h('button', {onClick: this.changeCount}, '改变count,看看有没有触发updateComponent'),
28 | h('p', {}, 'count:' + this.count)
29 | ])
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/example/updateComponent/Child.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/guide-toy-vue3.esm.js"
2 |
3 | export default {
4 | name: 'Child',
5 | setup(){
6 | return {
7 |
8 | }
9 | },
10 | render(){
11 | return h('div',{}, [
12 | h('div', {}, this.$props.msg)
13 | ])
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/updateComponent/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 更新组件
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/updateComponent/main.js:
--------------------------------------------------------------------------------
1 | import { App } from './App.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 |
5 | const rootContainer = document.querySelector('#app')
6 |
7 | createApp(App).mount(rootContainer)
8 |
--------------------------------------------------------------------------------
/example/updateElement/app.js:
--------------------------------------------------------------------------------
1 | import { ref, h, reactive } from '../../lib/guide-toy-vue3.esm.js'
2 | export const App = {
3 | name: 'app',
4 | setup() {
5 | const count = ref(0)
6 | let props = ref({
7 | foo: 'foo',
8 | bar: 'bar',
9 | })
10 | const click = () => {
11 | // 触发依赖
12 | count.value++
13 | }
14 | const changePropsDemo1 = () => {
15 | props.value.foo = 'new foo'
16 | }
17 | const changePropsDemo2 = () => {
18 | props.value.foo = undefined
19 | }
20 | const changePropsDemo3 = () => {
21 | props.value = {
22 | foo: 'foo'
23 | }
24 | }
25 | return {
26 | count,
27 | click,
28 | props,
29 | changePropsDemo1,
30 | changePropsDemo2,
31 | changePropsDemo3,
32 | }
33 | },
34 | render() {
35 | // this.count进行依赖收集,
36 | return h('div', { id: 'update', ...this.props }, [
37 | h('p', {}, `count: ${this.count}`),
38 | h('button', { onClick: this.click }, '点击更新'),
39 | h('button', { onClick: this.changePropsDemo1 }, '比较props,更新props'),
40 | h(
41 | 'button',
42 | { onClick: this.changePropsDemo2 },
43 | 'props属性值赋值为null或undefined,应该删除该属性'
44 | ),
45 | h(
46 | 'button',
47 | { onClick: this.changePropsDemo3 },
48 | '新props属性被删除,也应该删除该属性'
49 | ),
50 | ])
51 | },
52 | }
53 |
--------------------------------------------------------------------------------
/example/updateElement/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 测试
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/updateElement/main.js:
--------------------------------------------------------------------------------
1 | import {App} from './app.js'
2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js'
3 |
4 | // 流程:
5 | // template -> render() -> 生成vnode —> mountElement -> insert #app
6 |
7 | const rootContainer = document.querySelector('#app')
8 |
9 | createApp(App).mount(rootContainer)
10 |
11 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | export default {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "C:\\Users\\AaronGuo\\AppData\\Local\\Temp\\jest",
15 |
16 | // Automatically clear mock calls, instances and results before every test
17 | // clearMocks: false,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | collectCoverage: true,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | // collectCoverageFrom: undefined,
24 |
25 | // The directory where Jest should output its coverage files
26 | coverageDirectory: "coverage",
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | // coveragePathIgnorePatterns: [
30 | // "\\\\node_modules\\\\"
31 | // ],
32 |
33 | // Indicates which provider should be used to instrument code for coverage
34 | // coverageProvider: "babel",
35 |
36 | // A list of reporter names that Jest uses when writing coverage reports
37 | // coverageReporters: [
38 | // "json",
39 | // "text",
40 | // "lcov",
41 | // "clover"
42 | // ],
43 |
44 | // An object that configures minimum threshold enforcement for coverage results
45 | // coverageThreshold: undefined,
46 |
47 | // A path to a custom dependency extractor
48 | // dependencyExtractor: undefined,
49 |
50 | // Make calling deprecated APIs throw helpful error messages
51 | // errorOnDeprecated: false,
52 |
53 | // Force coverage collection from ignored files using an array of glob patterns
54 | // forceCoverageMatch: [],
55 |
56 | // A path to a module which exports an async function that is triggered once before all test suites
57 | // globalSetup: undefined,
58 |
59 | // A path to a module which exports an async function that is triggered once after all test suites
60 | // globalTeardown: undefined,
61 |
62 | // A set of global variables that need to be available in all test environments
63 | // globals: {},
64 |
65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
66 | // maxWorkers: "50%",
67 |
68 | // An array of directory names to be searched recursively up from the requiring module's location
69 | // moduleDirectories: [
70 | // "node_modules"
71 | // ],
72 |
73 | // An array of file extensions your modules use
74 | // moduleFileExtensions: [
75 | // "js",
76 | // "jsx",
77 | // "ts",
78 | // "tsx",
79 | // "json",
80 | // "node"
81 | // ],
82 |
83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
84 | // moduleNameMapper: {},
85 |
86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
87 | // modulePathIgnorePatterns: [],
88 |
89 | // Activates notifications for test results
90 | // notify: false,
91 |
92 | // An enum that specifies notification mode. Requires { notify: true }
93 | // notifyMode: "failure-change",
94 |
95 | // A preset that is used as a base for Jest's configuration
96 | // preset: undefined,
97 |
98 | // Run tests from one or more projects
99 | // projects: undefined,
100 |
101 | // Use this configuration option to add custom reporters to Jest
102 | // reporters: undefined,
103 |
104 | // Automatically reset mock state before every test
105 | // resetMocks: false,
106 |
107 | // Reset the module registry before running each individual test
108 | // resetModules: false,
109 |
110 | // A path to a custom resolver
111 | // resolver: undefined,
112 |
113 | // Automatically restore mock state and implementation before every test
114 | // restoreMocks: false,
115 |
116 | // The root directory that Jest should scan for tests and modules within
117 | // rootDir: undefined,
118 |
119 | // A list of paths to directories that Jest should use to search for files in
120 | // roots: [
121 | // ""
122 | // ],
123 |
124 | // Allows you to use a custom runner instead of Jest's default test runner
125 | // runner: "jest-runner",
126 |
127 | // The paths to modules that run some code to configure or set up the testing environment before each test
128 | // setupFiles: [],
129 |
130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
131 | // setupFilesAfterEnv: [],
132 |
133 | // The number of seconds after which a test is considered as slow and reported as such in the results.
134 | // slowTestThreshold: 5,
135 |
136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
137 | // snapshotSerializers: [],
138 |
139 | // The test environment that will be used for testing
140 | testEnvironment: "jsdom",
141 |
142 | // Options that will be passed to the testEnvironment
143 | // testEnvironmentOptions: {},
144 |
145 | // Adds a location field to test results
146 | // testLocationInResults: false,
147 |
148 | // The glob patterns Jest uses to detect test files
149 | // testMatch: [
150 | // "**/__tests__/**/*.[jt]s?(x)",
151 | // "**/?(*.)+(spec|test).[tj]s?(x)"
152 | // ],
153 |
154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
155 | // testPathIgnorePatterns: [
156 | // "\\\\node_modules\\\\"
157 | // ],
158 |
159 | // The regexp pattern or array of patterns that Jest uses to detect test files
160 | // testRegex: [],
161 |
162 | // This option allows the use of a custom results processor
163 | // testResultsProcessor: undefined,
164 |
165 | // This option allows use of a custom test runner
166 | // testRunner: "jest-circus/runner",
167 |
168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
169 | // testURL: "http://localhost",
170 |
171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
172 | // timers: "real",
173 |
174 | // A map from regular expressions to paths to transformers
175 | // transform: undefined,
176 |
177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
178 | // transformIgnorePatterns: [
179 | // "\\\\node_modules\\\\",
180 | // "\\.pnp\\.[^\\\\]+$"
181 | // ],
182 |
183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
184 | // unmockedModulePathPatterns: undefined,
185 |
186 | // Indicates whether each individual test should be reported during the run
187 | // verbose: undefined,
188 |
189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
190 | // watchPathIgnorePatterns: [],
191 |
192 | // Whether to use watchman for file crawling
193 | // watchman: true,
194 | };
195 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghx-mini-vue3",
3 | "version": "1.0.0",
4 | "description": "手写vue3核心逻辑代码",
5 | "main": "lib/guide-toy-vue3-cjs.js",
6 | "module": "lib/guide-toy-vue3-esm.js",
7 | "scripts": {
8 | "test": "jest",
9 | "build": "rollup -c rollup.config.js -w"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+ssh://git@github.com/HongxuanG/mini-vue3.git"
14 | },
15 | "keywords": [
16 | "mini-vue3",
17 | "vue3",
18 | "coding"
19 | ],
20 | "author": "HongxuanG",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/HongxuanG/mini-vue3/issues"
24 | },
25 | "homepage": "https://github.com/HongxuanG/mini-vue3#readme",
26 | "devDependencies": {
27 | "@babel/core": "^7.17.9",
28 | "@babel/preset-env": "^7.16.11",
29 | "@babel/preset-typescript": "^7.16.7",
30 | "@rollup/plugin-typescript": "^8.3.2",
31 | "@types/jest": "^27.4.1",
32 | "@types/node": "^17.0.24",
33 | "babel-jest": "^27.5.1",
34 | "jest": "^27.5.1",
35 | "rollup": "^2.72.1",
36 | "ts-node": "^10.7.0",
37 | "tslib": "^2.4.0",
38 | "typescript": "^4.6.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript'
2 | import pkg from './package.json'
3 | export default {
4 | input: './src/index.ts',
5 | output: [
6 | {
7 | file: './lib/guide-toy-vue3.cjs.js',
8 | format: 'cjs',
9 | sourcemap: true,
10 | },
11 | {
12 | file: './lib/guide-toy-vue3.esm.js',
13 | format: 'es',
14 | sourcemap: true,
15 | }
16 | ],
17 | plugins: [typescript()]
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // 统一打包入口
2 | export * from './runtime-dom'
3 | export * from './reactivity'
4 |
--------------------------------------------------------------------------------
/src/reactivity/baseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { track, trigger } from './effect'
2 | import { reactive, ReactiveFlags, readonly } from './reactive'
3 | import { extend, isObject } from '../shared'
4 | // 此处调用一次createSetter和getter,为了不在每次使用mutableHandlers的时候重复调用
5 | const get = createGetter()
6 | const set = createSetter()
7 | const readonlyGet = createGetter(true)
8 | const shallowReadonlyGet = createGetter(true, true)
9 | // shallowReactive的get操作
10 | const shallowGet = createGetter(false, true)
11 | // shallowReactive的set操作
12 | const shallowSet = createSetter(true)
13 |
14 | // 高阶函数
15 | export function createGetter(
16 | isReadonly = false,
17 | isShallow = false
18 | ) {
19 | return function get(target: T, key: string | symbol) {
20 | // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的
21 | if (key === ReactiveFlags.IS_REACTIVE) {
22 | return !isReadonly
23 | } else if (key === ReactiveFlags.IS_READONLY) {
24 | return isReadonly
25 | } else if (key === ReactiveFlags.IS_SHALLOW) {
26 | return isShallow
27 | } else if (key === ReactiveFlags.RAW) {
28 | return target
29 | }
30 | let res = Reflect.get(target, key)
31 |
32 | if (!isReadonly) {
33 | // 判断是否readonly
34 | // 依赖收集
35 | track(target, key as string)
36 | }
37 | if (isShallow) {
38 | return res
39 | }
40 | // 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive
41 | if (isObject(res)) {
42 | return isReadonly ? readonly(res) : reactive(res)
43 | }
44 | return res
45 | }
46 | }
47 | // 这个isShallow涉及到的是shallowReactive
48 | export function createSetter(isShallow = false) {
49 | return function set(target: T, key: string | symbol, value: any) {
50 | let success: boolean
51 | success = Reflect.set(target, key, value)
52 | // 触发依赖
53 | trigger(target, key as string)
54 | return success
55 | }
56 | }
57 |
58 | export const mutableHandlers: ProxyHandler