├── .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 = `` 7 | ``` 8 | 9 | ```ts 10 | render() { 11 | return h('div', {}, `Consumer: xxx`) 12 | } 13 | ``` 14 | 15 | 大概的编译流程是这样的: 16 | 17 | ![0parser-01](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/parser-01.png) 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 |
30 | hi, {{msg}} 31 |
32 | ``` 33 | 34 | - `element`:`div` 35 | - `text`:`hi` 36 | - `插值`:`{{msg}}` 37 | 38 | -------------------------------------------------------------------------------- /docs/10. 实现 shallowReactive.md: -------------------------------------------------------------------------------- 1 | # 实现 shallowReactive 2 | 3 | 在本小节中,我们将会去实现 shallowReactive API 4 | 5 | ## 1. 单元测试 6 | 7 | ```ts 8 | it('happy path', () => { 9 | const original = { foo: { bar: 1 } } 10 | const observed = shallowReactive(original) 11 | expect(isReactive(observed)).toBe(true) 12 | expect(isReactive(observed.foo)).toBe(false) 13 | }) 14 | ``` 15 | 16 | ## 2. 实现 shallowReactive 17 | 18 | 通过 shallowReadonly 我们发现这两者其实是非常相似的 19 | 20 | ```ts 21 | // reactive.ts 22 | export function shallowReactive(raw,{ 23 | return createActiveObject(raw, shalloReactiveHandlers) 24 | }) 25 | ``` 26 | 27 | ```ts 28 | // baseHandlers 29 | 30 | const shallowMutableGet = createGetter(false, true) 31 | 32 | // other code ... 33 | 34 | 35 | export const shallowMutableHandlers = extend({}, mutableHandlers, { 36 | get: shallowMutableGet, 37 | }) 38 | ``` 39 | 40 | 这样跑测试就可以通过了 -------------------------------------------------------------------------------- /docs/13. 实现 isRef 和 unRef.md: -------------------------------------------------------------------------------- 1 | # 实现 isRef 和 unRef 2 | 3 | 在本小节中,我们将会去实现 isRef 和 unRef 4 | 5 | ## 1. isRef 6 | 7 | 我们先看测试样例 8 | 9 | ```ts 10 | it('isRef', () => { 11 | expect(isRef(1)).toBe(false) 12 | expect(isRef(ref(1))).toBe(true) 13 | expect(isRef(reactive({ foo: 1 }))).toBe(false) 14 | }) 15 | ``` 16 | 17 | 这个实现起来就非常简单,只需要给其一个标识即可 18 | 19 | ```ts 20 | const enum RefFlags { 21 | IS_REF = '__v_isRef', 22 | } 23 | 24 | class RefImpl { 25 | // other code ... 26 | constructor(value) { 27 | this._value = isObject(value) ? reactive(value) : value 28 | // 给其一个标识 29 | this[RefFlags.IS_REF] = true 30 | } 31 | // other code ... 32 | } 33 | 34 | export function isRef(ref) { 35 | return !!ref[RefFlags.IS_REF] 36 | } 37 | ``` 38 | 39 | ## 2. unRef 40 | 41 | 先看测试样例 42 | 43 | ```ts 44 | it('unRef', () => { 45 | expect(unRef(ref(1))).toBe(1) 46 | expect(unRef(1)).toBe(1) 47 | }) 48 | ``` 49 | 50 | 实现: 51 | 52 | ```ts 53 | export function unRef(ref) { 54 | return ref[RefFlags.IS_REF] ? ref.value : ref 55 | } 56 | ``` 57 | 58 | 这样测试就跑过了 -------------------------------------------------------------------------------- /docs/20. 实现注册事件功能.md: -------------------------------------------------------------------------------- 1 | # 实现注册事件功能 2 | 3 | 我们在使用 `h` 函数时,发现是可以来注册事件的 4 | 5 | ```ts 6 | return h( 7 | 'div', 8 | { 9 | class: 'red', // event 10 | onClick() { 11 | console.log('click') 12 | }, 13 | onMousedown() { 14 | console.log('mousedown') 15 | }, 16 | }, 17 | ) 18 | ``` 19 | 20 | 所有的 event 的 key 都是以 `on` 开头,并且第二个字母是大写的 21 | 22 | 那我们是在哪里进行挂载 props 的呢?就是在 `mountElement` 中 23 | 24 | ```ts 25 | function mountElement(vnode, container) { 26 | // 在这里处理 props 27 | for (const prop in props) { 28 | domEl.setAttribute(prop, props[prop]) 29 | } 30 | } 31 | ``` 32 | 33 | 因为所有的注册事件都是规律的,所以在这里其实就可以写一个正则来提取出来 34 | 35 | ```ts 36 | const isOn = (key: string) => /^on[A-Z]/.test(key) 37 | for (const prop in props) { 38 | if (isOn(prop)) { 39 | // 因为第二个字母是大写,需要转为小写 40 | const event = prop.slice(2).toLowerCase() 41 | domEl.addEventListener(event, props[prop]) 42 | } else { 43 | domEl.setAttribute(prop, props[prop]) 44 | } 45 | } 46 | ``` 47 | 48 | 现在我们再测试一下,发现事件就被挂载上去了 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zx-projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/2. 实现 effect 返回 runner.md: -------------------------------------------------------------------------------- 1 | # 实现 effect 返回 runner 2 | 3 | 在本小节中,我们将会实现 effect 返回 runner 4 | 5 | ## 1. 测试样例 6 | 7 | ```ts 8 | it('runner', () => { 9 | // runner 就是 effect(fn) 返回一个函数,执行该函数就相当于重新执行了一次传入 effect 的 fn 10 | // 同时执行 runner 也会将 fn 的返回值返回 11 | let foo = 1 12 | const runner = effect(() => { 13 | foo++ 14 | return 'foo' 15 | }) 16 | expect(foo).toBe(2) 17 | // 调用 runner 18 | const r = runner() 19 | expect(foo).toBe(3) 20 | // 获取 fn 返回的值 21 | expect(r).toBe('foo') 22 | }) 23 | ``` 24 | 25 | ## 2. 实现 26 | 27 | 这个该如何实现呢?其实也是非常简单的: 28 | 29 | ```ts 30 | // effect.ts 31 | class ReactiveEffect { 32 | private _fn: any 33 | constructor(fn) { 34 | this._fn = fn 35 | } 36 | run() { 37 | activeEffect = this 38 | const res = this._fn() 39 | // [runner] return 运行的值 40 | return res 41 | } 42 | } 43 | 44 | // other code ... 45 | 46 | 47 | export function effect(fn) { 48 | const _effect = new ReactiveEffect(fn) 49 | _effect.run() 50 | // [runner]: 在这里将 run 方法 return 出去 51 | // 但是要注意 this 指向问题,所以可以 bind 后 return 出去 52 | return _effect.run.bind(_effect) 53 | } 54 | ``` 55 | 56 | 再次测试一下,测试样例就可以通过了 -------------------------------------------------------------------------------- /docs/43. codegen 生成 text.md: -------------------------------------------------------------------------------- 1 | # codegen 生成 text 2 | 3 | 在本小节中,我们需要将一个 template 生成为 render 4 | 5 | ```html 6 | hi 7 | ``` 8 | 9 | ```ts 10 | // 生成为 11 | export function render(_ctx, _cache) { return 'hi' } 12 | ``` 13 | 14 | ## 1. 测试样例 15 | 16 | 我们先写一个测试样例,这一次,我们可以采用快照的形式来看自己生成的 code string 17 | 18 | ```ts 19 | test('text', () => { 20 | const template = 'hi' 21 | const ast = baseParse(template) 22 | transform(ast) 23 | const code = codegen(ast) 24 | expect(code).toMatchSnapshot() 25 | }) 26 | ``` 27 | 28 | ## 2. 实现 29 | 30 | ```ts 31 | // 这里主要的实现就是将 code string 一直不停的追加 32 | export function codegen(ast) { 33 | const context = createCodegenContext() 34 | const { push } = context 35 | const funcName = 'render' 36 | push(`export `) 37 | const args = ['_ctx', '_cache'] 38 | const signature = args.join(', ') 39 | push(`function ${funcName}(${signature}) { `) 40 | push(`return `) 41 | genNode(ast.codegenNode, context) 42 | push(` }`) 43 | return context.code 44 | } 45 | 46 | function genNode(node, context) { 47 | const { push } = context 48 | push(`'${node.content}'`) 49 | } 50 | 51 | // 将 code 封装,同时将追加 code 方法也封装,降低耦合 52 | function createCodegenContext() { 53 | const context = { 54 | code: '', 55 | push(source: string) { 56 | context.code += source 57 | }, 58 | } 59 | return context 60 | } 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /docs/8. 实现 shallowReadonly.md: -------------------------------------------------------------------------------- 1 | # 实现 shallowReadonly 功能 2 | 3 | 在本小节中,我们将会实现 shallowReadonly 功能 4 | 5 | ## 1. happy path 测试样例 6 | 7 | ```ts 8 | it('happy path', () => { 9 | const original = { bar: { foo: 1 } } 10 | // shallow 的意思是浅的,默认 readonly 是嵌套的,而 shallowReadonly 刚好相反 11 | const shallow = shallowReadonly(original) 12 | expect(isReadonly(shallow)).toBe(true) 13 | expect(isReadonly(shallow.bar)).toBe(false) 14 | }) 15 | ``` 16 | 17 | ## 2. shallowReadonly 的实现 18 | 19 | ```ts 20 | // reactive.ts 21 | 22 | // other code ... 23 | 24 | export function shallowReadonly(raw) { 25 | return createActiveObject(raw, shallowReadonlyHandlers) 26 | } 27 | ``` 28 | 29 | ```ts 30 | // baseHandlers 31 | 32 | // other code ... 33 | 34 | const shallowReadonlyGet = createGetter(true, true) 35 | 36 | // 参数加上 shallow 37 | function createGetter(isReadonly = false, shallow = false) { 38 | return function get(target, key, receiver) { 39 | // other code ... 40 | 41 | // 如果是 shallow ,直接 return res 即可 42 | if (shallow) return res 43 | 44 | if (isObject(res)) { 45 | return isReadonly ? readonly(res) : reactive(res) 46 | } 47 | 48 | if (!isReadonly) { 49 | track(target, key) 50 | } 51 | return res 52 | } 53 | } 54 | 55 | 56 | // 这里我们发现 shalloReadonlyHandlers 和 readonly 的 set 一样 57 | // 就可以复制一份,复写 get 就好了 58 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 59 | get: shallowReadonlyGet, 60 | }) 61 | 62 | ``` 63 | 64 | 这样我们再跑测试就可以通过了 -------------------------------------------------------------------------------- /docs/14. 实现 proxyRefs.md: -------------------------------------------------------------------------------- 1 | # 实现 proxyRefs 2 | 3 | ## 1. get 4 | 5 | 先看测试样例 6 | 7 | ```ts 8 | it('proxyRef', () => { 9 | const foo = { 10 | bar: ref(1), 11 | baz: 'baz', 12 | } 13 | 14 | // get 15 | const proxyFoo = proxyRefs(foo) 16 | expect(foo.bar.value).toBe(1) 17 | expect(proxyFoo.bar).toBe(1) 18 | }) 19 | ``` 20 | 21 | 这里我们看通过 proxyRefs 包装一个对象,那么该对象的属性如果存在 ref,那么就自动 unRef,既然是一个对象,我们就可以用到 proxy 22 | 23 | ```ts 24 | export function proxyRefs(objectWithRefs) { 25 | return new Proxy(objectWithRefs, { 26 | get(target, key, receiver) { 27 | // 自动 unRef 28 | return unRef(Reflect.get(target, key, receiver)) 29 | }, 30 | }) 31 | } 32 | ``` 33 | 34 | ## 2. set 35 | 36 | ```ts 37 | proxyFoo.bar = 10 38 | expect(proxyFoo.bar).toBe(10) 39 | expect(foo.bar.value).toBe(10) 40 | 41 | proxyFoo.bar = ref(20) 42 | expect(proxyFoo.bar).toBe(20) 43 | expect(foo.bar.value).toBe(20) 44 | ``` 45 | 46 | 我们可以看到,set 是分为两种情况的,可能是一个 ref,也可能是一个原始值 47 | 48 | ```ts 49 | export function proxyRefs(objectWithRefs) { 50 | return new Proxy(objectWithRefs, { 51 | get(target, key, receiver) { 52 | // other code ... 53 | }, 54 | set(target, key, value, receiver) { 55 | // set 分为两种情况,如果原来的值是 ref,并且新的值不是 ref 56 | // 那么就去更新原来的 ref.value = newValue 57 | // 第二种情况就是原来的值是 ref,newValue 也是一个 ref 58 | // 那么就直接替换就 OK 了 59 | if (isRef(target[key]) && !isRef(value)) { 60 | return (target[key].value = value) 61 | } else { 62 | return Reflect.set(target, key, value, receiver) 63 | } 64 | }, 65 | }) 66 | } 67 | ``` 68 | 69 | 这样测试就可以跑通了 -------------------------------------------------------------------------------- /docs/0.初始化环境.md: -------------------------------------------------------------------------------- 1 | # 初始化环境 2 | 3 | 本文,主要是对于开发与测试环境的初始化。在本项目中,将采用 `jest` 作为功能测试框架。使用 `typescript` 作为主要开发语言。 4 | 5 | 请按照以下步骤初始化环境: 6 | 7 | ## 1. 初始化仓库 8 | 9 | 为了更好管理仓库的版本,请使用 Git 作为版本管理工具,请确保你的仓库中已初始化 Git 10 | 11 | 请确保你的仓库已经初始化 `package.json` 12 | 13 | 请确保该仓库使用的包管理工具是 `pnpm` 14 | 15 | ## 2. 安装必要的包 16 | 17 | ```bash 18 | # 安装 typescript 19 | pnpm add -D typescript 20 | # 配置 tsconfig.json 21 | npx tsc --init 22 | ``` 23 | 24 | ```bash 25 | # 安装 jest @types/jest 26 | pnpm add -D jest @types/jest 27 | ``` 28 | 29 | ## 3. 配置 npm script 30 | 31 | ```json 32 | { 33 | “scripts”: { 34 | "test": "jest" 35 | } 36 | } 37 | ``` 38 | 39 | 可以通过 `pnpm test` 来运行所有的单元测试,也可以通过 VS Code 插件 `Jest Runner` 来对某一个参数单独 Debug 或 Run。 40 | 41 | 这里需要说明一下,如果使用 Jest Runner 来单独对某个测试进行 Run 的话,可能会出现报错问题(v27.5.1)这个版本会出现,解决办法如下: 42 | 43 | 1. 创建 `/.vscode/settings.json` 44 | 45 | 2. 将下面的代码添加到此配置文件中 46 | 47 | ```json 48 | { 49 | "jestrunner.jestPath": "node_modules/jest/bin/jest.js" 50 | } 51 | ``` 52 | 53 | 3. [关于此问题的 issue](https://github.com/facebook/jest/issues/4751) 54 | 55 | ## 4. 配置 jest ESM support 56 | 57 | 为了更好的编写测试代码,我们需要让 jest 支持 ESM。方法如下: 58 | 59 | 1. 安装必要的包 60 | 61 | ```bash 62 | pnpm add -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript 63 | ``` 64 | 65 | 2. 创建 `bable.config.js`,并将下面的内容加入到配置文件中 66 | 67 | ```js 68 | module.exports = { 69 | presets: [ 70 | ['@babel/preset-env', {targets: {node: 'current'}}], 71 | '@babel/preset-typescript', 72 | ], 73 | }; 74 | ``` 75 | 76 | 3. 即可 77 | 78 | 79 | 80 | 以上,项目初始化配置已经配完。 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/39. 实现解析 text.md: -------------------------------------------------------------------------------- 1 | # 实现解析 text 2 | 3 | ## 1. 测试样例 4 | 5 | ```ts 6 | test('simple text', () => { 7 | const textStr = 'simple text' 8 | const ast = baseParse(textStr) 9 | expect(ast.children[0]).toStrictEqual({ 10 | type: NodeType.TEXT, 11 | content: 'simple text', 12 | }) 13 | }) 14 | ``` 15 | 16 | ## 2. 实现 17 | 18 | ### 2.1 实现 19 | 20 | ```ts 21 | function parseChildren(context: { source: string }): any { 22 | const nodes: any = [] 23 | let node 24 | const s = context.source 25 | if (s.startsWith('{{')) { 26 | node = parseInterpolation(context) 27 | } else if (s.startsWith('<') && /[a-z]/i.test(s[1])) { 28 | node = parseElement(context) 29 | } 30 | // 如果上面两种都无法解析,那么就是普通的 text 节点 31 | if (!node) { 32 | node = parseText(context) 33 | } 34 | nodes.push(node) 35 | return [node] 36 | } 37 | ``` 38 | 39 | ```ts 40 | function parseText(context: { source: string }): any { 41 | // 获取 content 42 | const content = context.source.slice(0) 43 | // 推进 44 | advanceBy(context, content.length) 45 | return { 46 | type: NodeType.TEXT, 47 | content, 48 | } 49 | } 50 | ``` 51 | 52 | ### 2.2 优化 53 | 54 | 我们发现 `context.source.slice` 在上面的代码也用到了,我们就可以抽离成为一个函数 55 | 56 | ```ts 57 | function parseText(context: { source: string }): any { 58 | // 抽离成一个函数 59 | const content = parseTextData(context, context.source.length) 60 | advanceBy(context, content.length) 61 | return { 62 | type: NodeType.TEXT, 63 | content, 64 | } 65 | } 66 | 67 | function parseTextData(context: { source: string }, length) { 68 | return context.source.slice(0, length) 69 | } 70 | ``` 71 | 72 | 在 `parseInterpolation` 这个函数中也有重复的代码,也可以进行重构 73 | 74 | -------------------------------------------------------------------------------- /docs/17. 配置 rollup.md: -------------------------------------------------------------------------------- 1 | # 配置 rollup 2 | 3 | 我们上一节已经写好了具体的逻辑,但是页面中要想使用到我们的代码,就需要去配置打包工具。这里使用 rollup 作为打包工具,这是因为 rollup 更多用于库的打包,webpack 更多用于应用的打包。 4 | 5 | ## 1. 安装 rollup 6 | 7 | ```bash 8 | pnpm i -D rollup 9 | ``` 10 | 11 | ```bash 12 | # 安装 typescript 插件 13 | # https://npmjs.com/package/@rollup/plugin-typescript 14 | pnpm i -D @rollup/plugin-typescript 15 | ``` 16 | 17 | ## 2. 配置 rollup 18 | 19 | 根目录创建 `rollup.config.js` 20 | 21 | ```js 22 | import typescript from '@rollup/plugin-typescript' 23 | 24 | export default { 25 | // 入口文件 26 | input: './src/index.ts', 27 | // 出口文件,可以配多个 28 | // 例如 esm、cjs 规范 29 | output: [ 30 | { 31 | format: 'cjs', 32 | file: './lib/mini-vue.cjs.js', 33 | }, 34 | { 35 | format: 'esm', 36 | file: './lib/mini-vue.esm.js', 37 | }, 38 | ], 39 | plugins: [typescript()], 40 | } 41 | ``` 42 | 43 | `src` 创建 `index.ts` 作为整个 mini-vue 库的出口文件 44 | 45 | 配置完成之后我们就可以试一下打包了,可以配置一个 npm script `"build": "rollup -c rollup.config.js"`,这里打包的时候可能会有一个 warning,让你把你的 tsconfig.json 中的 `module` 改为 `esnext` 46 | 47 | ## 3. 处理一下入口文件 48 | 49 | 我们已经创建了一个入口文件,下面就是将每个模块导入到入口文件当中 50 | 51 | 首先,进入 `runtime-core/index.ts`,将 `createApp` 和 `h` 导出 52 | 53 | ```ts 54 | export { createApp } from './createApp' 55 | export { h } from './h' 56 | ``` 57 | 58 | 然后在 入口文件中 直接导出 runtime-core 即可 59 | 60 | ```ts 61 | export * from './runtime-core/index' 62 | ``` 63 | 64 | 处理完成之后,我们就可以再次打包尝试一下了 65 | 66 | ## 4. h 67 | 68 | 现在我们还没有 h 函数,需要创建一下 h 函数 69 | 70 | ```ts 71 | // h.ts 72 | import { createVNode } from './vnode' 73 | 74 | export function h(type, props, children) { 75 | return createVNode(type, props, children) 76 | } 77 | ``` 78 | 79 | 最后,我们就可以进行测试了 -------------------------------------------------------------------------------- /docs/7. 实现 reactive 和 readonly 的嵌套转换.md: -------------------------------------------------------------------------------- 1 | # 实现 reactive 和 readonly 的嵌套转换 2 | 3 | 在本小节中,我们将会实现 reactive 和 readonly 的嵌套转换功能 4 | 5 | ## 1. reactive 嵌套转换单元测试 6 | 7 | ```ts 8 | it('nested reactive', () => { 9 | const original = { 10 | nested: { foo: 1 }, 11 | array: [{ bar: 2 }], 12 | } 13 | const observed = reactive(original) 14 | expect(isReactive(observed.nested)).toBe(true) 15 | expect(isReactive(observed.array)).toBe(true) 16 | expect(isReactive(observed.array[0])).toBe(true) 17 | }) 18 | ``` 19 | 20 | ## 2. 实现 reactive 嵌套 21 | 22 | ```ts 23 | // baseHandlers.ts 24 | 25 | 26 | function createGetter(isReadonly = false) { 27 | return function get(target, key, receiver) { 28 | // other code ... 29 | 30 | 31 | const res = Reflect.get(target, key, receiver) 32 | // [嵌套转换] 33 | // 在 shared 中写一个工具函数 isObject 用于判断是否是对象 34 | if (isObject(res)) { 35 | return reactive(res) 36 | } 37 | 38 | // other code ... 39 | } 40 | } 41 | ``` 42 | 43 | ```ts 44 | // shared/index.ts 45 | 46 | export function isObject(val) { 47 | return val !== null && typeof val === 'object' 48 | } 49 | ``` 50 | 51 | 这样测试就可以跑通了 52 | 53 | ## 3. readonly 嵌套测试样例 54 | 55 | ```ts 56 | it('should readonly nested object', () => { 57 | const nested = { foo: { innerFoo: 1 }, bar: [{ innerBar: 2 }] } 58 | const wrapped = readonly(nested) 59 | expect(isReadonly(wrapped.foo)).toBe(true) 60 | expect(isReadonly(wrapped.bar)).toBe(true) 61 | expect(isReadonly(wrapped.bar[0])).toBe(true) 62 | }) 63 | ``` 64 | 65 | ## 4. readonly 嵌套实现 66 | 67 | ```ts 68 | // 改一下即可 69 | 70 | // baseHandlers.ts 71 | 72 | if (isObject(res)) { 73 | return isReadonly ? readonly(res) : reactive(res) 74 | } 75 | ``` 76 | 77 | -------------------------------------------------------------------------------- /docs/3. 实现 effect 的 scheduler 功能.md: -------------------------------------------------------------------------------- 1 | # 实现 effect 的 scheduler 功能 2 | 3 | 在本小节中,我们将会实现 effect 的 scheduler 功能 4 | 5 | ## 1. 测试样例 6 | 7 | 我们先来看看测试样例 8 | 9 | ```ts 10 | it('scheduler', () => { 11 | // 1. scheduler 作为 effect 的一个 option 12 | // 2. 有了 scheduler 之后原来的 fn 参数只会执行初始化的一次 13 | // 3. 如果依赖更新时不会执行 fn ,而是会去执行 scheduler 14 | // 4. runner 不受影响 15 | let dummy 16 | let run: any 17 | const scheduler = jest.fn(() => { 18 | run = runner 19 | }) 20 | const obj = reactive({ foo: 1 }) 21 | // 在这里将 scheduler 作为一个 option 传入 effect 22 | const runner = effect( 23 | () => { 24 | dummy = obj.foo 25 | }, 26 | { scheduler } 27 | ) 28 | expect(scheduler).not.toHaveBeenCalled() 29 | // 会执行一次 effect 传入的 fn 30 | expect(dummy).toBe(1) 31 | obj.foo++ 32 | // 有了 scheduler 之后,原来的 fn 就不会执行了 33 | expect(scheduler).toHaveBeenCalledTimes(1) 34 | expect(dummy).toBe(1) 35 | run() 36 | expect(dummy).toBe(2) 37 | }) 38 | ``` 39 | 40 | ## 2. 实现 scheduler 41 | 42 | ```ts 43 | class ReactiveEffect { 44 | private _fn: any 45 | // [scheduler] 构造函数加入 options,这里使用 public 可以供外部使用 46 | constructor(fn, public options) { 47 | this._fn = fn 48 | } 49 | // other code ... 50 | } 51 | 52 | // other code ... 53 | 54 | 55 | export function trigger(target, key) { 56 | const depsMap = targetMap.get(target) 57 | const deps = depsMap.get(key) 58 | for (const effect of deps) { 59 | // [scheduler] 这里需要判断一下 scheduler,如果存在就去运行 scheduler 而不是 fn 60 | if (effect.options.scheduler) { 61 | effect.options.scheduler() 62 | } else { 63 | effect.run() 64 | } 65 | } 66 | } 67 | 68 | 69 | export function effect(fn, options: any = {}) { 70 | // [scheduler]在创建 ReactiveEffect 实例的时候,保存一下 options 71 | const _effect = new ReactiveEffect(fn, options) 72 | // other code ... 73 | } 74 | ``` 75 | 76 | 这样我们再运行单元测试,就可以通过了 -------------------------------------------------------------------------------- /docs/12. 实现 toRaw.md: -------------------------------------------------------------------------------- 1 | # 实现 toRaw 2 | 3 | 在本小节中,我们将会实现 toRaw API 4 | 5 | ## 1. happy path 6 | 7 | 先看测试样例 8 | 9 | ```ts 10 | it('happy path', () => { 11 | // toRaw 可以 return 通过 `reactive` 、 `readonly` 、`shallowReactive` 、`shallowReadonly` 包装的 origin 值 12 | const reactiveOrigin = { key: 'reactive' } 13 | expect(toRaw(reactive(reactiveOrigin))).toEqual(reactiveOrigin) 14 | const readonlyOrigin = { key: 'readonly' } 15 | expect(toRaw(readonly(readonlyOrigin))).toEqual(readonlyOrigin) 16 | const shallowReadonlyOrigin = { key: 'shallowReadonly' } 17 | expect(toRaw(shallowReadonly(shallowReadonlyOrigin))).toEqual( 18 | shallowReadonlyOrigin 19 | ) 20 | const shallowReactiveOrigin = { key: 'shallowReactive' } 21 | expect(toRaw(shallowReactive(shallowReactiveOrigin))).toEqual( 22 | shallowReactiveOrigin 23 | ) 24 | 25 | const nestedWrapped = { 26 | foo: { bar: { baz: 1 }, foo2: { bar: { baz: 2 } } }, 27 | } 28 | expect(toRaw(reactive(nestedWrapped))).toEqual(nestedWrapped) 29 | }) 30 | ``` 31 | 32 | 通过测试样例我们发现,toRaw 的作用就是将包装过的值的原始值返回回来,同时我们嵌套的值也要嵌套转换回来 33 | 34 | ```ts 35 | // reactive.ts 36 | 37 | // 创建 RAW 枚举 38 | export const enum ReactiveFlags { 39 | IS_REACTIVE = '__v_isReactive', 40 | IS_READONLY = '__v_isReadonly', 41 | RAW = '__v_raw', 42 | } 43 | 44 | 45 | export function toRaw(observed) { 46 | // 这里就是嵌套转换了 47 | const original = observed && observed[ReactiveFlags.RAW] 48 | return isProxy(original) ? toRaw(original) : original 49 | } 50 | ``` 51 | 52 | ```ts 53 | // baseHandlers 54 | 55 | 56 | function createGetter(isReadonly = false, shallow = false) { 57 | return function get(target, key, receiver) { 58 | if (key === ReactiveFlags.IS_REACTIVE) { 59 | return !isReadonly 60 | } else if (key === ReactiveFlags.IS_READONLY) { 61 | return isReadonly 62 | } else if (key === ReactiveFlags.RAW) { 63 | // 判断一下,如果访问的 key 是 ReactiveFlag.RAW,就直接返回就可以了 64 | return target 65 | } 66 | // other code ... 67 | } 68 | } 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /docs/41. 从有限状态机的角度看 parse 原理.md: -------------------------------------------------------------------------------- 1 | # 从有限状态机的角度看 parse 原理 2 | 3 | ## 1. 有限状态机定义 4 | 5 | > **有限状态机**(英语:finite-state machine,缩写:**FSM**)又称**有限状态自动机**(英语:finite-state automaton,缩写:**FSA**),简称**状态机**,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。 6 | 7 | 简单的理解来说,读取一组输入然后根据输入来更改为不同的状态 8 | 9 | ![fsm-1](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/fsm-1.png) 10 | 11 | - 例如上图,给状态 A 输入 `foo` 12 | - 状态 A 转向为状态 B 13 | - 给状态 A 输入 `bar` 14 | - 状态 A 转向状态 C 15 | 16 | 有限状态机经常被用在编译中。 17 | 18 | ## 2. parse 模块的原理 19 | 20 | 通过前几个小节中我们可以看出,其实 parse 模块也是这样的,大体会分为几个状态: 21 | 22 | - 初始状态 23 | - text 状态 24 | - element 状态 25 | - interpolation 状态 26 | - end 状态 27 | 28 | ![parse](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/parse.png) 29 | 30 | 总结来说大概是这样的: 31 | 32 | - 最开始是`初始状态`,也就是刚刚执行 createRoot,进入 parseChildren 33 | - 如果是 {{ 开头,进入 `插值状态`,如果是 }} 开头,进入 `end 状态`,再次回到 `初始状态` 34 | - 如果是 < 开头,进入 `element 状态`,接着会 `parseTag `,`parseChildren`,`parseTag 结束`,进入 `end 状态`,再次回到 `初始状态` 35 | - 如果以上都不是,那么进入 `text 状态`,进入 `end 状态`,再次回到 `初始状态` 36 | - 每次从三种状态转为 end 状态,都会推进,最终会以 context.source 消费完毕作为出口 37 | 38 | ## 3. 通过有限状态机模拟正则 39 | 40 | ```js 41 | function test(str) { 42 | function stateA(char) { 43 | if (char === 'a') { 44 | return stateB 45 | } 46 | return stateA 47 | } 48 | function stateB(char) { 49 | if (char === 'b') { 50 | return stateC 51 | } 52 | return stateA 53 | } 54 | function stateC(char) { 55 | if (char === 'c') { 56 | return stateEnd 57 | } 58 | return stateA 59 | } 60 | function stateEnd() { 61 | return stateEnd 62 | } 63 | let currentState = stateA 64 | for (let i = 0; i < str.length; i++) { 65 | currentState = currentState(str[i]) 66 | if (currentState === stateEnd) { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | console.log(test('ac')) 74 | ``` 75 | 76 | - 上面的例子我们就通过有限状态机模仿了正则中的 `/abc/` 的功能 77 | - 初始状态是 stateA 78 | - 如果输入的是 a 转为 stateB 79 | - 如果输入的是 b 转为 stateC 80 | - 如果输入的是 c 转为 end 81 | - 否则都重新转为初始状态 82 | 83 | 以上就是有限状态机的基本概念,以及 parse 模块的基本原理。 -------------------------------------------------------------------------------- /docs/6. 实现 isReactive 和 isReadonly.md: -------------------------------------------------------------------------------- 1 | # 实现 isReactive 和 isReadonly 2 | 3 | 在本小节中,我们将会实现 isReactive 和 isReadonly 这两个 API 4 | 5 | ## 1. isReactive 测试样例 6 | 7 | ```ts 8 | // reactive.spec.ts 9 | it('happy path', () => { 10 | // other code ... 11 | // 加入 isReactive 判断 12 | expect(isReactive(observed)).toBe(true) 13 | expect(isReactive(original)).toBe(false) 14 | }) 15 | ``` 16 | 17 | ## 2. isReactive 实现 18 | 19 | 这个该如何实现呢,其实也是非常简单的: 20 | 21 | ```ts 22 | // reactive.ts 23 | 24 | // other code ... 25 | 26 | export const enum ReactiveFlags { 27 | IS_REACTIVE = '__v_isReactive', 28 | } 29 | 30 | // other code ... 31 | 32 | export function isReactive(raw) { 33 | // 这里为什么需要取反,这是因为如果是一个 original Object 的话,是不会进入 proxy getter 的 34 | // 这里的就会返回一个 undefined,双重取反强制转换为 boolean 35 | return !!raw[ReactiveFlags.IS_REACTIVE] 36 | } 37 | ``` 38 | 39 | 测试之后就跑通了 40 | 41 | ## 3. isReadonly 测试样例 42 | 43 | ```ts 44 | // readonly.spec.ts 45 | it('happy path', () => { 46 | // other code ... 47 | 48 | // [isReadonly] 49 | expect(isReadonly(wrapped)).toBe(true) 50 | expect(isReadonly(original)).toBe(false) 51 | }) 52 | ``` 53 | 54 | ## 4. isReadonly 实现 55 | 56 | 这个实现也是很简单的,仿照 isReactive 的实现: 57 | 58 | ```ts 59 | // reacive.ts 60 | 61 | // other code ... 62 | 63 | export const enum ReactiveFlags { 64 | IS_REACTIVE = '__v_isReactive', 65 | // 添加枚举 66 | IS_READONLY = '__v_isReadonly', 67 | } 68 | 69 | // other code ... 70 | 71 | 72 | // 添加 API 73 | export function isReadonly(raw) { 74 | return !!raw[ReactiveFlags.IS_READONLY] 75 | } 76 | ``` 77 | 78 | ```ts 79 | // baseHandlers.ts 80 | 81 | // other code ... 82 | 83 | function createGetter(isReadonly = false) { 84 | return function get(target, key, receiver) { 85 | // 进行判断 86 | if (key === ReactiveFlags.IS_REACTIVE) { 87 | return !isReadonly 88 | } else if (key === ReactiveFlags.IS_READONLY) { 89 | return isReadonly 90 | } 91 | // other code ... 92 | } 93 | } 94 | 95 | // other code ... 96 | ``` 97 | 98 | 这个时候再进行测试发现就可以通过了 99 | 100 | -------------------------------------------------------------------------------- /docs/45. codegen 生成 element.md: -------------------------------------------------------------------------------- 1 | # codegen 生成 element 2 | 3 | 在本小节中,我们需要将一个 template 生成为 render 4 | 5 | ```html 6 |
7 | ``` 8 | 9 | ```ts 10 | // 生成为 11 | const { createElementVNode: _createElementVNode } = Vue 12 | export function render(_ctx, _cache) { return _createElementVNode( 13 | 'div') } 14 | ``` 15 | 16 | ## 1. 测试样例 17 | 18 | 我们先写一个测试样例,这一次,我们可以采用快照的形式来看自己生成的 code string 19 | 20 | ```ts 21 | test('simple element', () => { 22 | const template = '
' 23 | const ast = baseParse(template) 24 | const code = codegen(ast) 25 | expect(code).toMatchSnapshot() 26 | }) 27 | ``` 28 | 29 | ## 2. 实现 30 | 31 | 首先,我们在 `runtimeHelpers` 中可以加入这个类型 32 | 33 | ```ts 34 | export const TO_DISPLAY_STRING = Symbol('toDisplayString') 35 | // 加入 createElementVNode 36 | export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode') 37 | 38 | export const HelperNameMapping = { 39 | [TO_DISPLAY_STRING]: 'toDisplayString', 40 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 41 | } 42 | ``` 43 | 44 | 然后我们可以写一个处理 element 的 transform 插件,记得在 transform 模块中执行每个插件的时候再传递一个 context 参数。 45 | 46 | ```ts 47 | import { NodeType } from '../ast' 48 | import { CREATE_ELEMENT_VNODE } from '../runtimeHelpers' 49 | 50 | export function transformExpression(node, context) { 51 | if (node.type === NodeType.ELEMENT) { 52 | context.helper(CREATE_ELEMENT_VNODE) 53 | } 54 | } 55 | ``` 56 | 57 | 然后在测试的时候将这个插件加入其中。 58 | 59 | ```ts 60 | test('simple element', () => { 61 | const template = '
' 62 | const ast = baseParse(template) 63 | transform(ast, { 64 | // 加入处理插件 65 | nodeTransforms: [transformElement], 66 | }) 67 | const code = codegen(ast) 68 | expect(code).toMatchSnapshot() 69 | }) 70 | ``` 71 | 72 | 最后在 codegen 阶段加入对于 element 的处理。 73 | 74 | ```ts 75 | function genNode(node, context) { 76 | switch (node.type) { 77 | case NodeType.TEXT: 78 | genText(node, context) 79 | break 80 | case NodeType.INTERPOLATION: 81 | genInterpolation(node, context) 82 | break 83 | case NodeType.SIMPLE_EXPRESSION: 84 | genExpression(node, context) 85 | break 86 | // 加入对 element 的处理 87 | case NodeType.ELEMENT: 88 | genElement(node, context) 89 | break 90 | } 91 | } 92 | 93 | // 处理 element 94 | function genElement(node, context) { 95 | const { push, helper } = context 96 | const { tag } = node 97 | push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}')`) 98 | } 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /docs/38. 实现解析 element 标签.md: -------------------------------------------------------------------------------- 1 | # 实现解析 element 标签 2 | 3 | ## 1. 测试样例 4 | 5 | ```ts 6 | test('simple element', () => { 7 | const elementStr = '
' 8 | const ast = baseParse(elementStr) 9 | expect(ast.children[0]).toStrictEqual({ 10 | type: NodeType.ELEMENT, 11 | tag: 'div', 12 | }) 13 | }) 14 | ``` 15 | 16 | ## 2. 实现 17 | 18 | ### 2.1 伪实现 19 | 20 | 我们先给 `NodeType` 枚举添加一种 `ELEMENT` 类型 21 | 22 | ```ts 23 | function parseChildren(context: { source: string }): any { 24 | const nodes: any = [] 25 | let node 26 | // 将 context.source 提取出来 27 | const s = context.source 28 | if (s.startsWith('{{')) { 29 | node = parseInterpolation(context) 30 | // 如果第一位是 < 而且第二位是 a-z 的话,就进入到 parseElement 31 | } else if (s.startsWith('<') && /[a-z]/i.test(s[1])) { 32 | node = parseElement(context) 33 | } 34 | nodes.push(node) 35 | return [node] 36 | } 37 | ``` 38 | 39 | ```ts 40 | // 为了使测试快速通过,我们可以先写一个伪实现 41 | function parseElement(context: { source: string }): any { 42 | return { 43 | type: NodeType.ELEMENT, 44 | tag: 'div', 45 | } 46 | } 47 | ``` 48 | 49 | ### 2.2 实现 50 | 51 | 第一步就是将 `
` 中的 `div` 提取出来,我们可以来使用正则。 52 | 53 | ``` 54 | ^<[a-z]* 55 | ``` 56 | 57 | 通过这个正则可以将 ` 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 | ![diff-3](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-03.gif) 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 | ![diff-4](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-04.gif) 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 | ![diff-5](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-05.gif) -------------------------------------------------------------------------------- /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 | ![img](..\images\按位与.gif) 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 | ![img](..\images\按位或.gif) 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 | 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 | ![diff-6](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-06.gif) 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 | ![diff-7](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-07.gif) 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 | ![init](../mindmap/初始化流程图.png) 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(``)) { 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('

hi,

{{message}}
') 141 | expect(ast.children[0]).toStrictEqual({ 142 | type: NodeType.ELEMENT, 143 | tag: 'div', 144 | children: [ 145 | { 146 | type: NodeType.ELEMENT, 147 | tag: 'p', 148 | children: [ 149 | { 150 | type: NodeType.TEXT, 151 | content: 'hi,', 152 | }, 153 | ], 154 | }, 155 | { 156 | type: NodeType.INTERPOLATION, 157 | content: { 158 | type: NodeType.SIMPLE_EXPRESSION, 159 | content: 'message', 160 | }, 161 | }, 162 | ], 163 | }) 164 | }) 165 | ``` 166 | 167 | 这个测试样例会在 `element` 中嵌套 `element` 168 | 169 | ### 2.2 实现 170 | 171 | 那么问题出现在哪里?这是因为我们在处理 `text` 的时候,结束条件我们只写了 `{{`,结果遇到了 ` index 说明我们取最前面的 182 | if (index !== -1 && endIndex > index) { 183 | endIndex = index 184 | } 185 | } 186 | const content = parseTextData(context, endIndex) 187 | advanceBy(context, content.length) 188 | return { 189 | type: NodeType.TEXT, 190 | content, 191 | } 192 | } 193 | ``` 194 | 195 | 这里我们处理嵌套的就完毕了 196 | 197 | ## 3. 边缘情况二 198 | 199 | ### 3.1 测试样例 200 | 201 | ```ts 202 | test('should throw error when lack end tag', () => { 203 | expect(() => { 204 | baseParse('
') 205 | }).toThrow() 206 | }) 207 | ``` 208 | 209 | 这里我们将会处理,如果没有结束标签,那么就会抛出异常 210 | 211 | 此时我们进行测试,发现是会死循环的,这是因为我们在 `parseChildren` 中的循环,这里的情况显然是无法触发跳出循环的,因为 `context.source` 没有消费完,而且没有结束标签,所以就死循环了 212 | 213 | ### 3.2 实现 214 | 215 | 我们可以使用一种栈来储存 tag,在解析 tag 的时候将 tag 储存,并在解析完毕后将这个 tag 弹出。 216 | 217 | 而实现报错的方式,我们可以将当前的内容与前一个 tag 进行对比,如果对比不上就报错,例如图中例子的 218 | 219 | `
`,最终我们将 `context.source` 推进到了 ``,此时和上一个 `` 得出的 tag `span` 进行对比,对比不上,那么报错。 220 | 221 | #### 收集阶段 222 | 223 | ```ts 224 | export function baseParse(content: string) { 225 | const context = createContext(content) 226 | // 第二个参数,ancestors 227 | return createRoot(parseChildren(context, [])) 228 | } 229 | 230 | function parseChildren(context: { source: string }, ancestors): any { 231 | const nodes: any = [] 232 | // 在 isEnd 的时候进行消费 233 | while (!isEnd(context, ancestors)) { 234 | let node 235 | const s = context.source 236 | if (s.startsWith('{{')) { 237 | node = parseInterpolation(context) 238 | } else if (s.startsWith('<') && /[a-z]/i.test(s[1])) { 239 | // 在 parseElement 的时候进行收集 240 | node = parseElement(context, ancestors) 241 | } 242 | if (!node) { 243 | node = parseText(context) 244 | } 245 | nodes.push(node) 246 | } 247 | return nodes 248 | } 249 | 250 | function parseElement(context: { source: string }, ancestors): any { 251 | const element: any = parseTag(context, TagType.START) 252 | // 收集 253 | ancestors.push(element) 254 | // 这里的第二个参数记得改一下 255 | element.children = parseChildren(context, ancestors) 256 | // 弹出 257 | ancestors.pop() 258 | parseTag(context, TagType.END) 259 | return element 260 | } 261 | ``` 262 | 263 | #### 消费阶段 264 | 265 | ```ts 266 | function isEnd(context: { source: string }, ancestors) { 267 | const s = context.source 268 | // 2. 遇到结束标签 269 | if (s.startsWith('= 0; i--) { 272 | // 如果说栈里存在这个标签,那么就跳出循环 273 | const tag = ancestors[i].tag 274 | if (startsWithEndTagOpen(context.source, tag)) { 275 | return true 276 | } 277 | } 278 | } 279 | // 1. source 有值 280 | return !s 281 | } 282 | 283 | function startsWithEndTagOpen(source, tag) { 284 | const endTokenLength = ' `array` 的四种简单情况: 4 | 5 | - 新旧左端对比 6 | - 新旧右端对比 7 | - 新多新增 8 | - 旧多删除 9 | 10 | ## 1. 例子 11 | 12 | [例子](https://github.com/zx-projects/mini-vue/blob/main/example/patchChildren/ArrayToArray.js) 13 | 14 | - 1、2、3、4 15 | 16 | ## 2. 前导知识 17 | 18 | 我们先试想一下,这里的 patchChildren 的意义是什么?当然是找到新旧节点不同的地方,颗粒化的更新视图。那么我们一般开发中时,会出现什么情况呢: 19 | 20 | - 新节点比老节点多,创建多的新节点,在理想的情况下,新节点恰好在头部或者尾部 21 | - 老节点比新节点多,删除多的老节点,在理想的情况下,需要删除的老节点恰好在头部或者尾部 22 | - 如果在中间,那么会比较复杂,可能会涉及到移动、新增、删除等等。 23 | - 那么我们还如何确定需要更新的部分呢? 24 | - Vue 的 `diff` 采用双端对比的算法,首先对比头部,然后对比尾部,用于确认需要重点对比的中间部分 25 | 26 | 我们来看一张图: 27 | 28 | ![diff](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/mindmap/diff%20%E7%AE%97%E6%B3%95/%E4%B8%AD%E9%97%B4%E5%AF%B9%E6%AF%94.png) 29 | 30 | - 如何确定混乱的部分(图中使用绿色圈起来的部分) 31 | - 首先,对比新旧节点的首部,也就是 a,b 进行对比,对比到不同的部分,首部对比停止 32 | - 然后,对比新旧节点的尾部,也就是 f,g 进行对比,对比到不同的部分,尾部对比停止 33 | - 最后,就确定好了混乱的部分 34 | 35 | ## 3. 实现 36 | 37 | ### 3.1 核心 38 | 39 | 在本小节中,我们将只会对比头部和尾部,暂时不涉及到中间。那么对比是如何实现的呢?我们来看看下图: 40 | 41 | ![example](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/example.png) 42 | 43 | 其核心就是三个指针: 44 | 45 | - `e1`:旧节点的尾部 46 | - `e2`:新节点的尾部 47 | - `i`:当前对比的节点 48 | 49 | ### 3.2 实现新旧节点头部对比 50 | 51 | #### 例子 52 | 53 | ```ts 54 | const prevChildren = [ 55 | h('p', { key: 'A' }, 'A'), 56 | h('p', { key: 'B' }, 'B'), 57 | h('p', { key: 'C' }, 'C'), 58 | ] 59 | const nextChildren = [ 60 | h('p', { key: 'A' }, 'A'), 61 | h('p', { key: 'B' }, 'B'), 62 | h('p', { key: 'D' }, 'D'), 63 | h('p', { key: 'E' }, 'E'), 64 | ] 65 | ``` 66 | 67 | - 旧节点:A B C 68 | - 新节点:A B C D 69 | 70 | - 从头部开始对比 71 | 72 | #### 实现 73 | 74 | 还记得我们之前在 `render` 中的对比 children 的函数吗 75 | 76 | ```ts 77 | function patchChildren(c1, c2, container, parentInstance) { 78 | // other code ... 79 | // 这里已经进入到了新节点是 array 的情况了 80 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 81 | hostSetElementText(n1.el, '') 82 | mountChildren(c2, container, parentInstance) 83 | } else { 84 | // 对比 array -> array 85 | patchKeyedChildren(c1, c2, container, parentInstance) 86 | } 87 | } 88 | ``` 89 | 90 | 在 `patchKeyedChilren`中我们将会进行对比 91 | 92 | ```ts 93 | function patchKeyedChildren(c1, c2, container, parentInstance) { 94 | // 声明三个指针,e1 是老节点最后一个元素,e2 是新节点最后一个元素,i 是当前对比的元素 95 | let e1 = c1.length - 1 96 | let e2 = c2.length - 1 97 | let i = 0 98 | // 目前我们用来两个节点是否一样暂时只用 type 和 key 99 | function isSameVNode(n1, n2) { 100 | return n1.type === n2.type && n1.key === n2.key 101 | } 102 | // 首先我们要从新旧节点头部开始对比 103 | while (i <= e1 && i <= e2) { 104 | // 从头部开始对比,如果当前对比的 vnode 相同 105 | // 就进入 patch 阶段,如果不相等,直接中断掉这个循环 106 | if (isSameVNode(c1[i], c2[i])) { 107 | patch(c1[i], c2[i], container, parentInstance) 108 | } else { 109 | break 110 | } 111 | i += 1 112 | } 113 | } 114 | ``` 115 | 116 | 由于我们用到了 `key`,所以在创建一个 VNode 节点的时候还要初始化 `key` 117 | 118 | ```ts 119 | export function createVNode(type, props?, children?) { 120 | const vnode = { 121 | type, 122 | props, 123 | children, 124 | el: null, 125 | // 初始化 key 126 | key: props ? props.key : null, 127 | shapeFlags: getShapeFlags(type), 128 | } 129 | // other code ... 130 | } 131 | ``` 132 | 133 | 最后,让我们再看一段动画 134 | 135 | ![diff-01](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-01.gif) 136 | 137 | ### 3.2 实现新旧节点尾部对比 138 | 139 | #### 例子 140 | 141 | ```ts 142 | const prevChildren = [ 143 | h('p', { key: 'A' }, 'A'), 144 | h('p', { key: 'B' }, 'B'), 145 | h('p', { key: 'C' }, 'C'), 146 | ] 147 | const nextChildren = [ 148 | h('p', { key: 'D' }, 'D'), 149 | h('p', { key: 'E' }, 'E'), 150 | h('p', { key: 'B' }, 'B'), 151 | h('p', { key: 'C' }, 'C'), 152 | ] 153 | ``` 154 | 155 | - 旧节点:A B C 156 | - 新节点:D E B C 157 | 158 | - 现在我们再对比头部就会出现问题,因为第一个就不一样,所以`i` 就会停下了 159 | 160 | - 那么我们除了对比头部,还要对比尾部 161 | 162 | #### 实现 163 | 164 | ```ts 165 | function patchKeyedChildren(c1, c2, container, parentInstance) { 166 | // 声明三个指针,e1 是老节点最后一个元素,e2 是新节点最后一个元素,i 是当前对比的元素 167 | // 目前我们用来两个节点是否一样暂时只用 type 和 key 168 | // 首先我们要从新旧节点头部开始对比 169 | // 对比完了头部,我们还要对比尾部 170 | while (i <= e1 && i <= e2) { 171 | // 尾部这里就是对比尾部的了,所以是 n1[e1] 和 n1[e2] 172 | if (isSameVNode(c1[e1], c2[e2])) { 173 | patch(c1[e1], c2[e2], container, parentInstance) 174 | } else { 175 | break 176 | } 177 | // 这里也变成了 e1 -= 1, e2 -= 1 178 | e1 -= 1 179 | e2 -= 1 180 | } 181 | } 182 | ``` 183 | 184 | 我们来看看动画吧! 185 | 186 | ![diff-02](https://raw.githubusercontent.com/zx-projects/mini-vue-docs/main/images/diff/vue-diff-02-3.gif) 187 | 188 | ### 3.3 新节点比旧节点长,添加新节点 - 尾部 189 | 190 | #### 例子 191 | 192 | ```ts 193 | const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')] 194 | const nextChildren = [ 195 | h('p', { key: 'A' }, 'A'), 196 | h('p', { key: 'B' }, 'B'), 197 | h('p', { key: 'C' }, 'C'), 198 | h('p', { key: 'D' }, 'D'), 199 | ] 200 | ``` 201 | 202 | - 旧节点:A B 203 | - 新节点:A B C D 204 | - 新节点比旧节点长,此时我们需要先进行对比 205 | - 首先对比头部,最终 `i` 停留在 `2`也就是对比完`B` 206 | - 然后进行尾部对比,因为第一个尾部就不相同 `D !== B`,所以 `e1` 和 `e2` 没变 207 | 208 | #### 实现 209 | 210 | 最终我们进行了两轮对比后,发现这种情况下,其实是这样的: 211 | 212 | ```ts 213 | i > e1 && i <= e2 214 | ``` 215 | 216 | 所以就在对比的最后面: 217 | 218 | ```ts 219 | if (i > e1) { 220 | if (i <= e2) { 221 | while (i <= e2) { 222 | patch(null, c2[i], container, parentInstance) 223 | i += 1 224 | } 225 | } 226 | } 227 | ``` 228 | 229 | ### 3.3 新节点比旧节点长,添加新节点 - 头部 230 | 231 | #### 例子 232 | 233 | ```ts 234 | const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')] 235 | const nextChildren = [ 236 | h('p', { key: 'C' }, 'C'), 237 | h('p', { key: 'A' }, 'A'), 238 | h('p', { key: 'B' }, 'B'), 239 | ] 240 | ``` 241 | 242 | - 旧节点:A B 243 | - 新节点:C A B 244 | - 新节点比旧节点长,此时我们需要先进行对比 245 | - 首先对比头部,最终 `i` 停留在 `0`也就是对比完`A`和`C` 246 | - 然后进行尾部对比,最终 `e1 = -1; e2 = 0` 247 | 248 | #### 实现 249 | 250 | 最终进行两轮对比后,发现在这种情况下,其实是这样的: 251 | 252 | ```ts 253 | i > e1 && i <= e2 254 | ``` 255 | 256 | 此时我们直接使用 `patch` 就不对了,这是因为我们所写的逻辑都是 `$el.append()`,是直接追加的,所以像这种情况下我们不要直接追加而是需要加在最前面,这个时候我们可以改写我们的 DOM API 257 | 258 | ```ts 259 | // runtime-dom/index 260 | 261 | // 将 parent.append 修改为 insertBefore,这样就可以传入一个锚点 262 | // 将会在这个锚点之前插入元素 263 | // 如果这个锚点是 null,那么将会和 append 的行为一样 264 | function insert(el, parent, anchor) { 265 | parent.insertBefore(el, anchor || null) 266 | } 267 | ``` 268 | 269 | ```ts 270 | if (i > e1) { 271 | if (i <= e2) { 272 | // nextPos 就是需要追加元素的索引 273 | // 如果这个新元素的索引已经超过了新节点的长度,那么说明是追加到尾部 274 | // anchor = null,如果没有超过新节点的长度,那么就是插入到某个位置 275 | // 此时 anchor = c2[nextPos].el,也就是这个新加元素的下一个元素 276 | const nextPos = e2 + 1 277 | const anchor = nextPos < l2 ? c2[nextPos].el : null 278 | while (i <= e2) { 279 | patch(null, c2[i], container, parentInstance) 280 | i += 1 281 | } 282 | } 283 | } 284 | ``` 285 | 286 | 因为我们新添加了一个 `anchor` 参数,所以需要给函数们加入这个参数。 287 | 288 | 此时,添加新节点到头部的功能也已经实现了 289 | 290 | ### 3.4 新节点比旧节点短,删除旧节点 - 尾部 291 | 292 | #### 例子 293 | 294 | ```ts 295 | const prevChildren = [ 296 | h("p", { key: "A" }, "A"), 297 | h("p", { key: "B" }, "B"), 298 | h("p", { key: "C" }, "C"), 299 | ]; 300 | const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 301 | ``` 302 | 303 | - 旧节点:A B C 304 | - 新节点:A B 305 | - 新节点比旧节点短,此时我们需要先进行对比 306 | - 首先对比头部,最终 `i` 停留在 `2` 也就是对比完`B`和`B`,此时 i 已经 > e2,停止比对头部 307 | - 然后进行尾部对比,最终 `e1 = 2; e2 = 1` 308 | 309 | #### 实现 310 | 311 | 通过这个例子对比,我们可以看出来: 312 | 313 | ```ts 314 | i > e2 315 | ``` 316 | 317 | 我们可以在对比后再次添加一个判断 318 | 319 | ```ts 320 | function patchKeyedChildren(c1, c2, container, parentInstance, anchor) { 321 | // 对比头部和尾部 322 | if (i > e1) { 323 | // 新增 324 | } else if (i > e2) { 325 | // 删除 326 | while (i <= e1) { 327 | hostRemove(c1[i].el) 328 | i += 1 329 | } 330 | } 331 | } 332 | ``` 333 | 334 | ### 3.5 新节点比旧节点短,删除旧节点 - 头部 335 | 336 | #### 例子 337 | 338 | ```ts 339 | const prevChildren = [ 340 | h('p', { key: 'A' }, 'A'), 341 | h('p', { key: 'B' }, 'B'), 342 | h('p', { key: 'C' }, 'C'), 343 | ] 344 | const nextChildren = [h('p', { key: 'B' }, 'B'), h('p', { key: 'C' }, 'C')] 345 | ``` 346 | 347 | - 旧节点:A B C 348 | - 新节点:B C 349 | - 新节点比旧节点短,此时我们需要先进行对比 350 | - 首先对比头部,最终 `i` 停留在 `0` 因为新旧的第一个元素就不一样 351 | - 然后进行尾部对比,最终 `e1 = 0; e2 = -1` 352 | 353 | #### 实现 354 | 355 | 通过这个例子对比,我们可以看出来: 356 | 357 | ```ts 358 | i > e2 359 | ``` 360 | 361 | 这个时候我们发现和头部的判断是一样的,就可以复用头部的逻辑了 -------------------------------------------------------------------------------- /docs/46. codegen 生成联合 3 种类型.md: -------------------------------------------------------------------------------- 1 | # codegen 生成联合 3 种类型 2 | 3 | 在本小节中,我们将会实现 codegen 生成联合 3 种类型的 code 4 | 5 | ```html 6 |
hi,{{message}}
7 | ``` 8 | 9 | ```ts 10 | // 生成为 11 | const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue 12 | export function render(_ctx, _cache) { return _createElementVNode( 13 | 'div', null, 'hi,' + _toDisplayString(_ctx.message)) } 14 | ``` 15 | 16 | ## 1. 测试样例 17 | 18 | ```ts 19 | test('union 3 type', () => { 20 | const template = '
hi,{{message}}
' 21 | const ast = baseParse(template) 22 | transform(ast) 23 | const code = codegen(ast) 24 | expect(code).toMatchSnapshot() 25 | }) 26 | ``` 27 | 28 | ## 2. 实现 29 | 30 | 此时我们直接生成快照,发现是有问题的,这是因为我们在 `genElement` 的时候没有考虑到 children 31 | 32 | ```ts 33 | function genElement(node, context) { 34 | const { push, helper } = context 35 | const { tag } = node 36 | push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`) 37 | // 加入对 children 的处理 38 | const { children } = node 39 | if (children.length) { 40 | push(', null, ') 41 | for (let i = 0; i < children.length; i++) { 42 | genNode(children[i], context) 43 | } 44 | } 45 | push(')') 46 | } 47 | ``` 48 | 49 | 此时来看看我们的快照: 50 | 51 | ``` 52 | const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue 53 | export function render(_ctx, _cache) { return _createElementVNode('div', null, 'hi,'_toDisplayString(_ctx.message)) } 54 | ``` 55 | 56 | 此时我们发现一个问题,那就是没有加号。所以我们可以再创建一个类型:`compound` 复合类型: 57 | 58 | - 如果 text 、interpolation 相邻在一起,那么相邻在一起的就是 compound 类型 59 | 60 | ### 2.1 复合类型处理 61 | 62 | 首先,加入一种类型: 63 | 64 | ```ts 65 | export const enum NodeType { 66 | INTERPOLATION, 67 | SIMPLE_EXPRESSION, 68 | ELEMENT, 69 | TEXT, 70 | ROOT, 71 | // 加入复合类型 72 | COMPOUND_EXPRESSION, 73 | } 74 | ``` 75 | 76 | 此时我们需要去增加一个 `transformText` 77 | 78 | ```ts 79 | import { NodeType } from '../ast' 80 | 81 | export function transformText(node) { 82 | const { children } = node 83 | if (children.length) { 84 | let currentContainer 85 | for (let i = 0; i < children.length; i++) { 86 | const child = children[i] 87 | if (isText(child)) { 88 | for (let j = i + 1; j < children.length; j++) { 89 | const next = children[j] 90 | if (isText(next)) { 91 | // 相邻的是 text 或者 interpolation,那么就变成联合类型 92 | if (!currentContainer) { 93 | currentContainer = children[i] = { 94 | type: NodeType.COMPOUND_EXPRESSION, 95 | children: [child], 96 | } 97 | } 98 | // 在每个相邻的下一个之前加上一个 + 99 | currentContainer.children.push(' + ') 100 | currentContainer.children.push(next) 101 | // 遇到就删除 102 | children.splice(j, 1) 103 | // 修正索引,因为我们下一个循环就又 + 1 了。此时索引就不对了 104 | j -= 1 105 | } else { 106 | // 如果下一个不是 text 的了,那么就重置,并跳出循环 107 | currentContainer = undefined 108 | break 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | function isText(node) { 117 | return node.type === NodeType.TEXT || node.type === NodeType.INTERPOLATION 118 | } 119 | ``` 120 | 121 | 然后在测试中加入这个 plugin 122 | 123 | ```ts 124 | test('union 3 type', () => { 125 | const template = '
hi,{{message}}
' 126 | const ast = baseParse(template) 127 | transform(ast, { 128 | // 加入 transformText plugin 129 | nodeTransforms: [transformElement, transformExpression, transformText], 130 | }) 131 | const code = codegen(ast) 132 | expect(code).toMatchSnapshot() 133 | }) 134 | ``` 135 | 136 | 然后我们就可以在 codegen 阶段加入对 COMPOUD 类型的处理 137 | 138 | ```ts 139 | function genNode(node, context) { 140 | switch (node.type) { 141 | case NodeType.TEXT: 142 | genText(node, context) 143 | break 144 | case NodeType.INTERPOLATION: 145 | genInterpolation(node, context) 146 | break 147 | case NodeType.SIMPLE_EXPRESSION: 148 | genExpression(node, context) 149 | break 150 | case NodeType.ELEMENT: 151 | genElement(node, context) 152 | break 153 | // 加入对 compound 类型的处理 154 | case NodeType.COMPOUND_EXPRESSION: 155 | genCompoundExpression(node, context) 156 | } 157 | } 158 | 159 | function genCompoundExpression(node, context) { 160 | const { children } = node 161 | const { push } = context 162 | // 对 children 进行遍历 163 | for (let i = 0; i < children.length; i++) { 164 | const child = children[i] 165 | // 如果是 string,也就是我们手动添加的 + 166 | if (isString(child)) { 167 | // 直接 push 168 | push(child) 169 | } else { 170 | // 否则还是走 genNode 171 | genNode(child, context) 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | 此时我们就可以生成了。 178 | 179 | ### 2.2 优化 genElement 180 | 181 | 此时我们回过头来看看 `genElement` 我们发现是有问题的: 182 | 183 | ```ts 184 | function genElement(node, context) { 185 | // 我们这里写死了 props 给 null,以及直接用 tag 186 | const { push, helper } = context 187 | const { tag } = node 188 | push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`) 189 | const { children } = node 190 | if (children.length) { 191 | push(', null, ') 192 | for (let i = 0; i < children.length; i++) { 193 | genNode(children[i], context) 194 | } 195 | } 196 | push(')') 197 | } 198 | ``` 199 | 200 | 我们需要一个兼容层来处理 props 和 tag。在哪里处理呢?就是在 `transformElement` 的地方进行处理 201 | 202 | ```ts 203 | export function transformElement(node, context) { 204 | if (node.type === NodeType.ELEMENT) { 205 | context.helper(CREATE_ELEMENT_VNODE) 206 | // 中间处理层,处理 props 和 tag 207 | const vnodeTag = node.tag 208 | const vnodeProps = node.props 209 | 210 | const { children } = node 211 | let vnodeChildren = children 212 | 213 | const vnodeElement = { 214 | type: NodeType.ELEMENT, 215 | tag: vnodeTag, 216 | props: vnodeProps, 217 | children: vnodeChildren, 218 | } 219 | 220 | node.codegenNode = vnodeElement 221 | } 222 | } 223 | ``` 224 | 225 | ```ts 226 | function createRootCodegen(root) { 227 | const child = root.children[0] 228 | // 在这里进行判断,如果说 children[0] 的类型是 ELEMENT,那么直接修改为 child.codegenNode 229 | if (child.type === NodeType.ELEMENT) { 230 | root.codegenNode = child.codegenNode 231 | } else { 232 | root.codegenNode = root.children[0] 233 | } 234 | } 235 | ``` 236 | 237 | 然后就可以修改 `genElement` 238 | 239 | ```ts 240 | function genElement(node, context) { 241 | const { push, helper } = context 242 | const { tag, props } = node 243 | push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`) 244 | const { children } = node 245 | if (children.length) { 246 | // 这里的 props 就可以是活的了 247 | push(`, ${props}, `) 248 | for (let i = 0; i < children.length; i++) { 249 | genNode(children[i], context) 250 | } 251 | } 252 | push(')') 253 | } 254 | ``` 255 | 256 | ### 2.3 优化插件执行顺序 257 | 258 | 现在我们又发现了一个问题,虽然我们仍然引入了 `transformExpression` 插件,但是我们的 interpolation 还是没有 `_ctx`。这是因为我们修改了结构,此时第二层结构还存在一个 `COMPOUND_EXPRESSION`。那么我们就需要让 `transformExpression` 最先执行,他执行完毕后,再去执行其他的插件。也就是优化插件的执行顺序。 259 | 260 | 我们设计是这样的: 261 | 262 | - 如果一个插件执行返回的是一个函数,那么表示该插件会是一个退出执行函数 263 | - 在最后,会执行所有的退出函数 264 | 265 | ```ts 266 | function traverseNode(node, context) { 267 | const { nodeTransforms } = context 268 | const exitFns: any[] = [] 269 | for (let i = 0; i < nodeTransforms.length; i++) { 270 | const transform = nodeTransforms[i] 271 | const exitFn = transform(node, context) 272 | // 收集退出函数 273 | if (exitFn) exitFns.push(exitFn) 274 | } 275 | switch (node.type) { 276 | case NodeType.INTERPOLATION: 277 | context.helper(TO_DISPLAY_STRING) 278 | break 279 | case NodeType.ROOT: 280 | case NodeType.ELEMENT: 281 | traverseChildren(node, context) 282 | break 283 | default: 284 | break 285 | } 286 | let i = exitFns.length 287 | // 执行所有的退出函数 288 | while (i--) { 289 | exitFns[i]() 290 | } 291 | } 292 | ``` 293 | 294 | ### 2.4 优化空值 295 | 296 | ```ts 297 | function genElement(node, context) { 298 | const { push, helper } = context 299 | const { tag, props } = node 300 | push(`${helper(CREATE_ELEMENT_VNODE)}(`) 301 | const { children } = node 302 | // 在这里批量处理 tag,props 和 children,优化空值情况 303 | genNodeList(genNullable([tag, props, children]), context) 304 | push(')') 305 | } 306 | 307 | function genNodeList(nodes, context) { 308 | const { push } = context 309 | for (let i = 0; i < nodes.length; i++) { 310 | const node = nodes[i] 311 | if (isString(node)) { 312 | push(node) 313 | } else if (isArray(node)) { 314 | for (let j = 0; j < node.length; j++) { 315 | const n = node[j] 316 | genNode(n, context) 317 | } 318 | } else { 319 | genNode(node, context) 320 | } 321 | if (i < nodes.length - 1) { 322 | push(', ') 323 | } 324 | } 325 | } 326 | 327 | function genNullable(args) { 328 | return args.map(arg => arg || 'null') 329 | } 330 | ``` 331 | 332 | ## 3. 重构 333 | 334 | ### 3.1 抽离 vnode 335 | 336 | ```ts 337 | export function transformElement(node, context) { 338 | return () => { 339 | if (node.type === NodeType.ELEMENT) { 340 | context.helper(CREATE_ELEMENT_VNODE) 341 | const vnodeTag = `'${node.tag}'` 342 | const vnodeProps = node.props 343 | 344 | const { children } = node 345 | let vnodeChildren = children 346 | // 这里可以抽离 347 | const vnodeElement = { 348 | type: NodeType.ELEMENT, 349 | tag: vnodeTag, 350 | props: vnodeProps, 351 | children: vnodeChildren, 352 | } 353 | 354 | node.codegenNode = vnodeElement 355 | } 356 | } 357 | } 358 | ``` 359 | 360 | ```ts 361 | export function transformElement(node, context) { 362 | if (node.type === NodeType.ELEMENT) { 363 | return () => { 364 | // 中间处理层,处理 props 和 tag 365 | const vnodeTag = `'${node.tag}'` 366 | const vnodeProps = node.props 367 | 368 | const { children } = node 369 | const vnodeChildren = children 370 | // 抽离函数 371 | node.codegenNode = createVNodeCall( 372 | context, 373 | vnodeTag, 374 | vnodeProps, 375 | vnodeChildren 376 | ) 377 | } 378 | } 379 | } 380 | 381 | // ast.ts 382 | 383 | export function createVNodeCall(context, tag, props, children) { 384 | context.helper(CREATE_ELEMENT_VNODE) 385 | return { 386 | type: NodeType.ELEMENT, 387 | tag, 388 | props, 389 | children, 390 | } 391 | } 392 | ``` 393 | 394 | ### 3.2 抽离 isText 395 | 396 | 还可以将 `transformText` 中的 isText 抽离到 `utils.ts` 中。 397 | 398 | --------------------------------------------------------------------------------