├── .gitignore
├── images
├── 按位与.gif
├── 按位或.gif
├── fsm-1.png
├── parse.png
├── parser-01.png
└── diff
│ ├── example.png
│ ├── vue-diff-01.gif
│ ├── vue-diff-03.gif
│ ├── vue-diff-04.gif
│ ├── vue-diff-05.gif
│ ├── vue-diff-06.gif
│ ├── vue-diff-07.gif
│ └── vue-diff-02-3.gif
├── mindmap
├── 初始化流程图.png
├── patch 算法图解.png
├── slot 流程.xmind
├── vue3源码思维脑图.zip
├── 完整的流程调用图.xmind
├── diff 算法
│ ├── 中间对比.png
│ ├── 右侧对比.png
│ ├── 左侧对比.png
│ ├── 新的比老的长-创建-右侧.png
│ ├── 新的比老的长-创建-左侧.png
│ ├── 老的比新的长-删除-右侧.png
│ └── 老的比新的长-删除-左侧.png
├── reactivity.xmind
├── update 流程.xmind
├── compiler-core.xmind
├── patch 算法图解 5.2.png
├── happy path 初始化流程.xmind
└── patchElement 的流程.xmind
├── README.md
├── docs
├── 9. 实现 isProxy.md
├── 25. 实现 getCurrentInstance.md
├── 36. 编译模块概述.md
├── 10. 实现 shallowReactive.md
├── 13. 实现 isRef 和 unRef.md
├── 20. 实现注册事件功能.md
├── 2. 实现 effect 返回 runner.md
├── 43. codegen 生成 text.md
├── 8. 实现 shallowReadonly.md
├── 14. 实现 proxyRefs.md
├── 0.初始化环境.md
├── 39. 实现解析 text.md
├── 17. 配置 rollup.md
├── 7. 实现 reactive 和 readonly 的嵌套转换.md
├── 3. 实现 effect 的 scheduler 功能.md
├── 12. 实现 toRaw.md
├── 41. 从有限状态机的角度看 parse 原理.md
├── 6. 实现 isReactive 和 isReadonly.md
├── 45. codegen 生成 element.md
├── 38. 实现解析 element 标签.md
├── 42. transform 模块.md
├── 15. 实现 computed.md
├── 35. 视图异步更新 && nextTick API.md
├── 1.实现 effect & reactive & 依赖收集 & 触发依赖.md
├── 32. 更新 children(三).md
├── 24. 实现 Fragment 和 Text 节点.md
├── 30. 更新 children(一).md
├── 29. 更新 props.md
├── 37. 实现解析插值表达式.md
├── 18. 组件的代理对象.md
├── 5. 实现 readonly 功能.md
├── 27. 实现 customRenderer.md
├── 34. 更新 component.md
├── 22. 实现组件的 emit 功能.md
├── 4. 实现 effect 的 stop 功能.md
├── 28. 初始化 element 更新流程.md
├── 47. 实现 template 编译为 render.md
├── 44. codegen 生成插值类型.md
├── 11. 实现 ref.md
├── 19. 实现 shapeFlags.md
├── 21. 实现组件的 props 功能.md
├── 26. 实现 provide 和 inject.md
├── 23. 实现组件的 slot 功能.md
├── 33. 更新 children(四).md
├── 16. 组件的初始化流程.md
├── 40. 三种类型联合解析.md
├── 31. 更新 children(二).md
└── 46. codegen 生成联合 3 种类型.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | */.DS_Store
--------------------------------------------------------------------------------
/images/按位与.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/按位与.gif
--------------------------------------------------------------------------------
/images/按位或.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/按位或.gif
--------------------------------------------------------------------------------
/images/fsm-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/fsm-1.png
--------------------------------------------------------------------------------
/images/parse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/parse.png
--------------------------------------------------------------------------------
/images/parser-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/parser-01.png
--------------------------------------------------------------------------------
/mindmap/初始化流程图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/初始化流程图.png
--------------------------------------------------------------------------------
/mindmap/patch 算法图解.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/patch 算法图解.png
--------------------------------------------------------------------------------
/mindmap/slot 流程.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/slot 流程.xmind
--------------------------------------------------------------------------------
/mindmap/vue3源码思维脑图.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/vue3源码思维脑图.zip
--------------------------------------------------------------------------------
/mindmap/完整的流程调用图.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/完整的流程调用图.xmind
--------------------------------------------------------------------------------
/images/diff/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/example.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/中间对比.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/中间对比.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/右侧对比.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/右侧对比.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/左侧对比.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/左侧对比.png
--------------------------------------------------------------------------------
/mindmap/reactivity.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/reactivity.xmind
--------------------------------------------------------------------------------
/mindmap/update 流程.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/update 流程.xmind
--------------------------------------------------------------------------------
/images/diff/vue-diff-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-01.gif
--------------------------------------------------------------------------------
/images/diff/vue-diff-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-03.gif
--------------------------------------------------------------------------------
/images/diff/vue-diff-04.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-04.gif
--------------------------------------------------------------------------------
/images/diff/vue-diff-05.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-05.gif
--------------------------------------------------------------------------------
/images/diff/vue-diff-06.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-06.gif
--------------------------------------------------------------------------------
/images/diff/vue-diff-07.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-07.gif
--------------------------------------------------------------------------------
/mindmap/compiler-core.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/compiler-core.xmind
--------------------------------------------------------------------------------
/mindmap/patch 算法图解 5.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/patch 算法图解 5.2.png
--------------------------------------------------------------------------------
/images/diff/vue-diff-02-3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/images/diff/vue-diff-02-3.gif
--------------------------------------------------------------------------------
/mindmap/happy path 初始化流程.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/happy path 初始化流程.xmind
--------------------------------------------------------------------------------
/mindmap/patchElement 的流程.xmind:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/patchElement 的流程.xmind
--------------------------------------------------------------------------------
/mindmap/diff 算法/新的比老的长-创建-右侧.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/新的比老的长-创建-右侧.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/新的比老的长-创建-左侧.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/新的比老的长-创建-左侧.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/老的比新的长-删除-右侧.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/老的比新的长-删除-右侧.png
--------------------------------------------------------------------------------
/mindmap/diff 算法/老的比新的长-删除-左侧.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexzhang-dev/mini-vue-docs/HEAD/mindmap/diff 算法/老的比新的长-删除-左侧.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mini-vue docs
2 |
3 | ## what is this repo ?
4 |
5 | 这个库主要是对于 [mini-vue](https://github.com/zx-projects/mini-vue) 这个库的大体讲解。在 `docs` 文件夹中,每个文件是对于每一个模块的讲解。
6 |
7 | ## why create this repo ?
8 |
9 | 好记性不如烂笔头,学会 mini-vue,还需要完善的笔记支撑。
10 |
--------------------------------------------------------------------------------
/docs/9. 实现 isProxy.md:
--------------------------------------------------------------------------------
1 | # 实现 isProxy
2 |
3 | ## 1. 单测
4 |
5 | 这里就不放了,简单来说,isProxy 可以检测某个对象是否是由 `reactive` 和 `readonly` 创建出来的
6 |
7 | ## 2. 实现
8 |
9 | ```ts
10 | // reactive.ts
11 |
12 | export function isProxy(wrapped) {
13 | return isReactive(wrapped) || isReadonly(wrapped)
14 | }
15 | ```
16 |
17 | 这样就非常简单的实现了
--------------------------------------------------------------------------------
/docs/25. 实现 getCurrentInstance.md:
--------------------------------------------------------------------------------
1 | # 实现 getCurrentInstance
2 |
3 | 我们可以在 `setup` 中通过 `getCurrentInstance()` 来获取当前的组件实例
4 |
5 | 首先,我们需要在 `components` 中导出一个函数 `getCurrentInstance()`
6 |
7 | ```ts
8 | // 将 currentInstance 作为一个全局变量
9 | let currentInstance
10 |
11 | export function getCurrentInstance() {
12 | return currentInstance
13 | }
14 | ```
15 |
16 | 在调用组件 `setup` 函数的时候将 setupInstance 赋值
17 |
18 | ```ts
19 | function setupStatefulComponent(instance) {
20 | if (setup) {
21 | // 赋值
22 | setCurrentInstance(instance)
23 | const setupResult = setup(shallowReadonly(instance.props), {
24 | emit: instance.emit,
25 | })
26 | // 重置
27 | setCurrentInstance(null)
28 | handleSetupResult(instance, setupResult)
29 | }
30 | }
31 | ```
32 |
33 | 现在我们就已经实现了 `getCurrentInstance` 了。
--------------------------------------------------------------------------------
/docs/36. 编译模块概述.md:
--------------------------------------------------------------------------------
1 | # 编译模块概述
2 |
3 | 在本实现的编译模块,我们将自己编写的编译模块来解析并输出未 `render` 函数
4 |
5 | ```ts
6 | let template = `Consumer: xxx`
7 | ```
8 |
9 | ```ts
10 | render() {
11 | return h('div', {}, `Consumer: xxx`)
12 | }
13 | ```
14 |
15 | 大概的编译流程是这样的:
16 |
17 | 
18 |
19 | - 用户编写的 `template` 字符串
20 | - 通过 `parser` 转换为 AST 抽象语法树
21 | - 将 AST 传递到 `transform` 模块,对 AST 进行处理
22 | - 将处理好的 AST 传递到 `codegen` 最终输出 `render`
23 |
24 | [一个例子](https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PkhlbGxvIFdvcmxkITwvZGl2PiIsIm9wdGlvbnMiOnt9fQ==)
25 |
26 | 一个 `template` 会涉及到三种类型:
27 |
28 | ```html
29 |
84 | advanceBy(context, match![0].length + 1)
85 | return {
86 | type: NodeType.ELEMENT,
87 | tag,
88 | }
89 | }
90 | ```
91 |
92 | 最后,推进后我们的 `context.source` 还剩下 `
`,此时我们发现和我们写好的匹配 `` 的很像,只需要修改正则就好了
93 |
94 | ```
95 | ^<\/?[a-z]*
96 | ```
97 |
98 | ```ts
99 | function parseElement(context: { source: string }): any {
100 | // 这里调用两次 parseTag 处理前后标签
101 | const element = parseTag(context)
102 | parseTag(context)
103 | return element
104 | }
105 |
106 | function parseTag(context: { source: string }) {
107 | // 修改正则
108 | const match = /^<\/?([a-z]*)/i.exec(context.source)
109 | const tag = match![1]
110 | advanceBy(context, match![0].length + 1)
111 | return {
112 | type: NodeType.ELEMENT,
113 | tag,
114 | }
115 | }
116 | ```
117 |
118 | ### 2.3 优化
119 |
120 | 再处理结束标签的时候我们发现不需要 `return` 了,所以可以优化一下
121 |
122 | ```ts
123 | // 增加枚举
124 | const enum TagType {
125 | START,
126 | END,
127 | }
128 |
129 | function parseElement(context: { source: string }): any {
130 | // 传入类型
131 | const element = parseTag(context, TagType.START)
132 | parseTag(context, TagType.END)
133 | return element
134 | }
135 |
136 | function parseTag(context: { source: string }, type: TagType) {
137 | const match = /^<\/?([a-z]*)/i.exec(context.source)
138 | const tag = match![1]
139 | advanceBy(context, match![0].length + 1)
140 | // 如果是 TagType.END 就不需要 return
141 | if (type === TagType.END) return
142 | return {
143 | type: NodeType.ELEMENT,
144 | tag,
145 | }
146 | }
147 | ```
148 |
149 |
--------------------------------------------------------------------------------
/docs/42. transform 模块.md:
--------------------------------------------------------------------------------
1 | # transform 模块
2 |
3 | 在本小节中,我们将通过一个小例子,来看看 transform 模块的作用
4 |
5 | ## 1. 测试样例
6 |
7 | ```ts
8 | test('should change text content', () => {
9 | const ast = baseParse('
hi
')
10 | transform(ast)
11 | expect(ast.children[0].children[0].content).toEqual('hi mini-vue')
12 | })
13 | ```
14 |
15 | 我们这个测试样例要保证的就是,如果说 nodeType 是 text 的时候,修改其 content 的值。
16 |
17 | ## 2. 实现
18 |
19 | 实现起来还是非常简单的,我们只需要遍历整个树就可以了,这里我们使用递归来遍历。
20 |
21 | ```ts
22 | export function transform(root) {
23 | traverseNode(root)
24 | }
25 |
26 | function traverseNode(node) {
27 | // 在这里就可以对 node 进行操作
28 | const children = node.children
29 | if (children) {
30 | for (let i = 0; i < children.length; i++) {
31 | traverseNode(children[i])
32 | }
33 | }
34 | }
35 | ```
36 |
37 | 但是我们发现,其实将 content 的值追加 mini-vue 这类的需求,其实是非常特定的场景下的,但是我们设计程序是肯定要通用性最佳的,不可能要将特定的处理写在程序中。
38 |
39 | 所以我们就可以换一种思路,通过外部提供处理程序,内部再调用外部传入的处理程序。我们称之为插件(plugin)。
40 |
41 | ```ts
42 | // 改写测试
43 | test('should change text content', () => {
44 | const ast = baseParse('
hi
')
45 | // 外部提供处理
46 | const transformText = node => {
47 | if (node.type === NodeType.TEXT) {
48 | node.content += ' mini-vue'
49 | }
50 | }
51 | // 通过 options 传入内部,内部再调用
52 | transform(ast, {
53 | nodeTransforms: [transformText],
54 | })
55 | expect(ast.children[0].children[0].content).toEqual('hi mini-vue')
56 | })
57 | ```
58 |
59 | ```ts
60 | export function transform(root, options) {
61 | // 首先我们创建一个 transform 的上下文
62 | const context = createTransformContext(root, options)
63 | // 然后将这个上下文传入 traverseNode 中
64 | traverseNode(root, context)
65 | }
66 |
67 | function createTransformContext(root, options) {
68 | return {
69 | root,
70 | nodeTransforms: options.nodeTransforms || {},
71 | }
72 | }
73 |
74 | function traverseNode(node, context) {
75 | // 在这里对每个 node 通过 transforms 进行依次处理
76 | const { nodeTransforms } = context
77 | for (let i = 0; i < nodeTransforms.length; i++) {
78 | const transform = nodeTransforms[i]
79 | transform(node)
80 | }
81 | const children = node.children
82 | if (children) {
83 | for (let i = 0; i < children.length; i++) {
84 | traverseNode(children[i], context)
85 | }
86 | }
87 | }
88 | ```
89 |
90 | 这样,我们就可以通过外部传入的处理程序来对于内部的 node 进行处理了。
91 |
92 | ## 3. 重构
93 |
94 | 我们可以将 `traverseNode` 中的部分代码抽离出去
95 |
96 | ```ts
97 | function traverseNode(node, context) {
98 | const { nodeTransforms } = context
99 | for (let i = 0; i < nodeTransforms.length; i++) {
100 | const transform = nodeTransforms[i]
101 | transform(node)
102 | }
103 | // 将递归的部分抽离出去
104 | traverseChildren(node, context)
105 | }
106 |
107 | function traverseChildren(node, context) {
108 | const children = node.children
109 | if (children) {
110 | for (let i = 0; i < children.length; i++) {
111 | traverseNode(children[i], context)
112 | }
113 | }
114 | }
115 | ```
116 |
117 | ## 4. 为下个阶段 codegen 铺路
118 |
119 | 我们在看整个 compiler 模块的流程时,我们发现 transform 下个阶段就是 codegen,生成最终的 js string。那么目前我们在 codegen 模块可以直接对整个 ast 树进行做处理吗?肯定是不行的, 假设我们后期想要修改 ast 的结构,那么肯定还要修改 codegen 的代码。这是不合理的。
120 |
121 | ```ts
122 | export function transform(root, options = {}) {
123 | const context = createTransformContext(root, options)
124 | traverseNode(root, context)
125 | createRootCodegen(root)
126 | }
127 |
128 | function createRootCodegen(root) {
129 | // 所以我们为下个阶段 codegen 铺路,在 transform 指定 codegen 处理的节点
130 | root.codegenNode = root.children[0]
131 | }
132 | ```
133 |
134 | 现在我们的模块职责就比较清楚了,codegen 只处理 codegenNode
135 |
136 | 至于 parse 的 ast 怎么变成 codegenNode,那就是 transform 的事情了
--------------------------------------------------------------------------------
/docs/15. 实现 computed.md:
--------------------------------------------------------------------------------
1 | # 实现 computed
2 |
3 | 在本小节中,将会实现 computed
4 |
5 | ## 1. happy path
6 |
7 | 先看测试
8 |
9 | ```ts
10 | it('happy path', () => {
11 | const user = reactive({
12 | age: 1,
13 | })
14 |
15 | const age = computed(() => {
16 | return user.age
17 | })
18 |
19 | expect(age.value).toBe(1)
20 | })
21 | ```
22 |
23 | 接下来我们看如何实现
24 |
25 | ```ts
26 | class ComputedRefImpl {
27 | private _getter: any
28 | constructor(getter) {
29 | this._getter = getter
30 | }
31 | get value() {
32 | // 在调用 value 时将传入的 getter 的执行结果返回
33 | return this._getter()
34 | }
35 | }
36 |
37 | export function computed(getter) {
38 | return new ComputedRefImpl(getter)
39 | }
40 | ```
41 |
42 | ## 2. 缓存机制
43 |
44 | 我们来看看测试样例
45 |
46 | ```ts
47 | it('should computed lazily', () => {
48 | const value = reactive({ foo: 1 })
49 | const getter = jest.fn(() => value.foo)
50 | const cValue = computed(getter)
51 |
52 | // lazy
53 | expect(getter).not.toHaveBeenCalled()
54 | // 触发 get 操作时传入的 getter 会被调用一次
55 | expect(cValue.value).toBe(1)
56 | expect(getter).toHaveBeenCalledTimes(1)
57 |
58 | // 不会再次调用 computed
59 | cValue.value
60 | expect(getter).toHaveBeenCalledTimes(1)
61 | })
62 | ```
63 |
64 | 这里我们发现,再次读取 cValue.value 的时候是不会再次去计算的,而是拿的缓存。
65 |
66 | ```ts
67 | class ComputedRefImpl {
68 | private _getter: any
69 | // _value 缓存值
70 | private _value: any
71 | // _dirty 是否需要更新值
72 | private _dirty = false
73 | constructor(getter) {
74 | this._getter = getter
75 | }
76 | get value() {
77 | // 这里进行判断,如果还未初始化,执行 getter,缓存一份
78 | if (this._dirty) {
79 | this._value = this._getter()
80 | this._dirty = false
81 | }
82 | // 这里就直接返回缓存
83 | return this._value
84 | }
85 | }
86 |
87 | export function computed(getter) {
88 | return new ComputedRefImpl(getter)
89 | }
90 | ```
91 |
92 | 接下来,我们来看看进阶版的
93 |
94 | ```ts
95 | // 在不需要这个 computed 的时候 value 变了 computed 也不会执行
96 | value.foo = 2
97 | expect(getter).toHaveBeenCalledTimes(1)
98 |
99 | // 在需要这个 computed 的时候再次计算(如果 computed 依赖的值已经发生更改)
100 | expect(cValue.value).toBe(2)
101 | expect(getter).toHaveBeenCalledTimes(2)
102 |
103 | // 不变拿的就是缓存
104 | cValue.value
105 | expect(getter).toHaveBeenCalledTimes(2)
106 | ```
107 |
108 | 其实现在我们就已经有了思路了,为什么不执行 getter 是因为我们加了一把锁 `_dirty`,那么只需要在依赖的值所发生的改变的时候将这个 `_dirty = false` 就可以了,那么再次 get value 的时候就会因为锁打开了而重新执行并计算值
109 |
110 | 我们知道依赖的值发生改变的时候其实是进入了 `trigger` 方法里面,而 `trigger` 中有一个判断条件,那就是如果 ReactiveEffect option 有 `scheduler` 的话,是会执行 `scheduler` 而不是 `execution`,那么我们就可以通过这个 `scheduler` 做文章了。
111 |
112 | 那么现在问题又来了,`scheduler` 是存在 `EeactiveEffect` 实例上的,而该类是在 `effect` 中创建实例的,所以我们的 `computed` 其实也需要自己维护一个 effect,相当于把 getter 作为 effect。
113 |
114 | ```ts
115 | import { ReactiveEffect } from './effect'
116 |
117 | class ComputedRefImpl {
118 | private _getter: any
119 | private _value: any
120 | private _dirty = false
121 | private _effect: any
122 | constructor(getter) {
123 | this._getter = getter
124 | // 这里需要内部维护一个 ReactiveEffect 实例
125 | this._effect = new ReactiveEffect(getter, {
126 | scheduler: () => {
127 | // 在 scheduler 中把锁打开
128 | this._dirty = true
129 | },
130 | })
131 | }
132 | get value() {
133 | // 因为在依赖值更新的时候会进行 triiger, triiger 调用 scheduler,锁打开了
134 | // 再次 get value,因为锁是打开的,就可以重新计算值了
135 | if (this._dirty) {
136 | this._value = this._effect.run()
137 | this._dirty = false
138 | }
139 | return this._value
140 | }
141 | }
142 |
143 | export function computed(getter) {
144 | return new ComputedRefImpl(getter)
145 | }
146 | ```
147 |
148 | 这样我们的测试就跑通了
--------------------------------------------------------------------------------
/docs/35. 视图异步更新 && nextTick API.md:
--------------------------------------------------------------------------------
1 | # 视图异步更新 && nextTick API
2 |
3 | 在本小节中,我们将会实现视图异步更新,以及实现 `nextTick` API
4 |
5 | ## 1. 例子
6 |
7 | ```ts
8 | export default {
9 | setup() {
10 | const count = ref(1)
11 | function onClick() {
12 | for (let i = 0; i < 100; i++) {
13 | console.log('update')
14 | count.value = i
15 | }
16 | }
17 |
18 | return {
19 | onClick,
20 | count,
21 | }
22 | },
23 | render() {
24 | const button = h('button', { onClick: this.onClick }, 'update')
25 | const p = h('p', {}, 'count:' + this.count)
26 | return h('div', {}, [button, p])
27 | },
28 | }
29 | ```
30 |
31 | - 这个例子会进行 100 次循环,在每次循环时,对视图的依赖进行更新
32 | - 而由于视图的依赖触发了更新,那么就会触发重新生成 VNode、对比、更新
33 | - 这也就导致了,更新流程被触发了 100 次。
34 |
35 | - 这样的性能开销是非常大的
36 |
37 | ## 2. 视图异步更新
38 |
39 | 由于循环是同步代码,根据浏览器任务队列,微任务队列中的任务等待同步任务执行完毕后进行调用栈进行执行,所以核心优化点就是,**我们只需要将依赖更新导致触发更新逻辑的流程加入到微任务队列中就可以了**
40 |
41 | ```ts
42 | function setupRenderEffect(instance, vnode, container, anchor) {
43 | instance.update = effect(() => {
44 | // 渲染和更新逻辑
45 | // 如果视图依赖发生变化,那么会重新进入到该函数中,我们只需要将此次渲染任务加入到微任务队列中就可以了。
46 | })
47 | }
48 | ```
49 |
50 | 那么该怎么加呢?还记得 `scheduler` 嘛?当一个 `effect` 函数有了 `scheduler` 之后,后续的依赖更新将只会执行 `scheduler` 而不是传入的函数。
51 |
52 | ```ts
53 | function setupRenderEffect(instance, vnode, container, anchor) {
54 | instance.update = effect(
55 | () => {
56 | },
57 | {
58 | scheduler() {
59 | // 将本次 update 加入到任务队列中
60 | queueJobs(instance.update)
61 | },
62 | }
63 | )
64 | }
65 | ```
66 |
67 | ```ts
68 | // scheduler
69 | const queue: any[] = []
70 | let isFlushPending = false
71 |
72 | export function queueJobs(job) {
73 | if (!queue.includes(job)) {
74 | queue.push(job)
75 | }
76 | queueFlush()
77 | }
78 |
79 | function queueFlush() {
80 | // 使用 Promise 来在下一次异步任务清空时进行清空当前的视图渲染的异步队列
81 | // 使用 shift() 从头部开始清理
82 | if (isFlushPending) return
83 | isFlushPending = true
84 | Promise.resolve().then(() => {
85 | isFlushPending = false
86 | let job
87 | while ((job = queue.shift())) {
88 | job && job()
89 | }
90 | })
91 | }
92 | ```
93 |
94 | 现在我们的视图已经是异步更新了,等待所有同步代码执行完毕后,开始清空微任务队列中的更新视图任务
95 |
96 | ## 3. 实现 nextTick API
97 |
98 | 我们可以通过 `nextTick` API 来获取下一次更新视图时的数据
99 |
100 | ```ts
101 | // 举个例子
102 | const count = ref(1)
103 | const instance = getCurrentInstance()
104 | function onClick() {
105 | for (let i = 0; i < 100; i++) {
106 | count.value = i
107 | }
108 | // 此时 count 的值应该还是 1
109 | console.log({ instance })
110 | // 通过这两种方式可以等待到异步更新完毕视图后再执行代码
111 | nextTick(() => {
112 | console.log({ instance })
113 | })
114 | await nextTick()
115 | console.log({ instance })
116 | }
117 | ```
118 |
119 | ```ts
120 | const queue: any[] = []
121 | let isFlushPending = false
122 | let p = Promise.resolve()
123 |
124 | // 添加 nextTick API
125 | export function nextTick(fn) {
126 | return fn ? p.then(fn) : p
127 | }
128 |
129 | ```
130 |
131 | 然后我们可以重构这一部分的代码
132 |
133 | ```diff
134 | const queue: any[] = []
135 | let isFlushPending = false
136 | let p = Promise.resolve()
137 |
138 | export function nextTick(fn) {
139 | return fn ? p.then(fn) : p
140 | }
141 |
142 | export function queueJobs(job) {
143 | if (!queue.includes(job)) {
144 | queue.push(job)
145 | }
146 | queueFlush()
147 | }
148 |
149 | function queueFlush() {
150 | // 使用 Promise 来在下一次异步任务清空时进行清空当前的视图渲染的异步队列
151 | // 使用 shift() 从头部开始清理
152 | if (isFlushPending) return
153 | isFlushPending = true
154 | + nextTick(flushJobs)
155 | - Promise.resolve().then(() => {
156 | - isFlushPending = false
157 | - let job
158 | - while ((job = queue.shift())) {
159 | - job && job()
160 | - }
161 | - })
162 | }
163 |
164 | + function flushJobs() {
165 | + isFlushPending = false
166 | + let job
167 | + while ((job = queue.shift())) {
168 | + job && job()
169 | + }
170 | + }
171 | ```
172 |
173 | 最后就完事了
174 |
175 |
--------------------------------------------------------------------------------
/docs/1.实现 effect & reactive & 依赖收集 & 触发依赖.md:
--------------------------------------------------------------------------------
1 | # 实现 effect & reactive & 依赖收集 & 触发依赖
2 |
3 |
4 |
5 | 在本小节呢,我们将实现 effect & reactive & 依赖收集 & 触发依赖
6 |
7 | ## 1. 编写单元测试
8 |
9 | 我们先建一个单元测试:
10 |
11 | ```js
12 | describe('effect', () => {
13 | it('happy path', () => {
14 | const user = reactive({
15 | age: 10,
16 | })
17 | let nextAge
18 | effect(() => {
19 | nextAge = user.age + 1
20 | })
21 | expect(nextAge).toBe(11)
22 |
23 | // update
24 | user.age++
25 | expect(nextAge).toBe(12)
26 | })
27 | })
28 | ```
29 |
30 | 以上的单元测试,就是本文中重点需要通过的测试。在此之前,我们可以先去写一个 reactive API
31 |
32 | ## 2. reactive 实现
33 |
34 | ### 2.1 编写一个单元测试
35 |
36 | ```js
37 | // 编写 reactive 的 happy path
38 | describe('reactive', () => {
39 | it('happy path', () => {
40 | const original = { foo: 1 }
41 | const observed = reactive(original)
42 | // 期望包装后和源对象不一样
43 | expect(observed).not.toBe(original)
44 | // 期望包装后某个属性的值和源对象一样
45 | expect(observed.foo).toBe(original.foo)
46 | })
47 | })
48 | ```
49 |
50 | 那该如何实现呢?在这里我们就可以使用 Proxy + Reflect 来实现了
51 |
52 | ### 2.2 实现
53 |
54 | ```js
55 | // 可以使用简单的 Proxy 来实现
56 | export function reactive(raw) {
57 | return new Proxy(raw, {
58 | get(target, key, receiver) {
59 | const res = Reflect.get(target, key, receiver)
60 | return res
61 | },
62 | set(target, key, value, receiver) {
63 | const res = Reflect.set(target, key, value, receiver)
64 | return res
65 | },
66 | })
67 | }
68 | ```
69 |
70 | 运行一下 happy path,通过
71 |
72 | ## 3. effect 实现
73 |
74 | 下面,我们就回过头来看看最开始的单元测试,此时我们已经有了 reactive,接下来就是去实现一个 effect API。
75 |
76 | ### 3.1 v1 版本
77 |
78 | 首先,我们知道了 effect 接受一个参数,可以通过抽象一层:
79 |
80 | ```js
81 | class ReactiveEffect {
82 | private _fn: any
83 | constructor(fn) {
84 | this._fn = fn
85 | }
86 | run() {
87 | this._fn()
88 | }
89 | }
90 |
91 | export function effect(fn) {
92 | // 抽象一层
93 | const _effect = new ReactiveEffect(fn)
94 | // 去调用方法
95 | _effect.run()
96 | }
97 | ```
98 |
99 | 此时我们 update 之前的逻辑就可以跑通了,下面的难点在于 update
100 |
101 | ### 3.2 v2 版本
102 |
103 | 这个版本,我们主要是用于解决 update 的问题,我们来看看测试,发现在 get 操作的时候需要将依赖收集,在 set 操作的时候再去触发这个依赖,下面我们就可以手动在 reactive 中添加相应的逻辑
104 |
105 | ```js
106 | export function reactive(raw) {
107 | return new Proxy(raw, {
108 | get(target, key, receiver) {
109 | const res = Reflect.get(target, key, receiver)
110 | // 在 get 时收集依赖
111 | track(target, key)
112 | return res
113 | },
114 | set(target, key, value, receiver) {
115 | const res = Reflect.set(target, key, value, receiver)
116 | // 在 set 时触发依赖
117 | trigger(target, key)
118 | return res
119 | },
120 | })
121 | }
122 | ```
123 |
124 | 下面,我们就去编写一个 track 和 trigger
125 |
126 | ```js
127 | // track 相关代码
128 | class ReactiveEffect {
129 | // ...
130 | run() {
131 | // 保存一下当前的 activeEffect
132 | activeEffect = this
133 | this._fn()
134 | }
135 | }
136 |
137 | // 创建全局变量 targetMap
138 | const targetMap = new WeakMap()
139 | export function track(target, key) {
140 | // 我们在运行时,可能会创建多个 target,每个 target 还会可能有多个 key,每个 key 又关联着多个 effectFn
141 | // 而且 target -> key -> effectFn,这三者是树形的关系
142 | // 因此就可以创建一个 WeakMap 用于保存 target,取出来就是每个 key 对应这一个 depsMap,而每个 depsMap 又是一个 Set
143 | // 数据结构(避免保存重复的 effect)
144 | let depsMap = targetMap.get(target)
145 | if (!depsMap) {
146 | depsMap = new Map()
147 | targetMap.set(target, depsMap)
148 | }
149 | let dep = depsMap.get(key)
150 | if (!dep) {
151 | dep = new Set()
152 | depsMap.set(key, dep)
153 | }
154 | // 将 effect 加入到 set 中
155 | dep.add(activeEffect)
156 | }
157 |
158 | // 需要一个全局变量来保存当前的 effect
159 | let activeEffect
160 |
161 | export function effect(fn) {
162 | // ...
163 | }
164 | ```
165 |
166 | 下面是 trigger
167 |
168 | ```js
169 | export function trigger(target, key) {
170 | // trigger 的逻辑就更加简单了,我们只需要取出对应的 deps 这个 set,再遍历执行每个 effect 就可以了
171 | const depsMap = targetMap.get(target)
172 | const deps = depsMap.get(key)
173 | for (const effect of deps) {
174 | effect.run()
175 | }
176 | }
177 | ```
178 |
179 | 现在我们再跑测试,就发现通过了,现在我们已经实现了 effect、reactive 的 happy path 了
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/docs/32. 更新 children(三).md:
--------------------------------------------------------------------------------
1 | # 更新 chilren(二)
2 |
3 | 在本小节中,我们将会去实现 `array` => `array` 中间对比的一种简单情况:
4 |
5 | - 旧的比新的多,删除
6 |
7 | ## 1. 例子
8 |
9 | [例子](https://github.com/zx-projects/mini-vue/blob/main/example/patchChildren/ArrayToArray.js)
10 |
11 | - 5.1.1、5.1.2
12 |
13 | ## 2. 实现
14 |
15 | 我们在上一个小节中,已经实现了新旧节点的首尾对比,在这个小节中呢,我们将会涉及到对比中间节点了。
16 |
17 | ### 2.1 例子
18 |
19 | ```ts
20 | const prevChildren = [
21 | h('p', { key: 'A' }, 'A'),
22 | h('p', { key: 'B' }, 'B'),
23 | h('p', { key: 'C', id: 'c-prev' }, 'C'),
24 | h('p', { key: 'D' }, 'D'),
25 | h('p', { key: 'F' }, 'F'),
26 | h('p', { key: 'G' }, 'G'),
27 | ]
28 |
29 | const nextChildren = [
30 | h('p', { key: 'A' }, 'A'),
31 | h('p', { key: 'B' }, 'B'),
32 | h('p', { key: 'E' }, 'E'),
33 | h('p', { key: 'C', id: 'c-next' }, 'C'),
34 | h('p', { key: 'F' }, 'F'),
35 | h('p', { key: 'G' }, 'G'),
36 | ]
37 | ```
38 |
39 | - 旧节点:A B C D F G
40 | - 新节点:A B E C F G
41 | - 新旧节点对比我们发现旧的比新的多 D,所以需要删除掉旧的中的 D
42 |
43 | ### 2.2 实现
44 |
45 | 我们已经可以通过上一小节实现的前后对比确定混乱的部分:
46 |
47 | 
48 |
49 | 此时指针:
50 |
51 | - `i`:2
52 | - `e1`: 3
53 | - `e2`: 3
54 |
55 | 下面我们要做的就是要判断出老的部分是否是多出来了,我们可以使用映射来对比
56 |
57 | ```ts
58 | // render.ts
59 |
60 | function patchKeyedChildren(c1, c2, container, parentInstance, anchor) {
61 | // 前后对比结束
62 | if (i > e1) {
63 | // 新的比老的多
64 | } else if (i > e2) {
65 | // 新的比老的少
66 | } else {
67 | // 对比中间部分
68 | let s1 = i
69 | let s2 = i
70 | // c2 混乱部分映射
71 | const keyToNewIndexMap = new Map()
72 | // 添加映射
73 | for (let i = s2; i <= e2; i++) {
74 | const nextChild = c2[i]
75 | keyToNewIndexMap.set(nextChild.key, i)
76 | }
77 | // 循环老的,根据映射找
78 | for (let i = s1; i <= e1; i++) {
79 | const prevChild = c1[i]
80 | let newIndex
81 | // 如果当前老的子节点的 key 不是空的
82 | if (prevChild.key !== null) {
83 | // 就去映射表中找到新的对应的 newIndex
84 | newIndex = keyToNewIndexMap.get(prevChild.key)
85 | } else {
86 | // 如果老的子节点的 key 是空的,还需要再次遍历新节点,找到与当前老节点相同的 VNode,并将其索引赋给 j
87 | for (let j = s2; j <= e2; j++) {
88 | if (isSameVNode(prevChild, c2[j])) {
89 | newIndex = j
90 | break
91 | }
92 | }
93 | }
94 | // 如果新节点中不存在对应的老节点,那么就删除掉老节点
95 | if (newIndex === undefined) {
96 | hostRemove(prevChild.el)
97 | } else {
98 | // 如果存在,就进入到 patch 阶段,继续递归对比
99 | patch(prevChild, c2[newIndex], container, parentInstance, null)
100 | }
101 | }
102 | }
103 | }
104 | ```
105 |
106 | 
107 |
108 | - 以上的动画对应的是`老节点有 key` 的情况下
109 | - 如果老节点没有 `key` 那么,还需要遍历一次新节点通过 `isSameVNode`,以确认**老节点是否在新节点中**
110 |
111 | ## 3. 优化
112 |
113 | ### 3.1 例子
114 |
115 | ```ts
116 | const prevChildren = [
117 | h("p", { key: "A" }, "A"),
118 | h("p", { key: "B" }, "B"),
119 | h("p", { key: "C", id: "c-prev" }, "C"),
120 | h("p", { key: "E" }, "E"),
121 | h("p", { key: "D" }, "D"),
122 | h("p", { key: "F" }, "F"),
123 | h("p", { key: "G" }, "G"),
124 | ];
125 |
126 | const nextChildren = [
127 | h("p", { key: "A" }, "A"),
128 | h("p", { key: "B" }, "B"),
129 | h("p", { key: "E" }, "E"),
130 | h("p", { key: "C", id:"c-next" }, "C"),
131 | h("p", { key: "F" }, "F"),
132 | h("p", { key: "G" }, "G"),
133 | ];
134 | ```
135 |
136 | - 旧节点:A B C E D F G
137 | - 新节点:A B E C F G
138 | - 我们发现,旧节点是比新节点多的,按照现有逻辑来说,我们先遍历新节点进行储存映射
139 | - 旧节点因为比新节点多,所以我们可以直接把多出来的旧节点直接删除掉
140 |
141 | ### 3.2 实现
142 |
143 | 添加一个两个变量:
144 |
145 | ```ts
146 | // 添加变量 toBePatched,用于记录所有需要 patch 的节点,也就是目前新节点的混乱部分的个数
147 | const toBePatched = e2 - s2 + 1
148 | // patched 是当前的 patch 过的个数
149 | let patched = 0
150 | ```
151 |
152 | 在循环老节点的时候进行判断
153 |
154 | ```ts
155 | if (patched >= toBePatched) {
156 | // 如果当前 patched 的个数 >= 应该 patched 的个数
157 | // 那么直接删除
158 | hostRemove(prevChild.el)
159 | continue
160 | }
161 | ```
162 |
163 | 我们来看看动画:
164 |
165 | 
--------------------------------------------------------------------------------
/docs/24. 实现 Fragment 和 Text 节点.md:
--------------------------------------------------------------------------------
1 | # 实现 Fragment 节点和 Text 节点
2 |
3 | ## 1. Fragment 节点
4 |
5 | ### 1.1 例子
6 |
7 | 我们在上一篇中已经实现了 slots,但是我们再向 slots 添加内容的时候,发现如果添加的内容超过了 1 个,最终还是要通过一个 `div` 包裹这两个元素的,这是为什么呢?
8 |
9 | ```ts
10 | import { h } from '../h'
11 |
12 | export function renderSlots(slots, name = 'default', props) {
13 | const slot = slots[name]
14 | if (slot) {
15 | // 我们在 renderSlots 的时候就使用了 div 作为包裹
16 | // 这是因为我们没办法将 array 直接渲染出来
17 | return h('div', {}, slot(props))
18 | }
19 | }
20 | ```
21 |
22 | 下面我们改如何实现呢?
23 |
24 | ```ts
25 | // render.ts
26 |
27 | // other code ...
28 |
29 | export function patch(vnode, container) {
30 | // 我们在 patch 的时候对类型进行判断
31 | // 这个时候我们可以添加一个 Fragment 类型,来对 Fragment 进行判断
32 | const { shapeFlags } = vnode
33 | if (shapeFlags & ShapeFlags.ELEMENT) {
34 | processElement(vnode, container)
35 | } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
36 | processComponent(vnode, container)
37 | }
38 | }
39 |
40 | // other code ...
41 | ```
42 |
43 | ### 1.2 实现
44 |
45 | 首先,我们可以在 renderSlots 的时候从生成 div 到生成 Fragment
46 |
47 | ```ts
48 | export function renderSlots(slots, name = 'default', props) {
49 | // 此时 slots 就是 Object
50 | const slot = slots[name]
51 | if (slot) {
52 | // 从 div -> Fragment
53 | return h('Fragment', {}, slot(props))
54 | }
55 | }
56 | ```
57 |
58 | 为了避免与用户的组件重名,我们可以生成一个 Symbol
59 |
60 | ```ts
61 | // vnode.ts
62 | export const Fragment = Symbol('Fragment')
63 | ```
64 |
65 | ```ts
66 | import { Fragment } from '../vnode'
67 |
68 | export function renderSlots(slots, name = 'default', props) {
69 | const slot = slots[name]
70 | if (slot) {
71 | // 从 'Fragement' -> Symbol
72 | return h(Fragment, {}, slot(props))
73 | }
74 | }
75 | ```
76 |
77 | 这样我们在 patch 的时候就可以对类型进行判断了
78 |
79 | ```ts
80 | // render.ts
81 |
82 | export function patch(vnode, container) {
83 | const { type, shapeFlags } = vnode
84 | switch (type) {
85 | // 对类型进行判断
86 | // 如果是 Fragement
87 | case Fragment:
88 | // 走 processFragment 的逻辑
89 | processFragment(vnode, container)
90 | break
91 | default:
92 | if (shapeFlags & ShapeFlags.ELEMENT) {
93 | processElement(vnode, container)
94 | } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
95 | processComponent(vnode, container)
96 | }
97 | break
98 | }
99 | }
100 |
101 |
102 | function processFragment(vnode, container) {
103 | // 因为 fragment 就是用来处理 children 的
104 | mountChildren(vnode, container)
105 | }
106 | ```
107 |
108 | 现在我们 slots 多个子节点就不再需要使用 div 来包裹了
109 |
110 | ## 2. Text 节点
111 |
112 | 我们的 slots 目前也不支持直接渲染一个 TextContent 节点
113 |
114 | ### 2.1 例子
115 |
116 | ```ts
117 | const foo = h(
118 | Foo,
119 | {},
120 | {
121 | header: ({ count }) => h('div', {}, '123' + count),
122 | // 渲染一个节点是无法进行渲染的
123 | footer: () => 'hello TextNode',
124 | }
125 | )
126 | ```
127 |
128 | 所以我们需要新增一个 API,用户创建纯 TextNode
129 |
130 | ```ts
131 | footer: () => createTextVNode('hello TextNode'),
132 | ```
133 |
134 | ### 2.2 实现
135 |
136 | 我们在 VNode 中来实现这个 API
137 |
138 | ```ts
139 | // vnode.ts
140 |
141 | export const TextNode = Symbol('TextNode')
142 |
143 | export function createTextVNode(text) {
144 | return createVNode(TextNode, {}, text)
145 | }
146 | ```
147 |
148 | 在 render 中也要修改为对应的逻辑
149 |
150 | ```ts
151 | export function patch(vnode, container) {
152 | const { type, shapeFlags } = vnode
153 | switch (type) {
154 | case Fragment:
155 | processFragment(vnode, container)
156 | break
157 | // 新增这个判断
158 | case TextNode:
159 | processTextNode(vnode, container)
160 | break
161 | default:
162 | if (shapeFlags & ShapeFlags.ELEMENT) {
163 | processElement(vnode, container)
164 | } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
165 | processComponent(vnode, container)
166 | }
167 | break
168 | }
169 | }
170 | ```
171 |
172 | ```ts
173 | function processTextNode(vnode, container) {
174 | // TextNode 本身就是纯 text
175 | const element = (vnode.el = document.createTextNode(vnode.children))
176 | container.appendChild(element)
177 | }
178 | ```
179 |
180 | 现在我们也已经支持 TextNode 了。
--------------------------------------------------------------------------------
/docs/30. 更新 children(一).md:
--------------------------------------------------------------------------------
1 | # 更新 children(一)
2 |
3 | 在本小节中,我们将会实现 `children` 更新第一部分逻辑,即
4 |
5 | - `text` => `array`
6 | - `text` => `newText`
7 | - `array` => `text`
8 |
9 | 而最后一部分的逻辑:
10 |
11 | - `array` => `array` 由于涉及到 diff 算法,将会在后面篇章中重点讲解
12 |
13 | ## 1. 例子
14 |
15 | 查看 [例子](https://github.com/zx-projects/mini-vue/blob/main/example/patchChildren/App.js)
16 |
17 | ## 2. 实现
18 |
19 | ### 2.1 `array` => `text`
20 |
21 | 还记得我们是在哪里进行处理更新逻辑的呢?
22 |
23 | ```ts
24 | // render.ts
25 |
26 | function patchElement(n1, n2, container) {
27 | const oldProps = n1.props || EMPTY_OBJ
28 | const newProps = n2.props || EMPTY_OBJ
29 | const el = (n2.el = n1.el)
30 | patchProps(el, oldProps, newProps)
31 | patchChildren(n1, n2)
32 | }
33 | ```
34 |
35 | 下面我们处理过了 props 还要处理 children
36 |
37 | ```ts
38 | function patchChildren(n1, n2) {
39 | const prevShapeFlag = n1.shapeFlags
40 | const shapeFlag = n2.shapeFlags
41 | // 情况1:array => text
42 | // 对新的 shapeFlag 进行判断
43 | // 如果是文本
44 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
45 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
46 | // 如果老的 shapeFlag 是 array_children,需要做两件事
47 | // 1. 清空原有 children
48 | unmountChildren(n1.children)
49 | // 2. 挂载文本 children
50 | hostSetElementText(n2.el, n2.children)
51 | }
52 | }
53 | }
54 | ```
55 |
56 | 如果要从 array => text,需要做两件事情:
57 |
58 | - 清空原有 `array_children`
59 | - 挂载文本 `text_children`
60 |
61 | #### 清空原有 array_children
62 |
63 | ```ts
64 | function unmountChildren(children) {
65 | for (let i = 0; i < children.length; i++) {
66 | // 遍历 children,同时执行 remove 逻辑
67 | // 由于这里涉及到元素渲染的实际操作,所以我们要抽离出去作为一个API
68 | hostRemove(children[i].el)
69 | }
70 | }
71 | ```
72 |
73 | ```ts
74 | // runtime-dom/index
75 | function remove(child) {
76 | // 获取到父节点,【parentNode 是 DOM API】
77 | const parentElement = child.parentNode
78 | if (parentElement) {
79 | // 父节点存在,就从父节点中删除这个子节点
80 | parentElement.remove(child)
81 | }
82 | }
83 | ```
84 |
85 | #### 挂载 text_children
86 |
87 | 由于挂载 textChildren 也涉及到了视图渲染,所以需要
88 |
89 | ```ts
90 | // runtime-dom/index
91 | function setElementText(el, text) {
92 | el.textContent = text
93 | }
94 | ```
95 |
96 | 现在我们就已经实现了 `text` => `array` 了。
97 |
98 | ### 2.2 `text` => `newText`
99 |
100 | #### 1. 实现
101 |
102 | ```ts
103 | // 情况2:text -> newText
104 | function patchChildren(n1, n2) {
105 | const prevShapeFlag = n1.shapeFlags
106 | const shapeFlag = n2.shapeFlags
107 | const c1 = n1.children
108 | const c2 = n2.children
109 | // new 是 text,进来
110 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
111 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
112 | unmountChildren(n1.children)
113 | hostSetElementText(n2.el, n2.children)
114 | } else {
115 | // new 不是 array,进来
116 | if (c1 !== c2) {
117 | // 新旧 text 进行判断,如果不相等,再进行更新
118 | hostSetElementText(n2.el, c2)
119 | }
120 | }
121 | }
122 | }
123 | ```
124 |
125 | #### 2. 优化既有逻辑
126 |
127 | 上面那一段代码中,我们发现,`hostSetElementText` 出现了两次,根据判断条件的优化,我们可以这样:
128 |
129 | ```ts
130 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
131 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
132 | // 如果老的 shapeFlag 是 array_children,需要做两件事
133 | // 1. 清空原有 children
134 | unmountChildren(n1.children)
135 | // 2. 挂载文本 children
136 | }
137 | // 将设置新的文本节点模块放在一起,这样代码量就减少了
138 | // 逻辑也更清晰了一点
139 | if (c1 !== c2) {
140 | hostSetElementText(n2.el, c2)
141 | }
142 | }
143 | ```
144 |
145 | 至此,我们也已经实现了 `text` => `newText` 的逻辑
146 |
147 | ### 2.3 `text` => `array`
148 |
149 | ```ts
150 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
151 | // 处理 new 是 text_chilren
152 | } else {
153 | // 处理 new 是 array
154 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
155 | // 如果是 text -> array
156 | // 1. 清空 text
157 | hostSetElementText(n1.el, '')
158 | // 2. mountChildren
159 | // 这里注意传递的 container, parentInstance 都可以再上游传递过来
160 | mountChildren(c2, container, parentInstance)
161 | }
162 | }
163 | ```
164 |
165 | 至此,我们的 3 种更新情况就已经实现了。
--------------------------------------------------------------------------------
/docs/29. 更新 props.md:
--------------------------------------------------------------------------------
1 | # 更新 props
2 |
3 | 在本小节中,我们将会实现更新 props 的逻辑
4 |
5 | ## 1. 例子
6 |
7 | 首先,我们可以写一个例子,来看看更新 props 都会考虑到哪些情况:
8 |
9 | ```ts
10 | const props = ref({
11 | foo: 'foo',
12 | bar: 'bar',
13 | })
14 | function patchProp1() {
15 | // 逻辑1: old !== new
16 | props.value.foo = 'new-foo'
17 | }
18 | function patchProp2() {
19 | // 逻辑2: new === undefined || null, remove new
20 | props.value.bar = undefined
21 | }
22 | function patchProp3() {
23 | // 逻辑3: old 存在,new 不存在,remove new
24 | props.value = {
25 | bar: 'bar',
26 | }
27 | }
28 | ```
29 |
30 | ## 2. 实现
31 |
32 | 在上一小节中我们已经在 `render.ts` 中创建了 `patchElement` 用于所有的更新处理。接下来,我们需要在这个函数中加入对 props 的更新逻辑的处理。
33 |
34 | ```ts
35 | function patchElement(n1, n2, container) {
36 | const oldProps = n1.props || {}
37 | const newProps = n2.props || {}
38 | patchProps(oldProps, newProps)
39 | }
40 | ```
41 |
42 | 然后我们在 `patchProps` 中加入多种情况下对于 props 的处理
43 |
44 | ```ts
45 | function patchProps(oldProps, newProps) {
46 | // 情况1: old !== new 这个走更新的逻辑
47 | // 情况2: old 存在,new !== undefined,这个走删除的逻辑
48 | // 情况3: old 存在,new 不存在,这个也走删除的逻辑
49 | }
50 | ```
51 |
52 | ### 2.1 情况 1
53 |
54 | ```ts
55 | function patchElement(n1, n2, container) {
56 | const oldProps = n1.props || {}
57 | const newProps = n2.props || {}
58 | // 这里需要传递 el,我们需要考虑一点,到这一层的时候
59 | // n2.el 是 undefined,所以我们需要把 n1.el 赋给 n2.el
60 | // 这是因为在下次 patch 的时候 n2 === n1, 此刻的新节点变成旧节点,el 就生效了
61 | const el = (n2.el = n1.el)
62 | patchProps(el, oldProps, newProps)
63 | }
64 |
65 | function patchProps(el, oldProps, newProps) {
66 | // 如果 oldProps === newProps 那就不需要对比了
67 | if (oldProps === newProps) return
68 | // 情况1: old !== new 这个走更新的逻辑
69 | for (const propKey of Reflect.ownKeys(newProps)) {
70 | const oldProp = oldProps[propKey]
71 | const newProp = newProps[propKey]
72 | // 新旧属性进行对比,如果不相等
73 | if (oldProp !== newProp) {
74 | // 直接更新属性,这里 hostPatchProp 的时候我们又传入了一个新属性,也就是 oldPropValue
75 | // 在 `runtime-dom/index.ts/patchProp` 不要忘记添加这个参数
76 | // 这个是为了让用户自己也能主动处理新旧 prop 差异
77 | hostPatchProp(el, propKey, newProp, oldProp)
78 | }
79 | }
80 | }
81 | ```
82 |
83 | ### 2.2 情况2
84 |
85 | 在情况2 中,`newProp` 是 undefined 或者 null,由于我们处理的逻辑已经抽离到了 `runtime-dom/index` 中,所以这里我们需要去修改那里的代码。
86 |
87 | ```ts
88 | function patchProp(el, prop, val, oldVal) {
89 | if (isOn(prop)) {
90 | const event = prop.slice(2).toLowerCase()
91 | el.addEventListener(event, val)
92 | } else {
93 | console.log({ prop, val, oldVal })
94 | // 情况2: 如果 newVal === undefine || null 的时候,就删除
95 | if (val === undefined || null) el.removeAttribute(prop)
96 | else el.setAttribute(prop, val)
97 | }
98 | }
99 | ```
100 |
101 | ### 2.3 情况3
102 |
103 | 在情况 3 中,我们去单独循环 newProps 肯定是不对的了,我们还要再去循环一遍 oldProps
104 |
105 | ```ts
106 | function patchProps(el, oldProps, newProps) {
107 | if (oldProps === newProps) return
108 | for (const propKey of Reflect.ownKeys(newProps)) {
109 | const oldProp = oldProps[propKey]
110 | const newProp = newProps[propKey]
111 | if (oldProp !== newProp) {
112 | hostPatchProp(el, propKey, newProp, oldProp)
113 | }
114 | }
115 | // 情况3: old 存在,new 不存在,这个也走删除的逻辑
116 | // 在情况 3 中我们还要再次去循环一遍 oldProps
117 | for (const propKey of Reflect.ownKeys(oldProps)) {
118 | // 如果当前的 key 在 newProps 中没有
119 | if (!(propKey in oldProps)) {
120 | // 就去删除,第三个参数传 undefined 相当于走了情况 2,就调用了删除
121 | hostPatchProp(el, propKey, undefined, oldProps[propKey])
122 | }
123 | }
124 | }
125 | ```
126 |
127 | 接下来我们还可以进行优化一下:
128 |
129 | ```ts
130 | // shared/index
131 |
132 | // 创建一个常量
133 | export const EMPTY_OBJ = {}
134 | ```
135 |
136 | ```ts
137 | // render.ts
138 |
139 | function patchElement(n1, n2, container) {
140 | // 将 {} ==> EMPTY_OBJ
141 | const oldProps = n1.props || EMPTY_OBJ
142 | const newProps = n2.props || EMPTY_OBJ
143 | const el = (n2.el = n1.el)
144 | patchProps(el, oldProps, newProps)
145 | }
146 |
147 |
148 | // patchProps 逻辑中
149 | // 在循环 oldProps 之前进行判断
150 | // 如果 oldProps 是空的就不要进行循环了
151 | if (oldProps !== EMPTY_OBJ) {
152 | for (const propKey of Reflect.ownKeys(oldProps)) {
153 | if (!(propKey in newProps)) {
154 | hostPatchProp(el, propKey, undefined, oldProps[propKey])
155 | }
156 | }
157 | }
158 | ```
159 |
160 | 最后,这三种情况我们就都已经支持了。
--------------------------------------------------------------------------------
/docs/37. 实现解析插值表达式.md:
--------------------------------------------------------------------------------
1 | # 实现解析差值表达式
2 |
3 | ## 1. 测试样例
4 |
5 | 最终,我们将会让这个测试通过
6 |
7 | ```ts
8 | test('simple interpolation', () => {
9 | const interpolationStr = '{{message}}'
10 | const ast = baseParse(interpolationStr)
11 | expect(ast.children[0]).toStrictEqual({
12 | type: 'interpolation',
13 | content: {
14 | type: 'simple_expression',
15 | content: 'message',
16 | },
17 | })
18 | })
19 | ```
20 |
21 | - 接收一个字符串 `{{message}}`
22 | - 返回一个 ast
23 |
24 | ## 2. 实现
25 |
26 | ### 2.1 伪实现
27 |
28 | 我们首先可以将代码的功能分割为多个模块,先快速通过测试
29 |
30 | ```ts
31 | // parse.ts
32 |
33 | // 导出函数
34 | export function baseParse(content: string) {
35 | const context = createContext(content)
36 | return createRoot(parseChildren(context))
37 | }
38 |
39 | // 创建上下文
40 | function createContext(content: string) {
41 | return {
42 | source: content,
43 | }
44 | }
45 |
46 | // 创建 ast 根节点
47 | function createRoot(children) {
48 | return {
49 | children,
50 | }
51 | }
52 |
53 | // 创建 children
54 | function parseChildren(context: { source: string }): any {
55 | const nodes: any = []
56 | let node
57 | // 如果 context.source 以 {{ 开始
58 | if (context.source.startsWith('{{')) {
59 | node = parseInterpolation(context)
60 | }
61 | nodes.push(node)
62 | return [node]
63 | }
64 |
65 | // 解析插值表达式
66 | function parseInterpolation(context: { source: string }) {
67 | return {
68 | type: 'interpolation',
69 | content: {
70 | type: 'simple_expression',
71 | content: 'message',
72 | },
73 | }
74 | }
75 | ```
76 |
77 | - 将功能进行分层
78 | - 最终在 `parseInterpolation` 函数中进行解析插值
79 |
80 | ### 2.2 具体实现
81 |
82 | 目前,我们需要将 `{{message}}`中的 `message` 抽离出来,可以使用字符串的截取功能
83 |
84 | ```ts
85 | // 将字符串截取为 message}}
86 | const closeIndex = context.source.indexOf('}}', 2)
87 | // 然后将字符串前面的 {{ 舍弃掉,我们将其称之为【推进】
88 | context.source = context.source.slice(2)
89 | // 获取到 {{}} 中间值的长度
90 | const rawContentLength = closeIndex - 2
91 | // 并将中间这个值获取出来
92 | const content = context.source.slice(0, rawContentLength)
93 | // 继续【推进】
94 | context.source = context.source.slice(rawContentLength + 2)
95 | ```
96 |
97 | - 最终,我们可以通过 `content` 来获取到值
98 |
99 | ### 2.3 重构
100 |
101 | #### 1. 抽离字符串
102 |
103 | 在这一步,我们要将 `{{` `}}` 抽离为具有语义化的字符串
104 |
105 | ```ts
106 | const openDelimiter = '{{'
107 | const closeDelimiter = '}}'
108 | const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length)
109 | context.source = context.source.slice(openDelimiter.length)
110 | const rawContentLength = closeIndex - closeDelimiter.length
111 | const content = context.source.slice(0, rawContentLength)
112 | context.source = context.source.slice(
113 | rawContentLength + closeDelimiter.length
114 | )
115 | ```
116 |
117 | #### 2. 抽离推进逻辑
118 |
119 | 我们可以将推进的逻辑也抽离出来
120 |
121 | ```diff
122 | const openDelimiter = '{{'
123 | const closeDelimiter = '}}'
124 | const closeIndex = context.source.indexOf(
125 | closeDelimiter,
126 | openDelimiter.length
127 | )
128 | + advanceBy(context, openDelimiter.length)
129 | - context.source = context.source.slice(openDelimiter.length)
130 | const rawContentLength = closeIndex - openDelimiter.length
131 | const content = context.source.slice(0, rawContentLength)
132 | + advanceBy(context, rawContentLength + closeDelimiter.length)
133 | - context.source = context.source.slice(
134 | - rawContentLength + closeDelimiter.length
135 | - )
136 |
137 |
138 | + function advanceBy(context, length: number) {
139 | + context.source = context.source.slice(length)
140 | + }
141 | ```
142 |
143 | #### 3. 抽离 AST Node 类型
144 |
145 | ```ts
146 | // ast.ts
147 | export const enum NodeType {
148 | INTERPOLATION,
149 | SIMPLE_EXPRESSION,
150 | }
151 | ```
152 |
153 | ### 2.4 边缘情况
154 |
155 | 在这一块,我们需要修复一个边缘情况,在这里加入我们的插值表达式中存在空格,测试就会出现问题了,我们需要修复一下:
156 |
157 | ```diff
158 | const openDelimiter = '{{'
159 | const closeDelimiter = '}}'
160 | const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length)
161 | context.source = context.source.slice(openDelimiter.length)
162 | const rawContentLength = closeIndex - closeDelimiter.length
163 | -const content = context.source.slice(0, rawContentLength)
164 | +const rawContent = context.source.slice(0, rawContentLength)
165 | +const content = rawContent.trim()
166 | context.source = context.source.slice(
167 | rawContentLength + closeDelimiter.length
168 | )
169 | ```
170 |
171 |
--------------------------------------------------------------------------------
/docs/18. 组件的代理对象.md:
--------------------------------------------------------------------------------
1 | # 组件的代理对象
2 |
3 | 在这一小节呢,我们将会去实现组件的代理对象
4 |
5 | 在第 16 小节中我们写了一个 helloworld 的例子,在 render 方法中我们用到了 setup 返回的 state
6 |
7 | ## 1. 实现 setupState
8 |
9 | 那么在这个部分,我们就来实现以下 setupState,我们知道,要想在 `render` 函数获取到 this,就要把 setupState 给绑定进来。
10 |
11 | ```ts
12 | // render.ts
13 |
14 | // other code ...
15 |
16 | // 在这里呢我们将组件实例进行 render,所以在这里就可以将 render 方法的 this 给绑定未 setupState
17 | function setupRenderEffect(instance, container) {
18 | const subTree = instance.render()
19 | patch(subTree, container)
20 | }
21 | ```
22 |
23 | 首先,我们可以在这个 instance 中创建一个 setupState
24 |
25 | ```ts
26 | // component.ts
27 | export function createComponentInstance(vnode) {
28 | // 这里返回一个 component 结构的数据
29 | const component = {
30 | vnode,
31 | type: vnode.type,
32 | setupState: {},
33 | }
34 | return component
35 | }
36 | ```
37 |
38 | 而我们在处理 setup 函数时就已经挂载到 instance 上了
39 |
40 | ```ts
41 | // component.ts
42 | function handleSetupResult(instance, setupResult) {
43 | if (typeof setupResult === 'object') {
44 | // 这里就进行了挂载
45 | instance.setupState = setupResult
46 | }
47 | finishComponentSetup(instance)
48 | }
49 | ```
50 |
51 | 所以我们 render 中
52 |
53 | ```ts
54 | function setupRenderEffect(instance, container) {
55 | // 解构出 setupState
56 | const { setupState } = instance
57 | // 将 render.this 变成 setupState
58 | const subTree = instance.render.call(setupState)
59 | patch(subTree, container)
60 | }
61 | ```
62 |
63 | 此时我们再去测试模板,发现在 render 中就已经可以获取到 this 了。
64 |
65 | ## 2. 实现 $el
66 |
67 | 其实在文档中可以通过 this.$x 来获取某些数据
68 |
69 | [查看文档](https://vuejs.org/api/options-state.html)
70 |
71 | - $el
72 | - $data
73 | - $props
74 | - ......
75 |
76 | 再加上上面还可以获取到 setupState,为了能够解耦,我们就可以来统一管理 this,而不是零零散散的再挂载到 this 上。
77 |
78 | ### 2.1 拦截对于 instance 中 this 的操作
79 |
80 | ```ts
81 | function setupStatefulComponent(instance) {
82 | const component = instance.vnode.type
83 |
84 | // 在这里对于 instance 的 this 进行拦截
85 | instance.proxy = new Proxy({}, componentPublicInstanceProxyHandlers)
86 |
87 | // other code ...
88 | }
89 | ```
90 |
91 | ```ts
92 | export const componentPublicInstanceProxyHandlers = {
93 | get(target, key) {
94 | return target.setupState[key]
95 | },
96 | }
97 | ```
98 |
99 | 这样我们再回到 `render` 中,OK,添加上之后我们记得要去测试一下,这样的逻辑基本就可以跑通了
100 |
101 | - 首先我们在初始化的时候调用 setup 方法,获取到返回值
102 | - 将这个返回值挂载到 instance.setupState 上
103 | - 这里 instance.render 里的 this 就是 instance.proxy
104 | - 也就是我们在模板中的 render 方法里的 this 就是 instance.proxy
105 | - 例如我们访问 this.title,instance.proxy -> target.setupState['title']
106 |
107 | ```ts
108 | function setupRenderEffect(instance, container) {
109 | // 这里就进入到了上面的 proxy 中
110 | const subTree = instance.render.call(instance.proxy)
111 | patch(subTree, container)
112 | }
113 | ```
114 |
115 | ### 2.2 实现 $el
116 |
117 | 通过对于官网中 $el 的描述我们发现,通过对 this.$el 可以获取到组件中的根 DOM 节点。我们知道创建 DOM 是在 `mountElement` 函数中实现的,此时我们就可以将这个 el 存在 vnode 里面
118 |
119 | ```ts
120 | // render.ts
121 | function mountElement(vnode, container) {
122 | // 创建 dom,并将 dom 存在 vnode.el 中
123 | const domEl = (vnode.el = document.createElement(domElType))
124 |
125 | // other code ...
126 | }
127 | ```
128 |
129 | ```ts
130 | // vnode.ts
131 | export function createVNode(type, props?, children?) {
132 | // 这里先直接返回一个 VNode 结构
133 | return {
134 | type,
135 | props,
136 | children,
137 | // 再创建 vnode 时创建一个空的 el
138 | el: null,
139 | }
140 | }
141 | ```
142 |
143 | 接下来,我们就可以在那个 ProxyHandlers 里面写对于 $el 的数据了。但是现在的问题在于,$el 在 vnode 里面的,我们要把 vnode 给传到这个 setupStatefulComponent 中。
144 |
145 | ```ts
146 | // Proxy 传入 target,_ 作为 instance.vnode
147 | instance.proxy = new Proxy(
148 | { _: instance.vnode },
149 | componentPublicInstanceProxyHandlers
150 | )
151 | ```
152 |
153 | ```ts
154 | export const componentPublicInstanceProxyHandlers = {
155 | get(target, key) {
156 | // 如果我们访问的是 this.$el,那么就会返回 vnode.el
157 | if (key === '$el') {
158 | const { _: vnode } = target
159 | return vnode.el
160 | }
161 | return target.setupState[key]
162 | },
163 | }
164 | ```
165 |
166 | 但是现在呢我们还是获取不到 $el 的,这是因为我们走了两边的 patch,第一遍是我们需要的 vnode,但是我们是在第二遍获取到的 el,也就是在这里
167 |
168 | ```ts
169 | function setupRenderEffect(instance, vnode, container) {
170 | const { setupState } = instance
171 | const subTree = instance.render.call(setupState)
172 | // 将转换好的 component 进行 element 分支的 patch
173 | patch(subTree, container)
174 | // 通过 patch,走到 mountElement 方法中,将传入的 subTree.el 变成为根节点
175 | // 在这里再将 vnode.el = subTree.el 即可
176 | vnode.el = subTree.el
177 | }
178 | ```
179 |
180 | 此时我们再去测试发现就可以访问到 this.$el 了。
--------------------------------------------------------------------------------
/docs/5. 实现 readonly 功能.md:
--------------------------------------------------------------------------------
1 | # 实现 readonly 功能
2 |
3 | 在本小节中,我们将会实现 readonly API
4 |
5 | ## 1. happy path 单元测试
6 |
7 | ```ts
8 | it('happy path', () => {
9 | // not set
10 | const original = { foo: 1, bar: 2 }
11 | const wrapped = readonly(original)
12 | expect(wrapped).not.toBe(original)
13 | expect(wrapped.bar).toBe(2)
14 | wrapped.foo = 2
15 | // set 后不会更改
16 | expect(wrapped.foo).toBe(1)
17 | })
18 | ```
19 |
20 | ## 2. 实现 happy path
21 |
22 | 我们知道 readonly 和 reactive 的实现原理是一致的,都可以通过 Proxy 来实现一个包装类,唯一的区别在于,readonly 的不会被 track,而且 readonly 的属性值不可更改
23 |
24 | 那么该如何实现呢?
25 |
26 | ### 2.1 v1
27 |
28 | ```ts
29 | // reactive.ts
30 | export function readonly(raw) {
31 | return new Proxy(raw, {
32 | get(target, key, receiver) {
33 | const res = Reflect.get(target, key, receiver)
34 | return res
35 | },
36 | set() {
37 | return true
38 | },
39 | })
40 | }
41 | ```
42 |
43 | 在这个版本下,我们就实现了最简单的 readonly 的实现。但是我们可以发现其实 reactive 和 readonly 的部分代码是一样的,就可以提取重复代码变为函数:
44 |
45 | ### 2.2 v2
46 |
47 | ```ts
48 | import { track, trigger } from './effect'
49 |
50 |
51 | // version 2 版本就可以将重复的代码提取出来
52 | // 作为 createGetter 和 createSetter
53 | function createGetter(isReadonly = false) {
54 | return function get(target, key, receiver) {
55 | const res = Reflect.get(target, key, receiver)
56 | if (!isReadonly) {
57 | track(target, key)
58 | }
59 | return res
60 | }
61 | }
62 |
63 | function createSetter() {
64 | return function set(target, key, value, receiver) {
65 | const res = Reflect.set(target, key, value, receiver)
66 | trigger(target, key)
67 | return res
68 | }
69 | }
70 |
71 | export function reactive(raw) {
72 | return new Proxy(raw, {
73 | get: createGetter(),
74 | set: createSetter(),
75 | })
76 | }
77 |
78 | export function readonly(raw) {
79 | return new Proxy(raw, {
80 | get: createGetter(true),
81 | set() {
82 | return true
83 | },
84 | })
85 | }
86 | ```
87 |
88 | 为了更好的管理代码,在 v3 中,我们还可以直接将 createSetter 和 createGetter 分层出去
89 |
90 | ### 2.3 v3
91 |
92 | ```ts
93 | // reactivity/baseHandlers.ts
94 | import { track, trigger } from './effect'
95 |
96 | const get = createGetter()
97 | const readonlyGet = createGetter(true)
98 | const set = createSetter()
99 |
100 | function createGetter(isReadonly = false) {
101 | return function get(target, key, receiver) {
102 | const res = Reflect.get(target, key, receiver)
103 | // 在 get 时收集依赖
104 | if (!isReadonly) {
105 | track(target, key)
106 | }
107 | return res
108 | }
109 | }
110 |
111 | function createSetter() {
112 | return function set(target, key, value, receiver) {
113 | const res = Reflect.set(target, key, value, receiver)
114 | // 在 set 时触发依赖
115 | trigger(target, key)
116 | return res
117 | }
118 | }
119 |
120 | // mutable 可变的
121 | export const mutableHandlers = {
122 | get,
123 | set,
124 | }
125 |
126 | export const readonlyHandlers = {
127 | get: readonlyGet,
128 | set(target, key, value) {
129 | return true
130 | },
131 | }
132 |
133 | ```
134 |
135 | ```ts
136 | import { mutableHandlers, readonlyHandlers } from './baseHandlers'
137 |
138 | export function reactive(raw) {
139 | return new Proxy(raw, mutableHandlers)
140 | }
141 |
142 | export function readonly(raw) {
143 | return new Proxy(raw, readonlyHandlers)
144 | }
145 | ```
146 |
147 | 这样就可以将实现与入口分离开来了。下面我们看到其实 reactive 和 readonly 它的创建方式是差不多的都是通过 new Proxy 的方式来创建,那么这些步骤我们也可以来分离开
148 |
149 | ### 2.4 v4
150 |
151 | ```ts
152 | // reactive.ts
153 |
154 | import { mutableHandlers, readonlyHandlers } from './baseHandlers'
155 |
156 | function createActiveObject(raw, baseHandlers) {
157 | return new Proxy(raw, baseHandlers)
158 | }
159 |
160 | export function reactive(raw) {
161 | return createActiveObject(raw, mutableHandlers)
162 | }
163 |
164 | export function readonly(raw) {
165 | return createActiveObject(raw, readonlyHandlers)
166 | }
167 | ```
168 |
169 | 这样我们就实现了最少的代码了
170 |
171 | ## 3. 警告特性单元测试
172 |
173 | 这个单元测试,我们要让用户在设置一个 readonly prop value 时报一个警告
174 |
175 | ```ts
176 | it('should warn when update readonly prop value', () => {
177 | // 这里使用 jest.fn
178 | console.warn = jest.fn()
179 | const readonlyObj = readonly({ foo: 1 })
180 | readonlyObj.foo = 2
181 | expect(console.warn).toHaveBeenCalled()
182 | })
183 | ```
184 |
185 | ## 4. 实现警告特性
186 |
187 | 这里我们发现实现警告还是非常简单的,只需要找到 readonly proxy 的 set 即可
188 |
189 | ```ts
190 | // baseHandlers.ts
191 |
192 | export const readonlyHandlers = {
193 | get,
194 | set(target, key, value) {
195 | // 在这里警告
196 | console.warn(
197 | `key: ${key} set value: ${value} fail, because the target is readonly`,
198 | target
199 | )
200 | return true
201 | },
202 | }
203 | ```
204 |
205 | 这个时候我们再跑一边测试,发现就完全没问题了。
--------------------------------------------------------------------------------
/docs/27. 实现 customRenderer.md:
--------------------------------------------------------------------------------
1 | # 实现 customRenderer
2 |
3 | 在本小节中,我们将会实现 customRenderer
4 |
5 | ## 1. 目前渲染存在的问题
6 |
7 | 我们来看看目前我们自己的 mini-vue 渲染存在的问题,切换到 `render.ts`
8 |
9 | ```ts
10 | function mountElement(vnode, container, parentInstance) {
11 | const { type: domElType, props, children, shapeFlags } = vnode
12 | // document.createElement(type) 强依赖 DOM API
13 | const domEl = (vnode.el = document.createElement(domElType))
14 | const isOn = (key: string) => /^on[A-Z]/.test(key)
15 | for (const prop in props) {
16 | if (isOn(prop)) {
17 | const event = prop.slice(2).toLowerCase()
18 | // addEventListener,setAttribute 强依赖 DOM API
19 | domEl.addEventListener(event, props[prop])
20 | } else {
21 | domEl.setAttribute(prop, props[prop])
22 | }
23 | }
24 | if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
25 | domEl.textContent = children
26 | } else if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
27 | mountChildren(vnode, domEl, parentInstance)
28 | }
29 | // appendChild 强依赖 DOM API
30 | container.appendChild(domEl)
31 | }
32 | ```
33 |
34 | 我们发现最终渲染的 API 强依赖 DOM 的 API,这个问题就在于框架只能运行在浏览器中。为了让我们的框架通用性更强,我们需要将实际渲染的模块抽离出来,自己默认有一套,同时别人也可以自己来配置。
35 |
36 | 通过对代码的观察,发现需要抽离出三个 API:
37 |
38 | - `createElement`:用于创建元素
39 | - `patchProp`:用于给元素添加属性
40 | - `insert`:将于给父元素添加子元素
41 |
42 | ## 2. 实现 customRenderer
43 |
44 | ### 2.1 抽离强绑定 API
45 |
46 | ```ts
47 | // 默认给定面向 DOM 平台的渲染接口
48 | // 写在 runtime-dom/index.ts 中
49 | export function createElement(type) {
50 | return document.createElement(type)
51 | }
52 |
53 | const isOn = (key: string) => /^on[A-Z]/.test(key)
54 |
55 | export function patchProp(el, prop, props) {
56 | if (isOn(prop)) {
57 | const event = prop.slice(2).toLowerCase()
58 | el.addEventListener(event, props[prop])
59 | } else {
60 | el.setAttribute(prop, props[prop])
61 | }
62 | }
63 |
64 | export function insert(el, parent) {
65 | parent.appendChild(el)
66 | }
67 |
68 | export function selector(container) {
69 | return document.querySelector(container)
70 | }
71 | ```
72 |
73 | ```ts
74 | function mountElement(vnode, container, parentInstance) {
75 | const { type: domElType, props, children, shapeFlags } = vnode
76 | // 将强绑定 API 抽离
77 | const domEl = (vnode.el = createElement(domElType))
78 | for (const prop in props) {
79 | patchProp(domEl, prop, props)
80 | }
81 | if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
82 | domEl.textContent = children
83 | } else if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
84 | mountChildren(vnode, domEl, parentInstance)
85 | }
86 | insert(domEl, container)
87 | }
88 | ```
89 |
90 | ### 2.2 继续抽象逻辑
91 |
92 | 然后,我们可以将整个 `render.ts` 的逻辑包裹在 `createRenderer` 函数中。
93 |
94 | 在 `render.ts` 中使用到的 `createElement` 等等通过 `createRenderer` 的参数传递过来
95 |
96 | ```ts
97 | // render.ts
98 |
99 | export function createRenderer(options) {
100 | // 改名字是为了 debug 方便
101 | const {
102 | createElement: hostCreateElement,
103 | insert: hostInsert,
104 | patchProp: hostPatchProp,
105 | selector: hostSelector,
106 | } = options
107 | // other code ...
108 | }
109 | ```
110 |
111 | 将 `createApp` 也包裹在 `createAppAPI` 中。
112 |
113 | ```ts
114 | // 这里接收 renderer
115 | export function createAppAPI(renderer, selector) {
116 | return function createApp(rootComponent) {
117 | return {
118 | mount(rootContainer) {
119 | const vnode = createVNode(rootComponent)
120 | // 如果传过来了 selector,我们就用 selector 方法来获取 rootContainer
121 | // 如果没有传 selector,就直接用 rootContainer
122 | renderer(vnode, selector ? selector(rootContainer) : rootContainer)
123 | },
124 | }
125 | }
126 | }
127 | ```
128 |
129 | 在 `render.ts` 中的 `createRenderer` 返回一个
130 |
131 | ```ts
132 | return {
133 | createApp: createAppAPI(render, selector),
134 | }
135 | ```
136 |
137 | 然后在我们创建的 `runtime-dom/index` 中
138 |
139 | ```ts
140 | // 首先根据我们的实现的 DOM API 传入 createRenderer 中
141 | // 创建出一个渲染器
142 | const renderer: any = createRenderer({
143 | createElement,
144 | patchProp,
145 | insert,
146 | selector,
147 | })
148 |
149 | // 然后暴露出 createApp
150 | export const createApp = (...args) => {
151 | return renderer.createApp(...args)
152 | }
153 | ```
154 |
155 | 所以现在我们的 `crateApp` 逻辑就抽离出来了。
156 |
157 | 在这一个阶段呢,我们主要是做了以下这几件事情:
158 |
159 | - 将之前实现的 `createApp` 包裹一层为 `createAppAPI`,通过传递过来的 renderer,返回 `createApp`
160 |
161 | - 创建函数 `createRenderer`,接收自定义渲染器接口,并调用 `createAppAPI` 返回 `createApp`
162 | - 在 `runtime-dom/index` 中写 DOM 环境下的元素 API,并调用 `createRenderer`,传递写好的 API,获取到 `createApp`。
163 |
164 | 所以呢:
165 |
166 | - 默认情况下,Vue 提供的 `createApp` 就是在 DOM 平台下的
167 | - 我们也可以通过调用 `createRenderer` 来传入自己实现的元素 API
168 | - 获取特定的 `createApp`
169 |
170 | 我们的层级就从原来的:
171 |
172 | - mini-vue 入口 ----> `runtime-core`
173 | - mini-vue 入口 ----> `runtime-dom` ----> `runtime-core`
174 |
175 | 最后,我们再来试试之前写的 hello_world 能不能跑通吧!
176 |
177 | ## 3. 写一个 canvas 平台的例子
178 |
179 | 然后我们根据写好的 renderer 来写一个在 canvas 平台运行的例子吧!
180 |
181 | [例子](https://github.com/zx-projects/mini-vue/tree/main/example/customRenderer)
182 |
183 |
--------------------------------------------------------------------------------
/docs/34. 更新 component.md:
--------------------------------------------------------------------------------
1 | # 更新 component
2 |
3 | 在本小节中,我们将会实现更新 component 的逻辑
4 |
5 | ## 1. 例子
6 |
7 | [查看例子](https://github.com/zx-projects/mini-vue/tree/main/example/componentUpdate)
8 |
9 | ### 1.1 代码补充
10 |
11 | 在例子中涉及到了一个问题,我们可以在 `render` 函数中获取到 `this.$props.xx` 来拿到父组件传给子组件的 prop。所以我们需要加一下:
12 |
13 | ```ts
14 | // componentPublicInstance
15 |
16 | const PublicProxyGetterMapping = {
17 | $el: i => i.vnode.el,
18 | $slots: i => i.slots,
19 | // 加上一个 props
20 | $props: i => i.props,
21 | }
22 | ```
23 |
24 | ## 2. 实现
25 |
26 | ### 2.1 现有逻辑的问题
27 |
28 | 现在我们直接用现有的代码来运行例子会发现,每次更新的时候都会重新渲染一下子组件,这是因为我们之前缩写的任何 update 逻辑都是对于 `element` 所做的处理,没有写 `compoent` 的处理逻辑。
29 |
30 | ```ts
31 | // 因为 props 在父组件中,所以 props 更新会触发视图重新渲染
32 | function setupRenderEffect(instance, vnode, container, anchor) {
33 | effect(() => {
34 | if (instance.isMounted) {
35 | // update 逻辑
36 | // 此时进入 patch
37 | patch(preSubTree, subTree, container, instance, anchor)
38 | } else {
39 | // init 逻辑
40 | }
41 | })
42 | }
43 | ```
44 |
45 | ```ts
46 | // 在 processComponent 中只有 mount 没有 update
47 | function processComponent(vnode, container, parentInstance, anchor) {
48 | mountComponent(vnode, container, parentInstance, anchor)
49 | }
50 | ```
51 |
52 | ### 2.2 更新组件的初步实现
53 |
54 | 所以我们需要加入 `update` 的逻辑
55 |
56 | ```ts
57 | // 将 patch 中的 n1,n2 传过来,如果存在 n1,那么说明应该走 update 逻辑
58 | function processComponent(n1, n2, container, parentInstance, anchor) {
59 | if (n1) {
60 | // update component
61 | updateComponent(n1, n2)
62 | } else {
63 | // init component
64 | mountComponent(n2, container, parentInstance, anchor)
65 | }
66 | }
67 | ```
68 |
69 | 这里我们更新组件,其实就是更新组件的 `children` 和 `props`也就是重新运行下面这一段逻辑:
70 |
71 | ```ts
72 | // 子组件在 init 逻辑会走这一个函数,子组件自身状态改变时也会重新走一遍 update
73 | // 现在我们只需要在父组件更新时,如果涉及到子组件也需要更新,就调用一下子组件的这个方法就可以了
74 | function setupRenderEffect(instance, vnode, container, anchor) {
75 | effect(() => {
76 | if (instance.isMounted) {
77 | // update
78 | patch(preSubTree, subTree, container, instance, anchor)
79 | } else {
80 | // init
81 | }
82 | })
83 | }
84 | ```
85 |
86 | 所以我们可以将这个 effect 给挂载到 `instance` 中
87 |
88 | ```ts
89 | function setupRenderEffect(instance, vnode, container, anchor) {
90 | instance.update = effect(() => {})
91 | }
92 | ```
93 |
94 | 下面我们只需要在 `updateComponent` 中获取到这个`instance`就可以调用 `update`方法了。
95 |
96 | ```ts
97 | // 1. 在 vnode 中加入 component 这个属性,用于指向该 vnode 所在的 componentInstance
98 | export function createVNode(type, props?, children?) {
99 | // 这里先直接返回一个 VNode 结构
100 | const vnode = {
101 | type,
102 | props,
103 | children,
104 | el: null,
105 | // 初始化 component
106 | component: null,
107 | key: props ? props.key : null,
108 | shapeFlags: getShapeFlags(type),
109 | }
110 | return vnode
111 | }
112 |
113 | // 2. 在 mountComponent 的时候将填充 component
114 |
115 | function mountComponent(vnode, container, parentInstance, anchor) {
116 | // 填充 vnode.compeont
117 | const instance = (vnode.component = createComponentInstance(
118 | vnode,
119 | parentInstance
120 | ))
121 | }
122 | ```
123 |
124 | 然后我们就可以在 `updateComponent` 中获取到 `instance` 并调用了
125 |
126 | ```ts
127 | function updateComponent(n1, n2) {
128 | const instance = (n2.component = n1.component)
129 | // instance 挂载最新的虚拟节点
130 | instance.next = n2
131 | instance.update()
132 | }
133 | ```
134 |
135 | ```ts
136 | // 在 update 逻辑中,对更新进行处理
137 | // 如果 next 存在,那么就说明是组件更新的逻辑
138 | function setupRenderEffect(instance, vnode, container, anchor) {
139 | instance.update = effect(() => {
140 | if (instance.isMounted) {
141 | const { next, vnode } = instance
142 | if (next) {
143 | // 更新组件 el、props
144 | next.el = vnode.el
145 | // 然后具体更新代码
146 | updateComponentPreRender(instance, next)
147 | }
148 | } else {
149 | // init
150 | }
151 | })
152 | }
153 |
154 | // 具体的更新逻辑
155 | // 首先将 vnode 更新,然后更新 props
156 | // 最后将 nextVNode 设置为空
157 | function updateComponentPreRender(instance, nextVNode) {
158 | instance.vnode = nextVNode
159 | instance.props = nextVNode.props
160 | nextVNode = null
161 | }
162 | ```
163 |
164 | 现在已经可以在父组件中来更新子组件了。
165 |
166 | ### 2.3 若子组件不用更新不要触发更新逻辑
167 |
168 | 但是现在的逻辑还是有点问题的,例如我们不更新涉及到子组件的 props,只更新父组件的状态,那么还是会进入到 `update` 的逻辑,所以我们需要优化一下:
169 |
170 | ```ts
171 | function updateComponent(n1, n2) {
172 | const instance = (n2.component = n1.component)
173 | // 增加一个判断,如果需要更新,再进入更新的逻辑
174 | if (shouldUpdateComponent(n1, n2)) {
175 | instance.next = n2
176 | instance.update()
177 | } else {
178 | // 不需要更新,就重置就好了
179 | n2.el = n1.el
180 | instance.vnode = n2
181 | }
182 | }
183 | ```
184 |
185 | ```ts
186 | // componentUpdateUtils
187 | // 这边的函数就是对 props 进行判断,如果新旧 props 不相等,那么就意味着需要更新
188 | export function shouldUpdateComponent(prevVNode, nextVNode) {
189 | const { props: prevProps } = prevVNode
190 | const { props: nextProps } = nextVNode
191 | for (const key in nextProps) {
192 | if (nextProps[key] !== prevProps[key]) {
193 | return true
194 | }
195 | }
196 | return false
197 | }
198 | ```
199 |
200 | 至此,组件更新逻辑结束
--------------------------------------------------------------------------------
/docs/22. 实现组件的 emit 功能.md:
--------------------------------------------------------------------------------
1 | # 实现组件的 emit 功能
2 |
3 | ## 1. 什么是 emit?
4 |
5 | ```ts
6 | export const Foo = {
7 | setup(props, { emit }) {
8 | // setup 第二个参数是 ctx,里面有一个参数是 emit
9 | const handleClick = () => {
10 | // emit 是一个函数,第一个参数是触发的事件
11 | emit('add')
12 | }
13 | return {
14 | handleClick,
15 | }
16 | },
17 | render() {
18 | return h(
19 | 'button',
20 | {
21 | onClick: this.handleClick,
22 | },
23 | '点击我'
24 | )
25 | },
26 | }
27 | ```
28 |
29 | ```ts
30 | export default {
31 | render() {
32 | // 这里在写组件的时候,第二个参数就可以传入 on + emit's Event
33 | return h('div', {}, [h('p', {}, 'hello'), h(Foo, { onAdd: this.onAdd })])
34 | },
35 | setup() {
36 | function onAdd() {
37 | console.log('onAdd')
38 | }
39 | return {
40 | onAdd,
41 | }
42 | },
43 | }
44 |
45 | ```
46 |
47 | ## 2. 实现 emit
48 |
49 | 首先,setup 的第二个参数是一个对象,传入 emit
50 |
51 | ```ts
52 | // component.ts
53 |
54 | // 在 setupStatefulComponent 时调用组件的 component
55 | function setupStatefulComponent(instance) {
56 | // other code ...
57 | const setupResult = setup(shallowReadonly(instance.props), {
58 | // 传入的 emit 就可以直接使用 instance.emit
59 | emit: instance.emit,
60 | })
61 | handleSetupResult(instance, setupResult)
62 | }
63 | ```
64 |
65 | 那么我们需要在初始化 emit 的时候去注册一下 emit
66 |
67 | ```ts
68 | export function createComponentInstance(vnode) {
69 | // 这里返回一个 component 结构的数据
70 | const component = {
71 | vnode,
72 | type: vnode.type,
73 | setupState: {},
74 | props: {},
75 | emit: () => {},
76 | }
77 | component.emit = emit as any
78 | return component
79 | }
80 | ```
81 |
82 | 将 emit 的逻辑单独抽离出来,创建 `componentEmit.ts`
83 |
84 | ```ts
85 | // componentEmit.ts
86 |
87 | // 第一个参数接收一个 event 的值
88 | export function emit(event) {
89 | console.log('event', event)
90 | }
91 | ```
92 |
93 | 而我们的触发 event 的值是从哪里来的呢?我们再回头看看上面的例子,其实第二个参数中就传入了 emit 的值,所以就可以理解为,我们要触发的事件,其实就在 props 中。但是问题来了,这个函数该如何获取到 instance 呢(因为 props 在 instance 中)?
94 |
95 | ```ts
96 | export function createComponentInstance(vnode) {
97 | // 这里返回一个 component 结构的数据
98 | const component = {
99 | vnode,
100 | type: vnode.type,
101 | setupState: {},
102 | props: {},
103 | emit: () => {},
104 | }
105 | // 使用 bind 这个小技巧可以让 component 传入的 emit 参数中携带,同时不影响其他的参数
106 | component.emit = emit.bind(null, component) as any
107 | return component
108 | }
109 | ```
110 |
111 | ```ts
112 | export function emit(instance, event) {
113 | const { props } = instance
114 | }
115 | ```
116 |
117 | 接下来,我们就可以从 props 中来获取到具体的 event 了。但是目前还是有一个问题的,我们 `emit('add')` 这里的都是全部小写的,而上层父组件监听的则是大写的 `onAdd`,同时加了一个 on,我们需要这样
118 |
119 | ```ts
120 | export function emit(instance, event) {
121 | // 获取 props
122 | const { props } = instance
123 | const toUpperCase = (str: string) =>
124 | str.charAt(0).toUpperCase() + str.slice(1)
125 | // 将 event 第一个字母大写,同时加上 on
126 | const handler = props[`on${toUpperCase(event)}`]
127 | // 如果 props 中存在这个 handler,那么就触发这个 handler
128 | handler && handler()
129 | }
130 | ```
131 |
132 | 现在我们就已经可以实现 emit 了。
133 |
134 | ## 3. 完善 emit
135 |
136 | ### 3.1 emit 可以传递参数
137 |
138 | ```ts
139 | export const Foo = {
140 | setup(props, { emit }) {
141 | const handleClick = () => {
142 | // emit 可以传递多个参数
143 | emit('add', 1, 2)
144 | }
145 | return {
146 | handleClick,
147 | }
148 | },
149 | }
150 | ```
151 |
152 | 会在父组件监听的事件上进行接收。这个也是非常简单的,
153 |
154 | ```ts
155 | // 接收参数
156 | export function emit(instance, event, ...params) {
157 | const { props } = instance
158 | const toUpperCase = (str: string) =>
159 | str.charAt(0).toUpperCase() + str.slice(1)
160 | const handler = props[`on${toUpperCase(event)}`]
161 | // 触发时传递参数
162 | handler && handler(...params)
163 | }
164 | ```
165 |
166 | ### 3.2 事件名可以是短横线格式
167 |
168 | ```ts
169 | // emit 的事件名称也可以是短横线连接的
170 | emit('add-count', 1)
171 | ```
172 |
173 | ```ts
174 | // 在监听的时候需要换成驼峰的格式
175 | h(Foo, { onAdd: this.onAdd, onAddCount: this.onAddCount })
176 | ```
177 |
178 | 那么这个还如何解决呢?
179 |
180 | ```ts
181 | // 我们在处理 event 的时候仅仅处理了全部是小写的情况
182 | // 所以我们还需要处理一层,
183 | export function emit(instance, event, ...params) {
184 | const { props } = instance
185 | // toUpperCase 名字可以改为 capitalize,表述的更加准确
186 | const toUpperCase = (str: string) =>
187 | str.charAt(0).toUpperCase() + str.slice(1)
188 | const handler = props[`on${toUpperCase(event)}`]
189 | // 触发时传递参数
190 | handler && handler(...params)
191 | }
192 | ```
193 |
194 | 我们需要加一个 `camelize` 处理层,先将短横连接的转换为大写格式
195 |
196 | ```ts
197 | export function emit(instance, event, ...params) {
198 | const { props } = instance
199 | // 在这里进行正则匹配,将 横杠和第一个字母 -> 不要横杠,第一个字母大写
200 | const camelize = (str: string) => {
201 | return str.replace(/-(\w)/, (_, str: string) => {
202 | return str.toUpperCase()
203 | })
204 | }
205 | const capitalize = (str: string) =>
206 | str ? str.charAt(0).toUpperCase() + str.slice(1) : ''
207 | // 在这里先处理横杠,在处理大小写
208 | const handler = props[`on${capitalize(camelize(event))}`]
209 | handler && handler(...params)
210 | }
211 | ```
212 |
213 | 现在我们的 emit 就已经完善了
214 |
215 |
--------------------------------------------------------------------------------
/docs/4. 实现 effect 的 stop 功能.md:
--------------------------------------------------------------------------------
1 | # 实现 effect 的 stop 功能
2 |
3 | 在本小节中,我们去实现 effect 的 stop 功能
4 |
5 | ## 1. stop 的测试样例
6 |
7 | ```ts
8 | it('stop', () => {
9 | let dummy
10 | const obj = reactive({ prop: 1 })
11 | const runner = effect(() => {
12 | dummy = obj.prop
13 | })
14 | obj.prop = 2
15 | expect(dummy).toBe(2)
16 | // stop 一个 runner 之后
17 | stop(runner)
18 | obj.prop++
19 | // 依赖再次更新,当时传入的 effect 则不会重新执行
20 | expect(dummy).toBe(2)
21 | // runner 不受到影响
22 | runner()
23 | expect(dummy).toBe(3)
24 | })
25 | ```
26 |
27 | 通过一个测试样例,我们发现其实也是非常简单的
28 |
29 | ## 2. 实现 stop
30 |
31 | 首先,我们知道所有的 effect 存在 deps 中,也就是我们的 effects 是在 `track` 方法进行保存的,那么如果不想让这个 effect 执行,就可以找到 target - key 对应的 deps 中,删除掉我们的 effect 即可。
32 |
33 | 首先,我们在 ReactiveEffect 实例中去记录我们反向对应的 deps
34 |
35 | ### 2.1 v1
36 |
37 | ```ts
38 | // effect.ts
39 |
40 | class ReactiveEffect {
41 | // [stop] 反向记录自己对应的 dep 那个 set
42 | deps = []
43 | // other code
44 | }
45 |
46 |
47 | export function track(target, key) {
48 | // other code ...
49 | dep.add(activeEffect)
50 | // [stop]:反向追踪 activeEffect 的 dep
51 | // 因为一个 activeEffect 可能会对应多个 dep,每个 dep 是一个 set
52 | // 这里我们可以使用一个数组
53 | activeEffect.deps.push(dep)
54 | }
55 | ```
56 |
57 | 然后我们可以去实现一个 stop 的实例方法
58 |
59 | ```ts
60 | class ReactiveEffect {
61 | // other code ...
62 | // [stop] 这个方法的作用就是去根据 this.deps 删除 this 对应的 effect
63 | stop() {
64 | this.deps.forEach((dep: any) => {
65 | dep.delete(this)
66 | })
67 | }
68 | }
69 | ```
70 |
71 | 这样我们就可以去实现一个 stop 方法了
72 |
73 | ```ts
74 | export function stop(runner) {
75 | // [stop] 如何获取到当前所属的 effect 实例呢?
76 | // 这样就可以去调用 stop 方法了
77 | runner.effect.stop()
78 | }
79 |
80 |
81 | export function effect(fn, options: any = {}) {
82 | // other code
83 | const runner: any = _effect.run.bind(_effect)
84 | // [stop] 在这里挂载一下所属的 effect
85 | runner.effect = _effect
86 | return runner
87 | }
88 | ```
89 |
90 | ### 2.2 v2
91 |
92 | 但是我们在运行完单测后会出现问题,这是因为如果使用上面的方法的话,会存在重复收集的问题,例如我们在 stop 后,此时所属的 effect 其实已经清空过了,但是下面我们又对依赖项进行了 getter,也就是 track,那么就会再次将所属的 track 收集起来,那么 stop 删除的元素就等于是重新就加回来了,所以我们需要修改一下代码,加一个状态;
93 |
94 | ```ts
95 | class ReactiveEffect {
96 | // [stop] 该 effect 是否调用过 stop 方法了
97 | // true 未调用 false 调用
98 | active = true
99 |
100 | stop() {
101 | // 如果没调用这个方法,去清空所属的 effect
102 | if (this.active) {
103 | this.deps.forEach((dep: any) => {
104 | dep.delete(this)
105 | })
106 | this.active = false
107 | }
108 | }
109 | }
110 |
111 |
112 | // track 的代码也要改一下
113 |
114 | export function track(target, key) {
115 | // 如果该 activeEffect 还没有调用 stop 方法的时候,再去添加依赖和反向收集依赖
116 | if (activeEffect.active) {
117 | activeEffect.deps.push(dep)
118 | dep.add(activeEffect)
119 | }
120 | }
121 | ```
122 |
123 | 此时我们再去运行单元测试发现就没问题了
124 |
125 | 后面我们就可以去优化了,例如将删除依赖的函数作为单独的函数
126 |
127 | ```ts
128 | class ReactiveEffect {
129 | stop() {
130 | cleanupEffect(this)
131 | }
132 | }
133 |
134 | // 把清除的逻辑单独作为函数
135 | function cleanupEffect(effect) {
136 | if (effect.active) {
137 | effect.deps.forEach((dep: any) => {
138 | dep.delete(effect)
139 | })
140 | effect.active = false
141 | }
142 | }
143 | ```
144 |
145 | ## 3. onStop 的测试样例
146 |
147 | ```ts
148 | it('onStop', () => {
149 | const obj = reactive({
150 | foo: 1,
151 | })
152 | const onStop = jest.fn()
153 | let dummy
154 | // onStop 是一个函数,也是 effect 的 option
155 | const runner = effect(
156 | () => {
157 | dummy = obj.foo
158 | },
159 | {
160 | onStop,
161 | }
162 | )
163 | // 在调用 stop 的时候,onStop 也会执行
164 | stop(runner)
165 | expect(onStop).toBeCalledTimes(1)
166 | })
167 | ```
168 |
169 | ## 4. 实现 onStop
170 |
171 | ```ts
172 | export function effect(fn, options: any = {}) {
173 | const _effect = new ReactiveEffect(fn, options)
174 | // [stop] 这里我们 options 会接收一个 onStop 方法
175 | // 其实我们可以将 options 中的所有数据全部挂载在 effect 上面
176 | // extend = Object.assign 封装一下是为了语义化更好
177 | extend(_effect, options)
178 | // other code ...
179 | }
180 | ```
181 |
182 | ```ts
183 | // ./src/shared/index.ts
184 |
185 | export const extend = Object.assign
186 | ```
187 |
188 | 今后该项目中全局可用的工具函数可以放在 `shared` 目录下,这个也是为了与 `Vue` 的开发方式贴近
189 |
190 | 然后在清除的函数中去判断并执行
191 |
192 | ```ts
193 | function cleanupEffect(effect) {
194 | if (effect.active) {
195 | effect.deps.forEach((dep: any) => {
196 | dep.delete(effect)
197 | })
198 | // [onStop] 如果存在 onStop,就去运行 onStop
199 | if (effect.onStop) effect.onStop()
200 | effect.active = false
201 | }
202 | }
203 | ```
204 |
205 | 跑一下测试,发现也是可以通过的
206 |
207 | ## 5. 修复 happy path bug
208 |
209 | 这个时候我们再全局跑一下所有测试,发现在 `effect` 的 happy path 中出现了错误
210 |
211 | ```ts
212 | export function track(target, key) {
213 | // 这一行找不到 activeEffect
214 | if (activeEffect.active) {
215 | activeEffect.deps.push(dep)
216 | dep.add(activeEffect)
217 | }
218 | }
219 | ```
220 |
221 | 改成下面这样就可以了
222 |
223 | ```ts
224 | if (activeEffec && activeEffect.active) {
225 | activeEffect.deps.push(dep)
226 | dep.add(activeEffect)
227 | }
228 | ```
229 |
230 |
--------------------------------------------------------------------------------
/docs/28. 初始化 element 更新流程.md:
--------------------------------------------------------------------------------
1 | # 初始化 element 更新流程
2 |
3 | 在本小节中,我们将会初始化 element 的更新逻辑
4 |
5 | ## 1. 例子
6 |
7 | ```ts
8 | export default {
9 | setup() {
10 | const counter = ref(1)
11 | function inc() {
12 | counter.value += 1
13 | }
14 | return {
15 | counter,
16 | inc,
17 | }
18 | },
19 | render() {
20 | return h('div', {}, [
21 | h('div', {}, '' + this.counter),
22 | h('button', { onClick: this.inc }, 'inc'),
23 | ])
24 | },
25 | }
26 | ```
27 |
28 | 首先我们需要解决模板使用 ref 的问题,因为目前 counter 是一个 ref,所以我们需要在实际执行 setup 的时候给这个返回的对象再包一层 proxyRefs
29 |
30 | ```ts
31 | function setupStatefulComponent(instance) {
32 | // 在这里给最终 setup 运行后的结果再套一层 proxyRefs
33 | const setupResult = proxyRefs(
34 | setup(shallowReadonly(instance.props), {
35 | emit: instance.emit,
36 | })
37 | )
38 | }
39 | ```
40 |
41 | ## 2. 初始化更新逻辑
42 |
43 | ### 2.1 更新重新渲染视图
44 |
45 | 我们从例子中可以看出,`counter` 的更新并没有触发视图的更新,这是因为我们没有收集视图这个依赖,从而更改时也不会触发视图这个依赖。
46 |
47 | 那么我们最终是从哪里开始渲染视图的呢?
48 |
49 | ```ts
50 | // render.ts
51 |
52 | function setupRenderEffect(instance, vnode, container) {
53 | // 就是在这个函数中
54 | const subTree = instance.render.call(instance.proxy)
55 | patch(subTree, container, instance)
56 | vnode.el = subTree.el
57 | }
58 | ```
59 |
60 | 我们需要给这一层包一个 effect,现在我们就已经实现更新重新渲染视图了
61 |
62 | ```ts
63 | function setupRenderEffect(instance, vnode, container) {
64 | // 包一层 effect,让执行的时候去保存依赖
65 | // 并在值更新的时候重新渲染视图
66 | effect(() => {
67 | const subTree = instance.render.call(instance.proxy)
68 | patch(subTree, container, instance)
69 | vnode.el = subTree.el
70 | })
71 | }
72 | ```
73 |
74 | ### 2.2 获取上一个节点
75 |
76 | 但是目前我们的实现是非常有问题的,因为我们更新只会无脑的重新新加视图,而不是再对比视图,所以我们需要对视图进行判断
77 |
78 | 首先,我们需要给组件实例新增加一个状态,叫做 `isMounted`,用来记录该组件是否已经被渲染过,默认值是 `false`
79 |
80 | ```ts
81 | function setupRenderEffect(instance, vnode, container) {
82 | effect(() => {
83 | // 根据 instance.isMounted 状态进行判断
84 | if (instance.isMounted) {
85 | // update 逻辑,获取上一个 subTree
86 | const subTree = instance.render.call(instance.proxy)
87 | vnode.el = subTree.el
88 | // 获取上一个 subTree
89 | const preSubTree = instance.subTree
90 | // 然后将自己赋给 subTree
91 | instance.subTree = subTree
92 | console.log({ subTree, preSubTree })
93 | } else {
94 | // init 逻辑,存 subTree
95 | const subTree = (instance.subTree = instance.render.call(
96 | instance.proxy
97 | ))
98 | patch(subTree, container, instance)
99 | vnode.el = subTree.el
100 | instance.isMounted = true
101 | }
102 | })
103 | }
104 | ```
105 |
106 | 借此我们就可以获取到旧的 subTree 了。
107 |
108 | ### 2.3 patch 新旧
109 |
110 | 然后我们在 `patch` 的时候只做了 init 逻辑,所以可以这样:
111 |
112 | ```ts
113 | // 加入参数 n1,
114 | // n1 ==> preSubTree
115 | // n2 ==> currentSubTree
116 | function patch(n1, n2, container, parentInstance) {
117 | const { type, shapeFlags } = n2
118 | switch (type) {
119 | case Fragment:
120 | processFragment(n1, n2, container, parentInstance)
121 | break
122 | case TextNode:
123 | processTextNode(n2, container)
124 | break
125 | default:
126 | if (shapeFlags & ShapeFlags.ELEMENT) {
127 | processElement(n1, n2, container, parentInstance)
128 | } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
129 | processComponent(n2, container, parentInstance)
130 | }
131 | break
132 | }
133 | }
134 | ```
135 |
136 | 将上下游更改,同时我们触发渲染逻辑的时候
137 |
138 | ```ts
139 | function setupRenderEffect(instance, vnode, container) {
140 | effect(() => {
141 | if (instance.isMounted) {
142 | const subTree = instance.render.call(instance.proxy)
143 | vnode.el = subTree.el
144 | const preSubTree = instance.subTree
145 | instance.subTree = subTree
146 | // update 逻辑,preSubTree 传递
147 | patch(preSubTree, subTree, container, instance)
148 | } else {
149 | const subTree = (instance.subTree = instance.render.call(
150 | instance.proxy
151 | ))
152 | // init 逻辑,preSubTree 传递 null
153 | patch(null, subTree, container, instance)
154 | vnode.el = subTree.el
155 | instance.isMounted = true
156 | }
157 | })
158 | }
159 | ```
160 |
161 | 然后我们在处理 element 的时候
162 |
163 | ```ts
164 | function processElement(n1, n2, container, parentInstance) {
165 | // n1 存在,update 逻辑
166 | if (n1) {
167 | patchElement(n1, n2, container)
168 | } else {
169 | // 不存在 init 逻辑
170 | mountElement(n1, n2, container, parentInstance)
171 | }
172 | }
173 |
174 | function patchElement(n1, n2, container) {
175 | // 此函数用于对比 props 和 children
176 | }
177 | ```
178 |
179 | ## 3. 总结
180 |
181 | 最后呢,我们发现更新逻辑的核心就在于,将渲染视图部分放入 `effect` 中,这就导致了如果我们的数据是响应式的数据(例如 `ref`、`reactive`),那么这个响应式的数据更新的时候,就会重新触发视图。
182 |
183 | 然后我们保存一份上一个 `subTree`,与现在的 `subTree` 进行对比,从而实现新旧对比。
184 |
185 | 这就是更新的核心逻辑。
--------------------------------------------------------------------------------
/docs/47. 实现 template 编译为 render.md:
--------------------------------------------------------------------------------
1 | # 实现 template 编译为 render
2 |
3 | 在本小节,我们将实现从 template 编译为 render,并跑通一个 demo
4 |
5 | ## 1. 例子
6 |
7 | [查看例子](https://github.com/zx-projects/mini-vue/tree/main/example/compileBase)
8 |
9 | ## 2. 实现
10 |
11 | 前几个小节我们已经实现了零零散散的编译,下面我们需要将这些编译组合起来。
12 |
13 | 首先,创建 `compile.ts`
14 |
15 | ```ts
16 | import { codegen } from './codegen'
17 | import { baseParse } from './parse'
18 | import { transform } from './transform'
19 | import { transformElement } from './transforms/transformElement'
20 | import { transformExpression } from './transforms/transformExpression'
21 | import { transformText } from './transforms/transformText'
22 |
23 | // 大概就是这么个过程
24 | export function baseCompile(template: string) {
25 | const ast = baseParse(template)
26 | transform(ast, {
27 | nodeTransforms: [transformExpression, transformElement, transformText],
28 | })
29 | const code = codegen(ast)
30 | return {
31 | code,
32 | }
33 | }
34 | ```
35 |
36 | 然后在 `compiler-core` 模块的 `index` 中将这个导出
37 |
38 | ```ts
39 | // 作为 compiler-core 的出口
40 | export * from './compile'
41 | ```
42 |
43 | 那么我们该如何调用这个函数作为组件的 render 方法呢?早前我们写的是在 `runtime-core/component.ts` 中直接调用 `component.render` ,显然在这种情况下是不适用的,因为用户现在已经不提供 render 函数了。但是我们能直接在 `runtime-core` 中调用 `compiler-core` 中的函数吗?
44 |
45 | 在 vue 的角度看来,这显然是万万不可取的,如果说 `runtime-core` 强依赖 `compiler-core` 中的代码,会有两大缺陷:
46 |
47 | - 不利于低耦合
48 | - 用户若手写 render 函数,打包后的代码将会携带无用的 compiler 相关的代码
49 |
50 | Vue 的贡献者文档里给出这么一个解决方案:
51 |
52 | > 如果你实在想在 runtime 中使用 compiler 的代码,或者反之,你需要将其代码提取到 `@vue/shared` 中
53 |
54 | 那么在 mini-vue 中,我们就不用提取到 `@vue/shared` 了,我们可以直接在所有模块的最外层进行使用。
55 |
56 | ```ts
57 | // src/index.ts
58 |
59 | // other code..
60 |
61 | import { baseCompile } from './compiler-core/src'
62 | import * as runtimeDom from './runtime-dom'
63 |
64 | // 在这里对 compile 进行包装,通过编译出来的 code 来创建一个 render 函数
65 | function compileToFunction(template: string) {
66 | const { code } = baseCompile(template)
67 | const render = new Function('Vue', code)(runtimeDom)
68 | return render
69 | }
70 | ```
71 |
72 | 那么下个问题就是,怎么在 component 的地方用到这里的 render 函数。我们可以在 component 的地方也导出一个函数,用于将这里的 render 传递到 component 内部。
73 |
74 | ```ts
75 | // runtime-core/component.ts
76 |
77 | let compiler
78 |
79 | export function registerCompiler(_compiler) {
80 | compiler = _compiler
81 | }
82 | ```
83 |
84 | 层层导出,最终会在 `src/index` 中用到
85 |
86 | ```ts
87 | import { registerCompiler } from './runtime-dom'
88 |
89 | function compileToFunction(template: string) {
90 | const { code } = baseCompile(template)
91 | const render = new Function('Vue', code)(runtimeDom)
92 | return render
93 | }
94 |
95 | // 在这里将 compiler 传入到 component 内部中
96 | registerCompiler(compileToFunction)
97 | ```
98 |
99 | 现在,我们就可以在 `component` 中来调用 `compileToFunction` 了!这里也有一个小细节:
100 |
101 | - 如果用户同时提供了 render 和 template,那么会执行 render
102 |
103 | ```ts
104 | function finishComponentSetup(instance) {
105 | const component = instance.type
106 | // 在这里如果说有 compiler,而且 component 没有提供 render 的话
107 | if (!component.render && compiler) {
108 | if (component.template) {
109 | // 我们再将 template 编译成为 render
110 | component.render = compiler(component.template)
111 | }
112 | }
113 | if (!instance.render) {
114 | instance.render = component.render
115 | }
116 | }
117 | ```
118 |
119 | 现在我们的 demo 是跑不通的,会报一个 bug,那就是在调用 instance.render 的时候传入的参数问题,这是因为我们编译好的是这样的:
120 |
121 | ```ts
122 | return function render($ctx) {
123 | return $ctx.context
124 | }
125 | ```
126 |
127 | 由于这里的 render 需要接受一个参数,所以我们还需要传入参数
128 |
129 | ```ts
130 | // render.ts
131 |
132 | function setupEffect(instance, ...) {
133 | // other code ..
134 | // 传入参数
135 | // 下面还有一个调用 instance.render 也要传入一个参数
136 | const subTree = instance.render.call(instance.proxy, instance.proxy)
137 | }
138 | ```
139 |
140 | 现在我们的代码也已经解决了一个报错,现在报了另一个错误,那就是找不到 `toDisplayString` 函数。由于我们在生成 code string 的时候导出的两个函数 `toDisplayString` 和 `createElementVNode` ,所以我们需要实现这两个函数
141 |
142 | ```ts
143 | // shared/toDisplayString.ts
144 | export function toDisplayString(str) {
145 | return String(str)
146 | }
147 | ```
148 |
149 | 然后再 `runtime-core` 中
150 |
151 | ```ts
152 | export { h } from './h'
153 | export { renderSlots } from './helpers/renderSlots'
154 | // 将 createVNode 作为 createElementVNode 导出
155 | export { createTextVNode, createVNode as createElementVNode } from './vnode'
156 | export { getCurrentInstance, registerCompiler } from './component'
157 | export { provide, inject } from './apiInject'
158 | export { createRenderer } from './render'
159 | export { nextTick } from './scheduler'
160 | // 导出 toDisplayString
161 | export { toDisplayString } from '../shared'
162 | ```
163 |
164 | 那么现在我们再跑一下 demo,恭喜你!已经成功实现了 template 编译为 render!
165 |
166 | ## 3. 重构
167 |
168 | 我们发现代码中存在一些坏味道,例如我们再调用 render 的时候,我们直接是把第二个参数给了 `instance.proxy`,这是因为我们的编译代码需要一些参数。
169 |
170 | 抽离函数
171 |
172 | ```ts
173 | // componentRenderUtils
174 | export function renderComponentRoot(instance) {
175 | const { render, proxy } = instance
176 | const result = render.call(proxy, proxy)
177 | return result
178 | }
179 | ```
180 |
181 | ```ts
182 | // 修改代码
183 |
184 | // render.ts
185 |
186 | function setupEffect(instance, ...) {
187 | // other code ..
188 | // 下面还有一个调用 instance.render 也要修改
189 | const subTree = renderComponentRoot(instance)
190 | }
191 | ```
192 |
193 |
--------------------------------------------------------------------------------
/docs/44. codegen 生成插值类型.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # codegen 生成插值类型
4 |
5 | 在本小节中,我们需要将一个 template 生成为 render
6 |
7 | ```html
8 | {{message}}
9 | ```
10 |
11 | ```ts
12 | // 生成为
13 | const { toDisplayString: _toDisplayString } = Vue
14 | export function render(_ctx, _cache) { return _toDisplayString(_ctx.message) }
15 | ```
16 |
17 | ## 1. 测试样例
18 |
19 | 我们先写一个测试样例,这一次,我们可以采用快照的形式来看自己生成的 code string
20 |
21 | ```ts
22 | test('interpolation', () => {
23 | const template = '{{message}}'
24 | const ast = baseParse(template)
25 | transform(ast)
26 | const code = codegen(ast)
27 | expect(code).toMatchSnapshot()
28 | })
29 | ```
30 |
31 | ## 2. 实现
32 |
33 | ### 2.1 生成导入 code
34 |
35 | 首先,我们发现和生成 text 不同的是:
36 |
37 | - 会有一个导入,也就是 `const { toDisplayString: _toDisplayString } = Vue`
38 | - 以及还会有一个 `_toDisplayString(_ctx.message)`
39 |
40 | 首先,我们发现只有在 `type` 是 `NodeType.INTERPOLATION` 的情况下,才会有导入 `toDisplayString`,这部分我们最好在 transform 中做。然后在 codegen 阶段,我们直接对 ast 上挂在的 helpers 进行处理就好了
41 |
42 | ```ts
43 | // transform.ts
44 | export function transform(root, options = {}) {
45 | const context = createTransformContext(root, options)
46 | traverseNode(root, context)
47 | createRootCodegen(root)
48 | // 在根节点挂载 helpers
49 | root.helpers = [...context.helpers.keys()]
50 | }
51 |
52 | function createTransformContext(root, options) {
53 | const context = {
54 | root,
55 | nodeTransforms: options.nodeTransforms || {},
56 | helpers: new Map(),
57 | helper(name: string) {
58 | context.helpers.set(name, 1)
59 | },
60 | }
61 | return context
62 | }
63 | ```
64 |
65 | ```ts
66 | function traverseNode(node, context) {
67 | const { nodeTransforms } = context
68 | for (let i = 0; i < nodeTransforms.length; i++) {
69 | const transform = nodeTransforms[i]
70 | transform(node)
71 | }
72 | // 在这里遍历整棵树的时候,将根据不同的 node 的类型存入不同的 helper
73 | switch (node.type) {
74 | case NodeType.INTERPOLATION:
75 | context.helper('toDisplayString')
76 | break
77 | case NodeType.ROOT:
78 | case NodeType.ELEMENT:
79 | // 只有在 ROOT 和 ELEMENT 才会存在 children,所以这个方法里面的 children 判断也可以去掉了
80 | // 我们在 parse 模块中 createRoot 的时候记得加上类型
81 | traverseChildren(node, context)
82 | break
83 | default:
84 | break
85 | }
86 | }
87 | ```
88 |
89 | 下面的话,我们就可以在 codegen 里获取到 `ast.helpers`,并对其进行处理。
90 |
91 | ```ts
92 | export function codegen(ast) {
93 | const context = createCodegenContext()
94 | const { push, newLine } = context
95 |
96 | // 这里处理 code 头部,如果说 ast.helpers 是有值的情况下,那么再追加头部 code
97 | if (ast.helpers.length) {
98 | genFunctionPreamble(ast, context)
99 | }
100 |
101 | // other code ...
102 | }
103 |
104 | function genFunctionPreamble(ast, context) {
105 | const VueBinding = 'Vue'
106 | const { push, newLine } = context
107 | const aliasHelper = s => `${s}: _${s}`
108 | // 遍历 ast.helpers 并处理别名
109 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}`)
110 | newLine()
111 | }
112 | ```
113 |
114 | ### 2.2 生成插值 code
115 |
116 | 我们先来一个最初实现
117 |
118 | ```ts
119 | function genNode(node, context) {
120 | // 在 genNode 的时候通过 node 的类型进行不同的处理
121 | switch (node.type) {
122 | case NodeType.TEXT:
123 | genText(node, context)
124 | break
125 | case NodeType.INTERPOLATION:
126 | genInterpolation(node, context)
127 | break
128 | case NodeType.SIMPLE_EXPRESSION:
129 | genExpression(node, context)
130 | break
131 | }
132 | }
133 |
134 | function genExpression(node, context) {
135 | // 处理 SIMPLE_EXPRESSION
136 | const { push } = context
137 | push(`_ctx.${node.content}`)
138 | }
139 |
140 | function genInterpolation(node, context) {
141 | // 如果是插值,那么我们的 content 还可以通过 node.content 再处理一层
142 | const { push } = context
143 | push(`_toDisplayString(`)
144 | genNode(node.content, context)
145 | push(`)`)
146 | }
147 |
148 | function genText(node, context) {
149 | const { push } = context
150 | push(`'${node.content}'`)
151 | }
152 | ```
153 |
154 | 但是这样的代码是不好维护的,我们可以将这个处理 SIMPLE_EXPRESSION 的逻辑作为一个 transform 插件。
155 |
156 | ```ts
157 | // transforms/transformExpression
158 | import { NodeType } from '../ast'
159 |
160 | export function transformExpression(node) {
161 | if (node.type === NodeType.INTERPOLATION) {
162 | node.content = processExpression(node.content)
163 | }
164 | }
165 |
166 | function processExpression(node) {
167 | node.content = `_ctx.${node.content}`
168 | return node
169 | }
170 | ```
171 |
172 | ```ts
173 | // 在测试样例中
174 | test('interpolation', () => {
175 | const template = '{{message}}'
176 | const ast = baseParse(template)
177 | transform(ast, {
178 | // 将其作为一个插件导入
179 | nodeTransforms: [transformExpression],
180 | })
181 | const code = codegen(ast)
182 | expect(code).toMatchSnapshot()
183 | })
184 | ```
185 |
186 | ```ts
187 | function genExpression(node, context) {
188 | const { push } = context
189 | // 这样我们就可以不用在这里加上 _ctx. 了
190 | push(`${node.content}`)
191 | }
192 | ```
193 |
194 | 现在生成插值功能也已经完毕了
195 |
196 | ## 3. 重构
197 |
198 | 我们发现我们的代码中存在 `toDisplayString` 这个字符串,我们最好将其抽离出来,然后就可以将代码中写死的部分修改成活的了。
199 |
200 | ```ts
201 | // runtimeHelpers
202 | export const TO_DISPLAY_STRING = Symbol('toDisplayString')
203 |
204 | export const HelperNameMapping = {
205 | [TO_DISPLAY_STRING]: 'toDisplayString',
206 | }
207 | ```
208 |
209 |
--------------------------------------------------------------------------------
/docs/11. 实现 ref.md:
--------------------------------------------------------------------------------
1 | # 实现 ref
2 |
3 | 在本小节中,我们将会实现 ref
4 |
5 | ## 1. happy path
6 |
7 | ```ts
8 | it('happy path', () => {
9 | const refFoo = ref(1)
10 | expect(refFoo.value).toBe(1)
11 | })
12 | ```
13 |
14 | 下面我们去实现
15 |
16 | ```ts
17 | // ref.ts
18 | export function ref(value) {
19 | return { value }
20 | }
21 | ```
22 |
23 | ## 2. ref 应该是响应式
24 |
25 | ```ts
26 | it('ref should be reactive', () => {
27 | const r = ref(1)
28 | let dummy
29 | let calls = 0
30 | effect(() => {
31 | calls++
32 | dummy = r.value
33 | })
34 | expect(calls).toBe(1)
35 | expect(dummy).toBe(1)
36 | r.value = 2
37 | expect(calls).toBe(2)
38 | expect(dummy).toBe(2)
39 | })
40 | ```
41 |
42 | 通过对 ref 的说明,我们发现 ref 传入的值大多数情况下是一个原始值。那么我们就不能通过 Proxy 的特性来对值进行封装了,这个时候我们可以采用 class getter setter 的方式来进行实现收集依赖和触发依赖
43 |
44 | ### 3.1 happy path 通过 class 实现
45 |
46 | ```ts
47 | class RefImpl {
48 | private _value: any
49 | constructor(value) {
50 | this._value = value
51 | }
52 | get value() {
53 | return this._value
54 | }
55 | }
56 |
57 | export function ref(value) {
58 | return new RefImpl(value)
59 | }
60 | ```
61 |
62 | 我们先把 happy path 通过 class 的方式实现
63 |
64 | ### 3.2 实现响应式
65 |
66 | 这里我们要响应式其实还是和 reactive 的套路是一样的,在 get 中收集依赖,在 set 中触发依赖。这个时候我们就可以去复用 reactive 的 track 逻辑了。
67 |
68 | ```ts
69 | // 我们先看看现在 track 的逻辑
70 |
71 | export function track(target, key) {
72 | if (!isTracking()) return
73 | let depsMap = targetMap.get(target)
74 | if (!depsMap) {
75 | depsMap = new Map()
76 | targetMap.set(target, depsMap)
77 | }
78 | let dep = depsMap.get(key)
79 | if (!dep) {
80 | dep = new Set()
81 | depsMap.set(key, dep)
82 | }
83 | if (dep.has(activeEffect)) return
84 | activeEffect.deps.push(dep)
85 | dep.add(activeEffect)
86 | }
87 | ```
88 |
89 | 其实有很多是无法通用的,下面的收集依赖的逻辑我们就可以单独抽离出来了
90 |
91 | ```ts
92 | export function track(target, key) {
93 | if (!isTracking()) return
94 | let depsMap = targetMap.get(target)
95 | if (!depsMap) {
96 | depsMap = new Map()
97 | targetMap.set(target, depsMap)
98 | }
99 | let dep = depsMap.get(key)
100 | if (!dep) {
101 | dep = new Set()
102 | depsMap.set(key, dep)
103 | }
104 | trackEffect(dep)
105 | }
106 |
107 | // 抽离函数
108 | export function trackEffect(dep) {
109 | if (dep.has(activeEffect)) return
110 | activeEffect.deps.push(dep)
111 | dep.add(activeEffect)
112 | }
113 | ```
114 |
115 | 而 trigger 呢?
116 |
117 | ```ts
118 | export function trigger(target, key) {
119 | const depsMap = targetMap.get(target)
120 | const deps = depsMap.get(key)
121 | for (const effect of deps) {
122 | if (effect.options.scheduler) {
123 | effect.options.scheduler()
124 | } else {
125 | effect.run()
126 | }
127 | }
128 | }
129 | ```
130 |
131 | 同理,我们也可以将 trigger 单独的部分抽离出来,在重构了一个模块后,需要重新运行一遍测试,查看是否功能正常
132 |
133 | ```ts
134 | export function trigger(target, key) {
135 | const depsMap = targetMap.get(target)
136 | const deps = depsMap.get(key)
137 | triggerEffect(deps)
138 | }
139 |
140 | export function triggerEffect(deps) {
141 | for (const effect of deps) {
142 | if (effect.options.scheduler) {
143 | effect.options.scheduler()
144 | } else {
145 | effect.run()
146 | }
147 | }
148 | }
149 | ```
150 |
151 | 最后,我们就可以在 ref 中
152 |
153 | ```ts
154 | class RefImpl {
155 | private _value: any
156 | // 这里我们也需要一个 deps Set 用于储存所有的依赖
157 | public deps = new Set()
158 | constructor(value) {
159 | this._value = value
160 | }
161 | get value() {
162 | // 在 get 中进行依赖收集
163 | trackEffect(this.deps)
164 | return this._value
165 | }
166 | set value(newValue) {
167 | this._value = newValue
168 | // 在 set 中进行触发依赖
169 | triggerEffect(this.deps)
170 | }
171 | }
172 | ```
173 |
174 | 这样我们再去运行测试发现就可以通过了
175 |
176 | ### 3.3 相同的值不会触发依赖
177 |
178 | ```ts
179 | r.value = 2
180 | expect(calls).toBe(2)
181 | expect(dummy).toBe(2)
182 | ```
183 |
184 | 这个实现方法也非常简单:
185 |
186 | ```ts
187 | class RefImpl {
188 | // other code ...
189 | set value(newValue) {
190 | // 在这里进行判断
191 | if (newValue === this._value) return
192 | // other code ...
193 | }
194 | }
195 | ```
196 |
197 | 其实这里的判断我们可以写一个工具函数:
198 |
199 | ```ts
200 | // shared/index.ts
201 | export function hasChanged(val, newVal) {
202 | return !Object.is(val, newVal)
203 | }
204 | ```
205 |
206 | ```ts
207 | class RefImpl {
208 | // other code ...
209 | set value(newValue) {
210 | // 在这里用这个工具函数进行判断
211 | if (hasChanged(this._value, newValue)) {
212 | this._value = newValue
213 | triggerEffect(this.deps)
214 | }
215 | }
216 | }
217 | ```
218 |
219 | 这个时候我们再进行测试发现就没有问题了
220 |
221 | ## 3. 嵌套 prop 应该是 reactive 的
222 |
223 | 我们先看看单元测试
224 |
225 | ```ts
226 | it('should make nested properties reactive', () => {
227 | const a = ref({
228 | foo: 1,
229 | })
230 | let dummy
231 | effect(() => {
232 | dummy = a.value.foo
233 | })
234 | a.value.foo = 2
235 | expect(dummy).toBe(2)
236 | expect(isReactive(a.value)).toBe(true)
237 | })
238 | ```
239 |
240 | 那这个我们该怎么实现呢?
241 |
242 | ```ts
243 | class RefImpl {
244 | // other code ...
245 | constructor(value) {
246 | // 在这里进行一下判断,如果是 Object 的话,就对其进行 reacitve
247 | this._value = isObject(value) ? reactive(value) : value
248 | }
249 | // other code ...
250 | }
251 | ```
252 |
253 | 下面我们再跑一下测试,就可以跑通了
--------------------------------------------------------------------------------
/docs/19. 实现 shapeFlags.md:
--------------------------------------------------------------------------------
1 | # 实现 shapeFlags
2 |
3 | 我们在 `render.ts` 中对于 vnode 的类型进行判断,这里进行判断的类型我们发现一共有 4 种:
4 |
5 | - element:`vnode.type === string`
6 | - stateful_component:`isObject(vnode.type)`
7 | - text_children:`vnode.type === string`
8 | - array_children:`Array.isArray(children)`
9 |
10 | ```ts
11 | export function patch(vnode, container) {
12 | if (typeof vnode.type === 'string') {
13 | processElement(vnode, container)
14 | } else if (isObject(vnode.type)) {
15 | processComponent(vnode, container)
16 | }
17 | }
18 |
19 | function mountElement(vnode, container) {
20 | // other code ...
21 | if (typeof children === 'string') {
22 | domEl.textContent = children
23 | } else if (Array.isArray(children)) {
24 | mountChildren(vnode, domEl)
25 | }
26 | }
27 | ```
28 |
29 | ## 1. v1 版本
30 |
31 | 我们可以将所有的判断抽离出来,统一管理
32 |
33 | ```ts
34 | // 我们用 0 来代表 false,1 来代表 true
35 | const ShapeFlags = {
36 | element: 0,
37 | stateful_component: 0,
38 | text_children: 0,
39 | array_children: 0,
40 | }
41 | ```
42 |
43 | ```ts
44 | // 更新
45 | vnode.shapeFlags.element = 1
46 |
47 | // 查找
48 | if (vnode.shapeFlags.element === ShapeFlags.element) {
49 | }
50 | ```
51 |
52 | 此时我们已经将判断单独抽离开了,下面我们还可以进行性能方面的优化
53 |
54 | ## 2. v2 版本
55 |
56 | ### 2.1 & 和 |
57 |
58 | 首先,我们先复习一下 & 和 |
59 |
60 | #### 2.1.1 &
61 |
62 | &:按位与,用于对两个二进制操作数逐位进行比较,并根据下表所示的换算表返回结果。
63 |
64 | | 第一个数的值 | 第二个数的值 | 运算结果 |
65 | | ------------ | ------------ | -------- |
66 | | 1 | 1 | 1 |
67 | | 1 | 0 | 0 |
68 | | 0 | 1 | 0 |
69 | | 0 | 0 | 0 |
70 |
71 | 以 0 作为 false,以 1 作为 true,简单理解就是:
72 |
73 | - true & true = true
74 | - true & false = false
75 | - false & true = false
76 | - false & false = false
77 | - 两位都是 true 才是 true,反之所有的都是 false
78 |
79 | ```ts
80 | console.log(12 & 5); //返回值4
81 | ```
82 |
83 | 下图以算式的形式解析了 12 和 5 进行位与运算的过程。通过位与运算,只有第 3 位的值为全为 true,故返回 true,其他位均返回 false。
84 |
85 | 
86 |
87 | #### 2.1.2 |
88 |
89 | |:按位或,用于对两个二进制操作数逐位进行比较,并根据如表格所示的换算表返回结果。
90 |
91 | | 第一个数的值 | 第二个数的值 | 运算结果 |
92 | | ------------ | ------------ | -------- |
93 | | 1 | 1 | 1 |
94 | | 1 | 0 | 1 |
95 | | 0 | 1 | 1 |
96 | | 0 | 0 | 0 |
97 |
98 | 以 0 作为 false,以 1 作为 true,简单理解就是:
99 |
100 | - true & true = true
101 | - true & false = true
102 | - false & true = true
103 | - false & false = false
104 | - 两位都是 false 才是 false,反之所有的都是 true
105 |
106 | ```ts
107 | console.log(12 | 5); //返回值13
108 | ```
109 |
110 | 下图以算式的形式解析了 12 和 5 进行位或运算的过程。通过位或运算,除第 2 位的值为 false 外,其他位均返回 true。
111 |
112 | 
113 |
114 | ### 2.2 左移操作符
115 |
116 | **左移操作符 (`<<`)** 是将一个操作数按指定移动的位数向左移动。
117 |
118 | ```ts
119 | 1 << 1 // 0001 -> 0010
120 |
121 | 1 << 3 // 0001 -> 1000
122 | ```
123 |
124 | ### 2.3 实现 v2 版本
125 |
126 | 通过对于按位与、按位或和左移操作符的理解,我们不难想象,可以将一个 shapeFlag 修改为下面这样:
127 |
128 | ```ts
129 | const ShapeFlags = {
130 | element: 1, // 0001
131 | stateful_component: 1 << 1, // 0010
132 | text_children: 1 << 2, // 0100
133 | array_children: 1 << 3, // 1000
134 | }
135 | ```
136 |
137 | ```ts
138 | // 更新
139 | vnode.shapeFlag.element = ShapeFlags.element
140 |
141 | // 查询
142 | if(vnode.shapeFlag.element === ShapeFlags.element){
143 |
144 | }
145 | ```
146 |
147 | 下面我们就动手开始实现吧,首先在 shared 中创建一个文件 `ShapeFlags`
148 |
149 | ```ts
150 | // shared/ShapeFlags.ts
151 | export const enum ShapeFlags {
152 | ELEMENT = 1,
153 | STATEFUL_COMPONENT = 1 << 1,
154 | TEXT_CHILDREN = 1 << 2,
155 | ARRAY_CHILDREN = 1 << 3,
156 | }
157 | ```
158 |
159 | ```ts
160 | import { ShapeFlags } from '../shared/ShapeFlags'
161 |
162 | export function createVNode(type, props?, children?) {
163 | // 这里先直接返回一个 VNode 结构
164 | const vnode = {
165 | type,
166 | props,
167 | children,
168 | el: null,
169 | shapeFlags: getShapeFlags(type),
170 | }
171 | // 还要对于 children 进行处理
172 | if (typeof children === 'string') {
173 | // 或运算符,vnode.shapeFlags | ShapeFlags.TEXT_CHILDREN
174 | // 这里其实非常巧妙,例如我们现在是 0001,0001 | 0100 = 0101
175 | vnode.shapeFlags |= ShapeFlags.TEXT_CHILDREN
176 | } else if (Array.isArray(children)) {
177 | // 这里也是同理
178 | vnode.shapeFlags |= ShapeFlags.ARRAY_CHILDREN
179 | }
180 | return vnode
181 | }
182 |
183 | function getShapeFlags(type) {
184 | return typeof type === 'string'
185 | ? ShapeFlags.ELEMENT
186 | : ShapeFlags.STATEFUL_COMPONENT
187 | }
188 | ```
189 |
190 | 这里做好了判断,下面我们就可以去修改判断部分了
191 |
192 | ```ts
193 | // render.ts
194 | export function patch(vnode, container) {
195 | const { shapeFlags } = vnode
196 | // 这里就用到了按位与了,只有都是 true,才是 true,才会进入下面
197 | if (shapeFlags & ShapeFlags.ELEMENT) {
198 | processElement(vnode, container)
199 | } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
200 | processComponent(vnode, container)
201 | }
202 | }
203 |
204 |
205 | function mountElement(vnode, container) {
206 | // 解构出 shapeFlags
207 | const { type: domElType, props, children, shapeFlags } = vnode
208 | // 进行按位与判断
209 | if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
210 | domEl.textContent = children
211 | } else if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
212 | mountChildren(vnode, domEl)
213 | }
214 | }
215 | ```
216 |
217 | 下面我们再进行打包,查看功能是否正常
218 |
219 | ### 2.4 v2 版本有什么好处
220 |
221 | - 其实功能 v1 版本就已经实现了,但是我们为什么要舍弃可读性来实现 v2 版本呢?
222 | - 首先,这一块的判断应该是要非常频繁执行的,一般一个项目的 render 过程中要有非常多的节点,所以会进行非常多的判断
223 | - 虽然在程序开发中可读性 >> 性能,但是这一块的性能压力很大,而使用位运算会提升很大部分的性能
224 | - 所以这里就使用了位运算
225 |
226 |
--------------------------------------------------------------------------------
/docs/21. 实现组件的 props 功能.md:
--------------------------------------------------------------------------------
1 | # 实现组件的 props
2 |
3 | ## 1. 实现挂载一个组件
4 |
5 | 我们在进行 render 的时候,还可以挂载一个组件
6 |
7 | ```ts
8 | // Foo.js
9 | import { h } from '../../lib/mini-vue.esm.js'
10 |
11 | export const Foo = {
12 | setup(props) {},
13 | render() {
14 | return h('div', {}, 'Hello World')
15 | },
16 | }
17 | ```
18 |
19 | ```ts
20 | // App.js
21 | render() {
22 | return h(
23 | 'div',
24 | {
25 | class: 'red', // event
26 | onClick() {
27 | console.log('click')
28 | },
29 | onMousedown() {
30 | console.log('mousedown')
31 | },
32 | },
33 | [
34 | // 挂载一个组件
35 | h(Foo, { class: 'blue' }),
36 | ]
37 | )
38 | },
39 | ```
40 |
41 | 通过测试我们发现,我们之前写的代码其实是支持挂载一个组件的,我们再来复习一个 render 的调用流程:
42 |
43 | - `createApp`
44 |
45 | - `mount`
46 |
47 | - 创建 App.js 的 VNode,并调用 render 方法来挂载这个 VNode
48 |
49 | - 进入 `render` 方法,调用 `patch`
50 |
51 | - 进入 `patch` 方法,首先需要判断一下此时 App.js 组件的类型,是一个 statefulComponent
52 |
53 | - 进入 `processComponent` 方法,进入`mountComponent` 方法,创建 App.js 的组件实例
54 |
55 | - 进入 `setupComponent`,传入组件实例
56 |
57 | - 在这里进行初始化 `setupStatefulComponent`,处理 setup
58 |
59 | - `finishSetup`,将 component 的 render 挂载到 instance 的 render
60 |
61 | - 调用 `instance.render` 返回 VNode 树
62 |
63 | - 再次进入 `patch` 逻辑,此时我们的 VNode 已经是 element 了
64 |
65 | - 进行 patchElement,此时对 children 再次递归 patch
66 |
67 | - 如果某个 child 是 component,那么就可以进行挂载了
68 |
69 | - 所以这样就可以来挂载一个组件了。
70 |
71 | ## 2. 实现挂载 props
72 |
73 | 首先,一个组件的 setup 可以传入一个参数,该参数就是 props
74 |
75 | ```ts
76 | import { h } from '../../lib/mini-vue.esm.js'
77 |
78 | export const Foo = {
79 | // setup 第一个参数是 props
80 | setup(props) {
81 | console.log(props)
82 | },
83 | render() {
84 | return h('div', {}, 'Hello World')
85 | },
86 | }
87 | ```
88 |
89 | 那么这里的 props 是在哪里传入的呢?是在父组件的 render 的时候传递的
90 |
91 | ```ts
92 | return h(
93 | 'div',
94 | {},
95 | [
96 | // 第二个参数就是 props
97 | h(Foo, { count: 1 }),
98 | ]
99 | )
100 | ```
101 |
102 | 那么我们该如何实现呢?首先,我们在处理 h 的时候就是 createVNode,第二个参数会被挂载到 vnode 实例的 props 属性。那我们又是在哪里进行处理 statefulComponent 的呢?
103 |
104 | ```ts
105 | // components.ts
106 |
107 | export function setupComponent(instance) {
108 | // 在这里进行处理 props,此时传入 instance,instance.vnode.props
109 | initProps(instance, instance.vnode.props)
110 | setupStatefulComponent(instance)
111 | }
112 | ```
113 |
114 | ```ts
115 | // componentProps
116 | export function initProps(instance, rawProps) {
117 | // 在这里先处理 props 的情况
118 | // 因为某些组件可能没有 props,所以要给一个默认值
119 | instance.props = rawProps || {}
120 | // TODO attrs
121 | }
122 | ```
123 |
124 | 在调用 component.setup 的时候调用传入 props 参数
125 |
126 | ```ts
127 | function setupStatefulComponent(instance) {
128 | const component = instance.vnode.type
129 | // other code ...
130 | const { setup } = component
131 | if (setup) {
132 | // 在这里将 instance.props 传入
133 | const setupResult = setup(instance.props)
134 | handleSetupResult(instance, setupResult)
135 | }
136 | }
137 | ```
138 |
139 | 这样我们就可以获取 props上了。
140 |
141 | ## 3. props 挂载到 this 中
142 |
143 | ```ts
144 | import { h } from '../../lib/mini-vue.esm.js'
145 |
146 | export const Foo = {
147 | setup(props) {
148 | console.log(props)
149 | },
150 | render() {
151 | // props 也会被挂载到 this 中
152 | return h('div', {}, 'counter: ' + this.count)
153 | },
154 | }
155 | ```
156 |
157 | 还记得之前我们是如何将 setup 返回值挂载到 props 中的嘛?
158 |
159 | ```ts
160 | export const componentPublicInstanceProxyHandlers = {
161 | get({ _: instance }, key) {
162 | const { setupState } = instance
163 | // 在这里挂载 setupState
164 | // 此时我们还可以进行判断
165 | if(key in setupState){
166 | return setupState[key]
167 | }
168 | const publicGetter = PublicProxyGetterMapping[key]
169 | if(publicGetter){
170 | return publicGetter(instance)
171 | }
172 | },
173 | }
174 | ```
175 |
176 | ```ts
177 | export const componentPublicInstanceProxyHandlers = {
178 | get({ _: instance }, key) {
179 | const { setupState, props } = instance
180 | // 在这里也可以对 props 进行判断
181 | if(key in setupState){
182 | return setupState[key]
183 | }else if(key in props) {
184 | return props[key]
185 | }
186 | const publicGetter = PublicProxyGetterMapping[key]
187 | if(publicGetter){
188 | return publicGetter(instance)
189 | }
190 | },
191 | }
192 | ```
193 |
194 | 那么我们就可以将重复的部分抽离出来了
195 |
196 | ```ts
197 | // shared/index.ts
198 | export function hasOwn(target, key) {
199 | return Reflect.has(target, key)
200 | }
201 | ```
202 |
203 | ```ts
204 | export const componentPublicInstanceProxyHandlers = {
205 | get({ _: instance }, key) {
206 | const { setupState, props } = instance
207 | // 将重复的部分抽离出来
208 | if(hasOwn(setupState, key)){
209 | return Reflect.get(setupState, key)
210 | }else if(hasOwn(props, key)) {
211 | return Reflect.get(setupState, key)
212 | }
213 | const publicGetter = PublicProxyGetterMapping[key]
214 | if(publicGetter){
215 | return publicGetter(instance)
216 | }
217 | },
218 | }
219 | ```
220 |
221 | 然后我们再次测试一下,发现也是没有问题的
222 |
223 | ## 4. props 是 readonly 的
224 |
225 | 最后,props 是不可以被修改的,那么这个该怎么实现呢?还记得我们在 reactivity 的模块中 readonly 和 shallowReadonly 嘛?这里只需要包一层就可以了,所以需要用 shallowReadonly 来包一层
226 |
227 | ```ts
228 | // component.ts
229 | function setupStatefulComponent(instance) {
230 | // other code ...
231 | // 传入的 setup 用 shallowReadonly 包一层
232 | const setupResult = setup(shallowReadonly(instance.props))
233 | }
234 | ```
235 |
236 | 我们测试一下
237 |
238 | ```ts
239 | import { h } from '../../lib/mini-vue.esm.js'
240 |
241 | export const Foo = {
242 | setup(props) {
243 | console.log(props)
244 | props.count++
245 | console.log(props)
246 | },
247 | render() {
248 | return h('div', {}, 'counter: ' + this.count)
249 | },
250 | }
251 | ```
252 |
253 | 发现没有问题,props 的值是无法改变的
254 |
255 |
--------------------------------------------------------------------------------
/docs/26. 实现 provide 和 inject.md:
--------------------------------------------------------------------------------
1 | # 实现 provide 和 inject
2 |
3 | 在本小节中我们将去实现 provide 和 inject
4 |
5 | ## 1. 例子
6 |
7 | 我们先看看 provide 和 inject 的一个例子
8 |
9 | ```ts
10 | import { createApp, h, provide, inject } from '../../lib/mini-vue.esm.js'
11 |
12 | const Provider = {
13 | render() {
14 | return h('div', {}, [h('div', {}, 'Provider'), h(Consumer)])
15 | },
16 | setup() {
17 | // 在上层 provide
18 | provide('foo', 'foo')
19 | },
20 | }
21 |
22 | const Consumer = {
23 | render() {
24 | return h('div', {}, 'Consumer: ' + `inject foo: ${this.foo}`)
25 | },
26 | setup() {
27 | return {
28 | // 在下层 inject
29 | foo: inject('foo'),
30 | }
31 | },
32 | }
33 |
34 | createApp(Provider).mount('#app')
35 | ```
36 |
37 | ## 2. 最简版实现
38 |
39 | 首先,我们需要创建一个 `apiInjecet.ts`
40 |
41 | ```ts
42 | import { getCurrentInstance } from './component'
43 |
44 | export function provide(key, value) {
45 | const currentInstance = getCurrentInstance()
46 | if (currentInstance) {
47 | currentInstance.providers[key] = value
48 | }
49 | }
50 | export function inject(key) {
51 | const currentInstance = getCurrentInstance()
52 | if (currentInstance) {
53 | return currentInstance.providers[key]
54 | }
55 | }
56 | ```
57 |
58 | 然后我们需要在 `instance` 上面挂载一下,注意我们在 `inject` 的时候需要获得 parent,所以我们还需要挂载一个 parent,而 parent 则是在 `createComponentInstance` 时进行挂载,我们需要一层一层的向下传递
59 |
60 | ```ts
61 | import { getCurrentInstance } from './component'
62 |
63 | export function provide(key, value) {
64 | const currentInstance = getCurrentInstance()
65 | if (currentInstance) {
66 | currentInstance.providers[key] = value
67 | }
68 | }
69 | export function inject(key) {
70 | const currentInstance = getCurrentInstance()
71 | if (currentInstance) {
72 | const { parent } = currentInstance
73 | return parent.providers[key]
74 | }
75 | }
76 | ```
77 |
78 | ```ts
79 | export function createComponentInstance(vnode, parent) {
80 | // 这里返回一个 component 结构的数据
81 | const component = {
82 | vnode,
83 | type: vnode.type,
84 | setupState: {},
85 | props: {},
86 | emit: () => {},
87 | slots: {},
88 | providers: {},
89 | // 挂载 parent
90 | parent,
91 | }
92 | component.emit = emit.bind(null, component) as any
93 | return component
94 | }
95 |
96 | // 然后再一层一层的传递
97 |
98 | // 直到最上层
99 | // render.ts
100 |
101 | function setupRenderEffect(instance, vnode, container) {
102 | const subTree = instance.render.call(instance.proxy)
103 | // 这里的第三个参数,就是 parent
104 | patch(subTree, container, instance)
105 | vnode.el = subTree.el
106 | }
107 | ```
108 |
109 | 现在我们就已经可以实现 `provide`、`inject` 的最简版的逻辑了。
110 |
111 | ## 3. 多层传递
112 |
113 | 但是目前我们的代码是存在问题的,例如我们知道 provide 是跨层级传递的,假设我们再生产者和消费者之间再加一层,那么代码就会出现问题。
114 |
115 | ### 3.1 例子
116 |
117 | ```ts
118 | import { createApp, h, provide, inject } from '../../lib/mini-vue.esm.js'
119 |
120 | const Provider = {
121 | render() {
122 | return h('div', {}, [h('div', {}, 'Provider'), h(Provider2)])
123 | },
124 | setup() {
125 | provide('foo', 'foo')
126 | },
127 | }
128 |
129 | // 再 Provider 和 Provider2 加一层,那么最下层就获取不到了
130 | const Provider2 = {
131 | render() {
132 | return h('div', {}, [h('div', {}, 'Provider2'), h(Consumer)])
133 | },
134 | setup() {},
135 | }
136 |
137 | const Consumer = {
138 | render() {
139 | return h('div', {}, 'Consumer: ' + `inject foo: ${this.foo}`)
140 | },
141 | setup() {
142 | return {
143 | foo: inject('foo'),
144 | }
145 | },
146 | }
147 |
148 | createApp(Provider).mount('#app')
149 | ```
150 |
151 | ### 3.2 实现
152 |
153 | 那么这种该如何实现呢?其实我们就可以在初始化 instance 实例的时候:
154 |
155 | ```ts
156 | export function createComponentInstance(vnode, parent) {
157 | const component = {
158 | vnode,
159 | type: vnode.type,
160 | setupState: {},
161 | props: {},
162 | emit: () => {},
163 | slots: {},
164 | // 把初始化的 provides 默认指向父级的 provides
165 | providers: parent ? parent.providers : {},
166 | parent,
167 | }
168 | component.emit = emit.bind(null, component) as any
169 | return component
170 | }
171 | ```
172 |
173 | 这样我们跨层级的部分也已经解决了。
174 |
175 | 但是现在我们还存在一个问题,由于我们直接用的是直接覆盖,上面我们将 `instance.providers` 修改为 `parent.provides`,但是我们在 `provides` 中直接将自己的 provides 更改了,由于引用关系,导致自己的 `parent.provides` 也被修改了。
176 |
177 | 例如下面的应用场景:
178 |
179 | ```ts
180 | // 修改了自己的 provides.foo 也间接修改了 parent.provides.foo
181 | provide('foo', 'foo2')
182 | // 这时候取父亲的 provides.foo 发现就会被修改了
183 | const foo = inject('foo')
184 | ```
185 |
186 | 那么如何解决呢?
187 |
188 | ```ts
189 | // apiInject.ts
190 |
191 | export function provide(key, value) {
192 | const currentInstance = getCurrentInstance()
193 | if (currentInstance) {
194 | // 在这里不要直接设置
195 | currentInstance.providers[key] = value
196 | }
197 | }
198 | ```
199 |
200 | 我们其实可以通过原型链的方式巧妙的进行设置:
201 |
202 | ```ts
203 | if (currentInstance) {
204 | let provides = currentInstance.provides
205 | if (currentInstance.parent) {
206 | const parentProvides = currentInstance.parent.provides
207 | // 如果自己的 provides 和 parent.provides,那么就证明是初始化阶段
208 | if (provides === parentProvides) {
209 | // 此时将 provides 的原型链设置为 parent.provides
210 | // 这样我们在设置的时候就不会五绕道 parent.provides
211 | // 在读取的时候因为原型链的特性,我们也能读取到 parent.provides
212 | provides = currentInstance.provides = Object.create(parentProvides)
213 | }
214 | }
215 | provides[key] = value
216 | }
217 | ```
218 |
219 | 这样设置后我们就可以
220 |
221 | ## 4. inject 的默认值
222 |
223 | ### 4.1 例子
224 |
225 | 我们可以在 inject 时设置一个默认值,默认值可能时一个函数或者一个原始值
226 |
227 | ```ts
228 | const baseFoo = inject('baseFoo', 'base')
229 | const baseFoo = inject('baseBar', ()=> 'bar')
230 | ```
231 |
232 | ### 4.2 实现
233 |
234 | ```ts
235 | export function inject(key, defaultValue) {
236 | const currentInstance = getCurrentInstance()
237 | if (currentInstance) {
238 | const { parent } = currentInstance
239 | // 如果 key 存在于 parent.provides
240 | if (key in parent.provides) {
241 | return parent.provides[key]
242 | } else if (defaultValue) {
243 | // 如果不存在,同时 defaultValue 存在
244 | // 判断类型,同时执行或返回
245 | if (typeof defaultValue === 'function') return defaultValue()
246 | return defaultValue
247 | }
248 | }
249 | }
250 | ```
251 |
252 | 这样默认值的操作也已经可以了。
--------------------------------------------------------------------------------
/docs/23. 实现组件的 slot 功能.md:
--------------------------------------------------------------------------------
1 | # 实现组件的 slot 功能
2 |
3 | 在本小节中,我们将会实现组件的 slot 功能
4 |
5 | ## 1. 什么是 slot
6 |
7 | 我们先看看最简单的 h 函数中的 slot 是什么样子的
8 |
9 | ```ts
10 | import { h } from '../../lib/mini-vue.esm.js'
11 | import { Foo } from './Foo.js'
12 |
13 | export default {
14 | render() {
15 | // 我们在渲染一个组件的时候,向第 3 个函数挂载 h
16 | return h('div', {}, [h(Foo, {}, h('div', {}, '123'))])
17 | },
18 | setup() {},
19 | }
20 | ```
21 |
22 | ```ts
23 | import { h } from '../../lib/mini-vue.esm.js'
24 |
25 | export const Foo = {
26 | setup() {},
27 | render() {
28 | // 我们可以在这里通过 `this.$slots` 进行接收到挂载的 $slots
29 | return h('div', {}, this.$slots)
30 | },
31 | }
32 | ```
33 |
34 | 类似于模板中的这样
35 |
36 | ```html
37 |
38 | 123
39 |
40 | ```
41 |
42 | ## 2. 实现 slots
43 |
44 | ### 2.1 实现
45 |
46 | 通过对示例的研究,我们发现其实 slots 就是 component 的第三个参数
47 |
48 | 首先,我们在创建 `component` 实例的时候初始化一个 slots
49 |
50 | ```ts
51 | export function createComponentInstance(vnode) {
52 | const component = {
53 | vnode,
54 | type: vnode.type,
55 | setupState: {},
56 | props: {},
57 | emit: () => {},
58 | // 初始化 slots
59 | slots: {},
60 | }
61 | component.emit = emit.bind(null, component) as any
62 | return component
63 | }
64 | ```
65 |
66 | 在 `setupComponent` 的时候进行处理 slots
67 |
68 | ```ts
69 | export function setupComponent(instance) {
70 | initProps(instance, instance.vnode.props)
71 | // 处理 slots
72 | initSlots(instance, instance.vnode.children)
73 | setupStatefulComponent(instance)
74 | }
75 | ```
76 |
77 | ```ts
78 | // componentSlots.ts
79 |
80 | export function initSlots(instance, slots) {
81 | // 我们这里最粗暴的做法就是直接将 slots 挂载到 instance 上
82 | instance.slots = slots
83 | }
84 | ```
85 |
86 | 然后我们在拦截操作的时候加入对于 `$slots` 的处理
87 |
88 | ```ts
89 | import { hasOwn } from '../shared/index'
90 |
91 | const PublicProxyGetterMapping = {
92 | $el: i => i.vnode.el,
93 | // 加入对于 $slots 的处理
94 | $slots: i => i.slots,
95 | }
96 |
97 | // other code ...
98 | ```
99 |
100 | 现在我们就已经可以来实现挂载 slots 了。
101 |
102 | ### 2.2 优化
103 |
104 | 现在我们已经实现如何挂载 slots 了,但是如果我们传递多个 slots 呢?
105 |
106 | 模板中是这样
107 |
108 | ```ts
109 |
110 | 123
111 | 456
112 |
113 | ```
114 |
115 | 在 h 函数中是这样的:
116 |
117 | ```ts
118 | render() {
119 | return h('div', {}, [
120 | // 可以传递一个数组
121 | h(Foo, {}, [h('div', {}, '123'), h('div', {}, '456')]),
122 | ])
123 | }
124 | ```
125 |
126 | 我们再来看看接收 slots 的地方是怎么写的:
127 |
128 | ```ts
129 | render() {
130 | const foo = h('p', {}, 'foo')
131 | // 第三个参数只能接收 VNode,但是这里我们的 this.$slots 是一个数组
132 | // 所以就无法渲染出来
133 | // 这个时候就可以创建一个 VNode
134 | return h('p', {}, [foo, this.$slots])
135 | },
136 | ```
137 |
138 | ```ts
139 | return h('p', {}, [foo, h('div', {}, this.$slots)])
140 | ```
141 |
142 | 我们可以将这里的渲染 slots 抽离出来,例如我们抽离一个函数叫做 `renderSlots`
143 |
144 | ```ts
145 | // runtime-core/helpers/renderSlots
146 |
147 | import { h } from '../h'
148 |
149 | export function renderSlots(slots) {
150 | return h('div', {}, slots)
151 | }
152 | ```
153 |
154 | ```ts
155 | return h('p', {}, [foo, renderSlots(this.$slots)])
156 | ```
157 |
158 | 现在数组的形式已经可以实现了,但是单个的形式我们却无法实现了,所以我们需要改一下,我们是在 `initSlots` 的时候进行挂载 slots 的,我们进行一个判断,判断默认都是数组。
159 |
160 | ```ts
161 | export function initSlots(instance, slots) {
162 | // 进行类型判断
163 | slots = Array.isArray(slots) ? slots : [slots]
164 | instance.slots = slots
165 | }
166 | ```
167 |
168 | OK,现在我们无论是数组还是单个都可以实现了。
169 |
170 | ## 3. 具名 slots
171 |
172 | 我们在给定 slots 时,还可以给定名字。
173 |
174 | ### 3.1 例子
175 |
176 | 我们来看看一个具名插槽的例子
177 |
178 | 在模板中是这样的:
179 |
180 | ```html
181 |
182 |
183 |
184 |
185 | ```
186 |
187 | 在 h 函数中是这样的
188 |
189 | ```ts
190 | const foo = h(
191 | Foo,
192 | {},
193 | {
194 | header: h('div', {}, '123'),
195 | footer: h('div', {}, '456'),
196 | }
197 | )
198 | return h('div', {}, [app, foo])
199 | ```
200 |
201 | 我们在接收 slots 的时候是如何接收的呢?`renderSlots` 第二个参数可以指定 name
202 |
203 | ```ts
204 | return h('p', {}, [
205 | renderSlots(this.$slots, 'header'),
206 | foo,
207 | renderSlots(this.$slots, 'footer'),
208 | ])
209 | ```
210 |
211 | ### 3.2 实现
212 |
213 | 首先,我们在挂载的时候就从数组变成了对象。但是在这里我们还是要进行两次判断,第一个判断如果传入的是简单的值,那么就视为这个是 `default`。如果传入的是对象,那么再具体判断
214 |
215 | ```ts
216 | function initObjectSlots(instance, slots) {
217 | if(!slots) return
218 | // 单独传了一个 h
219 | if (slots.vnode) {
220 | instance.slots.default = [slots]
221 | return
222 | }
223 | // 传了一个数组
224 | if (Array.isArray(slots)) {
225 | instance.slots.default = slots
226 | return
227 | }
228 | // 传了一个对象
229 | for (const slotName of Object.keys(slots)) {
230 | instance.slots[slotName] = normalizeSlots(slots[slotName])
231 | }
232 | }
233 |
234 | function normalizeSlots(slots) {
235 | return Array.isArray(slots) ? slots : [slots]
236 | }
237 | ```
238 |
239 | 然后我们在渲染 `slots` 的时候,也要对多个类型进行判断
240 |
241 | ```ts
242 | export function renderSlots(slots, name = 'default') {
243 | // 此时 slots 就是 Object
244 | const slot = slots[name]
245 | if (slot) {
246 | return h('div', {}, slot)
247 | }
248 | }
249 | ```
250 |
251 | 好了,现在我们的具名插槽就也已经支持了。
252 |
253 | ## 4. 作用域插槽
254 |
255 | ### 4.1 例子
256 |
257 | 在 template 中,作用域插槽是这样的
258 |
259 | 注册方
260 |
261 | ```html
262 |
263 | ```
264 |
265 | 使用方
266 |
267 | ```ts
268 |
{{count}} 是 1
269 | ```
270 |
271 | 在 h 函数中是这样的
272 |
273 | 注册方
274 |
275 | ```ts
276 | return h('p', {}, [
277 | // 第三个参数就是 props
278 | renderSlots(this.$slots, 'header', {
279 | count: 1,
280 | }),
281 | foo,
282 | renderSlots(this.$slots, 'footer'),
283 | ])
284 | ```
285 |
286 | 使用方
287 |
288 | ```ts
289 | const foo = h(
290 | Foo,
291 | {},
292 | {
293 | // 这样我们的 slots 就变成一个函数了
294 | header: ({ count }) => h('div', {}, '123' + count),
295 | footer: () => h('div', {}, '456'),
296 | }
297 | )
298 | ```
299 |
300 | ### 4.2 实现
301 |
302 | 首先,在注册的时候,第三个参数是 props,而我们的 slots 也变成了函数
303 |
304 | ```ts
305 | export function renderSlots(slots, name = 'default', props) {
306 | // 此时 slots 就是函数
307 | const slot = slots[name]
308 | if (slot) {
309 | return h('div', {}, slot(props))
310 | }
311 | }
312 | ```
313 |
314 | 在初始化的时候
315 |
316 | ```ts
317 | // other code...
318 |
319 | function initObjectSlots(instance, slots) {
320 | // other code ...
321 | for (const slotName of Object.keys(slots)) {
322 | // 在这里的时候,我们通过 `slots[slotName]` 来获取到 slot 对应的值
323 | // 但是现在我们对应的值已经变成了函数,所以需要调用 `slots[slotName]()`
324 | // 但是我们在 render 时候,会将这一段整体作为一个函数进行调用
325 | // 所以结合上面我们的 `renderSlots`,就变成了这样
326 | // props => normalizeSlots(slots[slotName](props))
327 | instance.slots[slotName] = props => normalizeSlots(slots[slotName](props))
328 | }
329 | }
330 |
331 | // other code...
332 | ```
333 |
334 | 现在我们也已经支持作用域插槽了。
--------------------------------------------------------------------------------
/docs/33. 更新 children(四).md:
--------------------------------------------------------------------------------
1 | # 更新children(四)
2 |
3 | 在本小节中,我们将会去实现 `array` => `array` 中间对比的最复杂的情况:
4 |
5 | - 移动节点
6 | - 新增节点
7 |
8 | ## 1. 移动节点
9 |
10 | ### 1.1 例子
11 |
12 | ```ts
13 | const prevChildren = [
14 | h('p', { key: 'A' }, 'A'),
15 | h('p', { key: 'B' }, 'B'),
16 | h('p', { key: 'C' }, 'C'),
17 | h('p', { key: 'D' }, 'D'),
18 | h('p', { key: 'E' }, 'E'),
19 | h('p', { key: 'F' }, 'F'),
20 | h('p', { key: 'G' }, 'G'),
21 | ]
22 |
23 | const nextChildren = [
24 | h('p', { key: 'A' }, 'A'),
25 | h('p', { key: 'B' }, 'B'),
26 | h('p', { key: 'E' }, 'E'),
27 | h('p', { key: 'C' }, 'C'),
28 | h('p', { key: 'D' }, 'D'),
29 | h('p', { key: 'F' }, 'F'),
30 | h('p', { key: 'G' }, 'G'),
31 | ]
32 | ```
33 |
34 | - 旧节点:A B C D E F G
35 | - 新节点:A B E C D F G
36 | - 通过新旧对比我们发现,最终需要移动 E 的位置就好了
37 |
38 | ### 1.2 实现
39 |
40 | #### 试想
41 |
42 | 一种暴力解法:
43 |
44 | - 获取到混乱的部分,最终全部重排,虽然也是能够实现最终的效果的
45 | - 但是性能有很大的浪费,因为调用 DOM API 的性能是非常差的,所以我们还是需要一种算法,来找到最精准的点。
46 |
47 | 在 Vue 3 中,使用了`最长递增子序列`的方式来获取到了稳定的序列(也就是不会变的)序列,举个例子:
48 |
49 | - 老节点:B C D
50 | - 新节点:D B C
51 | - 其中 `B` 和 `C` 保持着一种稳定序列的关系,即 B 永远是在 C 的前面
52 | - 最长递增子序列的算法就是去找到某个序列中最长的稳定序列。
53 |
54 | 通过 `getSequence` 获取递增序列的在混乱部分中的索引:
55 |
56 | - 得到旧节点混乱部分的索引:C D E -> 2 3 4
57 | - 得到新节点混乱部分的索引:E C D 每一项对应旧节点最终得出是:4 2 3
58 | - 调用 `getSequence` 来获取到最长递增子序列在原数组中的索引是 `1 2`
59 | - 对比新节点第一项 E 对应的混乱索引是 0,在最长递增子序列中不存在
60 | - 表示要移动
61 |
62 | 
63 |
64 | #### 实现
65 |
66 | 我们需要一个映射用于储存每一项在旧节点中的索引
67 |
68 | ```ts
69 | // 储存旧节点混乱元素的索引,创建定长数组,性能更好
70 | const newIndexToOldIndexMap = new Array(toBePatched)
71 | // 循环初始化每一项索引,0 表示未建立映射关系
72 | for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
73 | ```
74 |
75 | 对每一项建立索引,我们在确定旧节点在新节点存在,并 `patch` 的时候,对每一项节点储存一下索引
76 |
77 | ```ts
78 | // 遍历老节点循环体中,此时 i = 当前遍历的老节点元素的索引
79 | if (newIndex === undefined) {
80 | hostRemove(prevChild.el)
81 | } else {
82 | // 确定新节点存在,储存索引映射关系
83 | // newIndex 获取到当前老节点在新节点中的元素,减去 s2 是要将整个混乱的部分拆开,索引归于 0
84 | // 为什么是 i + 1 是因为需要考虑 i 是 0 的情况,因为我们的索引映射表中 0 表示的是初始化状态
85 | // 所以不能是 0,因此需要用到 i + 1
86 | newIndexToOldIndexMap[newIndex - s2] = i + 1
87 | patch(prevChild, c2[newIndex], container, parentInstance, null)
88 | patched += 1
89 | }
90 | ```
91 |
92 | 在最后的部分,我们需要对索引映射进行处理,首先,我们需要调用 `getSequence` 来获取最长递增子序列在原数组中的索引
93 |
94 | ```ts
95 | // 最后部分
96 | // 获取最长递增子序列索引
97 | const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
98 | // 需要两个指针 i,j
99 | // j 指向获取出来的最长递增子序列的索引
100 | // i 指向我们新节点
101 | let j = 0
102 | for (let i = 0; i < toBePatched; i++) {
103 | if (i !== increasingNewIndexSequence[j]) {
104 | // 移动
105 | console.log('移动位置', c2[i + s2])
106 | } else {
107 | // 不移动
108 | console.log('不移动', c2[i + s2])
109 | j += 1
110 | }
111 | }
112 | ```
113 |
114 | 此时结合我们最开始的例子,就可以得出:
115 |
116 | - E,移动位置
117 | - C、D 不移动位置
118 |
119 | #### 优化
120 |
121 | 如果此时你发现直接顺序遍历,可能会出现很奇怪的情况,例如我们看看另一种情况:
122 |
123 | - 旧节点:A B C D E F G
124 | - 新节点:A B E C D F G
125 | - 我们按照顺序去循环新节点,虽然找到第一个要移动的 E 了,但是由于我们是通过 `insertBefore` 来实现的,需要一个锚点,此时我们的锚点就是就是 `C`(因为他是混乱元素的第一个),但是我们在当前的循环中,我们并不知道 `C` 是移动还是不移动,直接插入到 `C`元素之前是不可取的的行为。我们需要找到一个稳定的元素,就是最终的 `F`
126 | - 所以我们需要更改最终遍历的顺序,我们要倒序进行遍历。
127 |
128 | ```ts
129 | let j = increasingNewIndexSequence.length - 1
130 | for (let i = toBePatched - 1; i >= 0; i--) {
131 | // 获取元素的索引
132 | const nextIndex = i + s2
133 | // 获取到需要插入的元素
134 | const nextChild = c2[nextIndex]
135 | // 获取锚点
136 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
137 | if (i !== increasingNewIndexSequence[j]) {
138 | // 移动
139 | hostInsert(nextChild.el, container, anchor)
140 | } else {
141 | j -= 1
142 | }
143 | }
144 | ```
145 |
146 | 
147 |
148 | 最后,这个判断可以来优化一下:
149 |
150 | ```ts
151 | if (i !== increasingNewIndexSequence[j]) {
152 | // 移动
153 | hostInsert(nextChild.el, container, anchor)
154 | } else {
155 | j -= 1
156 | }
157 | ```
158 |
159 | 变成
160 |
161 | ```ts
162 | // 增加 j <= 0 判断
163 | if (j <= 0 || i !== increasingNewIndexSequence[j]) {
164 | // 移动
165 | hostInsert(nextChild.el, container, anchor)
166 | } else {
167 | j -= 1
168 | }
169 | ```
170 |
171 | #### 优化(二)
172 |
173 | 其实我们还是有优化的点的,如果说我们的新旧节点对比不需要移动,那么再去判断最长递增子序列就没有必要了。那么这个该如何判断呢?其实我们只需要判断节点是一直在递增的就可以了,如果节点是一直有递增关系的,那么就不需要移动,如果没有递增关系,那么就可以理解为是需要移动的。
174 |
175 | 增加两个变量
176 |
177 | ```ts
178 | // 应该移动
179 | let shouldMove = false
180 | // 目前最大的索引
181 | let maxNewIndexSoFar = 0
182 | ```
183 |
184 | ```ts
185 | if (newIndex === undefined) {
186 | hostRemove(prevChild.el)
187 | } else {
188 | // 在储存索引的时候
189 | // 判断是否需要移动
190 | // 如果说当前的索引 >= 记录的最大索引
191 | if (newIndex >= maxNewIndexSoFar) {
192 | // 就把当前的索引给到最大的索引
193 | maxNewIndexSoFar = newIndex
194 | } else {
195 | // 否则就不是一直递增,那么就是需要移动的
196 | shouldMove = true
197 | }
198 | newIndexToOldIndexMap[newIndex - s2] = i + 1
199 | patch(prevChild, c2[newIndex], container, parentInstance, null)
200 | patched += 1
201 | }
202 | ```
203 |
204 | 然后在计算最长递增子序列的时候进行判断,如果应该移动,那么就计算,如果不应该移动,直接给定一个空数组就好了
205 |
206 | ```ts
207 | const increasingNewIndexSequence = shouldMove
208 | ? getSequence(newIndexToOldIndexMap)
209 | : []
210 | for (let i = toBePatched - 1; i >= 0; i--) {
211 | const nextIndex = i + s2
212 | const nextChild = c2[nextIndex]
213 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
214 | // 在这里也进行一个判断
215 | if (shouldMove) {
216 | if (j < 0 || i !== increasingNewIndexSequence[j]) {
217 | hostInsert(nextChild.el, container, anchor)
218 | } else {
219 | j -= 1
220 | }
221 | }
222 | }
223 | ```
224 |
225 | ## 2. 创建节点
226 |
227 | ### 2.1 例子
228 |
229 | ```ts
230 | const prevChildren = [
231 | h("p", { key: "A" }, "A"),
232 | h("p", { key: "B" }, "B"),
233 | h("p", { key: "C" }, "C"),
234 | h("p", { key: "E" }, "E"),
235 | h("p", { key: "F" }, "F"),
236 | h("p", { key: "G" }, "G"),
237 | ];
238 |
239 | const nextChildren = [
240 | h("p", { key: "A" }, "A"),
241 | h("p", { key: "B" }, "B"),
242 | h("p", { key: "E" }, "E"),
243 | h("p", { key: "C" }, "C"),
244 | h("p", { key: "D" }, "D"),
245 | h("p", { key: "F" }, "F"),
246 | h("p", { key: "G" }, "G"),
247 | ];
248 | ```
249 |
250 | - 旧节点:A B C E F G
251 | - 新节点:A B E C D F G
252 | - 对比新旧节点我们发现,需要移动 E 的位置,同时创建 D
253 |
254 | ### 2.2 实现
255 |
256 | 在这里我们的主要逻辑其实就已经实现完毕了,因为我们建立了一个索引映射表,如果某一项的索引是 0,那么就说明这一项在旧节点中找不到,那么这一项就是需要新建的。
257 |
258 | ```ts
259 | // 在最终处理索引映射表的时候
260 | for (let i = toBePatched - 1; i >= 0; i--) {
261 | const nextIndex = i + s2
262 | const nextChild = c2[nextIndex]
263 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
264 | // 如果说某一项是0,证明这一项在旧节点中不存在,那么就需要创建了
265 | if (newIndexToOldIndexMap[i] === 0) {
266 | // 创建
267 | patch(null, nextChild, container, parentInstance, anchor)
268 | } else if (shouldMove) {
269 | // 处理移动
270 | }
271 | }
272 | ```
273 |
274 | 至此,diff 算法结束。
275 |
276 |
--------------------------------------------------------------------------------
/docs/16. 组件的初始化流程.md:
--------------------------------------------------------------------------------
1 | # 组件的初始化流程
2 |
3 | 在本小节呢,我们将会去探寻一个组件的初始化流程。
4 |
5 | 我们先放一下初始化的流程图(来源于崔大)
6 |
7 | 
8 |
9 | ## 1. happy path
10 |
11 | 我们可以写一个 happy path,来看看一个组件是如何实现 mount 的。
12 |
13 | ```html
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Document
21 |
22 |
23 |
24 |
25 |
26 |
27 | ```
28 |
29 | ```js
30 | // main.js
31 | import App from './App.js'
32 | // 和 Vue3 的 API 命名方式一样
33 | createApp(App).mount('#app')
34 | ```
35 |
36 | ```js
37 | // App.js
38 |
39 | export default {
40 | render() {
41 | return h('div', 'hi ' + this.title)
42 | },
43 | setup() {
44 | return {
45 | title: 'mini-vue',
46 | }
47 | },
48 | }
49 | ```
50 |
51 | 通过这个 happy path,我们就可以一窥 Vue3 的 App 初始化流程了,接下来让我们深究一下
52 |
53 | ## 2. createApp
54 |
55 | 首先,我们可以看到,最入口处其实就是 `createApp`,它接收一个 `rootComponent`,并内部包含一个 `mount` 方法,接受一个 `rootContainer`
56 |
57 | 下面我们就可以写了
58 |
59 | 我们目前写的是 `runtime-core` 的逻辑
60 |
61 | ```ts
62 | // createApp.ts
63 |
64 | export function createApp(rootComponent) {
65 | return {
66 | mount(rootContainer) {
67 | // 在 vue3 中,会将 rootComponent 转为一个虚拟节点 VNode
68 | // 后续所有的操作都会基于虚拟节点
69 | // 这里就调用了一个 createVNode 的 API 将 rootComponent 转换为一个虚拟节点
70 | // 【注意了】这个虚拟节点就是程序的入口,所有子节点递归处理
71 | const vnode = createVNode(rootComponent)
72 | // 调用 render 来渲染虚拟节点,第二个参数是容器【container】
73 | render(vnode, document.querySelector(rootContainer))
74 | },
75 | }
76 | }
77 | ```
78 |
79 | ### 2.1 createVNode
80 |
81 | ```ts
82 | // vnode.ts
83 | export function createVNode(type, props?, children?) {
84 | // 这里先直接返回一个 VNode 结构,props、children 非必填
85 | return {
86 | type,
87 | props,
88 | children,
89 | }
90 | }
91 | ```
92 |
93 | ### 2.2 render
94 |
95 | ```ts
96 | // render.ts
97 | export function render(vnode, container) {
98 | // 这里的 render 调用 patch 方法,方便对于子节点进行递归处理
99 | patch(vnode, container)
100 | }
101 | ```
102 |
103 | ### 2.3 patch
104 |
105 | ```ts
106 | export function patch(vnode, container) {
107 | // 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
108 | // 这里先只处理 component 类型
109 | processComponent(vnode, container)
110 | }
111 | ```
112 |
113 | ## 3. processComponent
114 |
115 | ```ts
116 | export function processComponent(vnode, container) {
117 | mountComponent(vnode, container)
118 | }
119 | ```
120 |
121 | ### 3.1 mountComponent
122 |
123 | ```ts
124 | function mountComponent(vnode, container) {
125 | // 通过 vnode 获取组件实例
126 | const instance = createComponentInstance(vnode)
127 | // setup component
128 | setupComponent(instance, container)
129 | // setupRenderEffect
130 | setupRenderEffect(instance, container)
131 | }
132 | ```
133 |
134 | ### 3.2 createComponentInstance
135 |
136 | ```ts
137 | export function createComponentInstance(vnode) {
138 | // 这里返回一个 component 结构的数据
139 | const component = {
140 | vnode
141 | }
142 | return component
143 | }
144 | ```
145 |
146 | ### 3.3 setupComponent
147 |
148 | ```ts
149 | export function setupComponent(instance, container) {
150 | // 初始化分为三个阶段
151 | // TODO initProps()
152 | // TODO initSlots()
153 | // 处理 setup 的返回值
154 | // 这个函数的意思是初始化一个有状态的 setup,这是因为在 vue3 中还有函数式组件
155 | // 函数式组件没有状态
156 | setupStatefulComponent(instance, container)
157 | }
158 | ```
159 |
160 | ### 3.4 setupStatefulComponent处理 setup、以及挂载 component 实例
161 |
162 | ```ts
163 | function setupStatefulComponent(instance, container) {
164 | // 这个函数的处理流程其实非常简单,只需要调用 setup() 获取到返回值就可以了
165 | // 那么第一步我们就是要获取用户自定义的 setup
166 | // 通过对初始化的逻辑进行梳理后我们发现,在 createVNode() 函数中将 rootComponent 挂载到了 vNode.type
167 | // 而 vNode 又通过 instance 挂载到的 instance.vnode 中
168 | // 所以就可以通过这里传入的 instance.vnode.type 获取到用户定义的 rootComponent
169 | const component = instance.vnode.type
170 | // 拿到 component 我们就可以拿到 setup 函数
171 | const { setup } = component
172 | // 这里需要判断一下,因为用户是不一定会写 setup 的,所以我们要给其一个默认值
173 | if (setup) {
174 | // 获取到 setup() 的返回值,这里有两种情况,如果返回的是 function,那么这个 function 将会作为组件的 render
175 | // 反之就是 setupState,将其注入到上下文中
176 | const setupResult = setup()
177 | handleSetupResult(instance, setupResult)
178 | }
179 | }
180 | ```
181 |
182 | ### 3.5 handleSetupResult
183 |
184 | ```ts
185 | function handleSetupResult(instance, setupResult) {
186 | // TODO function
187 | // 这里先处理 Object 的情况
188 | if (typeof setupResult === 'object') {
189 | // 如果是 object ,就挂载到实例上
190 | instance.setupState = setupResult
191 | }
192 | // 最后一步,调用初始化结束函数
193 | finishComponentSetup(instance)
194 | }
195 | ```
196 |
197 | ### 3.6 finishComponentSetup
198 |
199 | ```ts
200 | function finishComponentSetup(instance) {
201 | // 这里为了获取 component 方便,我们可以在 instance 上加一个 type 属性
202 | // 指向 vnode.type
203 | const component = instance.type
204 | // 如果 instance.render 没有的话,我们就让 component.render 赋给 instance.render
205 | // 而没有 component.render 咋办捏,其实可以通过编译器来自动生成一个 render 函数
206 | // 这里先不写
207 | if (!instance.render) {
208 | instance.render = component.render
209 | }
210 | }
211 | ```
212 |
213 |
214 |
215 | ### 3.7 setupRenderEffect
216 |
217 | 在 `mountElement` 中,我们还调用了 `setupRenderEffect`
218 |
219 | ```ts
220 | function setupRenderEffect(instance, container) {
221 | // 调用 render 和 patch 挂载 component
222 | const subTree = instance.render()
223 | // 下面就是 mountElement 了
224 | patch(subTree, container)
225 | }
226 | ```
227 |
228 | ## 4. mountElement
229 |
230 | 下面我们就可以来写一下 mountElement 的逻辑了,我们知道其实一个组件从初始化到挂载到 DOM 中是有调用了两次 patch 的,第一次是 `vnode` ,第二次应该是 element 了,所以我们要在 patch 逻辑中加入 processElement 的逻辑
231 |
232 | ```ts
233 | export function patch(vnode, container) {
234 | // 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
235 | // 如果是 element 就去处理 element 的逻辑
236 | // 因为两次的 vnode.type 的值不一样,所以我们就可以根据这个来进行判断了
237 | if (typeof vnode.type === 'string') {
238 | processElement(vnode, container)
239 | } else if (isObject(vnode.type)) {
240 | processComponent(vnode, container)
241 | }
242 | }
243 | ```
244 |
245 | ### 4.1 processElement
246 |
247 | ```ts
248 | function processElement(vnode, container) {
249 | // 分为 init 和 update 两种,这里先写 init
250 | mountElement(vnode, container)
251 | }
252 | ```
253 |
254 | ### 4.2 mountElement
255 |
256 | ```ts
257 | function mountElement(vnode, container) {
258 | // 此函数就是用来将 vnode -> domEl 的
259 | const { type: domElType, props, children } = vnode
260 | // 创建 dom
261 | const domEl = document.createElement(domElType)
262 | // 加入 attribute
263 | for (const prop in props) {
264 | domEl.setAttribute(prop, props[prop])
265 | }
266 | // 这里需要判断children两种情况,string or array
267 | if (typeof children === 'string') {
268 | domEl.textContent = children
269 | } else if (Array.isArray(children)) {
270 | // 如果是 array 就递归调用,并将自己作为 container
271 | mountChildren(vnode, domEl)
272 | }
273 | // 最后将 domEl 加入 dom 树中
274 | container.appendChild(domEl)
275 | }
276 | ```
277 |
278 | ### 4.3 mountChildren
279 |
280 | ```ts
281 | function mountChildren(vnode, container) {
282 | vnode.children.forEach(vnode => {
283 | // 如果 children 是一个 array,就递归 patch
284 | patch(vnode, container)
285 | })
286 | }
287 | ```
288 |
289 | ## 5. 初始化结束
290 |
291 | 这样一个初始化的基本流程就结束了,顺着本篇文章,你就会捋清楚,初始化的主要工作就是初始化 component 实例,处理 setup、props、slots,以及挂载组件(UI)。
292 |
293 |
--------------------------------------------------------------------------------
/docs/40. 三种类型联合解析.md:
--------------------------------------------------------------------------------
1 | # 三种类型联合解析
2 |
3 | 在本小节中,我们将会实现 `插值`、`element`、`text` 联合解析
4 |
5 | ## 1. happy path
6 |
7 | ### 1.1 测试样例
8 |
9 | ```ts
10 | test('happy path', () => {
11 | const ast = baseParse('
hi,{{message}}
')
12 | expect(ast.children[0]).toStrictEqual({
13 | type: NodeType.ELEMENT,
14 | tag: 'div',
15 | children: [
16 | {
17 | type: NodeType.TEXT,
18 | content: 'hi,',
19 | },
20 | {
21 | type: NodeType.INTERPOLATION,
22 | content: {
23 | type: NodeType.SIMPLE_EXPRESSION,
24 | content: 'message',
25 | },
26 | },
27 | ],
28 | })
29 | })
30 | ```
31 |
32 | ### 1.2 实现
33 |
34 | 首先,我们在 `parseElement` 的时候 parse 前 Tag 过后直接就 parse 了后 Tag,现在肯定是不可以,我们还需要 parseChildren
35 |
36 | ```ts
37 | function parseElement(context: { source: string }): any {
38 | const element: any = parseTag(context, TagType.START)
39 | // 增加 parseChildren
40 | element.children = parseChildren(context)
41 | parseTag(context, TagType.END)
42 | return element
43 | }
44 | ```
45 |
46 | #### 1.2.1 parseText
47 |
48 | 此时 `element.children` 长这样,我们发现在 `parseText` 的时候没有考虑到插值
49 |
50 | ```ts
51 | [ { type: 3, content: 'hi,{{message}}
' } ]
52 | ```
53 |
54 | ```ts
55 | function parseText(context: { source: string }): any {
56 | // 也就是在这里,我们直接截取到了字符串的结尾
57 | const content = parseTextData(context, context.source.length)
58 | advanceBy(context, content.length)
59 | return {
60 | type: NodeType.TEXT,
61 | content,
62 | }
63 | }
64 | ```
65 |
66 | 此时我们需要修改一下
67 |
68 | ```ts
69 | function parseText(context: { source: string }): any {
70 | // 如果 context.source 包含了 {{,那么我们就以 {{ 作为结束点
71 | const s = context.source
72 | const endToken = '{{'
73 | let endIndex = s.length
74 | const index = s.indexOf(endToken)
75 | if (index !== -1) {
76 | endIndex = index
77 | }
78 | // 此时我们的 content 就变成了 hi,
79 | const content = parseTextData(context, endIndex)
80 | advanceBy(context, content.length)
81 | return {
82 | type: NodeType.TEXT,
83 | content,
84 | }
85 | }
86 | ```
87 |
88 | #### 1.2.2 parseInterpolation
89 |
90 | 但是现在我们 `parseText` 之后就没有下文了,所以我们在 parseChildren 的时候其实可以写一个循环,循环处理
91 |
92 | ```ts
93 | function parseElement(context: { source: string }): any {
94 | const element: any = parseTag(context, TagType.START)
95 | // parentTag 是从这里来的
96 | element.children = parseChildren(context, element.tag)
97 | parseTag(context, TagType.END)
98 | return element
99 | }
100 |
101 | function parseChildren(context: { source: string }, parentTag: string): any {
102 | const nodes: any = []
103 | // 这里进行循环处理,parentTag 是从哪里来的?parseElement
104 | while (!isEnd(context, parentTag)) {
105 | let node
106 | const s = context.source
107 | if (s.startsWith('{{')) {
108 | node = parseInterpolation(context)
109 | } else if (s.startsWith('<') && /[a-z]/i.test(s[1])) {
110 | node = parseElement(context)
111 | }
112 | if (!node) {
113 | node = parseText(context)
114 | }
115 | nodes.push(node)
116 | }
117 | return nodes
118 | }
119 |
120 | // 循环的结束条件有两个:1. 遇到结束标签 2. context.source 没有值了
121 | function isEnd(context: { source: string }, parentTag: string) {
122 | const s = context.source
123 | // 2. 遇到结束标签
124 | if (parentTag && s.startsWith(`${parentTag}>`)) {
125 | return true
126 | }
127 | // 1. source 有值
128 | return !s
129 | }
130 | ```
131 |
132 | 现在我们的 `happy path` 测试就可以跑通了
133 |
134 | ## 2. 边缘情况一
135 |
136 | ### 2.1 测试样例
137 |
138 | ```ts
139 | test('nested element', () => {
140 | const ast = baseParse('