├── .gitignore ├── README.md ├── babel.config.js ├── example ├── apiInject │ ├── app.js │ ├── index.html │ └── main.js ├── componentEmit │ ├── Foo.js │ ├── app.js │ ├── index.html │ └── main.js ├── componentSlots │ ├── Foo.js │ ├── app.js │ ├── index.html │ └── main.js ├── helloWorld │ ├── Foo.js │ ├── app.js │ ├── index.html │ └── main.js ├── nextTicker │ ├── App.js │ ├── index.html │ └── main.js ├── updateChildren │ ├── app.js │ ├── arrayToArray.js │ ├── arrayToText.js │ ├── index.html │ ├── main.js │ ├── textToArray.js │ └── textToText.js ├── updateComponent │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js └── updateElement │ ├── app.js │ ├── index.html │ └── main.js ├── jest.config.ts ├── lib ├── guide-toy-vue3.cjs.js ├── guide-toy-vue3.cjs.js.map ├── guide-toy-vue3.esm.js └── guide-toy-vue3.esm.js.map ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── index.ts ├── reactivity │ ├── baseHandlers.ts │ ├── computed.ts │ ├── docs │ │ ├── 1.summary.md │ │ ├── 2.summary.md │ │ ├── 3.summary.md │ │ ├── 4.summary.md │ │ └── 5.summary.md │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── test │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ ├── shallowReactive.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── apiCreateApp.ts │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentRenderUtils.ts │ ├── componentSlots.ts │ ├── docs │ │ ├── 1.summary.md │ │ ├── 2.summary.md │ │ └── 3.summary.md │ ├── h.ts │ ├── helper │ │ └── renderSlot.ts │ ├── index.ts │ ├── renderer.ts │ ├── scheduler.ts │ └── vnode.ts ├── runtime-dom │ ├── docs │ │ ├── 1.summary.md │ │ ├── 2.summary.md │ │ └── 3.summary.md │ └── index.ts └── shared │ ├── index.ts │ └── shapeFlags.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toy-Vue3.x 2 | 3 | 手写 vue3 核心源码,理解其原理 by myself 4 | 5 | ## 🙌 目的 6 | 7 | 读懂 Vue3 源码可能是 2022 年要进大厂的必经之路了。 8 | 9 | 但是直接阅读源码的难度非常大,因为除了核心逻辑以外框架本身还要处理很多 edge case(边缘情况) 、错误处理、热更新等一系列工程问题。 10 | 11 | 为了学习源码,提高自己对 vue3 的理解,能在开发中规避一些不必要的 bug 以及迅速给出项目的优化方案,因此创建这个 repo。 12 | 13 | ## ✏ 吸取知识就要输出文章 14 | 15 | 掘金专栏 16 | 17 | [Vue3 核心原理代码解构](https://juejin.cn/user/493015847938664/columns) 18 | 19 | ## 🛠 功能清单 20 | reactivity部分 21 | 22 | - [x] 实现 effect & reactive 依赖收集和依赖触发 23 | - [x] 实现 effect 返回 runner 24 | - [x] 实现 effect 的 scheduler 功能 25 | - [x] 实现 effect 的 stop 功能 26 | - [x] 优化 stop 功能 27 | - [x] 实现 readonly 功能 28 | - [x] 实现 isReactive 和 isReadonly 功能 29 | - [x] readonly 和 reactive 嵌套对象功能 30 | - [x] 实现 shallowReadonly 功能 31 | - [x] 实现 shallowReactive 功能 32 | - [x] 实现 isProxy 功能 33 | - [x] 实现 isShallow 功能 34 | - [x] 实现 ref 功能 35 | - [x] 实现 isRef 和 unRef 功能 36 | - [x] 实现 proxyRefs 功能 37 | - [x] 实现 computed 计算属性功能 38 | 39 | 40 | runtime-core部分 41 | 42 | - [x] 实现初始化 component 主流程 43 | - [x] 实现初始化 element 主流程 (通过递归patch拆箱操作,最终都会走向mountElement这一步) 44 | - [x] 实现组件代理对象 (instance.proxy解决`render()`函数的this指向问题) 45 | - [x] 实现 shapeFlags (利用位运算 左移运算 对vnode添加标识,标识是什么类型:子级文本,子级数组,组件,HTML元素) 46 | - [x] 实现注册事件功能 (通过在vnode.props识别 props对象的key是以on开头并且后一个字母是大写来判断是否是事件) 47 | - [x] 实现组件 props 功能 (在render的h函数中可以用this访问到,并且是shallowReadonly) 48 | - [x] 实现组件 emit 功能 (获取组件的props并判断props的'on+事件名'是否是emit的第一个参数:事件名匹配,是的话就执行props的里面的事件) 49 | - [x] 实现组件 slots 功能 50 | - [x] 实现 Fragment 和 Text 类型节点 51 | - [x] 实现 getCurrentInstance 52 | - [x] 实现 provide-inject 功能 53 | - [X] 实现自定义渲染器 custom renderer 54 | - [X] 更新 element 流程搭建 55 | - [X] 更新 element 的 props 56 | - [X] 更新 element 的 children 57 | - [X] 更新 element 的双端对比 diff 算法 58 | - [X] 实现组件更新功能 59 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /example/apiInject/app.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export const Provider = { 4 | name: 'Provider', 5 | setup(){ 6 | 7 | provide('foo', 'fooVal') 8 | provide('bar', 'barVal') 9 | }, 10 | render(){ 11 | return h('div', {}, [ 12 | h('p', {}, 'Provider'), 13 | h(ProviderTwo) 14 | ]) 15 | } 16 | } 17 | const ProviderTwo = { 18 | name: 'ProviderTwo', 19 | setup(){ 20 | provide('foo', 'fooTwo') 21 | provide('bar', 'barTwo') 22 | // 期望得到provider的foo---fooVal,实际上得到的是fooTwo 23 | const foo = inject('foo') 24 | const bar = inject('bar') 25 | return { 26 | foo, 27 | bar 28 | } 29 | }, 30 | render(){ 31 | return h('div', {}, [ 32 | h('p', {}, `ProviderTwo-${this.foo}-${this.bar}`), 33 | h(Consumer) 34 | ]) 35 | } 36 | } 37 | const Consumer = { 38 | name: 'Consumer', 39 | setup(){ 40 | const fooVal = inject('foo') 41 | const barVal = inject('bar') 42 | return { 43 | fooVal, 44 | barVal 45 | } 46 | }, 47 | render(){ 48 | return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 测试 8 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/apiInject/main.js: -------------------------------------------------------------------------------- 1 | import {Provider} from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(Provider).mount(rootContainer) 10 | 11 | -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export default { 4 | name: 'Foo', 5 | render() { 6 | 7 | return h('div', {}, [ 8 | h('button', { 9 | onClick: this.onAdd 10 | }, '触发emit') 11 | ]) 12 | }, 13 | setup(props, {emit}) { 14 | // 1. 传入count 15 | console.log(props) 16 | // 3. shallow readonly 17 | props.count++ 18 | console.log(props, props.count) 19 | function onAdd(){ 20 | console.log('onAdd') 21 | emit('emitFooAddEvent', props.count) 22 | } 23 | 24 | return { 25 | onAdd 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/componentEmit/app.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/guide-toy-vue3.esm.js' 2 | import Foo from './Foo.js' 3 | 4 | 5 | window.self = null 6 | export default { 7 | name: 'App', 8 | render() { 9 | // 为什么这里不直接写window.self = this.$el呢?因为h还没执行,元素还没mount上document,借助引用传递拿到值 10 | window.self = this 11 | return h('div', { 12 | id: 'root', 13 | class: ['flex', 'container-r'], 14 | onClick(){ 15 | console.log('click event!') 16 | }, 17 | onMouseDown(){ 18 | console.log('mouse down!') 19 | } 20 | }, [ 21 | h('p', {class: 'red'}, 'red'), 22 | h('p', {class: 'blue'}, 'blue'), 23 | // 在Foo的props中寻找有没有on + emitFooAddEvent这个函数,有就执行 24 | h(Foo, { 25 | count: 1, 26 | onEmitFooAddEvent: this.takeEmitEvent 27 | }, '') 28 | ]) 29 | // this指向通过proxy 30 | return h('div', { 31 | id: 'root', 32 | class: ['flex', 'container'] 33 | }, this.name) 34 | }, 35 | setup() { 36 | function takeEmitEvent(count){ 37 | console.log('app take in count number:', count) 38 | } 39 | // 返回对象或者h()渲染函数 40 | return { 41 | name: 'hi my app', 42 | takeEmitEvent 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 测试 8 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import App from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(App).mount(rootContainer) 10 | -------------------------------------------------------------------------------- /example/componentSlots/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | renderSlot, 4 | createTextVNode 5 | } from '../../lib/guide-toy-vue3.esm.js' 6 | 7 | export default { 8 | name: 'Foo', 9 | render() { 10 | console.log('Foo--->', this.$slots) 11 | const foo = h('p', {}, '原本就在Foo里面的元素') 12 | return h('div', {}, [renderSlot(this.$slots, 'header', {age: 18}), foo, renderSlot(this.$slots, 'footer')]) 13 | // return h('div', {}, [renderSlot(this.$slots, 'default'), foo]) 14 | // return h('div', {}, [foo, renderSlot(this.$slots, 'default'), createTextVNode('aaa')]) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/componentSlots/app.js: -------------------------------------------------------------------------------- 1 | import { 2 | h 3 | } from '../../lib/guide-toy-vue3.esm.js' 4 | import Foo from './Foo.js' 5 | 6 | 7 | export default { 8 | name: 'App', 9 | render() { 10 | return h('div', { 11 | id: 'root', 12 | class: ['flex', 'container-r'], 13 | }, [ 14 | h('p', { 15 | class: 'red' 16 | }, 'red'), 17 | h('p', { 18 | class: 'blue' 19 | }, this.name), 20 | // h(Foo, {}, [h('p', {}, '我是slot1'), h('p', {}, '我是slot1')]) 21 | // h(Foo, {}, { 22 | // default: () => h('p', {}, '我是slot1') 23 | // }) 24 | // 具名插槽 25 | // h(Foo, {}, { 26 | // header: h('p', {}, '我是header slot1'), 27 | // footer: h('p', {}, '我是footer slot1') 28 | // }) 29 | // 作用域插槽 30 | h(Foo, {}, { 31 | header: ({age})=>h('p', {}, '我是header slot1' + age), 32 | footer: ()=>h('p', {}, '我是footer slot1') 33 | }) 34 | ]) 35 | }, 36 | setup() { 37 | // 返回对象或者h()渲染函数 38 | return { 39 | name: 'hi my app', 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 测试 8 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | import App from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(App).mount(rootContainer) 10 | -------------------------------------------------------------------------------- /example/helloWorld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export default { 4 | name: 'Foo', 5 | render() { 6 | // 2. 能在render中通过this访问到props 7 | return h('div', {}, 'foo: ' + this.count) 8 | }, 9 | setup(props) { 10 | // 1. 传入count 11 | console.log(props) 12 | // 3. shallow readonly 13 | props.count++ 14 | console.log(props) 15 | console.log('', getCurrentInstance()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/helloWorld/app.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js' 2 | import Foo from './Foo.js' 3 | 4 | 5 | window.self = null 6 | export default { 7 | name: 'App', 8 | render() { 9 | // 为什么这里不直接写window.self = this.$el呢?因为h还没执行,元素还没mount上document,借助引用传递拿到值 10 | window.self = this 11 | return h('div', { 12 | id: 'root', 13 | class: ['flex', 'container-r'], 14 | onClick(){ 15 | console.log('click event!') 16 | }, 17 | onMouseDown(){ 18 | console.log('mouse down!') 19 | } 20 | }, [ 21 | h('p', {class: 'red'}, 'red'), 22 | h('p', {class: 'blue'}, 'blue'), 23 | // 引入Foo组件 24 | h(Foo, { 25 | count: 1 26 | }, '') 27 | ]) 28 | // this指向通过proxy 29 | // return h('div', { 30 | // id: 'root', 31 | // class: ['flex', 'container'] 32 | // }, this.name) 33 | }, 34 | setup() { 35 | console.log(getCurrentInstance()); 36 | // 返回对象或者h()渲染函数 37 | return { 38 | name: 'hi my app' 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/helloWorld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 测试 8 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/helloWorld/main.js: -------------------------------------------------------------------------------- 1 | import App from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(App).mount(rootContainer) 10 | -------------------------------------------------------------------------------- /example/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | 2 | import { ref, h,getCurrentInstance } from '../../lib/guide-toy-vue3.esm.js' 3 | export const App = { 4 | name: 'app', 5 | setup() { 6 | const instance = getCurrentInstance() 7 | const count = ref(0) 8 | const changeCount = () => { 9 | 10 | for(let i = 0;i<100;i++){ 11 | console.log('update count'); 12 | count.value = i 13 | } 14 | console.log('当前实例',instance) 15 | } 16 | return { 17 | changeCount, 18 | count 19 | } 20 | }, 21 | render() { 22 | console.log(this); 23 | return h('div', {}, [ 24 | 25 | h('button', {onClick: this.changeCount}, 'update'), 26 | h('p', {}, 'count:' + this.count) 27 | ]) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /example/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 更新组件 9 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | 5 | const rootContainer = document.querySelector('#app') 6 | 7 | createApp(App).mount(rootContainer) 8 | -------------------------------------------------------------------------------- /example/updateChildren/app.js: -------------------------------------------------------------------------------- 1 | import { ref, h, reactive } from '../../lib/guide-toy-vue3.esm.js' 2 | import textToText from './textToText.js' 3 | import textToArray from './textToArray.js' 4 | import arrayToText from './arrayToText.js' 5 | import arrayToArray from './arrayToArray.js' 6 | export const App = { 7 | name: 'app', 8 | setup() {}, 9 | render() { 10 | 11 | // this.count进行依赖收集, 12 | return h('div', {}, [ 13 | h('p',{},'主页'), 14 | // 旧的是文本 新的是文本 15 | // h(textToText), 16 | // 旧的是文本 新的是数组 17 | // h(textToArray), 18 | // 旧的是数组 新的是文本 19 | // h(arrayToText), 20 | // 旧的是数组 新的也是数组 21 | h(arrayToArray), 22 | ]) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /example/updateChildren/arrayToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | // 1. 左侧的对比 4 | // (a b) c 5 | // (a b) d e 6 | // const prevChildren = [ 7 | // h('p', { key: 'A' }, 'A'), 8 | // h('p', { key: 'B' }, 'B'), 9 | // h('p', { key: 'C' }, 'C'), 10 | // ] 11 | // const nextChildren = [ 12 | // h('p', { key: 'A' }, 'A'), 13 | // h('p', { key: 'B' }, 'B'), 14 | // h('p', { key: 'D' }, 'D'), 15 | // h('p', { key: 'E' }, 'E'), 16 | // ] 17 | 18 | // 2. 右侧的对比 19 | // a (b c) 20 | // d e (b c) 21 | // const prevChildren = [ 22 | // h("p", { key: "A" }, "A"), 23 | // h("p", { key: "B" }, "B"), 24 | // h("p", { key: "C" }, "C"), 25 | // ]; 26 | // const nextChildren = [ 27 | // h("p", { key: "D" }, "D"), 28 | // h("p", { key: "E" }, "E"), 29 | // h("p", { key: "B" }, "B"), 30 | // h("p", { key: "C" }, "C"), 31 | // ]; 32 | 33 | // 3. 新的比老的长 34 | // 创建新的 35 | // 左侧 36 | // (a b) 37 | // (a b) c 38 | // i = 2, e1 = 1, e2 = 2 39 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 40 | // const nextChildren = [ 41 | // h("p", { key: "A" }, "A"), 42 | // h("p", { key: "B" }, "B"), 43 | // h("p", { key: "C" }, "C"), 44 | // h("p", { key: "D" }, "D"), 45 | // ]; 46 | 47 | // 右侧 48 | // (a b) 49 | // d c (a b) 50 | // i = 0, e1 = -1, e2 = 0 51 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 52 | // const nextChildren = [ 53 | // h("p", { key: "D" }, "D"), 54 | // h("p", { key: "C" }, "C"), 55 | // h("p", { key: "A" }, "A"), 56 | // h("p", { key: "B" }, "B"), 57 | // ]; 58 | 59 | // 4. 老的比新的长 60 | // 删除老的 61 | // 左侧 62 | // (a b) c 63 | // (a b) 64 | // i = 2, e1 = 2, e2 = 1 65 | // const prevChildren = [ 66 | // h("p", { key: "A" }, "A"), 67 | // h("p", { key: "B" }, "B"), 68 | // h("p", { key: "C" }, "C"), 69 | // ]; 70 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 71 | 72 | // 右侧 73 | // a (b c) 74 | // (b c) 75 | // i = 0, e1 = 0, e2 = -1 76 | 77 | // const prevChildren = [ 78 | // h("p", { key: "A" }, "A"), 79 | // h("p", { key: "B" }, "B"), 80 | // h("p", { key: "C" }, "C"), 81 | // ]; 82 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; 83 | 84 | // 5. 对比中间的部分 85 | // 删除老的 (在老的里面存在,新的里面不存在) 86 | // 5.1 87 | // a,b,(c,d),f,g 88 | // a,b,(e,c),f,g 89 | // D 节点在新的里面是没有的 - 需要删除掉 90 | // C 节点 props 也发生了变化 91 | 92 | // const prevChildren = [ 93 | // h("p", { key: "A" }, "A"), 94 | // h("p", { key: "B" }, "B"), 95 | // h("p", { key: "C", id: "c-prev" }, "C"), 96 | // h("p", { key: "D" }, "D"), 97 | // h("p", { key: "F" }, "F"), 98 | // h("p", { key: "G" }, "G"), 99 | // ]; 100 | 101 | // const nextChildren = [ 102 | // h("p", { key: "A" }, "A"), 103 | // h("p", { key: "B" }, "B"), 104 | // h("p", { key: "E" }, "E"), 105 | // h("p", { key: "C", id:"c-next" }, "C"), 106 | // h("p", { key: "F" }, "F"), 107 | // h("p", { key: "G" }, "G"), 108 | // ]; 109 | 110 | // 5.1.1 111 | // a,b,(c,e,d),f,g 112 | // a,b,(e,c),f,g 113 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 114 | // const prevChildren = [ 115 | // h("p", { key: "A" }, "A"), 116 | // h("p", { key: "B" }, "B"), 117 | // h("p", { key: "C", id: "c-prev" }, "C"), 118 | // h("p", { key: "E" }, "E"), 119 | // h("p", { key: "D" }, "D"), 120 | // h("p", { key: "F" }, "F"), 121 | // h("p", { key: "G" }, "G"), 122 | // ]; 123 | 124 | // const nextChildren = [ 125 | // h("p", { key: "A" }, "A"), 126 | // h("p", { key: "B" }, "B"), 127 | // h("p", { key: "E" }, "E"), 128 | // h("p", { key: "C", id:"c-next" }, "C"), 129 | // h("p", { key: "F" }, "F"), 130 | // h("p", { key: "G" }, "G"), 131 | // ]; 132 | 133 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 134 | 135 | // 2.1 136 | // a,b,(c,d,e),f,g 137 | // a,b,(e,c,d),f,g 138 | // 最长子序列: [1,2] 139 | 140 | // const prevChildren = [ 141 | // h("p", { key: "A" }, "A"), 142 | // h("p", { key: "B" }, "B"), 143 | // h("p", { key: "C" }, "C"), 144 | // h("p", { key: "D" }, "D"), 145 | // h("p", { key: "E" }, "E"), 146 | // h("p", { key: "F" }, "F"), 147 | // h("p", { key: "G" }, "G"), 148 | // ]; 149 | 150 | // const nextChildren = [ 151 | // h("p", { key: "A" }, "A"), 152 | // h("p", { key: "B" }, "B"), 153 | // h("p", { key: "E" }, "E"), 154 | // h("p", { key: "C" }, "C"), 155 | // h("p", { key: "D" }, "D"), 156 | // h("p", { key: "F" }, "F"), 157 | // h("p", { key: "G" }, "G"), 158 | // ]; 159 | 160 | // 2.2 161 | // a,b,(c,d,e,z),f,g 162 | // a,b,(d,c,y,e),f,g 163 | // 最长子序列: [1,3] 164 | // y 新增 165 | // e 删除 166 | // d 移动 167 | 168 | const prevChildren = [ 169 | h("p", { key: "A" }, "A"), 170 | h("p", { key: "B" }, "B"), 171 | h("p", { key: "C" }, "C"), 172 | h("p", { key: "D" }, "D"), 173 | h("p", { key: "E" }, "E"), 174 | h("p", { key: "Z" }, "Z"), 175 | h("p", { key: "F" }, "F"), 176 | h("p", { key: "G" }, "G"), 177 | ]; 178 | 179 | const nextChildren = [ 180 | h("p", { key: "A" }, "A"), 181 | h("p", { key: "B" }, "B"), 182 | h("p", { key: "D" }, "D"), 183 | h("p", { key: "C" }, "C"), 184 | h("p", { key: "Y" }, "Y"), 185 | h("p", { key: "E" }, "E"), 186 | h("p", { key: "F" }, "F"), 187 | h("p", { key: "G" }, "G"), 188 | ]; 189 | 190 | // 3. 创建新的节点 191 | // a,b,(c,e),f,g 192 | // a,b,(e,c,d),f,g 193 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 194 | // const prevChildren = [ 195 | // h("p", { key: "A" }, "A"), 196 | // h("p", { key: "B" }, "B"), 197 | // h("p", { key: "C" }, "C"), 198 | // h("p", { key: "E" }, "E"), 199 | // h("p", { key: "F" }, "F"), 200 | // h("p", { key: "G" }, "G"), 201 | // ]; 202 | 203 | // const nextChildren = [ 204 | // h("p", { key: "A" }, "A"), 205 | // h("p", { key: "B" }, "B"), 206 | // h("p", { key: "E" }, "E"), 207 | // h("p", { key: "C" }, "C"), 208 | // h("p", { key: "D" }, "D"), 209 | // h("p", { key: "F" }, "F"), 210 | // h("p", { key: "G" }, "G"), 211 | // ]; 212 | 213 | export default { 214 | name: 'PatchChildren', 215 | setup() { 216 | const isChange = ref(false) 217 | window.isChange = isChange 218 | const test = () => { 219 | isChange.value = !isChange.value 220 | } 221 | return { 222 | test, 223 | isChange, 224 | } 225 | }, 226 | render() { 227 | // return h('div', {}, [ 228 | // h( 229 | // 'button', 230 | // { 231 | // onClick: () => { 232 | // isChange.value = !isChange.value 233 | // }, 234 | // }, 235 | // '测试子组件之间的 patch 逻辑' 236 | // ), 237 | // h('children', {}, isChange.value === true ? nextChildren : prevChildren), 238 | // ]) 239 | return isChange.value 240 | ? h('div', {}, nextChildren) 241 | : h('div', {}, prevChildren) 242 | }, 243 | } 244 | -------------------------------------------------------------------------------- /example/updateChildren/arrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export default { 4 | name: 'arrayToText', 5 | setup() { 6 | const isChange = ref(false) 7 | window.isChange = isChange 8 | return { 9 | isChange, 10 | } 11 | }, 12 | render() { 13 | return this.isChange 14 | ? h('div', {}, 'new text') 15 | : h('div', {}, [h('div', {}, 'A'), h('div', {}, 'B')]) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /example/updateChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 更新子节点 9 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/updateChildren/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(App).mount(rootContainer) 10 | -------------------------------------------------------------------------------- /example/updateChildren/textToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export default { 4 | name: 'textToArray', 5 | setup() { 6 | const isChange = ref(false) 7 | window.isChange = isChange 8 | return { 9 | isChange, 10 | } 11 | }, 12 | render() { 13 | return this.isChange 14 | ? h('div', {}, [h('p', {}, 'p标签'), h('span', {}, 'span标签')]) 15 | : h('div', {}, 'old text') 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /example/updateChildren/textToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/guide-toy-vue3.esm.js' 2 | 3 | export default { 4 | name: 'textToText', 5 | setup(){ 6 | const isChange = ref(false) 7 | window.isChange = isChange 8 | return { 9 | isChange, 10 | } 11 | }, 12 | render(){ 13 | return this.isChange ? h('div', {}, 'new text') : h('div', {}, 'old text') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/updateComponent/App.js: -------------------------------------------------------------------------------- 1 | import Child from './Child.js' 2 | import { ref, h } from '../../lib/guide-toy-vue3.esm.js' 3 | export const App = { 4 | name: 'app', 5 | setup() { 6 | const msg = ref('hello') 7 | const count = ref(0) 8 | const changeMsg = () => { 9 | msg.value = 'world' 10 | } 11 | const changeCount = () => { 12 | count.value++ 13 | } 14 | return { 15 | msg, 16 | count, 17 | changeMsg, 18 | changeCount 19 | } 20 | }, 21 | render() { 22 | console.log(this); 23 | return h('div', {}, [ 24 | h('div', {}, 'hello world'), 25 | h(Child, {msg: this.msg}), 26 | h('button', {onClick: this.changeMsg}, '改变msg'), 27 | h('button', {onClick: this.changeCount}, '改变count,看看有没有触发updateComponent'), 28 | h('p', {}, 'count:' + this.count) 29 | ]) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /example/updateComponent/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-toy-vue3.esm.js" 2 | 3 | export default { 4 | name: 'Child', 5 | setup(){ 6 | return { 7 | 8 | } 9 | }, 10 | render(){ 11 | return h('div',{}, [ 12 | h('div', {}, this.$props.msg) 13 | ]) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/updateComponent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 更新组件 9 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/updateComponent/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | 5 | const rootContainer = document.querySelector('#app') 6 | 7 | createApp(App).mount(rootContainer) 8 | -------------------------------------------------------------------------------- /example/updateElement/app.js: -------------------------------------------------------------------------------- 1 | import { ref, h, reactive } from '../../lib/guide-toy-vue3.esm.js' 2 | export const App = { 3 | name: 'app', 4 | setup() { 5 | const count = ref(0) 6 | let props = ref({ 7 | foo: 'foo', 8 | bar: 'bar', 9 | }) 10 | const click = () => { 11 | // 触发依赖 12 | count.value++ 13 | } 14 | const changePropsDemo1 = () => { 15 | props.value.foo = 'new foo' 16 | } 17 | const changePropsDemo2 = () => { 18 | props.value.foo = undefined 19 | } 20 | const changePropsDemo3 = () => { 21 | props.value = { 22 | foo: 'foo' 23 | } 24 | } 25 | return { 26 | count, 27 | click, 28 | props, 29 | changePropsDemo1, 30 | changePropsDemo2, 31 | changePropsDemo3, 32 | } 33 | }, 34 | render() { 35 | // this.count进行依赖收集, 36 | return h('div', { id: 'update', ...this.props }, [ 37 | h('p', {}, `count: ${this.count}`), 38 | h('button', { onClick: this.click }, '点击更新'), 39 | h('button', { onClick: this.changePropsDemo1 }, '比较props,更新props'), 40 | h( 41 | 'button', 42 | { onClick: this.changePropsDemo2 }, 43 | 'props属性值赋值为null或undefined,应该删除该属性' 44 | ), 45 | h( 46 | 'button', 47 | { onClick: this.changePropsDemo3 }, 48 | '新props属性被删除,也应该删除该属性' 49 | ), 50 | ]) 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /example/updateElement/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 测试 9 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/updateElement/main.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js' 2 | import { createApp } from '../../lib/guide-toy-vue3.esm.js' 3 | 4 | // 流程: 5 | // template -> render() -> 生成vnode —> mountElement -> insert #app 6 | 7 | const rootContainer = document.querySelector('#app') 8 | 9 | createApp(App).mount(rootContainer) 10 | 11 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "C:\\Users\\AaronGuo\\AppData\\Local\\Temp\\jest", 15 | 16 | // Automatically clear mock calls, instances and results before every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "\\\\node_modules\\\\" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state before every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state and implementation before every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: "jsdom", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "\\\\node_modules\\\\" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "\\\\node_modules\\\\", 180 | // "\\.pnp\\.[^\\\\]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghx-mini-vue3", 3 | "version": "1.0.0", 4 | "description": "手写vue3核心逻辑代码", 5 | "main": "lib/guide-toy-vue3-cjs.js", 6 | "module": "lib/guide-toy-vue3-esm.js", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js -w" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+ssh://git@github.com/HongxuanG/mini-vue3.git" 14 | }, 15 | "keywords": [ 16 | "mini-vue3", 17 | "vue3", 18 | "coding" 19 | ], 20 | "author": "HongxuanG", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/HongxuanG/mini-vue3/issues" 24 | }, 25 | "homepage": "https://github.com/HongxuanG/mini-vue3#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.17.9", 28 | "@babel/preset-env": "^7.16.11", 29 | "@babel/preset-typescript": "^7.16.7", 30 | "@rollup/plugin-typescript": "^8.3.2", 31 | "@types/jest": "^27.4.1", 32 | "@types/node": "^17.0.24", 33 | "babel-jest": "^27.5.1", 34 | "jest": "^27.5.1", 35 | "rollup": "^2.72.1", 36 | "ts-node": "^10.7.0", 37 | "tslib": "^2.4.0", 38 | "typescript": "^4.6.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | import pkg from './package.json' 3 | export default { 4 | input: './src/index.ts', 5 | output: [ 6 | { 7 | file: './lib/guide-toy-vue3.cjs.js', 8 | format: 'cjs', 9 | sourcemap: true, 10 | }, 11 | { 12 | file: './lib/guide-toy-vue3.esm.js', 13 | format: 'es', 14 | sourcemap: true, 15 | } 16 | ], 17 | plugins: [typescript()] 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 统一打包入口 2 | export * from './runtime-dom' 3 | export * from './reactivity' 4 | -------------------------------------------------------------------------------- /src/reactivity/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { track, trigger } from './effect' 2 | import { reactive, ReactiveFlags, readonly } from './reactive' 3 | import { extend, isObject } from '../shared' 4 | // 此处调用一次createSetter和getter,为了不在每次使用mutableHandlers的时候重复调用 5 | const get = createGetter() 6 | const set = createSetter() 7 | const readonlyGet = createGetter(true) 8 | const shallowReadonlyGet = createGetter(true, true) 9 | // shallowReactive的get操作 10 | const shallowGet = createGetter(false, true) 11 | // shallowReactive的set操作 12 | const shallowSet = createSetter(true) 13 | 14 | // 高阶函数 15 | export function createGetter( 16 | isReadonly = false, 17 | isShallow = false 18 | ) { 19 | return function get(target: T, key: string | symbol) { 20 | // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的 21 | if (key === ReactiveFlags.IS_REACTIVE) { 22 | return !isReadonly 23 | } else if (key === ReactiveFlags.IS_READONLY) { 24 | return isReadonly 25 | } else if (key === ReactiveFlags.IS_SHALLOW) { 26 | return isShallow 27 | } else if (key === ReactiveFlags.RAW) { 28 | return target 29 | } 30 | let res = Reflect.get(target, key) 31 | 32 | if (!isReadonly) { 33 | // 判断是否readonly 34 | // 依赖收集 35 | track(target, key as string) 36 | } 37 | if (isShallow) { 38 | return res 39 | } 40 | // 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive 41 | if (isObject(res)) { 42 | return isReadonly ? readonly(res) : reactive(res) 43 | } 44 | return res 45 | } 46 | } 47 | // 这个isShallow涉及到的是shallowReactive 48 | export function createSetter(isShallow = false) { 49 | return function set(target: T, key: string | symbol, value: any) { 50 | let success: boolean 51 | success = Reflect.set(target, key, value) 52 | // 触发依赖 53 | trigger(target, key as string) 54 | return success 55 | } 56 | } 57 | 58 | export const mutableHandlers: ProxyHandler = { 59 | get, 60 | set, 61 | } 62 | export const readonlyHandlers: ProxyHandler = { 63 | get: readonlyGet, 64 | set(target, key, value) { 65 | console.warn( 66 | `${JSON.stringify(target)} do not set ${String( 67 | key 68 | )} value ${value}, because it is readonly` 69 | ) 70 | return true 71 | }, 72 | } 73 | export const shallowReadonlyHandlers: ProxyHandler = extend( 74 | {}, 75 | readonlyHandlers, 76 | { 77 | get: shallowReadonlyGet, 78 | } 79 | ) 80 | export const shallowReactiveHandlers: ProxyHandler = extend( 81 | {}, 82 | mutableHandlers, 83 | { 84 | get: shallowGet, 85 | set: shallowSet, 86 | } 87 | ) 88 | export function createReactiveObject( 89 | target: T, 90 | handlers: ProxyHandler 91 | ) { 92 | if (!isObject(target)) { 93 | console.warn(`target ${target} is not a object`) 94 | return target 95 | } 96 | return new Proxy(target, handlers) 97 | } 98 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "../shared" 2 | import { ReactiveEffect } from "./effect" 3 | 4 | export class ComputedRefImpl { 5 | private _value!: T 6 | private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute 7 | private _effect: ReactiveEffect // 进行依赖收集 8 | constructor( 9 | getter: ComputedGetter, 10 | private setter: ComputedSetter 11 | ) { 12 | this._effect = new ReactiveEffect(getter, ()=>{ 13 | // 把dirty重新赋值为true 14 | if(!this._dirty){ 15 | this._dirty = true 16 | } 17 | }) 18 | } 19 | get value() { 20 | // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler() 21 | if(this._dirty){ 22 | this._dirty = false 23 | this._value = this._effect.run() 24 | } 25 | return this._value 26 | } 27 | set value(newValue: T) { 28 | this.setter(newValue) 29 | } 30 | } 31 | 32 | export type ComputedGetter = (...args: any[])=> T 33 | // v 是 赋值 = 右边的值 34 | export type ComputedSetter = (v: T) => void 35 | export interface WritableComputedOptions { 36 | get: ComputedGetter; 37 | set: ComputedSetter; 38 | } 39 | // 函数重载 40 | export function computed(option: WritableComputedOptions): any 41 | export function computed(getter: ComputedGetter): ComputedRefImpl 42 | // 实现签名 43 | export function computed(getterOrOption: ComputedGetter | WritableComputedOptions) { 44 | let getter: ComputedGetter 45 | let setter: ComputedSetter 46 | // 区分入参是getter还是option 47 | if(isFunction(getterOrOption)){ 48 | getter = getterOrOption 49 | setter = () => console.error('错误, 因为是getter只读, 不能赋值') 50 | }else{ 51 | getter = getterOrOption.get 52 | setter = getterOrOption.set 53 | } 54 | return new ComputedRefImpl(getter, setter) 55 | } 56 | -------------------------------------------------------------------------------- /src/reactivity/docs/1.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀reactive & effect 且利用 Jest 测试实现数据响应式(一) 2 | 3 | ## 前言 4 | 5 | 🎉 很高兴在此分享给大家 Vue3 的数据响应原理,小老弟我表达能力有限,所以 6 | 7 | 🤣 如有错漏,请多指教 ❤ 8 | 9 | ## 实现思路 10 | 11 | vue3 的数据响应式实现我们想理清楚它的特征,才好往下写。 12 | 13 | 以下是基本的`reactive`+`effect`用法 14 | 15 | ```typescript 16 | let count = reactive({ num: 11 }) 17 | let result = 0 18 | effect(() => { 19 | result = count.num + 1 20 | }) 21 | // result得到的是12 看起来effect立即执行了呢~ 22 | expect(result).toBe(12) 23 | // 相当于count.num = count.num + 1 这里有count.num的get操作和set操作 24 | count.num++ 25 | // result得到的是13 看起来他又执行了一遍effect的回调函数了呢~🤯 26 | expect(result).toBe(13) 27 | ``` 28 | 29 | ### 两大点 30 | 31 | - 依赖收集 32 | - 触发依赖 33 | 34 | 下面讲一下大体思路: 35 | 36 | 1. `reactive`为源数据创建`proxy`对象,其中`proxy`的`getter`、`setter`分别用于数据的依赖收集,数据的依赖触发 37 | 2. `effect`立即执行一次回调函数,当回调函数内的依赖数据发生变化的时候会再次触发该回调函数 38 | 3. 收集依赖我们可以定义一个`track`函数,当 reactive 的数据发生 get 操作时,`track`用一个`唯一标识`(下面会讲这个`唯一标识`是什么)记录依赖到一个`容器`里面 39 | 4. 触发依赖我们可以定义一个`trigger`函数,当 reactive 的数据发生 set 操作时,`trigger`将关于这个数据的所有依赖从`容器`里面拿出来逐个执行一遍 40 | 41 | ## 简单实现(详细代码在最后) 42 | 43 | > 1. `reactive` 44 | 45 | 给源数据创建一个[proxy](https://es6.ruanyifeng.com/#docs/proxy)对象,就好像给源数据套上了一层盔甲,敌人只能攻击这层盔甲,无法之间攻击源数据,在这基础之上我们就可以有机会去做数据的拦截。 46 | 47 | reactive 通过传入一个对象作为参数,返回一个该对象的 proxy 对象,其中`Reflect.get(target, key)`返回 target 的 key 对应的属性值 res,`Reflect.set(target, key, value)`设置 target 的 key 对应的属性值为 value 48 | 49 | ```TypeScript 50 | export function reactive(target: Record) { 51 | return new Proxy(target, { 52 | get(target, key) { 53 | let res = Reflect.get(target, key) 54 | return res 55 | }, 56 | set(target, key, value) { 57 | let success: boolean 58 | success = Reflect.set(target, key, value) 59 | return success 60 | } 61 | }) 62 | } 63 | ``` 64 | 65 | 这时候我们可以编写一个 🛠 测试用例,跑一跑测试有没有问题 66 | 67 | ```Typescript 68 | describe('reactive', () => { 69 | it.skip('reactive test', () => { 70 | let original = { num: 1 } 71 | let count = reactive(original) 72 | expect(original).not.toBe(count) ✅ 73 | expect(count.num).toEqual(1) ✅ 74 | }) 75 | }) 76 | ``` 77 | 78 | 🤮 什么?你不是说 getter 和 setter 要分别做两件事情吗?😒 79 | 80 | - getter 进行依赖收集 👀 81 | - setter 进行触发依赖 🔌 82 | 83 | 别急!还不是时候! 84 | 85 | > 2. `effect` 86 | 87 | 根据官方给出的介绍:`effect`会立即触发回调函数,同时响应式追踪其依赖 88 | 89 | effect 的基本用法: 90 | 91 | ```typeScript 92 | let result = 0 93 | // 假设count.num == 1 94 | effect(() => { 95 | result = count.num + 1 96 | }) 97 | // 那么输出的result就是2 98 | console.log(result) // output: 2 99 | ``` 100 | 101 | 其中`count`是已经通过了`reactive`处理的 proxy 实例对象 102 | 103 | 根据上述的用法我们可以简单的写出一个`effect`函数 104 | 105 | ```typescript 106 | class ReactiveEffect { 107 | private _fn: Function 108 | constructor(fn: Function) { 109 | this._fn = fn 110 | } 111 | run() { 112 | this._fn() 113 | } 114 | } 115 | export function effect(fn: Function) { 116 | let _reactiveFunc = new ReactiveEffect(fn) 117 | _reactiveFunc.run() 118 | } 119 | ``` 120 | 121 | 再写一个测试用例验证一下 122 | 123 | ```Typescript 124 | describe('effect test', () => { 125 | it('effect', () => { 126 | // 创建proxy代理 127 | let count = reactive({ num: 11 }) 128 | let result = 0 129 | // 立即执行effect并跟踪依赖 130 | effect(() => { 131 | result = count.num + 1 132 | }) 133 | expect(result).toBe(12) ✅ 134 | 135 | count.num++ 136 | expect(result).toBe(13) ❌ 137 | }) 138 | }) 139 | 140 | ``` 141 | 142 | 欸!我们发现了测试最后一项没有通过,哦原来我们还没实现依赖收集和触发依赖啊。。。 143 | 144 | > 3. `track`做依赖收集 145 | 146 | 我想想,我们应该怎么进行依赖收集?对,上面我们提到过有一个`唯一标识`和一个`容器`。我们该去哪找这个依赖啊?欸是`容器`,那些依赖是我们需要被触发的呢?欸看`唯一标识` 147 | 148 | 唯一标识是什么? 149 | 150 | 假设数据 target 是一个对象`{num: 11}`,对象的属性名可以绑定很多依赖,这个属性名`num`+`target`就可以找到与`num`相关的所有依赖集合,所以这里的`num`相关的所有依赖集合的唯一标识就是`num`+`target` 151 | 152 | 容器是什么? 153 | 154 | 存储不同数据下的所有属性对应的所有依赖集合,我们可以用 Map 存储不同数据,命名为`targetMap`,每个数据作为`targetMap`的键名,再定义一个以属性名`key`为键名的`depsMap`作为`targetMap`的键值,`depsMap`的键值是一个 Set 集合,命名作`deps`,最终`deps`就是存放特定的`key`的依赖集合 155 | 156 | 类型定义: 157 | 158 | 我相信你们看到`targetMap`的类型定义的时候应该会理解我上面说的存储结构是怎么样的吧 159 | 160 | ```typescript 161 | type EffectKey = string 162 | type IDep = ReactiveEffect 163 | const targetMap = new Map, Map>>() 164 | let activeEffect: ReactiveEffect 165 | ``` 166 | 167 | 下面我们来写一下`track`函数的实现,要注意的是我们需要处理一下第一次没有存储 Map 的情况 168 | 169 | ```typescript 170 | export function track(target: Record, key: EffectKey) { 171 | // 寻找dep依赖的执行顺序 172 | // target -> key -> dep 173 | let depsMap = targetMap.get(target) 174 | // 解决初始化没有depsMap的情况 175 | if (!depsMap) { 176 | depsMap = new Map() 177 | targetMap.set(target, depsMap) 178 | } 179 | // deps是一个Set对象,存放着这个key相对应的所有依赖 180 | let deps = depsMap.get(key) 181 | // 如果没有key相对应的Set 初始化Set 182 | if (!deps) { 183 | deps = new Set() 184 | depsMap.set(key, deps) 185 | } 186 | // 将activeEffect实例对象add给deps 187 | deps.add(activeEffect) 188 | } 189 | ``` 190 | 191 | 解释一下 192 | 193 | ```typescript 194 | deps.add(activeEffect) 195 | ``` 196 | 197 | 这里的 activeEffect 就是我们的依赖,怎么获取到的呢? 198 | 199 | 其实当`effect`执行的时候,内部 new 了一个`ReactiveEffect`类,而`ReactiveEffect`类里面可以通过`this`获取到`activeEffect`,因为`activeEffect`本来就是`ReactiveEffect`类的实例 200 | 201 | 我们改写一下`ReactiveEffect`代码 202 | 203 | ```typescript 204 | class ReactiveEffect { 205 | private _fn: Function 206 | constructor(fn: Function) { 207 | this._fn = fn 208 | } 209 | run() { 210 | // 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后, 211 | // this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机 212 | activeEffect = this 213 | this._fn() 214 | } 215 | } 216 | ``` 217 | 218 | > 4. `trigger`做触发依赖 219 | 220 | 这个 trigger 的实现逻辑很简单:找出 target 的 key 对应的所有依赖,并依次执行 221 | 222 | 1. 用 target 作为键名拿到在 targetMap 里面键值 depsMap 223 | 2. 用 key 作为键名拿到 depsMap 的键值 deps 224 | 3. `然后遍历deps这个Set实例对象,deps里面存的都是`ReactiveEffect`实例对象 dep,我们依次执行 dep.run()就相当于执行了 effect 的回调函数了。 225 | 226 | ```typescript 227 | export function trigger(target: Record, key: EffectKey) { 228 | const depsMap = targetMap.get(target) 229 | const deps = depsMap?.get(key) 230 | // 注意deps可能为undefined的情况 231 | if (deps) { 232 | for (let dep of deps) { 233 | dep.run() 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | > 5. 添加`track`和`trigger`函数到`proxy`的 getter 和 setter 上 240 | 241 | ```typescript 242 | export function reactive(target: Record) { 243 | return new Proxy(target, { 244 | get(target, key) { 245 | let res = Reflect.get(target, key) 246 | // 依赖收集 247 | track(target, key as string) 248 | return res 249 | }, 250 | set(target, key, value) { 251 | let success: boolean 252 | success = Reflect.set(target, key, value) 253 | // 触发依赖 254 | trigger(target, key as string) 255 | return success 256 | }, 257 | }) 258 | } 259 | ``` 260 | 261 | 最后再用 jest 运行一下响应式数据的测试用例 262 | 263 | ```Typescript 264 | describe('effect test', () => { 265 | it('effect', () => { 266 | // 创建proxy代理 267 | let count = reactive({ num: 11 }) 268 | let result = 0 269 | // 立即执行effect并跟踪依赖 270 | effect(() => { 271 | // count.num触发get 存储依赖 272 | result = count.num + 1 273 | }) 274 | expect(result).toBe(12) ✅ 275 | // 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result 276 | count.num++ 277 | expect(result).toBe(13) ✅ 278 | }) 279 | }) 280 | ``` 281 | 282 | ## 总结 283 | 284 | 上述的测试用例 count 触发了一次 setter 操作,两次 getter 操作。 285 | 286 | 1. 第一次 getter 操作是在 effect 的回调函数执行的时候发生,effect 立即执行,在执行之前我们拿到了`activeEffect`,之后在 proxy 的 getter 中执行了 track 函数,以`num`为 key 的 depsMap 被第一次初始化,并初始化了`targetMap`,把`activeEffect`添加到`deps`这个 Set 对象中,这就完成了依赖收集。 287 | 288 | 2. 当代码执行到`count.num++`的时候,我们先执行的是 proxy 的 getter 操作,执行 1 的流程,之后执行的是 proxy 的 setter 操作 289 | ```typescript 290 | Reflect.set(target, key, value) 291 | ``` 292 | 这段代码把`{num: 11}` 加一变成了`{num: 12}`,并且在 targetMap 中寻找`{num: 12}`为键名的键值,之后进一步获取到了 depsMap 和 deps。通过循环把 deps 里面的所有 activeEffect 执行`run()`方法,这就完成了触发依赖。 293 | 294 | ## 最后@感谢阅读! 295 | 296 | ## 完整代码 297 | 298 | ```typescript 299 | // in effect.ts 300 | 301 | class ReactiveEffect { 302 | private _fn: Function 303 | constructor(fn: Function) { 304 | this._fn = fn 305 | } 306 | run() { 307 | // 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后, 308 | // this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机 309 | activeEffect = this 310 | this._fn() 311 | } 312 | } 313 | const targetMap = new Map, Map>>() 314 | // 当前正在执行的effect 315 | let activeEffect: ReactiveEffect 316 | type EffectKey = string 317 | type IDep = ReactiveEffect 318 | // 这个track的实现逻辑很简单:添加依赖 319 | export function track(target: Record, key: EffectKey) { 320 | // 寻找dep依赖的执行顺序 321 | // target -> key -> dep 322 | let depsMap = targetMap.get(target) 323 | /** 324 | * 这里有个疑问:target为{ num: 11 } 的时候我们能获取到depsMap,之后我们count.num++,为什么target为{ num: 12 } 的时候我们还能获取得到相同的depsMap呢? 325 | * 这里我的理解是 targetMap的key存的只是target的引用 存的字符串就不一样了 326 | */ 327 | // 解决初始化没有depsMap的情况 328 | if (!depsMap) { 329 | depsMap = new Map() 330 | targetMap.set(target, depsMap) 331 | } 332 | // deps是一个Set对象,存放着这个key相对应的所有依赖 333 | let deps = depsMap.get(key) 334 | // 如果没有key相对应的Set 初始化Set 335 | if (!deps) { 336 | deps = new Set() 337 | depsMap.set(key, deps) 338 | } 339 | // 将activeEffect实例对象add给deps 340 | deps.add(activeEffect) 341 | } 342 | // 这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行 343 | export function trigger(target: Record, key: EffectKey) { 344 | const depsMap = targetMap.get(target) 345 | const deps = depsMap?.get(key) 346 | if (deps) { 347 | for (let dep of deps) { 348 | dep.run() 349 | } 350 | } 351 | } 352 | // 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖 353 | export function effect(fn: Function, option = {}) { 354 | let _reactiveFunc = new ReactiveEffect(fn) 355 | _reactiveFunc.run() 356 | } 357 | ``` 358 | 359 | ```typescript 360 | // in reactive.ts 361 | 362 | import { track, trigger } from './effect' 363 | 364 | export function reactive(target: Record) { 365 | return new Proxy(target, { 366 | get(target, key) { 367 | let res = Reflect.get(target, key) 368 | // 依赖收集 369 | track(target, key as string) 370 | return res 371 | }, 372 | set(target, key, value) { 373 | let success: boolean 374 | success = Reflect.set(target, key, value) 375 | // 触发依赖 376 | trigger(target, key as string) 377 | return success 378 | }, 379 | }) 380 | } 381 | ``` 382 | 383 | ```typescript 384 | // in effect.spec.ts 385 | 386 | import { effect } from '../index' 387 | import { reactive } from '../index' 388 | describe('effect test', () => { 389 | it('effect', () => { 390 | // 创建proxy代理 391 | let count = reactive({ num: 11 }) 392 | let result = 0 393 | // 立即执行effect并跟踪依赖 394 | effect(() => { 395 | // count.num触发get 存储依赖 396 | result = count.num + 1 397 | }) 398 | expect(result).toBe(12) 399 | // 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result 400 | count.num++ 401 | expect(result).toBe(13) 402 | }) 403 | }) 404 | ``` 405 | -------------------------------------------------------------------------------- /src/reactivity/docs/3.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀实现Vue3的isReactive & isReadonly 且利用Jest进行测试 2 | 3 | > 山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》 4 | 5 | 6 | ## 前言 7 | 上篇对reactive & effect的补充暂告一段落,还没看过上一篇的看这里🎉 8 | > [🚀 reactive & effect 且利用Jest测试实现数据响应式(一)](https://juejin.cn/post/7089244580394041375) 9 | > 10 | > [🚀 reactive & effect 且利用Jest测试实现数据响应式(二)](https://juejin.cn/post/7090165509735317534) 11 | 12 | 整篇文章的通过`TDD`测试驱动开发,带你一步一步实现vue3源码,文章的最后还有完整代码哦。 13 | 14 | 本篇文章内容包括: 15 | 1. 讲解Readonly的实现以及对已有代码的重构 16 | 2. 讲解isReactive的实现思路 17 | 3. 讲解isReadonly的实现思路 18 | 4. 源数据对象嵌套结构的代理对象 19 | 20 | > 🤣如有错漏,请多指教❤ 21 | 22 | 23 | ## 实现 Readonly 24 | 25 | 上篇文章我们已经实现了`reactive`,其实`readonly`是`reactive`的一种特殊情况,只不过是只读的。它也是返回一个`proxy`对象,并没有set操作,所以readonly并没有依赖触发,既然没有依赖触发,那么它也不需要get操作的依赖收集。 26 | 27 | 先看看我们的`readonly`测试用例: 28 | ```typescript 29 | describe('readonly', () => { 30 | it('readonly not set', () => { 31 | let original = { 32 | foo: { 33 | fuck: { 34 | name: "i don't care", 35 | }, 36 | }, 37 | arr: [{color: '#fff'}] 38 | } 39 | let warper = readonly(original) 40 | expect(warper).not.toBe(original) 41 | expect(warper.foo.fuck.name).toBe("i don't care") 42 | }) 43 | it('warning when it be call set operation', () => { 44 | let original = { 45 | username: 'ghx', 46 | } 47 | let readonlyObj = readonly(original) 48 | const warn = jest.spyOn(console, 'warn') 49 | // 给readonly做set操作,将会得到一个warning 50 | readonlyObj.username = 'danaizi' 51 | expect(warn).toHaveBeenCalled() 52 | }) 53 | }) 54 | ``` 55 | 56 | 如果硬要给`reactive`的代理对象赋值,那么它将得到一个警告(warn) 57 | ```typescript 58 | export function readonly(target: T) { 59 | return new Proxy(target, { 60 | get(target, key) { 61 | let res = Reflect.get(target, key) 62 | // 无需依赖收集,删除了track()函数 63 | return res 64 | }, 65 | set(target, key, value) { 66 | // 无需触发依赖 67 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 68 | return true 69 | }, 70 | }) 71 | } 72 | ``` 73 | ### 对已有代码的重构 74 | 之前我们已经实现了 `reactive` 和 `readonly`,这时候我们应该反观一下代码,观察代码有没有重复的代码段是需要我们去优化的。 目前的不足:`reactive` 和 `readonly` 都有相似的实现,相同的代码段较多,可以抽离出来 75 | 简述: 76 | 1. `reactive` 和 `readonly`的传入相同的入参`target` 77 | 2. `reactive` 和 `readonly`都返回proxy对象 78 | 3. `reactive` 和 `readonly`的proxy对象都有get和set方法,但是内部的代码实现有点不同 79 | 80 | 为了统一处理这些相同的代码逻辑,我们不妨新建文件`baseHandlers.ts`作为`Proxy`的第二入参handler的定义文件,又因为返回的都是`new Proxy()`对象,所以我们可以定义一个`createReactiveObject`函数,用于统一创建proxy对象,增强代码的可读性。 81 | ```typescript 82 | // In baseHandlers.ts 83 | export function createReactiveObject(target: T, handlers: ProxyHandler) { 84 | return new Proxy(target, handlers) 85 | } 86 | ``` 87 | ```typescript 88 | // In reactive.ts 89 | 90 | export function reactive(target: T) { 91 | return createReactiveObject(target, mutableHandlers) 92 | } 93 | // 其实就是一个没有set操作的reactive 94 | export function readonly(target: T) { 95 | return createReactiveObject(target, readonlyHandlers) 96 | } 97 | ``` 98 | handlers通过作为对象统一传入`createReactiveObject`,这样就可以统一处理`reactive`和`readonly`的不同逻辑。 99 | ```typescript 100 | export const mutableHandlers: ProxyHandler = { 101 | get: function(target: T, key: string | symbol) { 102 | let res = Reflect.get(target, key) 103 | // 依赖收集 104 | track(target, key as string) 105 | return res 106 | }, 107 | set: function(target: T, key: string | symbol, value: any) { 108 | let success: boolean 109 | success = Reflect.set(target, key, value) 110 | // 触发依赖 111 | trigger(target, key as string) 112 | return success 113 | }, 114 | } 115 | export const readonlyHandlers: ProxyHandler = { 116 | get: function(target: T, key: string | symbol) { 117 | let res = Reflect.get(target, key) 118 | return res 119 | }, 120 | set(target, key, value) { 121 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 122 | return true 123 | }, 124 | } 125 | ``` 126 | 仔细观察上面的代码,我们都有相同的set和get,内部同样实现了取值操作,但有所不同的是readonly的set操作会抛出warn,这一点我们可以不需要处理。为了区分`reactive`和`readonly`set和get的不同逻辑,我们需要一个标识`isReadonly` 127 | 128 | 抽离相同的set和get代码,我们外部需要定义set和get函数,但是我们需要传入一个标识`isReadonly`区分该函数到底是`reactive`的代码逻辑还是`readonly`的代码逻辑,同时不能为`set`和`get`新增其他的入参以防破坏代码的可读性。 129 | 我们可以定义一个高阶函数,该函数返回set和set函数,入参是`isReadonly`。 130 | ```typescript 131 | // In baseHandlers.ts 132 | // 高阶函数,isReadonly默认为false 133 | export function createGetter(isReadonly = false) { 134 | return function get(target: T, key: string | symbol) { 135 | 136 | let res = Reflect.get(target, key) 137 | 138 | if (!isReadonly) { 139 | // 判断是否readonly 140 | // 依赖收集 141 | track(target, key as string) 142 | } 143 | return res 144 | } 145 | } 146 | export function createSetter() { 147 | return function set(target: T, key: string | symbol, value: any) { 148 | let success: boolean 149 | success = Reflect.set(target, key, value) 150 | // 触发依赖 151 | trigger(target, key as string) 152 | return success 153 | } 154 | } 155 | ``` 156 | 之后定义不同的handlers对象,用于作为入参传入`createReactiveObject` 157 | ```typescript 158 | // reactive的handlers 159 | export const mutableHandlers: ProxyHandler = { 160 | get: createGetter(), 161 | set: createSetter(), 162 | } 163 | // readonly的handlers 164 | export const readonlyHandlers: ProxyHandler = { 165 | get: createGetter(true), 166 | set(target, key, value) { 167 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 168 | return true 169 | }, 170 | } 171 | ``` 172 | 此处来藏了一个优化点 173 | 174 | ## 实现 isReadonly 175 | 176 | 🧠大家思考一下我们以当前的代码来看,用什么来判断一个代理对象是否`readonly`? 177 | 178 | 答案就是`createGetter`的入参 `isReadonly`,观察之前对readonly的实现可以知道,让源数据通过Proxy包装之后,就已经在handler的get操作中得知该代理对象是否为`readonly`代理对象了。 179 | 180 | 181 | 182 | 既然在get操作中才能得到`isReadonly`,我们不妨触发一下get操作吧。 183 | 184 | 触发get操作有一个前提,那就是通过访问代理对象的属性就能触发get操作。你可能会说我们随便访问这个代理的对象的已知属性就可以触发get操作,然后return是否为readonly的结果不就行了吗?但是如果用户真的只想访问这个代理对象的属性并不想知道你到底是`readonly`还是`reactive`,这不就出现bug了吗 185 | 186 | 为此我们需要捏造一个一个代理对象不存在的属性,就叫`__v_isReadonly` 187 | 188 | 定义一个函数`isReadonly`,用于判断一个代理对象是否为`readonly`代理对象,该函数通过触发代理对象的get操作,返回一个布尔值。 189 | 190 | 191 | 192 | ```typescript 193 | // 给value做类型批注,让value有以下几个可选属性,不然该死的value飘红 --isReactive函数和isReadonly函数 说的就是你们 194 | export interface Target { 195 | __v_isReadonly?: boolean; 196 | } 197 | export function isReadonly(value: unknown){ 198 | return (value as Target)['__v_isReadonly'] 199 | } 200 | ``` 201 | 另外,有了`__v_isReadonly`属性,我们就知道用户是想通过get操作判断代理对象是否是`readonly`,还是想通过get操作访问指定的属性值。 202 | 203 | 我们要做的就是把`isReadonly`return出去 204 | ```typescript 205 | export function createGetter(isReadonly = false) { 206 | return function get(target: T, key: string | symbol) { 207 | if(key === '__v_isReadonly'){ 208 | return isReadonly 209 | } 210 | let res = Reflect.get(target, key) 211 | 212 | if (!isReadonly) { 213 | // 判断是否readonly 214 | // 依赖收集 215 | track(target, key as string) 216 | } 217 | return res 218 | } 219 | } 220 | ``` 221 | 以下为isReadonly的测试用例 222 | ```typescript 223 | it('readonly not set', () => { 224 | let original = { 225 | foo: { 226 | fuck: { 227 | name: 'what', 228 | }, 229 | }, 230 | arr: [{color: '#fff'}] 231 | } 232 | let warper = readonly(original) 233 | expect(warper).not.toBe(original) 234 | expect(isReadonly(warper)).toBe(true) 235 | expect(isReadonly(original)).toBe(false) ❌ 236 | // 测试嵌套对象的reactive状态 237 | expect(isReadonly(warper.foo.fuck)).toBe(true) 238 | // expect(isReadonly(warper.foo.fuck.name)).toBe(true) // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef 239 | expect(isReadonly(warper.arr)).toBe(true) 240 | expect(isReadonly(warper.arr[0])).toBe(true) 241 | expect(warper.foo.fuck.name).toBe('what') 242 | }) 243 | ``` 244 | 开始执行测试很顺畅,isReadonly传入一个代理对象,返回true没问题,嗯?怎么执行传入源数据的时候会测试不通过呢? 245 | 246 | 原因是源数据并没有被代理,并不能触发get操作,结果就是`isReadonly(original)`只能返回 undefined,因为original根本就没有`__v_isReadonly`属性。 247 | 248 | 那我们只要让它返回false就好了。通过`!!`双叹号,将它转成布尔值。undefined 会转成false。 249 | 250 | ```typescript 251 | export function isReadonly(value: unknown){ 252 | return !!(value as Target)['__v_isReadonly'] 253 | } 254 | ``` 255 | 256 | 257 | ## 实现 isReactive 258 | 259 | isReactive很简单,因为createGetter的入参是个布尔值`isReadonly`,所以不是isReadonly,就是isReactive。 260 | 261 | 实现思路和isReadonly一样,只是把`isReadonly`换成`isReactive`,然后通过get操作,返回一个布尔值。 262 | 263 | ```typescript 264 | export interface Target { 265 | __v_isReadonly?: boolean; 266 | __v_isReactive?: boolean; 267 | } 268 | export function isReactive(value: unknown) { 269 | 270 | return !!(value as Target)['__v_isReactive'] 271 | } 272 | ``` 273 | ```typescript 274 | export function createGetter(isReadonly = false) { 275 | return function get(target: T, key: string | symbol) { 276 | // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的 277 | if (key === '__v_isReactive') { 278 | return !isReadonly 279 | } else if (key === '__v_isReadonly') { 280 | return isReadonly 281 | } 282 | let res = Reflect.get(target, key) 283 | // 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive 284 | if(isObject(res)){ 285 | return isReadonly ? readonly(res) : reactive(res) 286 | } 287 | if (!isReadonly) { 288 | // 判断是否readonly 289 | // 依赖收集 290 | track(target, key as string) 291 | } 292 | return res 293 | } 294 | } 295 | ``` 296 | 297 | 看着一个个字符串的状态,是不是觉得很不爽。我们用typescript的enum管理状态,增强代码可读性。 298 | ```typescript 299 | export enum ReactiveFlags { 300 | IS_REACTIVE = '__v_isReactive', 301 | IS_READONLY = '__v_isReadonly' 302 | } 303 | export interface Target { 304 | [ReactiveFlags.IS_REACTIVE]?: boolean; 305 | [ReactiveFlags.IS_READONLY]?: boolean; 306 | } 307 | ``` 308 | 之后只要将enum的key替换上面裸露的字符串就可以了,这里不都说。 309 | 310 | ### 遇到嵌套的对象 311 | 312 | 遇到嵌套的对象作为源数据生成代理对象时,代理对象的子对象作为参数调用isReactive或者调用isReadonly,会返回false,因为里面的对象并没有被代理。 313 | 314 | 以下是该情况的测试用例 315 | ```typescript 316 | it('nested reactive',()=>{ 317 | let original = { 318 | foo: { 319 | name: 'ghx' 320 | }, 321 | arr: [{age: 23}] 322 | } 323 | const nested = reactive(original) 324 | expect(isReactive(nested.foo)).toBe(true) ❌ 325 | expect(isReactive(nested.arr)).toBe(true) ❌ 326 | expect(isReactive(nested.arr[0])).toBe(true) ❌ 327 | expect(isReactive(nested.foo)).toBe(true) ❌ 328 | // expect(isReactive(nested.foo.name)).toBe(true) ❌ // 涉及到往后的知识点 isRef 329 | 330 | }) 331 | ``` 332 | 要想测试用例通过,我们就必须把嵌套的对象也转成reactive代理对象。 333 | 334 | 当触发get操作的得到的`res`,我们追加一个判断,如果发现 res 不是reactive或者readonly,并且`res`是对象,那么递归调用`reactive()`或者`readonly()`。 335 | 336 | 判断是否是对象我们定义一个`isObject`在shared/index.ts中。 337 | ```typescript 338 | // 判断value是否object或者array 339 | export const isObject = (value: unknown) => { 340 | return value !== null && typeof value === 'object' 341 | } 342 | ``` 343 | 因为要在get操作时判断得到的res,我们在`createGetter()`上面做文章 344 | ```typescript 345 | export function createGetter(isReadonly = false) { 346 | return function get(target: T, key: string | symbol) { 347 | if (key === ReactiveFlags.IS_REACTIVE) { 348 | return !isReadonly 349 | } else if (key === ReactiveFlags.IS_READONLY) { 350 | return isReadonly 351 | } 352 | let res = Reflect.get(target, key) 353 | // 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive 354 | if(isObject(res)){ 355 | return isReadonly ? readonly(res) : reactive(res) 356 | } 357 | if (!isReadonly) { 358 | // 判断是否readonly 359 | // 依赖收集 360 | track(target, key as string) 361 | } 362 | return res 363 | } 364 | } 365 | ``` 366 | 367 | ## 优化点 368 | 369 | 反观`mutableHandlers`和`readonlyHandlers` 370 | ```typescript 371 | // reactive的handlers 372 | export const mutableHandlers: ProxyHandler = { 373 | get: createGetter(), 374 | set: createSetter(), 375 | } 376 | // readonly的handlers 377 | export const readonlyHandlers: ProxyHandler = { 378 | get: createGetter(true), 379 | set(target, key, value) { 380 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 381 | return true 382 | }, 383 | } 384 | ``` 385 | 代理对象每次触发proxy的get操作的时候都会调用`createGetter()`,set操作也是一样的。为了优化代码,减少对`createGetter()`的调用次数,我们单独抽离createGetter()和createSetter(),用一个常量接收。 386 | ```typescript 387 | // 此处调用一次createSetter和createGetter,为了不在每次使用mutableHandlers的时候重复调用 388 | const get = createGetter() 389 | const set = createSetter() 390 | const readonlyGet = createGetter(true) 391 | ``` 392 | 所以`mutableHandlers`和`readonlyHandlers`应该被改写成 393 | ```typescript 394 | export const mutableHandlers: ProxyHandler = { 395 | get, 396 | set, 397 | } 398 | export const readonlyHandlers: ProxyHandler = { 399 | get: readonlyGet, 400 | set(target, key, value) { 401 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 402 | return true 403 | }, 404 | } 405 | ``` 406 | 如果还不明白为什么要抽离出来的同学[看这里](https://www.typescriptlang.org/play?#code/PTAEk34wZxMcyNBkIwwuQIyEJrAxgJwKYEMAumBlTXfdACgEoAoVAewDsBnXUAWwFddsAjAG0wAS2egBN+6RqAC8oAN5VQi0AHNiALlAYc+AOLFSlADQKljdZqx5C+zOWoBfKjQbNQ2bNND1MAd1AAFdFoADwBPMlk3bA0AckQAJgBmGNB7QzZOHn4hUXFGCgBuZyYWd3jPbz9AkPDI91iE5NT0ji4+QWExW3yiuhK3RIrfAKCwiKiGpJS0jLbszrzC4tdsABYhqtHaidA4qebZrI7c7qW+lYBWDZGa8frdxumWzPacrokzl1KANmvqsbq0Qe+xmrSOb0WvS+bgA7H8tncgXsmqCXvMTh8ilQQBAYAhvmhLPgiCRbJQqO4AHTuTwxRKrC4xCnYeLUjwyOkMpllNm0+mMxTM1k0jn87ks3mirlCyW7MVOABm7HoqFwAEsGBZtJg9KS7HITIpzrR+JTeLRlGQYphgphUCobOgYkslKAsLh2Oh6KAyFx0KpcBphKF0gAHIKh2y4UIAaUwoQ0zHQavoynSIkwjAwatDuFo6A01Uj6GjABFM9nc-mKNIAHwGw2ut3ET3egBKmAV-FVlIDvuw-uIYYjUdj8eorscjioSpV6s1WisJIMNfkruNpvNluttvtZj1zqKrvdrZ9foDQfoIdA4doxejcYToCTKbToAAbtheOxMBplQBrehaB8eh0wrZMqwLG571CcsswgvN0BrKR6zXJsjRcE1MDNC0rRtO1n0dQ8nHQxQTy9UBcHQH9GynJxzhYW8wnhW5AUmFF0jQxQLzPAceODYc71HR9Eyo18wPgnNEMLEcS1g8CpOrOs5GaRt9w0ftB0DNwr0EmCROfMTU3ST9v1-UAAKAkCJMraToNHODbKUlCDVI5sPQoqiaMnKh7GoIA) 407 | 408 | ## 总结 409 | 1. readonly的实现和reactive的实现有点相似,但是有点不同。没有set操作,没有依赖收集,触发依赖。 410 | 2. isReactive的实现和isReadonly的实现原理一样,都是通过`createGetter()`的入参`isReadonly`判断的。 411 | 3. 遇到嵌套对象的源数据要生成代理对象,代理对象的子对象也要被代理。我们通过判断是否是对象然后递归调用`reactive()`或者`readonly()`来实现。 412 | 413 | ## 最后@感谢阅读 414 | 415 | 不念过去,不畏将来。 416 | 417 | ## 完整代码 418 | 419 | ```typescript 420 | // In share/index.ts 421 | // 判断value是否object或者array 422 | export const isObject = (value: unknown) => { 423 | return value !== null && typeof value === 'object' 424 | } 425 | 426 | ``` 427 | ```typescript 428 | // In reactive.ts 429 | 430 | import { createReactiveObject, mutableHandlers, readonlyHandlers } from './baseHandlers' 431 | 432 | export enum ReactiveFlags { 433 | IS_REACTIVE = '__v_isReactive', 434 | IS_READONLY = '__v_isReadonly' 435 | } 436 | // 给value做类型批注,让value有以下几个可选属性,不然该死的value飘红 --isReactive函数和isReadonly函数 说的就是你们 437 | export interface Target { 438 | [ReactiveFlags.IS_REACTIVE]?: boolean; 439 | [ReactiveFlags.IS_READONLY]?: boolean; 440 | } 441 | 442 | export function reactive(target: T) { 443 | return createReactiveObject(target, mutableHandlers) 444 | } 445 | // 其实就是一个没有set操作的reactive 446 | export function readonly(target: T) { 447 | return createReactiveObject(target, readonlyHandlers) 448 | } 449 | 450 | export function isReactive(value: unknown) { 451 | // target没有__v_isReactive这个属性,为什么还要写target['__v_isReactive']呢?因为这样就会触发proxy的get操作, 452 | // 通过判断createGetter传入的参数isReadonly是否为true,否则isReactive为true 453 | // 优化点:用enum管理状态,增强代码可读性 454 | return !!(value as Target)[ReactiveFlags.IS_REACTIVE] 455 | } 456 | export function isReadonly(value: unknown){ 457 | // 同上 458 | return !!(value as Target)[ReactiveFlags.IS_READONLY] 459 | } 460 | 461 | ``` 462 | ```typescript 463 | // In baseHandlers.ts 464 | 465 | import { track, trigger } from './effect' 466 | import { reactive, ReactiveFlags, readonly } from './reactive' 467 | import { isObject } from '../shared' 468 | // 此处调用一次createSetter和getter,为了不在每次使用mutableHandlers的时候重复调用 469 | const get = createGetter() 470 | const set = createSetter() 471 | const readonlyGet = createGetter(true) 472 | 473 | // 高阶函数, 474 | export function createGetter(isReadonly = false) { 475 | return function get(target: T, key: string | symbol) { 476 | // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的 477 | if (key === ReactiveFlags.IS_REACTIVE) { 478 | return !isReadonly 479 | } else if (key === ReactiveFlags.IS_READONLY) { 480 | return isReadonly 481 | } 482 | let res = Reflect.get(target, key) 483 | // 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive 484 | if(isObject(res)){ 485 | return isReadonly ? readonly(res) : reactive(res) 486 | } 487 | if (!isReadonly) { 488 | // 判断是否readonly 489 | // 依赖收集 490 | track(target, key as string) 491 | } 492 | return res 493 | } 494 | } 495 | export function createSetter() { 496 | return function set(target: T, key: string | symbol, value: any) { 497 | let success: boolean 498 | success = Reflect.set(target, key, value) 499 | // 触发依赖 500 | trigger(target, key as string) 501 | return success 502 | } 503 | } 504 | 505 | export const mutableHandlers: ProxyHandler = { 506 | get, 507 | set, 508 | } 509 | export const readonlyHandlers: ProxyHandler = { 510 | get: readonlyGet, 511 | set(target, key, value) { 512 | console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`) 513 | return true 514 | }, 515 | } 516 | export function createReactiveObject(target: T, handlers: ProxyHandler) { 517 | return new Proxy(target, handlers) 518 | } 519 | 520 | ``` 521 | ```typescript 522 | // In readonly.spec.ts 523 | import { readonly, isReadonly } from '../reactive' 524 | describe('readonly', () => { 525 | it('readonly not set', () => { 526 | let original = { 527 | foo: { 528 | fuck: { 529 | name: 'what', 530 | }, 531 | }, 532 | arr: [{color: '#fff'}] 533 | } 534 | let warper = readonly(original) 535 | expect(warper).not.toBe(original) 536 | expect(isReadonly(warper)).toBe(true) 537 | expect(isReadonly(original)).toBe(false) 538 | // 测试嵌套对象的reactive状态 539 | expect(isReadonly(warper.foo.fuck)).toBe(true) 540 | // expect(isReadonly(warper.foo.fuck.name)).toBe(true) // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef 541 | expect(isReadonly(warper.arr)).toBe(true) 542 | expect(isReadonly(warper.arr[0])).toBe(true) 543 | expect(warper.foo.fuck.name).toBe('what') 544 | }) 545 | it('warning when it be call set operation', () => { 546 | let original = { 547 | username: 'ghx', 548 | } 549 | let readonlyObj = readonly(original) 550 | const warn = jest.spyOn(console, 'warn') 551 | readonlyObj.username = 'danaizi' 552 | expect(warn).toHaveBeenCalled() 553 | }) 554 | }) 555 | 556 | ``` 557 | ```typescript 558 | // In reactice.spec.ts 559 | 560 | import { reactive, isReactive } from '../reactive' 561 | 562 | describe('reactive', () => { 563 | it('reactive test', () => { 564 | let original = { num: 1 } 565 | let count = reactive(original) 566 | expect(original).not.toBe(count) 567 | expect(count.num).toEqual(1) 568 | expect(isReactive(original)).toBe(false) 569 | expect(isReactive(count)).toBe(true) 570 | }) 571 | it('nested reactive',()=>{ 572 | let original = { 573 | foo: { 574 | name: 'ghx' 575 | }, 576 | arr: [{age: 23}] 577 | } 578 | const nested = reactive(original) 579 | expect(isReactive(nested.foo)).toBe(true) 580 | expect(isReactive(nested.arr)).toBe(true) 581 | expect(isReactive(nested.arr[0])).toBe(true) 582 | expect(isReactive(nested.foo)).toBe(true) 583 | // expect(isReactive(nested.foo.name)).toBe(true) // 涉及到往后的知识点 isRef 584 | 585 | }) 586 | }) 587 | 588 | ``` 589 | -------------------------------------------------------------------------------- /src/reactivity/docs/4.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀实现 shallowReadonly 和 ref 功能 且利用 Jest 测试 2 | 3 | > 山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》 4 | 5 | ## 前言 6 | 7 | 上篇对 reactive & effect 的补充暂告一段落,还没看过上一篇的看这里 🎉 8 | 9 | > [🚀reactive & effect 且利用 Jest 测试实现数据响应式(一)](https://juejin.cn/post/7089244580394041375) 10 | > 11 | > [🚀reactive & effect 且利用 Jest 测试实现数据响应式(二)](https://juejin.cn/post/7090165509735317534) 12 | 13 | > [🚀实现 isReactive & isReadonly 且利用 Jest 测试实现数据响应式(一)](https://juejin.cn/post/7091866529414774814) 14 | 15 | 整篇文章的通过`TDD`测试驱动开发,带你一步一步实现 vue3 源码。 16 | 17 | 本篇文章内容包括: 18 | 19 | 1. 讲解 shallowReadonly 和 shallowReactive 的实现 20 | 2. 讲解 isShallow 的实现思路 21 | 3. 讲解 isProxy 的实现思路 22 | 4. 讲解 toRaw 的实现思路 23 | 5. 讲解 ref 的实现思路 24 | 25 | > 🤣 如有错漏,请多指教 ❤ 26 | 27 | ## 实现 shallowReadonly 28 | 29 | 首先要搞清楚 shallowReadonly 是什么东西,以及它的用法,咱们才好往下实现它。 30 | 31 | [官方说明](https://v3.cn.vuejs.org/api/basic-reactivity.html#shallowreadonly) 32 | 33 | > 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(暴露原始值) 34 | 35 | 我们逐句讲解: 36 | 37 | 1. **创建一个 proxy** 38 | 39 | 跟 readonly 一样,我们也需要 new 一个 Proxy,并通过传入 handler。 40 | 41 | 2. **使其自身的 property 为只读** 42 | 43 | 跟 readonly 一样,我们需要传入 handler 的 getter 和 setter,用户对数据进行 set 赋值操作的时候会得到一个警告。 44 | 45 | 3. **不执行嵌套对象的深度只读转换** 46 | 47 | 与 readonly 不一样的地方在于,我们不用通过`isObject()`判断 get 操作之后得到的`res`是否是对象然后再次执行 readonly(res)。只需要判断数据是否 isShallow,如果是,直接`return res`即可。 48 | 49 | 下面是我们的测试用例: 50 | 51 | ```typescript 52 | it('shallowReadonly basic test', () => { 53 | let original = { 54 | foo: { 55 | name: 'ghx', 56 | }, 57 | } 58 | let obj = shallowReadonly(original) 59 | expect(isReadonly(obj)).toBe(true) 60 | // 因为只做表层的readonly,深层的数据还不是proxy 61 | expect(isReadonly(obj.foo)).toBe(false) 62 | expect(isReactive(obj.foo)).toBe(false) 63 | }) 64 | ``` 65 | 66 | 1. **创建一个 proxy**需要传入不一样的 handlers 67 | 68 | ```typescript 69 | export function shallowReadonly(target: T) { 70 | return createReactiveObject(target, shallowReadonlyHandlers) 71 | } 72 | ``` 73 | 74 | 2. **使其自身的 property 为只读** 75 | 76 | 这里我通过 extend 拓展了一下`readonlyhandlers`,但是 get 操作需要单独处理。 77 | 78 | ```typescript 79 | export const shallowReadonlyHandlers: ProxyHandler = extend({}, readonlyHandlers, { 80 | get: shallowReadonlyGet, 81 | }) 82 | ``` 83 | 84 | 3. **不执行嵌套对象的深度只读转换** 85 | 86 | 判断数据是否 isShallow,改写`createGetter`,加多一个入参`isShallow` 87 | 88 | ```typescript 89 | export function createGetter(isReadonly = false, isShallow = false) { 90 | return function get(target: T, key: string | symbol) { 91 | if (key === ReactiveFlags.IS_REACTIVE) { 92 | return !isReadonly 93 | } else if (key === ReactiveFlags.IS_READONLY) { 94 | return isReadonly 95 | } 96 | let res = Reflect.get(target, key) 97 | 98 | if (!isReadonly) { 99 | track(target, key as string) 100 | } 101 | // 这句是当前功能的关键 102 | if (isShallow) { 103 | return res 104 | } 105 | if (isObject(res)) { 106 | return isReadonly ? readonly(res) : reactive(res) 107 | } 108 | return res 109 | } 110 | } 111 | ``` 112 | 113 | 为什么不把 isShallow 判断放在 isReadonly 之前? 114 | 115 | 这就涉及到后面的 shallowReactive 实现了,shallowReactive 除了要返回 get 到的值之外,同时还要进行依赖收集。 116 | 117 | 之后创建一个常量用于缓存 createGetter 返回的函数即可。 118 | 119 | ```typescript 120 | const shallowReadonlyGet = createGetter(true, true) 121 | ``` 122 | 123 | ## 实现 shallowReactive 124 | 125 | [官方说明](https://v3.cn.vuejs.org/api/basic-reactivity.html#shallowreactive) 126 | 127 | > 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。 128 | 129 | ```typescript 130 | // shallowReactive的get操作 131 | const shallowGet = createGetter(false, true) 132 | // shallowReactive的set操作 133 | const shallowSet = createSetter(true) 134 | 135 | export const shallowReactiveHandlers: ProxyHandler = extend({}, mutableHandlers, { 136 | get: shallowGet, 137 | set: shallowSet, 138 | }) 139 | export function shallowReactive(target: T) { 140 | return createReactiveObject(target, shallowReactiveHandlers) 141 | } 142 | ``` 143 | 上面有讲isShallow,那我们也实现一个判断是否开启shallow模式的函数吧。 144 | ## 实现isShallow 145 | 146 | ```typescript 147 | export enum ReactiveFlags { 148 | IS_REACTIVE = '__v_isReactive', 149 | IS_READONLY = '__v_isReadonly', 150 | IS_SHALLOW = '__v_isShallow', 151 | RAW = '__v_raw' 152 | } 153 | // 检查对象是否 开启 shallow mode 154 | export function isShallow(value: unknown){ 155 | return !!(value as Target)[ReactiveFlags.IS_SHALLOW] 156 | } 157 | ``` 158 | 上面就是故意触发proxy的get操作,因为`createGetter()`的入参有个`isShallow`,这已经为我们标识当前proxy是否是shallow了。 159 | 160 | 我们改写以下createGetter,加多一层判断,如果get操作的`key`值是`ReactiveFlags.IS_SHALLOW`,那么直接返回入参`isShallow`的状态。 161 | 162 | ```typescript 163 | export function createGetter(isReadonly = false, isShallow = false) { 164 | return function get(target: T, key: string | symbol) { 165 | // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的 166 | if (key === ReactiveFlags.IS_REACTIVE) { 167 | return !isReadonly 168 | } else if (key === ReactiveFlags.IS_READONLY) { 169 | return isReadonly 170 | } else if (key === ReactiveFlags.IS_SHALLOW) { 171 | // 主要代码: 172 | return isShallow 173 | } else if (key === ReactiveFlags.RAW) { 174 | return target 175 | } 176 | let res = Reflect.get(target, key) 177 | 178 | if (!isReadonly) { 179 | track(target, key as string) 180 | } 181 | if (isShallow) { 182 | return res 183 | } 184 | if (isObject(res)) { 185 | return isReadonly ? readonly(res) : reactive(res) 186 | } 187 | return res 188 | } 189 | } 190 | ``` 191 | ## 实现 isProxy 192 | 193 | [官方说明](https://v3.cn.vuejs.org/api/basic-reactivity.html#isproxy) 194 | 195 | > 检查对象是否是由 reactive 或 readonly 创建的 proxy。 196 | 197 | 判断 reactive 和 readonly 生成的 proxy,在上一章节已经讲了,所以这里实现起来比较容易。 198 | 199 | ```typescript 200 | export function isProxy(value: unknown) { 201 | return isReactive(value) || isReadonly(value) 202 | } 203 | ``` 204 | 205 | ## 实现 toRaw 功能 206 | 207 | [官方说明](https://v3.cn.vuejs.org/api/basic-reactivity.html#toraw) 208 | 209 | > 返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改 210 | 211 | 总而言之就是可以通过`toRaw`获取到`reactive`或者`readonly`的原始值。 212 | 213 | 以下是测试用例 214 | ```typescript 215 | it('toRaw', () => { 216 | const original = { foo: 1 } 217 | const observed = reactive(original) 218 | // 输出的结果必须要等于原始值 219 | expect(toRaw(observed)).toBe(original) 220 | expect(toRaw(original)).toBe(original) 221 | }) 222 | it('nested reactive toRaw', () => { 223 | const original = { 224 | foo: { 225 | name: 'ghx', 226 | }, 227 | } 228 | const observed = reactive(original) 229 | const raw = toRaw(observed) 230 | expect(raw).toBe(original) 231 | expect(raw.foo).toBe(original.foo) 232 | }) 233 | ``` 234 | 235 | 怎么获取原始数据呢? 236 | 237 | 可以在 get 操作直接 return `get()`的第一个参数`target`。 238 | 239 | 那我们怎么才能知道什么时候应该 return `target`呢?我们需要一个标识,这个标识也是之前讲过,判断 isReadonly 和 isReactive 也是用的这个标识。 240 | 241 | 在 ReactiveFlags 定义一个枚举成员`RAW`,用于标记当前 get 操作是因为 raw 引起的 242 | 243 | ```typescript 244 | export enum ReactiveFlags { 245 | IS_REACTIVE = '__v_isReactive', 246 | IS_READONLY = '__v_isReadonly', 247 | IS_SHALLOW = '__v_isShallow', 248 | RAW = '__v_raw', 249 | } 250 | ``` 251 | 252 | 之后我们就可以通过故意触发`proxy`的 get 操作,让 get 操作 return 原始值了。 253 | 254 | ```typescript 255 | // 返回 reactive 或 readonly 代理的原始对象 256 | export function toRaw(observed: T): T { 257 | // observed存在,触发get操作,在createGetter直接return target 258 | const raw = observed && (observed as Target)[ReactiveFlags.RAW] 259 | return raw ? toRaw(raw) : observed 260 | } 261 | ``` 262 | 263 | ## 实现 ref 264 | 265 | [官方说明](https://v3.cn.vuejs.org/api/refs-api.html#ref) 266 | 267 | > 1. 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。 268 | 269 | > 2. 如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。 270 | 271 | `内部值`一般指值类型(string,number,boolean。。。) 272 | 273 | 为什么要有 ref 呢,reactive 不行吗? 274 | 275 | 因为`reactive`用的是 proxy,而`proxy`只能针对对象去监听数据变化,基本数据类型并不能用 proxy。 276 | 277 | 所以我们想到了`class`里面的取值函数 getter 和存值函数 getter,他们都能在数据变化的时候对数据加以操作。 278 | 279 | 我们依旧如此编写一个测试用例来驱动开发 280 | 281 | ```typescript 282 | it('should hold a value', () => { 283 | const a = ref(1) 284 | expect(a.value).toBe(1) 285 | a.value = 2 286 | expect(a.value).toBe(2) 287 | }) 288 | ``` 289 | 290 | 定义一个 RefImpl 类,实例化的就是 ref 对象 291 | 292 | ```typescript 293 | class RefImpl { 294 | private _value: T 295 | constructor(value: any) { 296 | this._value = value 297 | } 298 | get value() { 299 | return this._value 300 | } 301 | set value(newValue: any) { 302 | this._value = newValue 303 | } 304 | } 305 | ``` 306 | 307 | 很好,上面的测试用例我们已经跑通了 308 | 309 | 但是光跑通上面的简单用例还不行,我们必须让 ref 具有响应式行为 310 | 311 | ```typescript 312 | it('should be reactive', () => { 313 | const a = ref(1) 314 | let dummy 315 | let calls = 0 316 | effect(() => { 317 | calls++ 318 | dummy = a.value 319 | }) 320 | expect(calls).toBe(1) 321 | expect(dummy).toBe(1) 322 | a.value = 2 323 | expect(calls).toBe(2) 324 | expect(dummy).toBe(2) 325 | // same value should not trigger 326 | a.value = 2 327 | expect(calls).toBe(2) 328 | }) 329 | ``` 330 | 331 | 在实现`reactive`数据响应式的时候我们就已经实现了依赖收集和触发依赖功能,为了对代码能有更好的可读性和性能优化,我们会选择复用`reactive`的依赖收集和触发依赖的相关代码逻辑。 332 | 333 | 但是这些代码逻辑已经写死在 trigger()和 track()函数里面了,为了代码能复用,我们可以把这些逻辑抽离出来。 334 | 335 | 定义一个`trackEffect`函数,对依赖收集的逻辑进行封装 336 | 337 | ```typescript 338 | export type Dep = Set 339 | export function trackEffect(dep: Dep) { 340 | // 避免不必要的add操作 341 | if (dep.has(activeEffect)) return 342 | // 将activeEffect实例对象add给deps 343 | dep.add(activeEffect) 344 | // activeEffect的deps 接收 Set类型的deps 345 | // 供删除依赖的时候使用(停止监听依赖) 346 | activeEffect.deps.push(dep) 347 | } 348 | ``` 349 | 350 | 定义一个`triggerEffect`函数,对触发依赖的逻辑进行封装 351 | 352 | ```typescript 353 | export function triggerEffect(dep: Dep) { 354 | for (let effect of dep) { 355 | if (effect.scheduler) { 356 | effect.scheduler() 357 | } else { 358 | effect.run() 359 | } 360 | } 361 | } 362 | ``` 363 | 364 | 我们就可以在 RefImpl 类的 get 和 set 进行依赖收集和触发依赖,不过在此之前,我们还需要定义一个`dep`公有成员函数,用于存储这一个`ref`对象的依赖 365 | 366 | ```typescript 367 | class RefImpl { 368 | private _value: T 369 | public dep?: Dep = undefined 370 | constructor(value: any) { 371 | this._value = value 372 | this.dep = new Set() 373 | } 374 | get value() { 375 | trackRefValue(this) 376 | return this._value 377 | } 378 | set value(newValue: any) { 379 | // 触发依赖 380 | 381 | // 注意这里先赋值再触发依赖 382 | this._value = newValue 383 | triggerEffect(this.dep as Dep) 384 | } 385 | } 386 | function trackRefValue(ref: RefImpl) { 387 | // 有时候根本就没有调用effect(),也就是说activeEffect是undefined的情况 388 | // isTracking 的实现在之前的章节讲到 389 | if (isTracking()) { 390 | // 依赖收集 391 | trackEffect(ref.dep as Dep) 392 | } 393 | } 394 | ``` 395 | 396 | 当 ref 对象被赋予相同的值的时候,我们要做到不触发依赖的行为。 397 | 398 | 怎么才能判断是不是相同的值呢? 399 | 400 | 我们必须给 RefImpl 存入一个原始值`_rawValue`,它存放着 ref 的入参(ref 原本传入的值),我们只需要把当前执行 set 赋值操作的`value`和`_rawValue`进行比较,如果相同,我们就不触发依赖。 401 | 402 | 比较的方式我们选择使用[ES6 的 Object.is](https://es6.ruanyifeng.com/#docs/object-methods#Object-is) 403 | 404 | ```typescript 405 | export function hasChanged(value: any, oldValue: any) { 406 | return !Object.is(value, oldValue) 407 | } 408 | ``` 409 | 410 | 比较结果为 true,说明`value`已经改变了,这时我们应该更新`_rawValue`的值,并触发依赖。 411 | 412 | ```typescript 413 | class RefImpl { 414 | private _value: T 415 | public dep?: Dep = undefined 416 | private _rawValue: T 417 | constructor(value: any) { 418 | this._value = value 419 | this._rawValue = value 420 | this.dep = new Set() 421 | } 422 | get value() { 423 | trackRefValue(this) 424 | return this._value 425 | } 426 | set value(newValue: any) { 427 | // 触发依赖 428 | 429 | // 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点 430 | if (hasChanged(newValue, this._rawValue)) { 431 | // 注意这里先赋值再触发依赖 432 | this._value = newValue 433 | this._rawValue = newValue 434 | triggerEffect(this.dep as Dep) 435 | } 436 | } 437 | } 438 | ``` 439 | 440 | 如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。 441 | 442 | reactive 函数的深层响应式对象功能,在之前的篇章里我们已经实现了。 443 | 444 | 我们只需要判断`value`是否是对象,是对象就用`reactive`处理,不是对象就直接给`_value` 445 | 446 | ```typescript 447 | function convert(value: any) { 448 | return isObject(value) ? reactive(value) : value 449 | } 450 | class RefImpl { 451 | private _value: T 452 | public dep?: Dep = undefined 453 | private _rawValue: T 454 | constructor(value: any) { 455 | this._value = convert(value) 456 | this._rawValue = value 457 | this.dep = new Set() 458 | } 459 | get value() { 460 | trackRefValue(this) 461 | return this._value 462 | } 463 | set value(newValue: any) { 464 | // 触发依赖 465 | 466 | // 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点 467 | if (hasChanged(newValue, this._rawValue)) { 468 | // 注意这里先赋值再触发依赖 469 | this._value = convert(newValue) 470 | this._rawValue = newValue 471 | triggerEffect(this.dep as Dep) 472 | } 473 | } 474 | } 475 | ``` 476 | 477 | 至此,测试用例已全部跑通。 478 | 479 | ## 总结 480 | 481 | `shallowReactive`和`shallowReadonly`只能对表层的数据提供`reactive`和`readonly`操作,对于深层的数据我们并只能暴露原始值。 482 | 483 | isProxy 实现的如此简单是因为我们在之前有刻意的封装函数,希望我们在写代码的时候也要有这种意识,有效的封装函数会是代码写的更简单。 484 | 485 | ref 和 reactive 的区别: 486 | 487 | 1. ref 一般接受的是值类型,reactive 接受的是引用类型,虽然 ref 能接受对象类型,但是其内部还是使用了 reactive(),所以某些情况下直接使用 reactive 会更好一些。(为什么?你问 proxy 这个大哥给不给你传入值类型啊!) 488 | 2. ref 需要通过.value 才能拿到值。 489 | 490 | 还有其他区别欢迎补充! 491 | 492 | ## 最后@感谢阅读 493 | -------------------------------------------------------------------------------- /src/reactivity/docs/5.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀 实现 computed 和 proxyRefs 功能 且利用 Jest 测试 2 | > 山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》 3 | 4 | ## 前言 5 | 上篇对`ref`实现的暂告一段落,还没看过上一篇的看这里🎉 6 | > [🚀实现 shallowReadonly 和 ref 功能 且利用 Jest 测试](https://juejin.cn/post/7093166158991327239) 7 | 8 | 接下来我们来实现一个简单的计算属性`computed`,并且利用 Jest 测试它的变化。 9 | 10 | 本篇文章内容包括: 11 | 12 | 1. 讲解 isRef 的实现思路和代码实现 13 | 2. 讲解 unref 的实现思路和代码实现 14 | 3. 讲解 proxyRefs 的实现思路和代码实现 15 | 4. 讲解 computed 的实现思路和代码实现 16 | 17 | 18 | > 🤣 如有错漏,请多指教 ❤ 19 | 20 | ## 实现 isRef 21 | 22 | > 检查值是否为一个 ref 对象。 23 | 24 | 以下是测试用例: 25 | ```typescript 26 | test('isRef', () => { 27 | expect(isRef(ref(1))).toBe(true) 28 | 29 | expect(isRef(0)).toBe(false) 30 | expect(isRef(1)).toBe(false) 31 | // an object that looks like a ref isn't necessarily a ref 32 | expect(isRef({ value: 0 })).toBe(false) 33 | }) 34 | ``` 35 | 36 | 我们可以定义一个`isRef`函数,入参传入一个对象,内部通过有意的访问一个只有 ref 才有的公有属性,来判断这个对象是否是ref对象。 37 | 38 | ```typescript 39 | class RefImpl { 40 | ... 41 | public __v_isRef = true // 标识是ref对象 42 | constructor(value: any) { 43 | ... 44 | } 45 | ... 46 | } 47 | // 检查值是否为一个 ref 对象。 48 | export function isRef(ref: any) { 49 | return !!(ref && ref.__v_isRef) 50 | } 51 | ``` 52 | `(ref && ref.__v_isRef)`有可能为undefined,所以我们加上`!!`让`undefined`转为`boolean`类型。 53 | 54 | 55 | ## 实现 unref 56 | 57 | > 如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。 58 | 59 | 以下是测试用例: 60 | ```typescript 61 | test('isRef', () => { 62 | expect(isRef(ref(1))).toBe(true) 63 | 64 | expect(isRef(0)).toBe(false) 65 | expect(isRef(1)).toBe(false) 66 | // an object that looks like a ref isn't necessarily a ref 67 | expect(isRef({ value: 0 })).toBe(false) 68 | }) 69 | ``` 70 | 71 | 基本的实现原理就是`val = isRef(val) ? val.value : val` 72 | 73 | ```typescript 74 | export function unref(ref: any) { 75 | return isRef(ref) ? ref.value : ref 76 | } 77 | ``` 78 | 79 | ## 实现 proxyRefs 80 | 81 | 虽然 `proxyRefs`并没有在官方文档给出介绍和api用法,但是它的实际作用是在``模板里面解构出ref的value,这样我们在模板里面就不需要再书写.value来获取ref的值了。它同时兼容reactive对象的传入。 82 | 83 | ### 测试用例 84 | 85 | ```typescript 86 | test('proxyRefs', ()=>{ 87 | const user = { 88 | age: ref(10), 89 | name: 'ghx' 90 | } 91 | const original = { 92 | k: 'v' 93 | } 94 | const r1 = reactive(original) 95 | // 传入reactive对象 96 | const p1 = proxyRefs(r1) 97 | // objectWithRefs对象 (带ref 的object) 98 | const proxyUser = proxyRefs(user) 99 | 100 | expect(p1).toBe(r1) 101 | 102 | expect(user.age.value).toBe(10) 103 | expect(proxyUser.age).toBe(10) 104 | expect(proxyUser.name).toBe('ghx') 105 | 106 | proxyUser.age = 20 107 | expect(proxyUser.age).toBe(20) 108 | expect(user.age.value).toBe(20) 109 | 110 | proxyUser.age = ref(10) 111 | proxyUser.name = 'superman' 112 | expect(proxyUser.age).toBe(10) 113 | expect(proxyUser.name).toBe('superman') 114 | expect(user.age.value).toBe(10) 115 | }) 116 | ``` 117 | 118 | ### 特点 119 | 120 | * 如果入参是reactive对象,那么直接返回reactive。 121 | 122 | * 如果入参是子属性值带ref对象的普通对象,那么返回一个proxy对象,其中的属性值是ref对象的属性,通过调用`unref`函数获取实际的值(.value)。 123 | 124 | set操作我们也得区分两种情况: 125 | 126 | 赋值操作状态下 127 | 128 | 1. 如果属性为ref对象,并且值为普通类型,那么需要把值赋给ref对象的.value属性。 129 | ```typescript 130 | const user = { 131 | age: ref(10), 132 | name: 'ghx' 133 | } 134 | const proxyUser = proxyRefs(user) 135 | proxyUser.age = 20 136 | expect(proxyUser.age).toBe(20) 137 | expect(user.age.value).toBe(20) 138 | ``` 139 | 2. 否则其他情况下都应该把值直接赋值给属性。 140 | ```typescript 141 | proxyUser.age = ref(10) 142 | proxyUser.name = 'superman' 143 | expect(proxyUser.age).toBe(10) 144 | expect(proxyUser.name).toBe('superman') 145 | expect(user.age.value).toBe(10) 146 | ``` 147 | ### 基本实现 148 | ```typescript 149 | export function proxyRefs(obj: T){ 150 | return isReactive(obj) 151 | ? obj 152 | : new Proxy(obj, { 153 | get(target, key){ 154 | // unref已经处理了obj是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值 155 | return unref(Reflect.get(target, key)) 156 | }, 157 | set(target, key, value){ 158 | // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value 159 | if (isRef(target[key]) && !isRef(value)) { 160 | target[key].value = value 161 | return true 162 | } else { 163 | return Reflect.set(target, key, value) 164 | } 165 | } 166 | }) 167 | } 168 | ``` 169 | 170 | 171 | ## 实现 computed 172 | 173 | 在``中放入过多的计算逻辑会让模板难以维护 174 | 175 | ```html 176 |

My book:

177 |
{{ghx.books.length > 10 ? '多' : '少'}}
178 |
{{ghx.books.length > 10 ? '多' : '少'}}
179 |
{{ghx.books.length > 10 ? '多' : '少'}}
180 |
{{ghx.books.length > 10 ? '多' : '少'}}
181 |
{{ghx.books.length > 10 ? '多' : '少'}}
182 |
{{ghx.books.length > 10 ? '多' : '少'}}
183 | ``` 184 | 185 | 这时候的template就不单单是简单的,它包含了一些逻辑,让整体上的template看起来更加复杂,所以我们为了解决这个问题,我们需要使用computed。 186 | 187 | ### 特点 188 | 1. 具有对数据的响应依赖关系的缓存功能。computed的依赖没有更新的情况下,**再次**发生ref对象的get操作并不会导致数据的重新计算。 189 | 2. 懒计算。computed的依赖被更新,也不会立即重新计算结果,而是当computed返回的ref对象发生get操作的时候才会计算结果。 190 | 191 | ### 基本用法 192 | ```typescript 193 | // 第一种: 194 | const count = ref(1) 195 | const plusOne = computed(() => count.value + 1) 196 | 197 | console.log(plusOne.value) // 2 198 | 199 | plusOne.value++ // 错误 200 | 201 | // 第二种: 202 | const count = ref(1) 203 | const plusOne = computed({ 204 | get: () => count.value + 1, 205 | set: val => { 206 | count.value = val - 1 207 | } 208 | }) 209 | 210 | plusOne.value = 1 211 | console.log(count.value) // 0 212 | ``` 213 | 214 | computed可以接收一个具有get和set方法的对象作为参数,也可以接收一个get方法作为参数,都返回一个ref对象。 215 | 216 | 这就要用到函数重载了。 217 | 218 | * 当computed只接受一个get方法的时候,那么该computed为只读,写入数据将会得到一个错误 219 | * 当computed接受具有get和set方法的对象的时候,那么该computed为可读写 220 | 221 | 我们以通关的形式来实现computed,总共有两关,第一关简单实现computed类,第二关实现computed的懒计算和缓存功能 222 | 223 | 1. **第一关的测试用例** 224 | ```typescript 225 | it('should return updated value', () => { 226 | const value = reactive({ foo: 1 }) 227 | const cValue = computed(() => value.foo) 228 | expect(cValue.value).toBe(1) 229 | }) 230 | ``` 231 | 创建一个`computed`函数,顺便定义get和set的类型,对不同函数签名进行重载 232 | 233 | ```typescript 234 | type ComputedGetter = (...args: any[])=> T 235 | // v 是 要带赋值的值 236 | type ComputedSetter = (v: T) => void 237 | interface WritableComputedOptions { 238 | get: ComputedGetter; 239 | set: ComputedSetter; 240 | } 241 | export function computed(option: WritableComputedOptions): any 242 | export function computed(getter: ComputedGetter): ComputedRefImpl 243 | // 实现函数签名 244 | export function computed(getterOrOption: ComputedGetter | WritableComputedOptions){ 245 | let getter: ComputedGetter 246 | let setter: ComputedSetter 247 | if (isFunction(getterOrOption)) { 248 | getter = getterOrOption 249 | setter = () => console.error('错误, 因为是getter只读, 不能赋值') 250 | } else { 251 | getter = getterOrOption.get 252 | setter = getterOrOption.set 253 | } 254 | return new ComputedRefImpl(getter, setter) 255 | } 256 | ``` 257 | 接着我们来实现`ComputedRefImpl`类。 258 | 259 | 我们希望的computed,它是可以使用`.value`来访问值的,并能控制value的读写操作,所以我们在内部定义一个私有属性`_value`,然后通过取值函数`getter`和存值函数`setter`控制我们的value,当用户触发get操作的时候,我们会传入的getter函数,把返回值传回给`_value`,当用户触发set操作的时候,`set value(newValue)`的newValue会传给用户传进来的setter函数。 260 | ```typescript 261 | export class ComputedRefImpl { 262 | private _value!: T 263 | public _getter: ComputedGetter 264 | constructor( 265 | getter: ComputedGetter, 266 | private setter: ComputedSetter 267 | ) { 268 | this._getter = getter 269 | } 270 | get value() { 271 | this._value = this.getter() 272 | return this._value 273 | } 274 | set value(newValue: T) { 275 | this.setter(newValue) 276 | } 277 | } 278 | ``` 279 | 到此上面的测试用例就能跑通了。 280 | 281 | 2. **第二关的测试用例** 282 | 283 | 看起来有点复杂,我们通过注释讲解一下每一步吧。 284 | ```typescript 285 | it('should compute lazily', () => { 286 | const value = reactive({ foo: 1 }) // 创建一个reactive对象 287 | const getter = jest.fn(() => value.foo) // 通过jest.fn()创建一个模拟函数,后续会检测被调用该函数次数 288 | const cValue = computed(getter) // 创建一个computed对象,并传入getter函数 289 | 290 | // lazy功能 291 | expect(getter).not.toHaveBeenCalled() // 因为还没触发cValue的get操作,所以getter是不会被调用的。 292 | 293 | expect(cValue.value).toBe(1) // cValue的get操作被触发,getter执行 294 | expect(getter).toHaveBeenCalledTimes(1) // getter被调用一次 295 | // 缓存功能 296 | // should not compute again 297 | cValue.value // cValue的get操作被触发,又因为value.foo并没有发生改变 298 | expect(getter).toHaveBeenCalledTimes(1) // 这里的getter还是被调用了一次 299 | 300 | // should not compute until needed 301 | value.foo = 2 // 这里的value.foo发生了改变,但是cValue的get操作还没被触发 302 | expect(getter).toHaveBeenCalledTimes(1) // 所以这里getter仍然只会被调用一次 303 | 304 | // now it should compute 305 | expect(cValue.value).toBe(2) // 这里的cValue的get操作被触发,getter执行 306 | expect(getter).toHaveBeenCalledTimes(2) // 这里getter被调用了两次 307 | // should not compute again 308 | cValue.value // cValue的get操作被触发,又因为value.foo并没有发生改变 309 | expect(getter).toHaveBeenCalledTimes(2) // 这里的getter还是被调用了两次 310 | }) 311 | ``` 312 | 313 | 要实现缓存功能,依赖没有发生改变不能让computed重新计算,我们就需要在get操作的时候,添加一把`锁`,_dirty,dirty有肮脏的意思,当内部依赖的数据发生改变的时候,就说明肮脏了,`_dirty`等于true,这时候才能重新计算。 314 | 315 | 我们声明`_dirty`为类的私有成员变量。 316 | ```typescript 317 | export class ComputedRefImpl { 318 | private _value!: T 319 | private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute 320 | public _getter: ComputedGetter 321 | constructor( 322 | getter: ComputedGetter, 323 | private setter: ComputedSetter 324 | ) { 325 | this._getter = getter 326 | } 327 | get value() { 328 | // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler() 329 | if(this._dirty){ 330 | this._dirty = false 331 | this._value = this.getter() 332 | } 333 | return this._value 334 | } 335 | set value(newValue: T) { 336 | this.setter(newValue) 337 | } 338 | } 339 | ``` 340 | 后来,为了收集computed的回调函数到依赖集中去,我们为ComputedRefImpl添加`_effect`,用于存储`ReactiveEffect`对象(ReactiveEffect到底是什么,[在这里](https://juejin.cn/post/7090165509735317534)有讲到)。底层做了让track可以收集computed的回调函数这件事。 341 | 342 | ```typescript 343 | export class ComputedRefImpl { 344 | private _value!: T 345 | private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute 346 | private _effect: ReactiveEffect // 进行依赖收集 347 | constructor( 348 | getter: ComputedGetter, 349 | private setter: ComputedSetter 350 | ) { 351 | this._effect = new ReactiveEffect(getter) 352 | } 353 | get value() { 354 | if(this._dirty){ 355 | this._dirty = false 356 | this._value = this._effect.run() 357 | } 358 | return this._value 359 | } 360 | set value(newValue: T) { 361 | this.setter(newValue) 362 | } 363 | } 364 | ``` 365 | `value.foo = 2`的时候,会执行trigger,触发依赖,调用用户的getter函数。 366 | ```typescript 367 | export function triggerEffect(dep: Dep){ 368 | for (let effect of dep) { 369 | if (effect.scheduler) { 370 | effect.scheduler() 371 | } else { 372 | effect.run() 373 | } 374 | } 375 | } 376 | ``` 377 | 378 | 但是在这里我们并不希望用户的getter的函数被触发,我们要实现懒计算,就是让用户访问computed返回的ref的时候重新计算,所以为了`value.foo = 2`的时候不让trigger导致用户的getter函数被触发,并且希望`_dirty`重新变为true,我们通过`scheduler()`来把_dirty重新赋值为true。(重新变为true,在下一次computed的返回值ref函数触发get操作的时候才能调用`effect.run()`重新计算) 379 | 380 | ```typescript 381 | 382 | export class ComputedRefImpl { 383 | private _value!: T 384 | private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute 385 | private _effect: ReactiveEffect // 进行依赖收集 386 | constructor( 387 | getter: ComputedGetter, 388 | private setter: ComputedSetter 389 | ) { 390 | this._effect = new ReactiveEffect(getter, ()=>{ 391 | // 把dirty重新赋值为true 392 | if(!this._dirty){ 393 | this._dirty = true 394 | } 395 | }) 396 | } 397 | get value() { 398 | // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler() 399 | if(this._dirty){ 400 | this._dirty = false 401 | this._value = this._effect.run() 402 | } 403 | return this._value 404 | } 405 | set value(newValue: T) { 406 | this.setter(newValue) 407 | } 408 | } 409 | ``` 410 | 到这里,测试用例就跑通了。 411 | 412 | ## 总结 413 | 414 | 最难最绕的就是computed的实现思路 415 | 416 | 整体来看computed的实现也是一个发布-订阅者模式。 417 | 418 | 419 | 420 | ## 最后@感谢阅读 421 | 422 | ## 完整代码 423 | ```typescript 424 | // In computed.ts 425 | 426 | import { isFunction } from "../shared" 427 | import { ReactiveEffect } from "./effect" 428 | 429 | export class ComputedRefImpl { 430 | private _value!: T 431 | private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute 432 | private _effect: ReactiveEffect // 进行依赖收集 433 | constructor( 434 | getter: ComputedGetter, 435 | private setter: ComputedSetter 436 | ) { 437 | this._effect = new ReactiveEffect(getter, ()=>{ 438 | // 把dirty重新赋值为true 439 | if(!this._dirty){ 440 | this._dirty = true 441 | } 442 | }) 443 | } 444 | get value() { 445 | // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler() 446 | if(this._dirty){ 447 | this._dirty = false 448 | this._value = this._effect.run() 449 | } 450 | return this._value 451 | } 452 | set value(newValue: T) { 453 | this.setter(newValue) 454 | } 455 | } 456 | 457 | type ComputedGetter = (...args: any[])=> T 458 | // v 是 赋值 = 右边的值 459 | type ComputedSetter = (v: T) => void 460 | interface WritableComputedOptions { 461 | get: ComputedGetter; 462 | set: ComputedSetter; 463 | } 464 | // 函数重载 465 | export function computed(option: WritableComputedOptions): any 466 | export function computed(getter: ComputedGetter): ComputedRefImpl 467 | // 实现签名 468 | export function computed(getterOrOption: ComputedGetter | WritableComputedOptions) { 469 | let getter: ComputedGetter 470 | let setter: ComputedSetter 471 | // 区分入参是getter还是option 472 | if(isFunction(getterOrOption)){ 473 | getter = getterOrOption 474 | setter = () => console.error('错误, 因为是getter只读, 不能赋值') 475 | }else{ 476 | getter = getterOrOption.get 477 | setter = getterOrOption.set 478 | } 479 | return new ComputedRefImpl(getter, setter) 480 | } 481 | 482 | ``` 483 | ```typescript 484 | // In ref.ts 485 | 486 | // 检查值是否为一个 ref 对象。 487 | export function isRef(ref: any) { 488 | return !!(ref && ref.__v_isRef) 489 | } 490 | // 如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。 491 | export function unref(ref: any) { 492 | return isRef(ref) ? ref.value : ref 493 | } 494 | // 通常用在vue3 template里面ref取值,在template里面不需要.value就可以拿到ref的值 495 | export function proxyRefs(obj: T){ 496 | return isReactive(obj) 497 | ? obj 498 | : new Proxy(obj, { 499 | get(target, key){ 500 | // unref已经处理了是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值 501 | return unref(Reflect.get(target, key)) 502 | }, 503 | set(target, key, value){ 504 | // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value 505 | if (isRef(target[key]) && !isRef(value)) { 506 | target[key].value = value 507 | return true 508 | } else { 509 | return Reflect.set(target, key, value) 510 | } 511 | } 512 | }) 513 | } 514 | 515 | ``` 516 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '../shared' 2 | 3 | export type EffectScheduler = (...args: any[]) => any 4 | export type Dep = Set 5 | export class ReactiveEffect { 6 | public deps: Dep[] = [] 7 | public active = true // 该effect是否存活 8 | public onStop?: () => void 9 | constructor(public fn: Function, public scheduler?: EffectScheduler) {} 10 | run() { 11 | // 如果effect已经被杀死了,被删除了(stop()函数相关) 12 | if (!this.active) { 13 | return this.fn() 14 | } 15 | // 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后, 16 | // this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机 17 | activeEffect = this 18 | shouldTrack = true // 把开关打开让他可以收集依赖 19 | let returnValue = this.fn() // 执行fn的时候,fn里面会执行get操作,之后就会执行track收集依赖,因为shouldTrack是true,所以依赖收集完成 20 | // 之后把shouldTrack关闭,这样就没办法在track函数里面收集依赖了 21 | shouldTrack = false 22 | 23 | return returnValue 24 | } 25 | stop() { 26 | // 追加active 标识是为了性能优化,避免每次循环重复调用stop同一个依赖的时候 27 | if (this.active) { 28 | cleanupEffect(this) 29 | this.onStop?.() 30 | this.active = false 31 | } 32 | } 33 | } 34 | // 清除指定依赖 35 | function cleanupEffect(effect: ReactiveEffect) { 36 | // 对effect解构,解出deps,减少对象在词法环境寻找属性的次数 37 | const { deps } = effect 38 | if (deps.length !== 0) { 39 | for (let i = 0; i < deps.length; i++) { 40 | deps[i].delete(effect) 41 | } 42 | deps.length = 0 43 | } 44 | } 45 | const targetMap = new Map, Map>>() 46 | // 当前正在执行的effect 47 | let activeEffect: ReactiveEffect 48 | let shouldTrack = false 49 | type EffectKey = string 50 | type IDep = ReactiveEffect 51 | // 这个track的实现逻辑很简单:添加依赖 52 | export function track(target: Record, key: EffectKey) { 53 | // 这里为什么要多一层非空判断呢? 54 | // 我们查看reactive.spec.ts里面的测试用例 55 | // 测试用例里根本就没有调用effect(),所以没有执行ReactiveEffect的run()自然activeEffect也就是undefined了 56 | // if (!activeEffect) return 57 | // 应不应该收集依赖,从而避免删了依赖又重新添加新的依赖 58 | // if (!shouldTrack) return 59 | if (!isTracking()) return 60 | // 寻找dep依赖的执行顺序 61 | // target -> key -> dep 62 | let depsMap = targetMap.get(target) 63 | /** 64 | * 这里有个疑问:target为{ num: 11 } 的时候我们能获取到depsMap,之后我们count.num++,为什么target为{ num: 12 } 的时候我们还能获取得到相同的depsMap呢? 65 | * 这里我的理解是 targetMap的key存的只是target的引用 存的字符串就不一样了 66 | */ 67 | // 解决初始化没有depsMap的情况 68 | if (!depsMap) { 69 | depsMap = new Map() 70 | targetMap.set(target, depsMap) 71 | } 72 | // dep是一个Set对象,存放着这个key相对应的所有依赖 73 | let dep = depsMap.get(key) 74 | // 如果没有key相对应的Set 初始化Set 75 | if (!dep) { 76 | dep = new Set() 77 | depsMap.set(key, dep) 78 | } 79 | trackEffect(dep) 80 | } 81 | // 依赖收集 82 | export function trackEffect(dep: Dep) { 83 | // 避免不必要的add操作 84 | if (dep.has(activeEffect)) return 85 | // 将activeEffect实例对象add给deps 86 | dep.add(activeEffect) 87 | // activeEffect的deps 接收 Set类型的deps 88 | // 供删除依赖的时候使用(停止监听依赖) 89 | activeEffect.deps.push(dep) 90 | } 91 | export function isTracking() { 92 | return activeEffect !== undefined && shouldTrack 93 | } 94 | // 这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行 95 | export function trigger(target: Record, key: EffectKey) { 96 | const depsMap = targetMap.get(target) 97 | const dep = depsMap?.get(key) 98 | if (dep) { 99 | triggerEffect(dep) 100 | } 101 | } 102 | // 触发依赖 103 | export function triggerEffect(dep: Dep) { 104 | for (let effect of dep) { 105 | if (effect.scheduler) { 106 | effect.scheduler() 107 | } else { 108 | effect.run() 109 | } 110 | } 111 | } 112 | export interface EffectOption { 113 | scheduler?: EffectScheduler 114 | onStop?: () => void 115 | } 116 | // 里面存有一个匿名函数 117 | export interface EffectRunner { 118 | (): T 119 | effect: ReactiveEffect 120 | } 121 | // 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖 122 | export function effect( 123 | fn: () => T, 124 | option?: EffectOption 125 | ): EffectRunner { 126 | let _effect = new ReactiveEffect(fn) 127 | if (option) { 128 | extend(_effect, option) 129 | } 130 | _effect.run() 131 | // 注意这里的this指向,return 出去的run方法,方法体里需要用到this,且this必须指向ReactiveEffect的实例对象 132 | // 不用bind重新绑定this,this会指向undefined 133 | let runner = _effect.run.bind(_effect) as EffectRunner 134 | // 这里的effect挂载在了函数runner上,作为属性,这是利用了js中函数可以挂在属性的特性 135 | // 之后呢,实现stop的时候runner就能拿到ReactiveEffect实例对象了 136 | runner.effect = _effect 137 | return runner 138 | } 139 | // 删除依赖 140 | export function stop(runner: EffectRunner) { 141 | runner.effect.stop() 142 | } 143 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ReactiveFlags, 3 | reactive, 4 | readonly, 5 | shallowReadonly, 6 | shallowReactive, 7 | isReactive, 8 | isReadonly, 9 | isProxy, 10 | isShallow, 11 | toRaw, 12 | } from './reactive' 13 | export { ref, isRef, unref, proxyRefs } from './ref' 14 | export { 15 | track, 16 | trackEffect, 17 | trigger, 18 | triggerEffect, 19 | effect, 20 | stop, 21 | } from './effect' 22 | export { 23 | ComputedGetter, 24 | ComputedSetter, 25 | WritableComputedOptions, 26 | computed, 27 | } from './computed' 28 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createReactiveObject, 3 | mutableHandlers, 4 | readonlyHandlers, 5 | shallowReadonlyHandlers, 6 | shallowReactiveHandlers, 7 | } from './baseHandlers' 8 | 9 | export enum ReactiveFlags { 10 | IS_REACTIVE = '__v_isReactive', 11 | IS_READONLY = '__v_isReadonly', 12 | IS_SHALLOW = '__v_isShallow', 13 | RAW = '__v_raw', 14 | } 15 | // 给value做类型批注,让value有以下几个可选属性,不然该死的value飘红 --isReactive函数和isReadonly函数 说的就是你们 16 | export interface Target { 17 | [ReactiveFlags.IS_REACTIVE]?: boolean 18 | [ReactiveFlags.IS_READONLY]?: boolean 19 | [ReactiveFlags.IS_SHALLOW]?: boolean 20 | [ReactiveFlags.RAW]?: any 21 | } 22 | 23 | export function reactive(target: T) { 24 | return createReactiveObject(target, mutableHandlers) 25 | } 26 | 27 | // 其实就是一个没有set操作的reactive(会深层readonly) 28 | export function readonly(target: T) { 29 | return createReactiveObject(target, readonlyHandlers) 30 | } 31 | 32 | // 浅浅的readonly一下,创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值) 33 | export function shallowReadonly(target: T) { 34 | return createReactiveObject(target, shallowReadonlyHandlers) 35 | } 36 | export function shallowReactive(target: T) { 37 | return createReactiveObject(target, shallowReactiveHandlers) 38 | } 39 | 40 | export function isReactive(value: unknown) { 41 | // target没有__v_isReactive这个属性,为什么还要写target['__v_isReactive']呢?因为这样就会触发proxy的get操作, 42 | // 通过判断createGetter传入的参数isReadonly是否为true,否则isReactive为true 43 | // 优化点:用enum管理状态,增强代码可读性 44 | return !!(value as Target)[ReactiveFlags.IS_REACTIVE] 45 | } 46 | 47 | export function isReadonly(value: unknown) { 48 | // 同上 49 | return !!(value as Target)[ReactiveFlags.IS_READONLY] 50 | } 51 | // 检查对象是否是由 reactive 或 readonly 创建的 proxy。 52 | export function isProxy(value: unknown) { 53 | return isReactive(value) || isReadonly(value) 54 | } 55 | // 检查对象是否 开启 shallow mode 56 | export function isShallow(value: unknown) { 57 | return !!(value as Target)[ReactiveFlags.IS_SHALLOW] 58 | } 59 | // 返回 reactive 或 readonly 代理的原始对象 60 | export function toRaw(observed: T): T { 61 | // observed存在,触发get操作,在createGetter直接return target 62 | const raw = observed && (observed as Target)[ReactiveFlags.RAW] 63 | return raw ? toRaw(raw) : observed 64 | } 65 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | // 为什么要有ref呢,reactive不行吗? 2 | // 因为reactive用的是proxy,而proxy只能针对对象去监听数据变化,基本数据类型并不能用proxy 3 | // 所以我们想到了class里面的取值函数getter和存值函数getter,他们都能在数据变化的时候对数据加以操作。 4 | import { hasChanged, isObject } from '../shared' 5 | import { Dep } from './effect' 6 | import { triggerEffect, trackEffect, isTracking } from './effect' 7 | import { isReactive, reactive } from './reactive' 8 | export interface Ref { 9 | value: T 10 | } 11 | // 定义一个RefImpl类 12 | class RefImpl { 13 | private _value: T 14 | private _rawValue: T 15 | public dep?: Dep = undefined 16 | public __v_isRef = true // 标识是ref对象 17 | constructor(value: any) { 18 | this._value = convert(value) 19 | this._rawValue = value 20 | this.dep = new Set() 21 | } 22 | get value() { 23 | trackRefValue(this) 24 | return this._value 25 | } 26 | set value(newValue: any) { 27 | // 触发依赖 28 | 29 | // 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点 30 | if (hasChanged(newValue, this._rawValue)) { 31 | // 注意这里先赋值再触发依赖 32 | this._value = convert(newValue) 33 | this._rawValue = newValue 34 | triggerEffect(this.dep as Dep) 35 | } 36 | } 37 | } 38 | function trackRefValue(ref: RefImpl) { 39 | // 有时候根本就没有调用effect(),也就是说activeEffect是undefined的情况 40 | if (isTracking()) { 41 | // 依赖收集 42 | trackEffect(ref.dep as Dep) 43 | } 44 | } 45 | // 判断value是否是对象,是:reactive ,否:基本数据类型,直接返回 46 | function convert(value: any) { 47 | return isObject(value) ? reactive(value) : value 48 | } 49 | export function ref(value: T): Ref { 50 | return new RefImpl(value) 51 | } 52 | // 检查值是否为一个 ref 对象。 53 | export function isRef(ref: any) { 54 | return !!(ref && ref.__v_isRef) 55 | } 56 | // 如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。 57 | export function unref(ref: any) { 58 | return isRef(ref) ? ref.value : ref 59 | } 60 | // 通常用在vue3 template里面ref取值,在template里面不需要.value就可以拿到ref的值 61 | export function proxyRefs(obj: T) { 62 | return isReactive(obj) 63 | ? obj 64 | : new Proxy(obj, { 65 | get(target, key) { 66 | // unref已经处理了是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值 67 | return unref(Reflect.get(target, key)) 68 | }, 69 | set(target, key, value) { 70 | // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value 71 | if (isRef(target[key]) && !isRef(value)) { 72 | target[key].value = value 73 | return true 74 | } else { 75 | return Reflect.set(target, key, value) 76 | } 77 | }, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/reactivity/test/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import {computed} from '../computed' 2 | import { reactive } from '../reactive' 3 | 4 | describe('reactivity/computed', () => { 5 | it('should return updated value', () => { 6 | const value = reactive({ foo: 1 }) 7 | const cValue = computed(() => value.foo) 8 | // return 一个 ref对象 9 | expect(cValue.value).toBe(1) 10 | }) 11 | 12 | it('should compute lazily', () => { 13 | const value = reactive({ foo: 1 }) 14 | const getter = jest.fn(() => value.foo) 15 | const cValue = computed(getter) 16 | 17 | // lazy功能 18 | expect(getter).not.toHaveBeenCalled() 19 | 20 | expect(cValue.value).toBe(1) 21 | expect(getter).toHaveBeenCalledTimes(1) 22 | // 缓存功能 23 | // should not compute again 24 | cValue.value 25 | expect(getter).toHaveBeenCalledTimes(1) 26 | 27 | // should not compute until needed 28 | value.foo = 2 29 | expect(getter).toHaveBeenCalledTimes(1) 30 | 31 | // now it should compute 32 | expect(cValue.value).toBe(2) 33 | expect(getter).toHaveBeenCalledTimes(2) 34 | // should not compute again 35 | cValue.value 36 | expect(getter).toHaveBeenCalledTimes(2) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/reactivity/test/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect, stop } from "../effect" 2 | import { reactive } from "../reactive" 3 | describe('effect test', () => { 4 | it('effect', () => { 5 | // 创建proxy代理 6 | let count = reactive({ num: 11 }) 7 | let result = 0 8 | // 立即执行effect并跟踪依赖 9 | effect(() => { 10 | // count.num触发get 存储依赖 11 | result = count.num + 1 12 | }) 13 | expect(result).toBe(12) 14 | // 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result 15 | count.num++ 16 | expect(result).toBe(13) 17 | }) 18 | // 实现effect返回runner函数 这个runner函数其实就是effect的回调函数 19 | it('should return runner when effect was called', () => { 20 | let foo = 1 21 | let runner = effect(() => { 22 | foo++ 23 | return 'foo' 24 | }) 25 | expect(foo).toBe(2) 26 | let returnValue = runner() 27 | expect(foo).toBe(3) 28 | expect(returnValue).toBe('foo') 29 | }) 30 | // 实现effect的scheduler功能 31 | // 该功能描述: 32 | // 1. effect首次执行的时候不执行scheduler,直接执行回调函数 33 | // 2. 之后每次触发trigger函数的时候都会执行scheduler函数,不执行effect回调函数 34 | // 3. 当调用run的时候才会触发runner,也就是说调用effect的回调函数 35 | it('scheduler', () => { 36 | let dummy 37 | let run: any 38 | const scheduler = jest.fn(() => { 39 | run = runner 40 | }) 41 | const obj = reactive({ foo: 1 }) 42 | const runner = effect( 43 | () => { 44 | dummy = obj.foo 45 | }, 46 | { scheduler } 47 | ) 48 | expect(scheduler).not.toHaveBeenCalled() 49 | expect(dummy).toBe(1) 50 | // should be called on first trigger set操作的时候,也就是说在trigger被调用的时候 51 | obj.foo++ 52 | expect(scheduler).toHaveBeenCalledTimes(1) 53 | // should not run yet 54 | expect(dummy).toBe(1) 55 | // manually run 会触发effect的回调函数 56 | run() 57 | // should have run 58 | expect(dummy).toBe(2) 59 | }) 60 | // 实现effect的stop功能 61 | // 功能描述: 62 | // 通过stop可以停止监听依赖,怎么样停止监听依赖呢?可以通过删除deps依赖,那么trigger被调用的时候就不会被循环调用这个依赖了 63 | it('stop', () => { 64 | let dummy 65 | const obj = reactive({ prop: 1 }) 66 | const runner = effect(() => { 67 | dummy = obj.prop 68 | }) 69 | obj.prop = 2 70 | expect(dummy).toBe(2) 71 | stop(runner) 72 | // 单单只是检查set操作是不行的,还必须检查代码通过get操作之后,是否还能执行依赖 73 | // obj.prop = 3 74 | // 很明显如果换成obj.prop++,expect(dummy).toBe(2)就飘红了 75 | // 这是因为obj.prop还有一个get操作,经过get操作之后,经过track函数之后原来被删除的effect又被add到deps上面去了 76 | // 所以我们这里必须添加shouldtrack变量来表示应不应该被track 详细见effect.ts的track函数,控制shouldTrack开关在ReactiveEffect的run方法 77 | obj.prop++ 78 | expect(dummy).toBe(2) 79 | 80 | // stopped effect should still be manually callable 81 | runner() 82 | expect(dummy).toBe(3) 83 | }) 84 | // 实现onStop 85 | // 功能描述: 86 | // 1. 当stop对一个runner执行的时候,runner对应的依赖的onStop就会被执行,相当于事件触发 87 | it('onStop', () => { 88 | const obj = reactive({ foo: 1 }) 89 | const onStop = jest.fn() 90 | let dummy 91 | const runner = effect( 92 | () => { 93 | dummy = obj.foo 94 | }, 95 | { 96 | onStop, 97 | } 98 | ) 99 | stop(runner) 100 | // 被调用1次 101 | expect(onStop).toBeCalledTimes(1) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /src/reactivity/test/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive, isReactive, isProxy, toRaw } from '../reactive' 2 | 3 | describe('reactive', () => { 4 | it('reactive test', () => { 5 | let original = { num: 1 } 6 | let count = reactive(original) 7 | expect(original).not.toBe(count) 8 | expect(count.num).toEqual(1) 9 | expect(isReactive(original)).toBe(false) 10 | expect(isReactive(count)).toBe(true) 11 | expect(isProxy(count)).toBe(true) 12 | }) 13 | it('nested reactive', () => { 14 | let original = { 15 | foo: { 16 | name: 'ghx', 17 | }, 18 | arr: [{ age: 23 }], 19 | } 20 | const nested = reactive(original) 21 | expect(isReactive(nested.foo)).toBe(true) 22 | expect(isReactive(nested.arr)).toBe(true) 23 | expect(isReactive(nested.arr[0])).toBe(true) 24 | expect(isReactive(nested.foo)).toBe(true) 25 | // expect(isReactive(nested.foo.name)).toBe(true) // 涉及到往后的知识点 isRef 26 | }) 27 | test('toRaw', () => { 28 | const original = { foo: 1 } 29 | const observed = reactive(original) 30 | // 输出的结果必须要等于原始值 31 | expect(toRaw(observed)).toBe(original) 32 | expect(toRaw(original)).toBe(original) 33 | }) 34 | it('nested reactive toRaw', () => { 35 | const original = { 36 | foo: { 37 | name: 'ghx', 38 | }, 39 | } 40 | const observed = reactive(original) 41 | const raw = toRaw(observed) 42 | expect(raw).toBe(original) 43 | expect(raw.foo).toBe(original.foo) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/reactivity/test/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { readonly, isReadonly, isProxy } from '../reactive' 2 | describe('readonly', () => { 3 | it('readonly not set', () => { 4 | let original = { 5 | foo: { 6 | fuck: { 7 | name: 'what', 8 | }, 9 | }, 10 | arr: [{color: '#fff'}] 11 | } 12 | let warper = readonly(original) 13 | expect(warper).not.toBe(original) 14 | expect(isReadonly(warper)).toBe(true) 15 | expect(isReadonly(original)).toBe(false) 16 | // 测试嵌套对象的reactive状态 17 | expect(isReadonly(warper.foo.fuck)).toBe(true) 18 | // expect(isReadonly(warper.foo.fuck.name)).toBe(true) // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef 19 | expect(isReadonly(warper.arr)).toBe(true) 20 | expect(isReadonly(warper.arr[0])).toBe(true) 21 | expect(warper.foo.fuck.name).toBe('what') 22 | expect(isProxy(warper)).toBe(true) 23 | expect(isProxy(warper.foo.fuck.name)).toBe(false) 24 | 25 | }) 26 | it('warning when it be call set operation', () => { 27 | let original = { 28 | username: 'ghx', 29 | } 30 | let readonlyObj = readonly(original) 31 | const warn = jest.spyOn(console, 'warn') 32 | readonlyObj.username = 'danaizi' 33 | expect(warn).toHaveBeenCalled() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/reactivity/test/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { ref, isRef, unref, proxyRefs } from '../ref' 2 | import {effect} from '../effect' 3 | import { computed } from '../computed' 4 | import { reactive } from '../reactive' 5 | describe('ref',()=>{ 6 | it('should hold a value', () => { 7 | const a = ref(1) 8 | expect(a.value).toBe(1) 9 | a.value = 2 10 | expect(a.value).toBe(2) 11 | }) 12 | 13 | it('should be reactive', () => { 14 | const a = ref(1) 15 | let dummy 16 | let calls = 0 17 | effect(() => { 18 | calls++ 19 | dummy = a.value 20 | }) 21 | expect(calls).toBe(1) 22 | expect(dummy).toBe(1) 23 | a.value = 2 24 | expect(calls).toBe(2) 25 | expect(dummy).toBe(2) 26 | // same value should not trigger 27 | a.value = 2 28 | expect(calls).toBe(2) 29 | }) 30 | 31 | it('should make nested properties reactive', () => { 32 | const a = ref({ 33 | count: 1, 34 | }) 35 | let dummy 36 | effect(() => { 37 | dummy = a.value.count 38 | }) 39 | expect(dummy).toBe(1) 40 | a.value.count = 2 41 | expect(dummy).toBe(2) 42 | }) 43 | test('isRef', () => { 44 | expect(isRef(ref(1))).toBe(true) 45 | 46 | expect(isRef(0)).toBe(false) 47 | expect(isRef(1)).toBe(false) 48 | // an object that looks like a ref isn't necessarily a ref 49 | expect(isRef({ value: 0 })).toBe(false) 50 | }) 51 | test('unref', () => { 52 | expect(unref(1)).toBe(1) 53 | expect(unref(ref(1))).toBe(1) 54 | }) 55 | test('proxyRefs', ()=>{ 56 | const user = { 57 | age: ref(10), 58 | name: 'ghx' 59 | } 60 | const original = { 61 | k: 'v' 62 | } 63 | const r1 = reactive(original) 64 | const p1 = proxyRefs(r1) 65 | const proxyUser = proxyRefs(user) 66 | 67 | expect(p1).toBe(r1) 68 | 69 | expect(user.age.value).toBe(10) 70 | expect(proxyUser.age).toBe(10) 71 | expect(proxyUser.name).toBe('ghx') 72 | 73 | proxyUser.age = 20 74 | expect(proxyUser.age).toBe(20) 75 | expect(user.age.value).toBe(20) 76 | 77 | proxyUser.age = ref(10) 78 | proxyUser.name = 'superman' 79 | expect(proxyUser.age).toBe(10) 80 | expect(proxyUser.name).toBe('superman') 81 | expect(user.age.value).toBe(10) 82 | }) 83 | test('should support setter', ()=>{ 84 | const count = ref(1) 85 | const plusOne = computed({ 86 | get: () => count.value + 1, 87 | set: val => { 88 | count.value = val - 1 89 | } 90 | }) 91 | plusOne.value = 1 92 | expect(count.value).toBe(0) 93 | }) 94 | 95 | }) 96 | -------------------------------------------------------------------------------- /src/reactivity/test/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowReactive, isReactive, isShallow, shallowReadonly, reactive } from '../reactive' 2 | describe('shallowReactive test', () => { 3 | it('should not make non-reactive properties reactive', () => { 4 | const original = { 5 | foo: { 6 | name: 'ghx', 7 | }, 8 | } 9 | const props = shallowReactive(original) 10 | expect(isReactive(props)).toBe(true) 11 | expect(isReactive(props.foo)).toBe(false) 12 | expect(isShallow(props)).toBe(true) 13 | expect(isShallow(props.foo)).toBe(false) 14 | }) 15 | it('isShallow', () => { 16 | expect(isShallow(shallowReactive({}))).toBe(true) 17 | expect(isShallow(shallowReadonly({}))).toBe(true) 18 | }) 19 | test('should keep reactive properties reactive', () => { 20 | const props: any = shallowReactive({ n: reactive({ foo: 1 }) }) 21 | props.n = reactive({ foo: 2 }) 22 | expect(isReactive(props.n)).toBe(true) 23 | }) 24 | // shadowReactive只是监听了第一层的属性变化,第一层属性变化,视图会更新;其他层属性变化,视图不会更新 25 | // 虽然proxy里面的深层属性已经改变了,但是并没有在视图上体现出来,视图上的值仍然是旧值。 26 | it('should be non-reactive in nested object', () => { 27 | const raw = { 28 | foo: 1, 29 | nested: { 30 | bar: 2, 31 | }, 32 | } 33 | const state = shallowReactive(raw) 34 | 35 | // 改变 state 本身的性质是响应式的 36 | state.foo++ 37 | expect(raw.foo).toBe(2) 38 | expect(state.foo).toBe(2) 39 | // ...但是不转换嵌套对象 40 | expect(isReactive(state.nested)).toBe(false) // false 41 | state.nested.bar++ // 非响应式 42 | expect(raw.nested.bar).toBe(3) 43 | // 视图上的值仍然是2。 44 | expect(state.nested.bar).toBe(3) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/reactivity/test/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import {isReactive, isReadonly, shallowReadonly} from '../reactive' 2 | describe('shallowReadonly test', () => { 3 | it('shallowReadonly basic test', () => { 4 | let original = { 5 | foo: { 6 | name: 'ghx' 7 | } 8 | } 9 | let obj = shallowReadonly(original) 10 | expect(isReadonly(obj)).toBe(true) 11 | // 因为只做表层的readonly,深层的数据还不是proxy 12 | expect(isReadonly(obj.foo)).toBe(false) 13 | expect(isReactive(obj.foo)).toBe(false) 14 | }) 15 | it('should change value in nested obj', () => { 16 | const raw = { 17 | foo: 1, 18 | nested: { 19 | bar: 2, 20 | }, 21 | } 22 | const state = shallowReadonly(raw) 23 | 24 | // 改变 state 本身的 property 将失败 25 | state.foo++ 26 | expect(state.foo).toBe(1) 27 | // ...但适用于嵌套对象 28 | expect(isReadonly(state.nested)).toBe(false) // false 29 | state.nested.bar++ // 适用 30 | expect(state.nested.bar).toBe(3) 31 | expect(raw.nested.bar).toBe(3) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/runtime-core/apiCreateApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | export function createAppAPI(render: any) { 4 | return function createApp(rootComponent: any) { 5 | const mount = (rootContainer: any) => { 6 | const vnode = createVNode(rootComponent) 7 | 8 | render(vnode, rootContainer) 9 | } 10 | return { 11 | mount, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../shared' 2 | import { getCurrentInstance } from './component' 3 | // 跨组件数据共享 4 | export function provide(key: string | number, value: T){ 5 | // 提供者 6 | // key和value存在哪呢?挂在instance的provides属性上吧! 7 | 8 | const currentInstance: any = getCurrentInstance() 9 | if(currentInstance){ 10 | let { provides } = currentInstance 11 | const parentProvides = currentInstance.parent?.provides 12 | if(provides === parentProvides){ 13 | // 把provide原型指向父组件的provide 14 | provides = currentInstance.provides = Object.create(parentProvides) 15 | } 16 | provides[key] = value 17 | } 18 | } 19 | export function inject(key: string, defaultValue?: T){ 20 | // 接收者 21 | // 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide 22 | const currentInstance: any = getCurrentInstance() 23 | if(currentInstance){ 24 | const parentProvides = currentInstance.parent.provides 25 | 26 | if (key in parentProvides){ 27 | return parentProvides[key] 28 | }else{ // 找不到注入的 29 | // 如果默认值是函数,执行函数 30 | if (isFunction(defaultValue)) { 31 | return defaultValue() 32 | } 33 | return defaultValue 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs } from '../reactivity' 2 | import { shallowReadonly } from '../reactivity/reactive' 3 | import { isFunction, isObject } from '../shared' 4 | import { emit } from './componentEmit' 5 | import { initProps } from './componentProps' 6 | import { publicInstanceProxyHandlers } from './componentPublicInstance' 7 | import { initSlots } from './componentSlots' 8 | 9 | export type Data = Record 10 | 11 | export function createComponentInstance(vnode: any, parentComponent: any) { 12 | console.log('createComponentInstance', parentComponent) 13 | const type = vnode.type 14 | const instance = { 15 | vnode, // 老的vnode 16 | type, 17 | render: null, 18 | next: null, // 新的vnode 19 | setupState: {}, 20 | props: {}, 21 | emit: () => {}, 22 | slots: {}, 23 | isMounted: false, 24 | subTree: {}, 25 | update: null, 26 | provides: parentComponent ? parentComponent.provides : {} as Record, // 确保中间层的组件没有提供provide时,子组件拿最近的有provide的组件的数据 27 | parent: parentComponent, // 父组件的组件实例 28 | } 29 | instance.emit = emit.bind(null, instance) as any 30 | return instance 31 | } 32 | // 33 | export function setupComponent(instance: any) { 34 | // 初始化组件外部传给组件的props 35 | initProps(instance, instance.vnode.props) 36 | initSlots(instance, instance.vnode.children) 37 | setupStatefulComponent(instance) 38 | } 39 | // 初始化有状态的组件 40 | function setupStatefulComponent(instance: any) { 41 | const Component = instance.type 42 | // 解决render返回的h()函数里面this的问题,指向setup函数 43 | instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers) 44 | const { setup } = Component 45 | // 有时候用户并没有使用setup() 46 | if (setup) { 47 | // 处理setup的返回值,如果返回的是对象,那么把对象里面的值注入到template上下文中 48 | // 如果是一个函数h(),那么直接render 49 | setCurrentInstance(instance) 50 | const setupResult = setup(shallowReadonly(instance.props), { 51 | emit: instance.emit 52 | }) 53 | 54 | handleSetupResult(instance, setupResult) 55 | setCurrentInstance(null) 56 | } 57 | finishComponentSetup(instance) 58 | } 59 | // 处理组件的setup的返回值 60 | function handleSetupResult(instance: any, setupResult: any) { 61 | // TODO handle function 62 | if (isFunction(setupResult)) { 63 | instance.render = setupResult 64 | } else if (isObject(setupResult)) { 65 | // 把setup返回的对象挂载到setupState上 proxyRefs对setupResult解包(在template上不必书写.value来获取ref值) 66 | instance.setupState = proxyRefs(setupResult) 67 | } 68 | } 69 | // 结束组件的安装 70 | function finishComponentSetup(instance: any) { 71 | const Component = instance.type // 遇到h('div',{}, this.name) 这里Component将为'div' 72 | 73 | if (instance) { 74 | instance.render = Component.render 75 | } 76 | } 77 | export let currentInstance = null 78 | export function getCurrentInstance(){ 79 | return currentInstance 80 | } 81 | function setCurrentInstance(instance: any){ 82 | currentInstance = instance 83 | } 84 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, toHandlerKey } from "../shared" 2 | 3 | // 触发父组件自定义事件就好像vue2的$emit 4 | export function emit(instance: any, event: string, ...args: unknown[]){ 5 | const {props} = instance 6 | 7 | 8 | const eventName = toHandlerKey(camelCase(event)) 9 | console.log(eventName) 10 | const handler = props[eventName] 11 | handler && handler(...args) 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance: any, rawProps: any) { 2 | instance.props = rawProps || {} 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '../shared' 2 | 3 | export type PublicPropertiesMap = Record any> 4 | // 实例property 5 | const publicPropertiesMap: PublicPropertiesMap = { 6 | $el: (i: any) => i.vnode.el, 7 | $slots: (i: any) => i.slots, 8 | $props: (i: any) => i.props 9 | } 10 | // render函数的this指向,将会指向setup的返回值 11 | export const publicInstanceProxyHandlers: ProxyHandler = { 12 | get({ _: instance }, key: string) { 13 | const { setupState, props } = instance 14 | // 在 setup 的 return 中寻找key setupState 是 setup 返回的对象 15 | if (hasOwn(setupState, key)) { 16 | return setupState[key] 17 | // 在setup的参数props中寻找key 18 | } else if (hasOwn(props, key)) { 19 | return props[key] 20 | } 21 | // 在publicPropertiesMap中寻找key,并调用,返回结果 解决 this.$props.anything 引用 22 | const publicGetter = publicPropertiesMap[key] 23 | if (publicGetter) { 24 | return publicGetter(instance) 25 | } 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/runtime-core/componentRenderUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(oldVNode: any, newVNode: any){ 2 | const {props: oldProps} = oldVNode 3 | const {props: newProps} = newVNode 4 | 5 | for(let key in newProps){ 6 | if(newProps[key] !== oldProps[key]){ 7 | return true 8 | } 9 | } 10 | return false 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { isArray, ShapeFlags } from '../shared' 2 | // 如果children里面有slot,那么把slot挂载到instance上 3 | export function initSlots(instance: any, children: any) { 4 | const { vnode } = instance 5 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 6 | normalizeObjectSlots(instance.slots, children) 7 | } 8 | } 9 | // 具名name作为instance.slots的属性名,属性值是vnode 10 | function normalizeObjectSlots(slots: any, children: any) { 11 | console.log('slots children===>' ,children) 12 | // 遍历对象 13 | for (let key in children) { 14 | const value = children[key] 15 | slots[key] = (props: any) => normalizeSlotValue(value(props)) 16 | // slots[key] = normalizeSlotValue(value) 17 | } 18 | // slots = normalizeSlotValue(slots) 19 | } 20 | function normalizeVNodeSlots(slots: any, children: any){ 21 | 22 | } 23 | // 转成数组 24 | function normalizeSlotValue(value: any) { 25 | return isArray(value) ? value : [value] 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime-core/docs/1.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀vue3 从 template 到真实 DOM 的渲染(一) 2 | 3 | ## 前言 4 | 5 | 本文将会实现一个简单的 vue 运行时 runtime,通过 rollup 打包 runtime,并确保能在 html 中渲染出元素。 6 | 7 | 本文的函数命名全部采用 vue3 的函数名称,降低对 vue3 源码的学习成本。在巩固自己对 vue3 的理解的同时,把知识分享给大家是我的乐趣所在。 8 | 9 | > 🤣 如有错漏,请多指教 ❤ 10 | 11 | ## 简述一下大致的过程 12 | 13 | 1. template 编译成 render 函数 14 | 2. 根据编译后的结果生成 vnode 15 | 3. patch 函数对 vnode 进行`拆箱`操作 16 | 4. 通过 createElement 把 vnode 转成真实的 DOM 17 | 5. 挂载 dom 到指定的 目标 容器上 18 | 19 | ## 预览本 demo 的使用方法 20 | > 本demo暂时只实现组件和HTML元素挂载DOM功能。实现组件节点更新功能在后续章节。 21 | > 22 | 万事开头,有一个最终结果往往能让我们朝着一个方向前进。 23 | 24 | ```html 25 | 45 | 46 |
47 | 48 | 49 | ``` 50 | ```javascript 51 | // In main.js 52 | 53 | const rootContainer = document.querySelector('#app') 54 | 55 | createApp(App).mount(rootContainer) 56 | ``` 57 | ```javascript 58 | // In app.js 59 | // 这里就是template经过编译后,得到的根组件组合对象(如果用户使用optionsAPI,测绘得到根组件选项对象),里面会包含一个render()函数 60 | export default { 61 | render() { 62 | return h('div', { 63 | id: 'root', 64 | class: ['flex', 'container-r'] 65 | }, [ 66 | h('p', {class: 'red'}, 'red'), 67 | h('p', {class: 'blue'}, 'blue') 68 | ]) 69 | }, 70 | setup() { 71 | // 返回对象或者h()渲染函数 72 | return { 73 | name: 'hi my app' 74 | } 75 | } 76 | } 77 | 78 | ``` 79 | 80 | ## 编译 template 成 render 函数 81 | 这里关于template怎么编译成具有render函数的特殊对象,具体代码实现我们暂时忽略。它涉及AST的知识。 82 | 83 | 流程大致分为三大点: 84 | 1. parse 85 | 86 | * 解析template生成AST节点 87 | 88 | 2. transform 89 | 90 | * 针对AST进行一些转换 91 | 3. codegen 92 | 93 | * 根据不同的AST节点调用代码生成函数输出代码字符串,进而生成render函数 94 | 95 | 96 | 97 | ## 生成虚拟节点 vnode 98 | 为什么要有vnode这个东西? 99 | 100 | vnode是真实dom的抽象存在,当我们业务越来越复杂,这时候大量操作dom显然会消耗大量的性能,而有了vnode 的存在,我们可以预先在vnode进行操作,操作完成之后,在统一把真实想要的dom的样子渲染处理,不必在dom每次操作一次渲染一次。 101 | 102 | 那什么时候生成vnode呢? 103 | 104 | 创建vue上下文的时候,createApp接受根组件选项对象或者根组件组合对象,并返回一系列的应用API(component、mount、config、use等等),其中mount功能把传入的根组件选项对象或者根组件组合对象挂载到指定的根节点中。在这个挂载之前必须把根组件对象转换成vnode,这时候就要调用`createVNode` 105 | 106 | 我们可以简单实现以下createApp 107 | ```typescript 108 | export function createApp(rootComponent: any) { 109 | 110 | // mount函数可接受dom实例对象或者dom的ID属性(string) 111 | const mount = (rootContainer: any) => { 112 | const vnode = createVNode(rootComponent) 113 | // 根组件的vnode 114 | render(vnode, rootContainer) 115 | } 116 | return { 117 | mount, 118 | } 119 | } 120 | ``` 121 | 122 | type是组件对象或者元素,props是组件的内联属性,children是组件的子组件或者子元素。 123 | ```typescript 124 | 125 | // 由于只是实现简单的渲染vnode功能,所以目前只需要返回vnode对象 126 | export function createVNode(type: any, props?: any, children?: any) { 127 | const vnode = { 128 | type, 129 | props, 130 | children, 131 | } 132 | return vnode 133 | } 134 | 135 | ``` 136 | 按照官方文档来说[`h()`函数](https://v3.cn.vuejs.org/api/global-api.html#h)的实现也是`createVNode()`,返回一个虚拟节点。 137 | ```typescript 138 | export function h(type: any, props?: any, children?: any) { 139 | return createVNode(type, props, children) 140 | } 141 | ``` 142 | 143 | render内部用于执行patch()(什么是patch,见下方*经过 patch 拆箱*) 144 | ```typescript 145 | export function render(vnode: any, container: any) { 146 | // 做patch算法 147 | patch(vnode, container) 148 | } 149 | ``` 150 | 151 | ## 经过 patch 拆箱 152 | 153 | patch会判断当前传入参数的`vnode.type`属性是什么类型。 154 | 155 | * 如果vnode.type是`string`类型,说明这个vnode是普通元素标签,patch内部会调用processElement进行对普通元素的vnode继续处理,processElement内部又用了mountElement()把vnode.type用createElement创建出dom元素。 156 | 157 | * 如果vnode.type是`object`类型,说明这个vnode是一个组件。再次调用vnode.type.render()可以得到子元素的vnode,用子元素的vnode再次调用patch()进行拆箱操作,直到vnode.type是普通元素标签为止。 158 | ```typescript 159 | // 传入vnode,递归对一个组件或者普通元素进行拆箱,在内部对vnode的type判断执行不同的处理函数 160 | function patch(vnode: any, container: any) { 161 | // 检查是什么类型的vnode 162 | console.log('vnode', vnode.type) 163 | if(typeof vnode.type === 'string'){ 164 | // 是一个普通元素?处理vnode是普通标签的情况 165 | processElement(vnode, container) 166 | }else if(isObject(vnode.type)){ 167 | // 是一个组件?处理vnode是组件的情况 168 | processComponent(vnode, container) 169 | } 170 | } 171 | ``` 172 | ## processComponent 处理组件 173 | 174 | 处理组件的事情大致分为三件事: 175 | 1. 创建组件实例`instance`。 176 | 2. 把setup的返回值`setupState`挂载在组件实例`instance`上。 177 | 3. 把render函数挂载在组件实例`instance`上,以便对render返回的vnode做patch()拆箱处理。 178 | 179 | 这些事情都在mountComponent里面完成 180 | 181 | ```typescript 182 | 183 | function processComponent(vnode: any, container: any) { 184 | mountComponent(vnode, container) 185 | } 186 | function mountComponent(vnode: any, container: any) { 187 | const instance = createComponentInstance(vnode) 188 | // 安装组件 189 | setupComponent(instance) 190 | 191 | // 192 | setupRenderEffect(instance, container) 193 | } 194 | // 创建组件实例 195 | export function createComponentInstance(vnode: any) { 196 | const type = vnode.type 197 | const instance = { 198 | vnode, 199 | type, 200 | } 201 | return instance 202 | } 203 | function setupRenderEffect(instance: any, container: any) { 204 | console.log(instance) 205 | // 这个render()已经在finishComponentSetup处理过了,就是 instance.type.render() 特殊对象的render() 206 | const subTree = instance.render() 207 | // 对子树进行拆箱操作 208 | patch(subTree, container) 209 | } 210 | // 211 | export function setupComponent(instance: any) { 212 | // initProps() 213 | // initSlots() 214 | setupStatefulComponent(instance) 215 | } 216 | // 初始化组件的状态 217 | function setupStatefulComponent(instance: any) { 218 | const Component = instance.type 219 | const { setup } = Component 220 | // 有时候用户并没有使用setup() 221 | if (setup) { 222 | // 处理setup的返回值,如果返回的是对象,那么把对象里面的值注入到template上下文中 223 | // 如果是一个函数h(),那么直接render 224 | 225 | const setupResult = setup() 226 | 227 | handleSetupResult(instance, setupResult) 228 | } 229 | finishComponentSetup(instance) 230 | } 231 | // 处理组件的setup的返回值 232 | function handleSetupResult(instance: any, setupResult: any) { 233 | if (isFunction(setupResult)) { 234 | // TODO handle function 235 | } else if (isObject(setupResult)) { 236 | // 把setup返回的对象挂载到setupState上 237 | instance.setupState = setupResult 238 | } 239 | } 240 | // 结束组件的安装 241 | function finishComponentSetup(instance: any) { 242 | const Component = instance.type // 遇到h('div',{}, this.name) 这里Component将为'div' 243 | 244 | if (instance) { 245 | instance.render = Component.render 246 | } 247 | } 248 | ``` 249 | 250 | ## processElement 处理元素 251 | 252 | 处理元素的事情大致分为三件事: 253 | 1. 根据vnode.type创建HTML元素。 254 | 2. 根据vnode.children的类型判断是string还是array,如果是string,那么说明children是文本节点,如果是array,我们并不知道每个元素到底是HTML元素还是组件,这点同样通过patch处理。 255 | 3. 根据vnode.props对象,遍历来设置HTML元素的属性。 256 | 257 | 258 | 259 | ```typescript 260 | // 此时的vnode.type是一个string类型的HTML元素 261 | function processElement(vnode: any, container: any) { 262 | mountElement(vnode, container) 263 | } 264 | 265 | ``` 266 | 267 | ## 生成真实 DOM 节点 268 | 269 | 生成真实的DOM节点,其实逻辑就在mountElement()里面。 270 | 271 | > mountElement()函数传入两个参数:`vnode`和`container`。 272 | 273 | 因为此时调用mountElement的vnode.type已经被认定为是普通的HTMLElement,那么就能用`document.createElement(vnode.type)`创建dom节点,注意:这里的vnode.type是string类型。 274 | 275 | 除此之外,我们还必须处理vnode.props属性,它是包含着这个HTML元素的所有内联属性的对象,比如`id`、`class`、`style`等等。如果class有多个类名,通过数组表示。处理props属性我们联想到了遍历props对象然后调用`setAttribute()`函数,把属性添加到dom节点上。 276 | 277 | 我们还得处理dom节点的子节点,这里我们分两种情况: 278 | 1. vnode.children是一个`string`类型,说明子节点是文本节点。 279 | 2. vnode.children是一个`array`类型,不清楚里面的子元素是文本节点还是组件,这时我们可以通过`patch()`做拆箱处理。 280 | 281 | 定义一个mountChildren()用于循环每个子元素,逐个调用patch()函数拆箱。 282 | 283 | 里面的实现大致就是:通过遍历vnode.children数组,让每个子元素都执行patch() 284 | ```typescript 285 | // 处理子节点 286 | function mountChildren(vnode: any, container: any){ 287 | vnode.children.forEach((vnode: any) => { 288 | patch(vnode, container) 289 | }); 290 | } 291 | ``` 292 | 293 | ```typescript 294 | function mountElement(vnode: any, container: any) { 295 | const el = document.createElement(vnode.type) as HTMLElement 296 | let { children, props } = vnode 297 | if (isString(children)) { 298 | el.textContent = children 299 | } else if (Array.isArray(children)) { 300 | mountChildren(vnode, el) 301 | } 302 | // 对vnode的props进行处理,把虚拟属性添加到el 303 | for (let key of Object.getOwnPropertyNames(props).values()) { 304 | if(Array.isArray(props[key])){ 305 | el.setAttribute(key, props[key].join(' ')) 306 | }else{ 307 | el.setAttribute(key, props[key]) 308 | } 309 | } 310 | container.append(el) 311 | } 312 | 313 | 314 | ``` 315 | ## 使用rollup打包代码 316 | 1. 安装rollup 317 | 318 | 终端输入命令`pnpm i rollup @rollup/plugin-typescript typescript tslib -D` 319 | 320 | 没有pnpm的可以使用npm 321 | 2. 在根目录新建rollup.config.js 322 | 323 | cjs代表打包的代码符合commonjs规范 324 | 325 | es代表打包的代码符合ESM模块规范 326 | ```typescript 327 | import typescript from '@rollup/plugin-typescript' 328 | export default { 329 | input: './src/index.ts', 330 | output: [ 331 | { 332 | file: './lib/guide-toy-vue3.cjs.js', 333 | format: 'cjs' 334 | }, 335 | { 336 | file: './lib/guide-toy-vue3.esm.js', 337 | format: 'es' 338 | } 339 | ], 340 | plugins: [typescript()] 341 | } 342 | ``` 343 | 3. package.json添加执行脚本 344 | 345 | ```json 346 | "scripts": { 347 | "build": "rollup -c rollup.config.js" 348 | }, 349 | ``` 350 | 351 | ## 最后@感谢阅读 352 | 353 | 当学习成为一种习惯,知识就变成了常识。 354 | 355 | ## 本阶段的完整源码 356 | 本代码已经挂在stackblitz上了,大家进去之后,他会自动帮我们安装依赖。 357 | 358 | * 查看效果`example/index.html`,只需要在终端输入`npm start` 359 | * 打包runtime-core的代码可以在终端输入`npm run build` 360 | 361 | 362 | [完整源码](https://stackblitz.com/edit/ghx-vue3-runtime-core) 363 | -------------------------------------------------------------------------------- /src/runtime-core/docs/3.summary.md: -------------------------------------------------------------------------------- 1 | # 🚀Vue3 getCurrentInstance以及provide&inject的实现 2 | 3 | 4 | ## getCurrentInstance 的实现 5 | 6 | getCurrentInstance 可以获取到内部函数的实例 7 | 8 | > 注意:只能在 setup 或生命周期中使用 9 | 10 | 基于[官网描述](https://v3.cn.vuejs.org/api/composition-api.html#getcurrentinstance)的特性,我们来试一下实现它。 11 | 12 | 既然只能在 setup 内部使用,我们自然联想到[之前章节](https://juejin.cn/post/7100561910239592456)的`setupStatefulComponent()`,它内部调用了`instance.type.setup()`函数 13 | 14 | ```typescript 15 | function setupStatefulComponent(instance: any) { 16 | const Component = instance.type 17 | instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers) 18 | const { setup } = Component 19 | if (setup) { 20 | const setupResult = setup(shallowReadonly(instance.props), { 21 | emit: instance.emit, 22 | }) 23 | 24 | handleSetupResult(instance, setupResult) 25 | } 26 | finishComponentSetup(instance) 27 | } 28 | ``` 29 | 30 | 为了提升该对象的公用性,我们在全局定义一个`currentInstance`变量 31 | 32 | ```typescript 33 | export let currentInstance = null 34 | // 获取当前实例 35 | export function getCurrentInstance() { 36 | return currentInstance 37 | } 38 | // 设置当前实例 39 | function setCurrentInstance(instance: any) { 40 | currentInstance = instance 41 | } 42 | ``` 43 | 44 | `setCurrentInstance`的调用时机决定了`currentInstance`当前指向哪一个组件实例。 45 | 46 | 那什么时候设置`setCurrentInstance`为当前组件实例最好呢? 47 | 48 | 其实是确保`instance.type.setup`有值并且在`instance.type.setup`调用之前这个时机就是最佳时机。 49 | 50 | ```typescript 51 | function setupStatefulComponent(instance: any) { 52 | const Component = instance.type 53 | instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers) 54 | const { setup } = Component 55 | if (setup) { 56 | // currentInstance设置为instance 57 | setCurrentInstance(instance) 58 | const setupResult = setup(shallowReadonly(instance.props), { 59 | emit: instance.emit, 60 | }) 61 | handleSetupResult(instance, setupResult) 62 | // 让currentInstance变为null 63 | setCurrentInstance(null) 64 | } 65 | finishComponentSetup(instance) 66 | } 67 | ``` 68 | 69 | 最后我们把`currentInstance`变为 null,其实是为了使 currentInstance 在 setup 内部有值,这遵循了官方给出的特性。 70 | 71 | 这就是 getCurrentInstance 的实现。 72 | 73 | ## provide & inject 实现 74 | 75 | 父组件和子组件之间共享数据我们可以通过很多种方式: 76 | 77 | 1. `vuex` 78 | 2. `props`和`emit` 79 | 3. 全局对象`globalProperties` 80 | 4. parent & 模板 ref 81 | 82 | > 注意:eventbus 在 vue3 中已不再支持,需要自己手写实现。废除了`$on`、`$children`、$listeners 83 | 84 | 那如果子组件和祖先组件数据如何共享?props 一层一层的传递?emit 一层一层的暴露方法给父组件?这未免也太不优雅了吧! 85 | 86 | `provide`和`inject`是这种应用场景的解决方案。 87 | 88 | 如何实现 provide 和 inject? 89 | 90 | > 下面我们把使用 provide 的组件称为`提供者`,把使用 inject 的组件称为`接收者`。 91 | 92 | 把提供者实例上的 provide 属性作为一个`容器`,这个容器就是提供给接收者的共享数据。 93 | 94 | 接收者如何拿到提供者的`共享数据`呢? 95 | 96 | 可以在接收者实例上添加一个 parent 字段,用来指定该组件实例的父组件实例是谁,从而拿到父组件实例的身上的 provide 这个容器。 97 | 98 | 在创建组件实例的时候为 instance 新增两个属性(provides、parent) 99 | 100 | ```typescript 101 | export function createComponentInstance(vnode: any, parentComponent: any) { 102 | const type = vnode.type 103 | const instance = { 104 | vnode, 105 | type, 106 | render: null, 107 | setupState: {}, 108 | props: {}, 109 | emit: () => {}, 110 | slots: {}, 111 | provides: {} as Record, // 新增 112 | parent: parentComponent, // 新增 父组件的组件实例 113 | } 114 | instance.emit = emit.bind(null, instance) as any 115 | return instance 116 | } 117 | ``` 118 | 119 | 根据官方的描述和例子,provide 接收两个参数`name`和`value`,name 用于标识那些提供给子组件的数据,value 就是我们要对外提供的数据。 120 | 121 | 这里我把`instance.provides`初始化为一个对象,之所以选择用对象作为容器是因为 provide 具有键值关系。 122 | 123 | `provide`把要提供的数据存储起来,所以大体上`provide`的实现如下: 124 | 125 | ```typescript 126 | export function provide(key: string | number, value: T) { 127 | // 提供者 128 | 129 | const currentInstance: any = getCurrentInstance() 130 | if (currentInstance) { 131 | let { provides } = currentInstance 132 | provides[key] = value 133 | } 134 | } 135 | ``` 136 | 137 | `provide`已经实现了,那么`inject`他就是一个从容器中拿取数据的一个过程,不过这个容器要在 parent(父组件)上获取父组件的 provides。 138 | 139 | ```typescript 140 | export function inject(key: string, defaultValue?: unknown) { 141 | // 接收者 142 | // 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide 143 | const currentInstance: any = getCurrentInstance() 144 | if (currentInstance) { 145 | const parentProvides = currentInstance.parent.provides 146 | 147 | return parentProvides[key] 148 | } 149 | } 150 | ``` 151 | 152 | 此时我在这里准备的 demo 就已经可以使用 provide 和 inject 完成父子组件的数据传参了。 153 | 154 | ```typescript 155 | // 提供者 156 | const Provider = { 157 | name: 'Provider', 158 | setup() { 159 | provide('foo', 'fooVal') 160 | provide('bar', 'barVal') 161 | }, 162 | render() { 163 | return h('div', {}, [h('p', {}, 'Provider'), h(Consumer)]) 164 | }, 165 | } 166 | // 接收者 167 | const Consumer = { 168 | name: 'Consumer', 169 | setup() { 170 | const fooVal = inject('foo') 171 | const barVal = inject('bar') 172 | return { 173 | fooVal, 174 | barVal, 175 | } 176 | }, 177 | render() { 178 | return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`) 179 | }, 180 | } 181 | ``` 182 | 183 | [![XdJRrF.md.png](https://s1.ax1x.com/2022/06/04/XdJRrF.md.png)](https://imgtu.com/i/XdJRrF) 184 | 185 | 当然,不一定是父子组件这种关系这么简单,它可以是爷爷和孙子组件的关系、太爷爷和太孙子组件的关系......,这种情况给你如何处理?比如下面的情况: 186 | 187 | 我在 Provider 和 Consumer 中间加了一层组件叫`ProviderTwo`,用来模拟跨组件数据共享这样的情景。我们依然沿用之前的逻辑,发现`Consumer`的 foo 和 bar 为 undefined。 188 | 189 | ```typescript 190 | const Provider = { 191 | name: 'Provider', 192 | setup() { 193 | provide('foo', 'fooVal') 194 | provide('bar', 'barVal') 195 | }, 196 | render() { 197 | return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)]) 198 | }, 199 | } 200 | const ProviderTwo = { 201 | name: 'ProviderTwo', 202 | setup() {}, 203 | render() { 204 | return h('div', {}, [h('p', {}, `ProviderTwo`), h(Consumer)]) 205 | }, 206 | } 207 | const Consumer = { 208 | name: 'Consumer', 209 | setup() { 210 | const fooVal = inject('foo') 211 | const barVal = inject('bar') 212 | return { 213 | fooVal, 214 | barVal, 215 | } 216 | }, 217 | render() { 218 | return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`) 219 | }, 220 | } 221 | ``` 222 | 223 | [![XdNaDO.md.png](https://s1.ax1x.com/2022/06/04/XdNaDO.md.png)](https://imgtu.com/i/XdNaDO) 224 | 225 | 这是为什么呢?其实 Consumer 的父组件 ProviderTwo 并没有给 provide 属性提供数据,是个空对象。Consumer 在使用 inject 的时候拿了 ProviderTwo 的空对象,结果当然为 undefined。 226 | 227 | ```typescript 228 | export function createComponentInstance(vnode: any, parentComponent: any) { 229 | const type = vnode.type 230 | const instance = { 231 | vnode, 232 | type, 233 | render: null, 234 | setupState: {}, 235 | props: {}, 236 | emit: () => {}, 237 | slots: {}, 238 | provides: {} as Record, 239 | parent: parentComponent, // 父组件的组件实例 240 | } 241 | instance.emit = emit.bind(null, instance) as any 242 | return instance 243 | } 244 | ``` 245 | 246 | 我能想到的就,provides 不再指向空对象,而是指向上一级父组件的 provides,一层一层的指向父组件的 provides,直到没有父组件为止。 247 | 248 | 我们来改写一下`createComponentInstance()`: 249 | 250 | ```typescript 251 | export function createComponentInstance(vnode: any, parentComponent: any) { 252 | const type = vnode.type 253 | const instance = { 254 | vnode, 255 | type, 256 | render: null, 257 | setupState: {}, 258 | props: {}, 259 | emit: () => {}, 260 | slots: {}, 261 | provides: parentComponent ? parentComponent.provides : ({} as Record), // 确保中间层的组件没有提供provide时,子组件拿最近的有provide的父组件的数据 262 | parent: parentComponent, // 父组件的组件实例 263 | } 264 | instance.emit = emit.bind(null, instance) as any 265 | return instance 266 | } 267 | ``` 268 | 269 | 这样就解决了。 270 | [![XdU1L8.md.png](https://s1.ax1x.com/2022/06/04/XdU1L8.md.png)](https://imgtu.com/i/XdU1L8) 271 | 272 | 需求再次升级,这次我们想在 ProviderTwo 组件内使用 provide 和 inject,期望是:ProviderTwo 组件能接收 Provider 的 provide 数据,Consumer 能接收 ProviderTwo 的 provide 数据。 273 | 274 | ```typescript 275 | export const Provider = { 276 | name: 'Provider', 277 | setup() { 278 | provide('foo', 'fooVal') 279 | provide('bar', 'barVal') 280 | }, 281 | render() { 282 | return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)]) 283 | }, 284 | } 285 | const ProviderTwo = { 286 | name: 'ProviderTwo', 287 | setup() { 288 | provide('foo', 'fooTwo') 289 | provide('bar', 'barTwo') 290 | // 期望得到provider的foo---fooVal,实际上得到的是fooTwo 291 | const foo = inject('foo') 292 | const bar = inject('bar') 293 | return { 294 | foo, 295 | bar, 296 | } 297 | }, 298 | render() { 299 | return h('div', {}, [h('p', {}, `ProviderTwo-${this.foo}-${this.bar}`), h(Consumer)]) 300 | }, 301 | } 302 | const Consumer = { 303 | name: 'Consumer', 304 | setup() { 305 | const fooVal = inject('foo') 306 | const barVal = inject('bar') 307 | return { 308 | fooVal, 309 | barVal, 310 | } 311 | }, 312 | render() { 313 | return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`) 314 | }, 315 | } 316 | ``` 317 | 318 | [![XdYhy8.md.png](https://s1.ax1x.com/2022/06/04/XdYhy8.md.png)](https://imgtu.com/i/XdYhy8) 319 | 320 | 我们期望 ProviderTwo 的 foo 和 bar 应该是 Provider 所提供的 fooVal 和 barVal,现实却是 fooTwo 和 barTwo。这是为什么呢? 321 | 322 | 原因是:在`createComponentInstance()`的时候 instance 的 provides 是直接指向父组件的 provides,而 ProviderTwo 组件中 provides 被重新赋值为 fooTwo 和 barTwo,又因为 provides 是引用类型,所以它事实上间接改变了父组件的 provides 的值。 323 | 324 | 举个栗子 🌰: 325 | 326 | ```typescript 327 | let father = { 328 | foo: 'fooVal', 329 | } 330 | let obj3 = { 331 | provides: father, 332 | } 333 | 334 | obj3.provides['foo'] = 'changed' 335 | 336 | console.log(father.foo) // output: changed 337 | ``` 338 | 339 | 那么如何解决这个问题呢? 340 | 341 | 我们可以用原型链的思想,为当前的组件的 provides 创建一个原型链,原型对象指向父组件的 provides。这样就不必担心对象的引用问题,当前组件的 provides 没有该数据的时候,他会沿着原型链向上寻找该数据,知道找不到为止。如下:我用`Object.create`创建一个原型对象是 father 的对象。 342 | 343 | ```typescript 344 | let father = { 345 | foo: 'fooVal', 346 | } 347 | let obj3 = { 348 | provides: father, 349 | } 350 | 351 | obj3.provides = Object.create(father) 352 | 353 | obj3.provides['foo'] = 'changed' 354 | 355 | console.log(father.foo) // output: fooVal 356 | console.log(obj3.provides['foo']) // output: changed 357 | ``` 358 | 359 | 用这种思想解决 provide 的问题,代码将会是如下: 360 | 361 | ```typescript 362 | export function provide(key: string | number, value: T) { 363 | // 提供者 364 | // key和value存在哪呢?挂在instance的provides属性上吧! 365 | 366 | const currentInstance: any = getCurrentInstance() 367 | if (currentInstance) { 368 | let { provides } = currentInstance 369 | const parentProvides = currentInstance.parent?.provides 370 | if (provides === parentProvides) { 371 | // 把provide原型指向父组件的provide 372 | provides = currentInstance.provides = Object.create(parentProvides) 373 | } 374 | provides[key] = value 375 | } 376 | } 377 | ``` 378 | 379 | ❗ 里面的判断条件`provides === parentProvides`是为了避免重复使用 provide 造成组件实例的 provides 被初始化。 380 | 381 | 这样 provide 和 inject 就实现了! 382 | [![Xd4NtA.md.png](https://s1.ax1x.com/2022/06/05/Xd4NtA.md.png)](https://imgtu.com/i/Xd4NtA) 383 | 384 | 如果你还想实现 inject 的默认值功能,代码将会是如下: 385 | 386 | ```typescript 387 | export function inject(key: string, defaultValue?: T) { 388 | // 接收者 389 | // 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide 390 | const currentInstance: any = getCurrentInstance() 391 | if (currentInstance) { 392 | const parentProvides = currentInstance.parent.provides 393 | 394 | if (key in parentProvides) { 395 | return parentProvides[key] 396 | } else { 397 | // 找不到注入的 398 | // 如果默认值是函数,执行函数 399 | if (isFunction(defaultValue)) { 400 | return defaultValue() 401 | } 402 | return defaultValue 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | 对了,这个默认值功能支持传入一个返回默认值的函数。 409 | 410 | 用法: 411 | 412 | ```typescript 413 | let injectValue = inject('foo', () => 'this is default value') 414 | ``` 415 | ## 最后@肝血阅读,栓 Q 416 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | export function h(type: any, props?: any, children?: any) { 4 | console.log('type===>', type) 5 | return createVNode(type, props, children) 6 | } 7 | -------------------------------------------------------------------------------- /src/runtime-core/helper/renderSlot.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from '../vnode' 2 | // slots已经在initSlots中做了处理(把slots挂载到instance.slots上) 3 | export function renderSlot(slots: any, name: string = 'default', props: any) { 4 | const slot = slots[name] 5 | console.log('slot==>', slots, slot) 6 | if (slot) { 7 | if (typeof slot === 'function') { 8 | // slots有可能是对象,数组 9 | // 但是这里额外渲染了一层div,怎么去解决呢?定义一个vnode.type叫Fragment,内部只处理children 10 | // 就好像走了processElement()逻辑一样,不用的是他不会给Fragment生成HTML元素节点 11 | // return createVNode('div', {}, slot(props)) 12 | // 执行slot(props)会返回一个vnode数组 13 | return createVNode(Fragment, {}, slot(props)) 14 | // return createVNode(Fragment, {}, slot) 15 | } 16 | } else { 17 | return slots 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | // template渲染成真实dom的大致流程: 2 | // 3 | // 1. 经过编译 -> 具有render()的对象 4 | // 2. 经过createVNode() -> vnode对象 5 | // 3. 经过mountElement() -> DOM 6 | // 4. 插入到 -> root的#app 7 | export { h } from './h' 8 | export { renderSlot } from './helper/renderSlot' 9 | export { createTextVNode } from './vnode' 10 | export { getCurrentInstance } from './component' 11 | export { provide, inject } from './apiInject' 12 | export { createRenderer } from './renderer' 13 | export { nextTick } from './scheduler' 14 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../reactivity' 2 | import { EMPTY_OBJ, ShapeFlags } from '../shared' 3 | import { createAppAPI } from './apiCreateApp' 4 | import { createComponentInstance, setupComponent } from './component' 5 | import { shouldUpdateComponent } from './componentRenderUtils' 6 | import { queueJobs } from './scheduler' 7 | import { Fragment, Text } from './vnode' 8 | 9 | interface RendererOptions { 10 | createElement: (type: string) => any 11 | patchProp: (el: any, key: string, oldValue: any, newValue: any) => void 12 | insert: (el: any, container: any, anchor?: any) => void 13 | remove: (el: any) => void 14 | setElementText: (el: any, text: string) => void 15 | } 16 | 17 | export function createRenderer(options: RendererOptions) { 18 | const { 19 | createElement: hostCreateElement, 20 | patchProp: hostPatchProp, 21 | insert: hostInsert, 22 | remove: hostRemove, 23 | setElementText: hostSetElementText, 24 | } = options 25 | 26 | function render(vnode: any, container: any) { 27 | // 做patch算法 28 | patch(null, vnode, container, null, null) 29 | } 30 | 31 | // 例如: 32 | /** 33 | * template被编译成 {...., setup(){}, render(){}, ....} 这样一个特殊对象 34 | * 或者{..., data, methods, render(){}, ...} 35 | * 36 | * 之后 这个特殊对象作为参数会传入 createVNode() 创建虚拟dom 37 | */ 38 | // 传入vnode,递归对一个组件或者普通元素进行拆箱,在内部对vnode的type判断执行不同的处理函数 39 | // n1是老的vnode,n2是新的vnode 40 | function patch( 41 | n1: any, 42 | n2: any, 43 | container: any, 44 | parentComponent: any, 45 | anchor: any 46 | ) { 47 | // 检查是什么类型的vnode 48 | const { type } = n2 49 | switch (type) { 50 | // 这里有个面试题就是:为什么vue2书写template的时候要一个根元素,而vue3不用根元素? 51 | // 那是因为有fragment的原因:不再重新生成一个div去包裹template里的元素,而是直接patch children 52 | case Fragment: 53 | processFragment(n2, container, parentComponent, anchor) 54 | break 55 | case Text: 56 | processText(n2, container) 57 | break 58 | default: { 59 | // & 左右两边同时为1 则为1 可以应用在 0001 & 0010 判断指定的位置是否为1 这个案例会输出0000 所以为false 指定的位置并没有相同 60 | if (n2.shapeFlag & ShapeFlags.ELEMENT) { 61 | // 是一个普通元素?处理vnode是普通标签的情况 62 | processElement(n1, n2, container, parentComponent, anchor) 63 | } else if (n2.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 64 | // 是一个组件?处理vnode是组件的情况 65 | processComponent(n1, n2, container, parentComponent, anchor) 66 | } 67 | break 68 | } 69 | } 70 | } 71 | function processText(n2: any, container: any) { 72 | mountText(n2, container) 73 | } 74 | function processFragment( 75 | n2: any, 76 | container: any, 77 | parentComponent: any, 78 | anchor: any 79 | ) { 80 | mountChildren(n2.children, container, parentComponent, anchor) 81 | } 82 | // 处理组件的情况 83 | function processComponent( 84 | n1: any, 85 | n2: any, 86 | container: any, 87 | parentComponent: any, 88 | anchor: any 89 | ) { 90 | if (!n1) { 91 | mountComponent(n2, container, parentComponent, anchor) 92 | } else { 93 | updateComponent(n1, n2, container, parentComponent, anchor) 94 | } 95 | } 96 | function updateComponent( 97 | n1: any, 98 | n2: any, 99 | container: any, 100 | parentComponent: any, 101 | anchor: any 102 | ) { 103 | // n1 和 n2 都是组件类型的vnode节点 104 | // n1.component 在mountComponent的时候已经被赋值了。 105 | const instance = (n2.component = n1.component) 106 | console.log('旧节点', n1) 107 | console.log('新节点', n2) 108 | // 优化:判断新节点和旧节点是否在props上有更改,如果有就执行update,否则跳过 109 | if (shouldUpdateComponent(n1, n2)) { 110 | console.log('执行update'); 111 | instance.next = n2 112 | // 手动触发 effect 113 | instance.update() 114 | } else { 115 | console.log('跳过不执行update') 116 | n2.el = n1.el 117 | instance.vnode = n2 118 | } 119 | 120 | } 121 | // 处理元素的情况 122 | function processElement( 123 | n1: any, 124 | n2: any, 125 | container: any, 126 | parentComponent: any, 127 | anchor: any 128 | ) { 129 | if (!n1) { 130 | mountElement(n2, container, parentComponent, anchor) 131 | } else { 132 | patchElement(n1, n2, container, parentComponent, anchor) 133 | } 134 | } 135 | // 最后,它把setup()的返回值挂载在组件的instance的setupState上 136 | // instance.type的render()函数挂载在组件的instance的render上 137 | function mountComponent( 138 | vnode: any, 139 | container: any, 140 | parentComponent: any, 141 | anchor: any 142 | ) { 143 | // 创建组件实例的时候 也给 vnode的component 赋值 在updateComponent的时候需要拿到组件实例 144 | const instance = (vnode.component = createComponentInstance( 145 | vnode, 146 | parentComponent 147 | )) 148 | // 安装组件 149 | setupComponent(instance) 150 | 151 | // 对render函数进行依赖收集 152 | setupRenderEffect(instance, vnode, container, anchor) 153 | } 154 | function mountElement( 155 | vnode: any, 156 | container: any, 157 | parentComponent: any, 158 | anchor: any 159 | ) { 160 | // 注意:这个vnode并非是组件的vnode,而是HTML元素的vnode 161 | const el = (vnode.el = hostCreateElement(vnode.type) as HTMLElement) 162 | let { children, props } = vnode 163 | // 子节点是文本节点 164 | if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) { 165 | el.textContent = children 166 | // 子节点是数组 167 | } else if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 168 | mountChildren(vnode.children, el, parentComponent, anchor) 169 | } 170 | let val: any 171 | // 对vnode的props进行处理,把虚拟属性添加到el 172 | for (let key of Object.getOwnPropertyNames(props).values()) { 173 | val = props[key] 174 | hostPatchProp(el, key, null, val) 175 | } 176 | // insert操作 177 | hostInsert(el, container, anchor) 178 | } 179 | // 对比element 180 | function patchElement( 181 | n1: any, 182 | n2: any, 183 | container: any, 184 | parentComponent: any, 185 | anchor: any 186 | ) { 187 | console.log('patchElement') 188 | const oldProps = n1.props || EMPTY_OBJ 189 | const newProps = n2.props || EMPTY_OBJ 190 | // 为什么这里n1的el对象要赋值给n2的el? 191 | // 因为第一次挂载的时候调用patch,走的mountElement,内部给vnode的el赋值了 192 | // 而往后的patch都不会走mountElement,而是走patchElement,内部并没有给新的vnode的el赋值,所以这里是属于补救的措施。 193 | const el = (n2.el = n1.el) 194 | patchChildren(n1, n2, el, parentComponent, anchor) 195 | patchProps(el, oldProps, newProps) 196 | } 197 | function patchChildren( 198 | n1: any, 199 | n2: any, 200 | container: any, 201 | parentComponent: any, 202 | anchor: any 203 | ) { 204 | const prevShapeFlag = n1.shapeFlag 205 | const newShapeFlag = n2.shapeFlag 206 | const c1 = n1.children 207 | const c2 = n2.children 208 | if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) { 209 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 210 | unmountChildren(container) 211 | } 212 | if (c1 !== c2) { 213 | hostSetElementText(container, c2) 214 | } 215 | } else { 216 | // text to array 217 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 218 | hostSetElementText(container, '') 219 | mountChildren(c2, container, parentComponent, anchor) 220 | } else { 221 | // 处理子节点和子节点之间,这里面就是diff算法了 222 | console.log('array to array') 223 | patchKeyedChildren(c1, c2, container, parentComponent, anchor) 224 | } 225 | } 226 | } 227 | // c1是旧节点的子节点数组 228 | // c2是新节点的子节点数组 229 | function patchKeyedChildren( 230 | c1: any, 231 | c2: any, 232 | container: any, 233 | parentComponent: any, 234 | anchor: any 235 | ) { 236 | let i = 0 237 | let l2 = c2.length 238 | // 指针1 239 | let e1 = c1.length - 1 240 | // 指针2 241 | let e2 = l2 - 1 242 | // 判断是否相同vnode节点 243 | function isSameVNodeType(n1: any, n2: any) { 244 | return n1.type === n2.type && n1.key === n2.key 245 | } 246 | // 左端对比 247 | while (i <= e1 && i <= e2) { 248 | const n1 = c1[i] 249 | const n2 = c2[i] 250 | if (isSameVNodeType(n1, n2)) { 251 | patch(n1, n2, container, parentComponent, anchor) 252 | } else { 253 | break 254 | } 255 | i++ 256 | } 257 | console.log(i) 258 | // 右端对比 259 | while (i <= e1 && i <= e2) { 260 | const n1 = c1[e1] 261 | const n2 = c2[e2] 262 | if (isSameVNodeType(n1, n2)) { 263 | patch(n1, n2, container, parentComponent, anchor) 264 | } else { 265 | break 266 | } 267 | e1-- 268 | e2-- 269 | } 270 | console.log('e1', e1) 271 | console.log('e2', e2) 272 | // 旧的节点数组没有,新的节点数组有(新增) 273 | if (i > e1) { 274 | if (i <= e2) { 275 | const nextPos = e2 + 1 276 | const anchor = nextPos < l2 ? c2[nextPos].el : null 277 | 278 | while (i <= e2) { 279 | // 新增 280 | patch(null, c2[i], container, parentComponent, anchor) 281 | i++ 282 | } 283 | } 284 | } else if (i > e2) { 285 | while (i <= e1) { 286 | hostRemove(c1[i].el) 287 | i++ 288 | } 289 | } else { 290 | // 处理中间部分 291 | // a b (c d) e f 292 | // a b (e c) e f 293 | let s1 = i 294 | let s2 = i 295 | let toBePatched = e2 - s2 + 1 296 | let patched = 0 297 | // 为了能使用映射查找方式需要的map容器,提高patch的效率 298 | const keyToNewIndexMap = new Map() 299 | // 定义数组映射,用于查找出需要移动的元素以及移动的位置在哪(这极大的减少了使用insert api的次数) 300 | const newIndexToOldIndexMap = new Array(toBePatched) // 定长的数组比不定长的数组性能更好 301 | let moved = false 302 | let maxNewIndexSoFar = 0 303 | for (let i = 0; i < toBePatched; i++) { 304 | newIndexToOldIndexMap[i] = 0 305 | } 306 | // 遍历c2中间部分 307 | for (let i = s2; i <= e2; i++) { 308 | const nextChild = c2[i] 309 | keyToNewIndexMap.set(nextChild.key, i) 310 | } 311 | // 遍历c1中间部分,找出c1在c2对应的元素,如果找不到,就删除,找到了就更新走patch 312 | // 遍历旧节点 313 | for (let i = s1; i <= e1; i++) { 314 | const prevChild = c1[i] 315 | let newIndex 316 | // 这里是针对删除做优化,c2的中间部分都已经被遍历过了,c1剩下的部分就没必要处理了,直接删除就好。 317 | if (patched >= toBePatched) { 318 | hostRemove(prevChild.el) 319 | continue 320 | } 321 | // 优先使用keyToNewIndexMap查找映射值,优化性能 322 | if (prevChild.key != null) { 323 | newIndex = keyToNewIndexMap.get(prevChild.key) 324 | } else { 325 | // 改为使用双重循环查找映射值 326 | for (let j = s2; j <= e2; j++) { 327 | if (isSameVNodeType(prevChild, c2[j])) { 328 | newIndex = j 329 | break 330 | } 331 | } 332 | } 333 | // 找不到映射关系,删除该节点 334 | if (newIndex === undefined) { 335 | hostRemove(prevChild.el) 336 | } else { 337 | if (newIndex >= maxNewIndexSoFar) { 338 | maxNewIndexSoFar = newIndex 339 | } else { 340 | moved = true 341 | } 342 | // 能确认新的节点是存在的 343 | newIndexToOldIndexMap[newIndex - s2] = i + 1 // 这里为什么要+1,因为为0另外代表着该元素在c1上没有,需要新增 344 | 345 | patch(prevChild, c2[newIndex], container, parentComponent, null) 346 | patched++ 347 | } 348 | } 349 | // 根据映射表生成最长递增子序列 350 | const increasingNewIndexSequence = moved 351 | ? getSequence(newIndexToOldIndexMap) 352 | : [] 353 | // let j = 0 // 指针 354 | // for (let i = 0; i < toBePatched; i++) { 355 | // if (i !== increasingNewIndexSequence[j]) { 356 | // console.log('需要移动'); 357 | // }else{ 358 | // // 不需要移动 指针j++,判断递增子序列的下一个索引是否与 i 相等 359 | // j++ 360 | // } 361 | // } 362 | // 为什么需要倒序遍历呢?因为需要一个固定的节点作为锚点,正序遍历只能确定一个不稳定的锚点 363 | // looping backwards so that we can use last patched node as anchor 364 | // 365 | let j = increasingNewIndexSequence.length - 1 // 指针 366 | for (let i = toBePatched - 1; i >= 0; i--) { 367 | const nextIndex = i + s2 368 | const nextChild = c2[nextIndex] 369 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null // 防止nextIndex + 1超出l2范围 370 | // 新增节点 371 | if (newIndexToOldIndexMap[i] === 0) { 372 | patch(null, nextChild, container, parentComponent, anchor) 373 | } else if (moved) { 374 | // j < 0 也是考虑到性能优化(increasingNewIndexSequence[-1]已经不存在了) 375 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 376 | console.log('需要移动') 377 | hostInsert(nextChild.el, container, anchor) 378 | } else { 379 | // 不需要移动 指针j++,判断递增子序列的下一个索引是否与 i 相等 380 | j-- 381 | } 382 | } 383 | } 384 | } 385 | } 386 | function unmountChildren(child: any) { 387 | for (let i = 0; i < child.length; i++) { 388 | hostRemove(child[i]) 389 | } 390 | } 391 | // 对比props 392 | function patchProps(el: any, oldProps: any, newProps: any) { 393 | // 相同的props没必要比较 394 | if (oldProps !== newProps) { 395 | for (let key in newProps) { 396 | const newProp = newProps[key] 397 | const oldProp = oldProps[key] 398 | if (newProp !== oldProp) { 399 | hostPatchProp(el, key, oldProp, newProp) 400 | } 401 | } 402 | // 老props是空对象就没必要循环 403 | if (oldProps !== EMPTY_OBJ) { 404 | for (let key in oldProps) { 405 | // 新的props没有该属性 406 | if (!(key in newProps)) { 407 | hostPatchProp(el, key, oldProps[key], null) 408 | } 409 | } 410 | } 411 | } 412 | } 413 | function mountText(vnode: any, container: any) { 414 | const { children } = vnode 415 | const textNode = (vnode.el = document.createTextNode(children)) 416 | container.append(textNode) 417 | } 418 | function mountChildren( 419 | children: any, 420 | container: any, 421 | parentComponent: any, 422 | anchor: any 423 | ) { 424 | children.forEach((vnode: any) => { 425 | patch(null, vnode, container, parentComponent, anchor) 426 | }) 427 | } 428 | function setupRenderEffect( 429 | instance: any, 430 | vnode: any, 431 | container: any, 432 | anchor: any 433 | ) { 434 | instance.update = effect(() => { 435 | // 通过一个变量isMounted区分是初始化还是更新 436 | if (!instance.isMounted) { 437 | // 这个render()已经在finishComponentSetup处理过了,就是 instance.type.render() 特殊对象的render() 438 | // render函数内部的this指向 修改为 setupStatefulComponent中定义的proxy对象 439 | const subTree = instance.render.call(instance.proxy) 440 | instance.subTree = subTree 441 | // 对子树进行拆箱操作 递归进去 442 | console.log('subTree===>', subTree) 443 | patch(null, subTree, container, instance, anchor) 444 | // 代码到了这里,组件内的所有element已经挂在到document里面了 445 | vnode.el = subTree.el 446 | instance.isMounted = true 447 | } else { 448 | console.log('updated') 449 | 450 | // TODO:更新组件el和props 451 | // 需要获取新的vnode 452 | const {next, vnode} = instance 453 | if(next){ 454 | next.el = vnode.el 455 | updateComponentPreRender(instance, next) 456 | } 457 | 458 | 459 | const subTree = instance.render.call(instance.proxy) 460 | 461 | 462 | 463 | const prevSubTree = instance.subTree 464 | instance.subTree = subTree 465 | patch(prevSubTree, subTree, container, instance, anchor) 466 | } 467 | },{ 468 | scheduler(){ 469 | // 得益于 effect 的 scheduler 我们就可以实现异步更新dom了 470 | queueJobs(instance.update) 471 | } 472 | }) 473 | } 474 | // 用于更新组件 475 | function updateComponentPreRender(instance: any, nextVNode: any){ 476 | // 替换到老的vnode 477 | instance.vnode = nextVNode 478 | // 新的vnode初始化为null 479 | instance.next = null 480 | // 更新props 481 | instance.props = nextVNode.props 482 | } 483 | return { 484 | createApp: createAppAPI(render), 485 | render, 486 | } 487 | } 488 | 489 | // 最长递增子序列 490 | function getSequence(arr: number[]): number[] { 491 | const p = arr.slice() 492 | const result = [0] 493 | let i, j, u, v, c 494 | const len = arr.length 495 | for (i = 0; i < len; i++) { 496 | const arrI = arr[i] 497 | if (arrI !== 0) { 498 | j = result[result.length - 1] 499 | if (arr[j] < arrI) { 500 | p[i] = j 501 | result.push(i) 502 | continue 503 | } 504 | u = 0 505 | v = result.length - 1 506 | while (u < v) { 507 | c = (u + v) >> 1 508 | if (arr[result[c]] < arrI) { 509 | u = c + 1 510 | } else { 511 | v = c 512 | } 513 | } 514 | if (arrI < arr[result[u]]) { 515 | if (u > 0) { 516 | p[i] = result[u - 1] 517 | } 518 | result[u] = i 519 | } 520 | } 521 | } 522 | u = result.length 523 | v = result[u - 1] 524 | while (u-- > 0) { 525 | result[u] = v 526 | v = p[v] 527 | } 528 | return result 529 | } 530 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = [] 2 | let isFlushPending = false 3 | const resolvePromise = Promise.resolve() 4 | export const queueJobs = (job: any) => { 5 | if (!queue.includes(job)) { 6 | queue.push(job) 7 | } 8 | queueFlush(queue) 9 | } 10 | const queueFlush = (queue: any[]) => { 11 | // isFlushPending 是为了不让创建那么多个 promise 12 | if (isFlushPending) return 13 | isFlushPending = true 14 | // 把 instance.update放到微任务里面执行 15 | nextTick(flushJobs) 16 | } 17 | const flushJobs = () => { 18 | isFlushPending = false 19 | let job 20 | while ((job = queue.shift())) { 21 | job && job() 22 | } 23 | } 24 | type NextTickCallback = (...arg: any) => void 25 | export const nextTick = (callback: NextTickCallback) => { 26 | return callback ? resolvePromise.then(callback) : resolvePromise 27 | } 28 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isObject, isString } from '../shared' 2 | import { ShapeFlags } from '../shared/shapeFlags' 3 | 4 | // fragment用来创建一个碎片组件,这个碎片组件并不会真正的渲染出一个 5 | // 他的作用就是渲染slots的时候摆脱div的包裹,让slots直接渲染在父组件上。 6 | export const Fragment = Symbol('Fragment') 7 | export const Text = Symbol('Text') 8 | 9 | // type是 经过编译之后具有render()函数的对象,此外还有__file和__hmrId这些无关的属性 10 | export function createVNode(type: any, props?: any, children?: any) { 11 | const vnode = { 12 | type, 13 | props, 14 | children, 15 | component: null, // 组件实例 16 | key: props && props.key, 17 | shapeFlag: getShapeFlag(type), 18 | el: null, 19 | } 20 | normalizeChildren(vnode, children) 21 | return vnode 22 | } 23 | // 根据vnode.type标志vnode类型 24 | function getShapeFlag(type: any) { 25 | return isString(type) 26 | ? ShapeFlags.ELEMENT 27 | : isObject(type) 28 | ? ShapeFlags.STATEFUL_COMPONENT 29 | : 0 30 | } 31 | // 给vnode.shapeFlag追加标识 32 | function normalizeChildren(vnode: any, children: any) { 33 | // | 左右两边为0 则为0 可以用于给二进制指定的位数修改成1 例如:0100 | 0001 = 0101 34 | // 在这里相当于给vnode追加额外的标识 35 | if (isString(children)) { 36 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 37 | // 子级是数组 38 | } else if (isArray(children)) { 39 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN 40 | } 41 | // vnode是组件 42 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 43 | // 子级是对象 44 | if (isObject(children)) { 45 | vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN 46 | } 47 | } 48 | } 49 | // 创建文本虚拟节点 为什么需要创建文本虚拟节点?直接填上文本不行吗?h('div',{},[Foo, '我是文本']) 50 | // 挂载html的时候因为children是数组,必然经过mountChildren的循环,然后patch,单纯填上文本是没办法渲染出来的 51 | // 因为patch并没有针对纯文本做处理,你只能通过div(或者其他html元素)包裹起来生成一个vnode才行,像这样:h('div',{},[Foo, h('div',{}, '我是文本')]) 52 | export function createTextVNode(text: string) { 53 | return createVNode(Text, {}, text) 54 | } 55 | -------------------------------------------------------------------------------- /src/runtime-dom/docs/1.summary.md: -------------------------------------------------------------------------------- 1 | # Vue3 手写 Element 元素的props和children更新逻辑 2 | 3 | 之前我们实现了 element 元素的挂载,还没有实现当元素内的数据发生改变的时候,要对元素进行更新操作。 4 | 5 | 为此我准备了一个小 demo,期望用户在点击 button 按钮的时候能让`count`加一: 6 | 7 | ```typescript 8 | import { ref, h } from '../../lib/guide-toy-vue3.esm.js' 9 | export const App = { 10 | name: 'app', 11 | setup() { 12 | const count = ref(0) 13 | const click = () => { 14 | count.value++ 15 | } 16 | return { 17 | count, 18 | click, 19 | } 20 | }, 21 | render() { 22 | return h('div', {}, [ 23 | h('p', {}, `count: ${this.count}`), 24 | h('button', { onClick: this.click }, '点击更新'), 25 | ]) 26 | }, 27 | } 28 | ``` 29 | 30 | [![XwtJqU.md.png](https://s1.ax1x.com/2022/06/05/XwtJqU.md.png)](https://imgtu.com/i/XwtJqU) 31 | 32 | 可以看到 this.count 并没有打印出数值而是一个`[Object object]`,为什么会这样呢?我们再进一步打印`this.count`是什么。 33 | 34 | [![Xwtosf.md.png](https://s1.ax1x.com/2022/06/05/Xwtosf.md.png)](https://imgtu.com/i/Xwtosf) 35 | 36 | 他是一个 ref 对象,我们想要的值在`_value`里面,那要怎么去除这个值呢?我们之前是不是实现了`proxyRefs`,我当时说过他是用来在 h 函数里面自动解包 ref 的。 37 | 38 | 生怕你们忘了我在这里再贴一次这段代码的实现吧。 39 | 40 | ```typescript 41 | // 通常用在vue3 template里面ref取值,在template里面不需要.value就可以拿到ref的值 42 | export function proxyRefs(obj: T) { 43 | return isReactive(obj) 44 | ? obj 45 | : new Proxy(obj, { 46 | get(target, key) { 47 | // unref已经处理了是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值 48 | return unref(Reflect.get(target, key)) 49 | }, 50 | set(target, key, value) { 51 | // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value 52 | if (isRef(target[key]) && !isRef(value)) { 53 | target[key].value = value 54 | return true 55 | } else { 56 | return Reflect.set(target, key, value) 57 | } 58 | }, 59 | }) 60 | } 61 | ``` 62 | 63 | 我们只需要对 setup 的返回值进行解包就好了。 64 | 65 | 在 runtime-core 里面的 component.ts 里面添加 proxyRefs 的调用操作 66 | 67 | ```typescript 68 | function handleSetupResult(instance: any, setupResult: any) { 69 | // TODO handle function 70 | if (isFunction(setupResult)) { 71 | instance.render = setupResult 72 | } else if (isObject(setupResult)) { 73 | // 把setup返回的对象挂载到setupState上 proxyRefs对setupResult解包 74 | instance.setupState = proxyRefs(setupResult) // 新增 75 | } 76 | } 77 | ``` 78 | 79 | 这样 ref 的 value 就能正常显示出来了。 80 | 81 | [![XwUu3n.md.png](https://s1.ax1x.com/2022/06/05/XwUu3n.md.png)](https://imgtu.com/i/XwUu3n) 82 | 83 | 下一步我们来完成元素的更新 84 | 85 | 页面上的元素都是虚拟 dom 对象 86 | 87 | ```typescript 88 | { 89 | type: 'div', 90 | props: {}, 91 | children:{}, 92 | el: {}, 93 | shapeFlag: 2 94 | ... 95 | } 96 | ``` 97 | 98 | 元素更新的时候需要生成新的 vnode 对象,然后通过旧的 vnode 和新的 vnode 进行对比,从而找出要更新的地方。如果是文本更新,那我们可以用 dom.textContext 属性更新。 [![XgflVI.md.png](https://s1.ax1x.com/2022/06/12/XgflVI.md.png)](https://imgtu.com/i/XgflVI) 99 | 100 | 前提是我们需要获取旧的 vnode 和新的 vnode。新的 vnode 怎么获取?重新执行一次`render`函数就可以了啊,那什么时候应该重新执行一次 render 函数呢?上面的 count 值是一个 ref 响应式对象,只要 count 发生改变,视图中的 text 就应该发生更新啊,render 函数就应该重新执行一次生成一个 vnode 对象。 101 | 102 | 之前的章节我们实现了`effect`,它的主要作用是响应式对象的值发生改变的时候,会执行一次对该响应式对象相关联的`effect`(触发依赖)。 103 | 104 | 我们只需要在 render 函数被执行的函数里面,用 effect 包裹起来。等到 count 发生改变的时候,就会重新执行一次 render 函数了。 105 | 106 | ```typescript 107 | // In renderer.ts 108 | function setupRenderEffect(instance: any, vnode: any, container: any) { 109 | effect(() => { 110 | const subTree = instance.render.call(instance.proxy) 111 | console.log('subTree===>', subTree) 112 | patch(subTree, container, instance) 113 | vnode.el = subTree.el 114 | instance.isMounted = true 115 | }) 116 | } 117 | ``` 118 | 119 | ### 依赖收集 120 | 121 | ```typescript 122 | render() { 123 | // 触发了ref的get操作(依赖收集) 124 | return h('div', {}, [ 125 | h('p', {}, `count: ${this.count}`), 126 | h('button', { onClick: this.click }, '点击更新'), 127 | ]) 128 | }, 129 | ``` 130 | 131 | ### 触发依赖 132 | 133 | ```typescript 134 | setup() { 135 | const count = ref(0) 136 | const click = () => { 137 | // 触发了ref的set操作(执行effect回调函数) 138 | count.value++ 139 | } 140 | return { 141 | count, 142 | click, 143 | } 144 | }, 145 | ``` 146 | 147 | [![XgH4PA.md.png](https://s1.ax1x.com/2022/06/12/XgH4PA.md.png)](https://imgtu.com/i/XgH4PA) 148 | 149 | 根据现在的逻辑,可以看到多次点击 button 之后,虽然 count 更新了,但是重复的元素也被 mount 进 dom 树上面。原因是执行 render 的函数的时候它并不知道组件是否已经被挂载。 150 | 151 | 所以需要在组件实例上新增一个属性`isMounted`,该属性值是 boolean 类型,表示组件是否已经被挂载。 152 | 153 | ```typescript 154 | export function createComponentInstance(vnode: any, parentComponent: any) { 155 | console.log('createComponentInstance', parentComponent) 156 | const type = vnode.type 157 | const instance = { 158 | vnode, 159 | type, 160 | render: null, 161 | setupState: {}, 162 | props: {}, 163 | emit: () => {}, 164 | slots: {}, 165 | isMounted: false, // 新增 166 | provides: parentComponent 167 | ? parentComponent.provides 168 | : ({} as Record), 169 | parent: parentComponent, 170 | } 171 | instance.emit = emit.bind(null, instance) as any 172 | return instance 173 | } 174 | ``` 175 | 176 | 更改 setupRenderEffect 内部逻辑 177 | 178 | ```typescript 179 | function setupRenderEffect(instance: any, vnode: any, container: any) { 180 | effect(() => { 181 | if (!instance.isMounted) { 182 | // 这里处理第一次被挂载的逻辑 183 | const subTree = instance.render.call(instance.proxy) 184 | instance.subTree = subTree 185 | patch(null, subTree, container, instance) 186 | vnode.el = subTree.el 187 | instance.isMounted = true 188 | } else { 189 | // 这里处理更新的逻辑 190 | console.log('update') 191 | } 192 | }) 193 | } 194 | ``` 195 | 196 | > 注意:第一次被挂载的逻辑完成之后,还需要把 isMounted 设置为 true,这样下次更新的时候,就不会执行第一次的逻辑了。 197 | 198 | 我们把注意力聚焦到更新的逻辑。 199 | 200 | 我们需要两个 vnode,新的和旧的,所以我们同样要执行一遍 render 函数,为了能在更新的时候拿到老的 vnode,我们还需要在上一次 render 函数执行的时候得到的 vnode 赋值给一个新的属性`subTree`,这个属性同样是在组件实例 instance 上。 201 | 202 | 这就很好解释了为什么我在第一次挂载的逻辑中加入了`instance.subTree = subTree`这个表达式。 203 | 204 | ```typescript 205 | export function createComponentInstance(vnode: any, parentComponent: any) { 206 | console.log('createComponentInstance', parentComponent) 207 | const type = vnode.type 208 | const instance = { 209 | vnode, 210 | type, 211 | render: null, 212 | setupState: {}, 213 | props: {}, 214 | emit: () => {}, 215 | slots: {}, 216 | isMounted: false, 217 | subTree: {}, // 新增 218 | provides: parentComponent 219 | ? parentComponent.provides 220 | : ({} as Record), 221 | parent: parentComponent, // 父组件的组件实例 222 | } 223 | instance.emit = emit.bind(null, instance) as any 224 | return instance 225 | } 226 | 227 | function setupRenderEffect(instance: any, vnode: any, container: any) { 228 | effect(() => { 229 | if (!instance.isMounted) { 230 | // 这里处理第一次被挂载的逻辑 231 | const subTree = instance.render.call(instance.proxy) 232 | instance.subTree = subTree 233 | patch(null, subTree, container, instance) 234 | vnode.el = subTree.el 235 | instance.isMounted = true 236 | } else { 237 | // 这里处理更新的逻辑 238 | // 新的vnode 239 | const subTree = instance.render.call(instance.proxy) 240 | // 老的vnode 241 | const prevSubTree = instance.subTree 242 | // 存储这一次的vnode,下一次更新逻辑作为老的vnode 243 | instance.subTree = subTree 244 | patch(prevSubTree, subTree, container, instance) 245 | } 246 | }) 247 | } 248 | ``` 249 | 250 | ## element 的 props 更新 251 | 252 | 我们讲一下元素的属性的更新。 253 | 254 | 有三种情况需要我们处理: 255 | 256 | 1. 新的 props 和旧的 props 不同(新增,修改) 257 | 2. 新的 props 被赋值为 null 或者 undefined(删除) 258 | 3. 旧的 props 有些属性在新的 props 中没有(删除) 259 | 260 | [![X2VJHg.png](https://s1.ax1x.com/2022/06/12/X2VJHg.png)](https://imgtu.com/i/X2VJHg) 261 | 262 | 目前我们只处理元素的情况,所以我们在`patch`中只修改`processElement`内部的逻辑。 263 | 264 | 给 patch 传入`n1`和`n2`,其中 n1 代表旧的 vnode,n2 代表新的 vnode。 265 | 266 | ```typescript 267 | function patch(n1: any, n2: any, container: any, parentComponent: any) { 268 | // 检查是什么类型的vnode 269 | const { type } = n2 270 | switch (type) { 271 | case Fragment: 272 | processFragment(n2, container, parentComponent) 273 | break 274 | case Text: 275 | processText(n2, container) 276 | break 277 | default: { 278 | if (n2.shapeFlag & ShapeFlags.ELEMENT) { 279 | processElement(n1, n2, container, parentComponent) 280 | } else if (n2.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 281 | // 是一个组件?处理vnode是组件的情况 282 | processComponent(n2, container, parentComponent) 283 | } 284 | break 285 | } 286 | } 287 | } 288 | ``` 289 | 290 | 当 n2 的 shapeFlag 表示元素的时候,我们同样把 n1 和 n2 传入 processElement 函数。 291 | 292 | ```typescript 293 | // 处理元素的情况 294 | function processElement( 295 | n1: any, 296 | n2: any, 297 | container: any, 298 | parentComponent: any 299 | ) { 300 | if (!n1) { 301 | mountElement(n2, container, parentComponent) 302 | } else { 303 | patchElement(n1, n2, container) 304 | } 305 | } 306 | ``` 307 | 308 | 其中需要处理 n1 为 null 的情况,即第一次挂载到 dom 上的情况,否则对 n1 和 n2 做对比 patch。我把对比的操作抽离成了一个函数 `patchElement`。 309 | 310 | patchElement 的具体实现如下: 311 | 312 | ```typescript 313 | function patchElement(n1: any, n2: any, container: any) { 314 | // 这里的EMPTY_OBJ是一个空对象,在shareFlags.ts中定义:const EMPTY_OBJ = {} 315 | const oldProps = n1.props || EMPTY_OBJ 316 | const newProps = n2.props || EMPTY_OBJ 317 | const el = (n2.el = n1.el) 318 | patchProps(el, oldProps, newProps) 319 | } 320 | ``` 321 | 322 | 在`patchProps()`内部遍历 n2(新的 vnode)的 props,看看 n1 的 props 和 n2 的 props 是否相同。 323 | 324 | ```typescript 325 | // newProps就是n2.props 326 | // oldProps就是n1.props 327 | for (let key in newProps) { 328 | const newProp = newProps[key] 329 | const oldProp = oldProps[key] 330 | if (newProp !== oldProp) { 331 | patchProp(el, key, oldProp, newProp) 332 | } 333 | } 334 | ``` 335 | 336 | `patchProp()`在之前的篇章已经出现过了,它是通过 DOM API 将 h 函数的 prop(h 函数的第二个参数)赋值给真实的 dom 元素。这里我可以重新贴一下具体的代码实现。 337 | 338 | ```typescript 339 | export function patchProp(el: any, key: string, oldValue: any, newValue: any) { 340 | // 这里处理属性值有多个的情况,比如class="flex flex-column flex-grow-1" 341 | if (Array.isArray(newValue)) { 342 | el.setAttribute(key, newValue.join(' ')) 343 | } else if (isOn(key) && isFunction(newValue)) { 344 | el.addEventListener(key.slice(2).toLowerCase(), newValue) 345 | } else { 346 | el.setAttribute(key, newValue) 347 | } 348 | } 349 | ``` 350 | 351 | 其实到这里后已经实现了三种情况的第一种了(新增和修改) 352 | 353 | 当 n2 的 props 有属性值是 null 或者 undefined 的时候,我们也应该移除这个属性。此时我们可以修改`patchProp` 354 | 355 | ```typescript 356 | export function patchProp(el: any, key: string, oldValue: any, newValue: any) { 357 | if (Array.isArray(newValue)) { 358 | el.setAttribute(key, newValue.join(' ')) 359 | } else if (isOn(key) && isFunction(newValue)) { 360 | // 添加事件 361 | el.addEventListener(key.slice(2).toLowerCase(), newValue) 362 | } else { 363 | // props属性的属性值是undefined或者null,删除该属性 364 | if (newValue === null || newValue === undefined) { 365 | el.removeAttribute(key) 366 | } else { 367 | el.setAttribute(key, newValue) 368 | } 369 | } 370 | } 371 | ``` 372 | 373 | 这样就是实现了第二种情况了 374 | 375 | 接着因为需要检查 n1 的 props 是不是在 n2 的 props 中被删除了,所以我们需要遍历 n1 的 props,看看是不是在 n2 的 props 中不复存在。 376 | 377 | ```typescript 378 | for (let key in oldProps) { 379 | // 新的props没有该属性 380 | if (!(key in newProps)) { 381 | patchProp(el, key, oldProps[key], null) 382 | } 383 | } 384 | ``` 385 | 386 | 要想删除属性,只需要给 patchProp 传入 null 即可,在内部会调用`el.removeAttribute`。 387 | 388 | 好了,三种情况都已经解决了。 389 | 390 | ## element 的 children 更新 391 | 392 | element 的 children 更新涉及到以下四种情况: 393 | 394 | 1. 旧的子节点是 array 类型 ==> 新的子节点是 string 类型 395 | 2. 旧的子节点是 string 类型 ==> 新的子节点是 string 类型 396 | 3. 旧的子节点是 string 类型 ==> 新的子节点是 array 类型 397 | 4. 旧的子节点是 array 类型 ==> 新的子节点是 array 类型 398 | 399 | 当然其他情况比如旧的子节点是空的,新的子节点可能是空的。 400 | 401 | ### 第一种情况:子节点从数组变为字符串 402 | 403 | [![XOMoz4.png](https://s1.ax1x.com/2022/06/18/XOMoz4.png)](https://imgtu.com/i/XOMoz4) 404 | 405 | > 其中`shapeFlag`是区分 vnode 的一个标识 406 | 407 | 以下为用于解决这种元素 children 更新的测试 demo 408 | 409 | ```typescript 410 | export const App = { 411 | name: 'app', 412 | setup() {}, 413 | render() { 414 | // this.count进行依赖收集, 415 | return h('div', {}, [ 416 | h('p', {}, '主页'), 417 | // 旧的是数组 新的是文本 418 | h(arrayToText), 419 | ]) 420 | }, 421 | } 422 | export default { 423 | name: 'arrayToText', 424 | setup() { 425 | const isChange = ref(false) 426 | // 把ref对象挂载在window是为了方便在外部改变ref的值 427 | window.isChange = isChange 428 | return { 429 | isChange, 430 | } 431 | }, 432 | render() { 433 | return this.isChange 434 | ? h('div', {}, 'new text') 435 | : h('div', {}, [h('div', {}, 'A'), h('div', {}, 'B')]) 436 | }, 437 | } 438 | ``` 439 | 440 | 预期需求: 441 | 442 | 当我在 console 中输入`isChange.value = true`,然后在浏览器中查看结果,会发现子节点从显示'A'和'B'更新为'new text'。这样就能达到数据驱动视图的效果。 443 | 444 | 由于之前已经实现了`setupRenderEffect()`内部的逻辑,可以让 ref 更新的时候,重新执行 patch 函数。我们只需要在调用`patchProps()`的地方上新增实现`patchChildren`函数 445 | 446 | patchChildren 内部逻辑是怎样的呢? 447 | 448 | 当子节点的 shapeFlag 是数组的时候,遍历这个数组,并依次 removeChild 做卸载,之后通过 textContent 把文本更新上去。 449 | 450 | ```typescript 451 | function patchChildren(n1: any, n2: any, container: any, parentComponent: any) { 452 | const prevShapeFlag = n1.shapeFlag 453 | const newShapeFlag = n2.shapeFlag 454 | const c1 = n1.children 455 | const c2 = n2.children 456 | if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) { 457 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 458 | // 卸载旧的子节点 459 | unmountChildren(container) 460 | // 设置元素文本 461 | setElementText(container, c2) 462 | } 463 | } 464 | } 465 | function unmountChildren(child: any) { 466 | for (let i = 0; i < child.length; i++) { 467 | remove(child[i]) 468 | } 469 | } 470 | export function remove(child: HTMLElement) { 471 | // 这里为什么不直接用child.remove()呢?可以用,不过这样写兼容性更好而已 472 | const parent = child.parentNode 473 | if (parent) { 474 | parent.removeChild(child) 475 | } 476 | } 477 | // 设置元素文本 478 | export function setElementText(el: HTMLElement, text: string) { 479 | el.textContent = text 480 | } 481 | ``` 482 | 483 | ### 第二种情况:子节点从字符串变为数组 484 | 485 | [![XOYrKU.png](https://s1.ax1x.com/2022/06/18/XOYrKU.png)](https://imgtu.com/i/XOYrKU) 486 | 487 | 以下为用于解决这种元素 children 更新的测试 demo 488 | 489 | ```typescript 490 | export const App = { 491 | name: 'app', 492 | setup() {}, 493 | render() { 494 | return h('div', {}, [ 495 | h('p', {}, '主页'), 496 | // 旧的是文本 新的是数组 497 | h(textToArray), 498 | ]) 499 | }, 500 | } 501 | export default { 502 | name: 'textToArray', 503 | setup() { 504 | const isChange = ref(false) 505 | window.isChange = isChange 506 | return { 507 | isChange, 508 | } 509 | }, 510 | render() { 511 | return this.isChange 512 | ? h('div', {}, [h('p', {}, 'p标签'), h('span', {}, 'span标签')]) 513 | : h('div', {}, 'old text') 514 | }, 515 | } 516 | ``` 517 | 518 | 预期需求: 519 | 520 | 跟情况一正好相反,当我在 console 中输入`isChange.value = true`,然后在浏览器中查看结果,会发现子节点从显示'old text'更新为'A'和'B'。这样就能达到数据驱动视图的效果。 521 | 522 | 实现思路: 523 | 524 | 先把子节点重新通过 textContext 设置为空字符串,再通过已经实现的 mountChildren 函数,把新的子节点挂载上去。不过在此之前还是要通过 shapeFlag 判断一下 vnode 的类型。 525 | 526 | ```typescript 527 | // n1是旧节点 n2是新节点 528 | function patchChildren(n1: any, n2: any, container: any, parentComponent: any) { 529 | const prevShapeFlag = n1.shapeFlag 530 | const newShapeFlag = n2.shapeFlag 531 | const c1 = n1.children 532 | const c2 = n2.children 533 | // 判断新节点的shapeFlag是否是文本 534 | if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) { 535 | // 判断旧节点的shapeFlag是否是数组 536 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 537 | // array to text 538 | unmountChildren(container) 539 | setElementText(container, c2) 540 | } 541 | } else { 542 | // text to array 543 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 544 | setElementText(container, '') 545 | // 挂载新的子节点 546 | mountChildren(c2, container, parentComponent) 547 | } 548 | } 549 | } 550 | ``` 551 | 552 | ### 第三种情况:子节点从旧的文本更新为新的文本 553 | 554 | 这种情况相对简单,只需要判断 n1.children 和 n2.children 是不是相等的,如果不是则利用 textContext 更新文本即可。相等就没必要往下了,节省性能。 555 | 556 | [![XONPOO.png](https://s1.ax1x.com/2022/06/18/XONPOO.png)](https://imgtu.com/i/XONPOO) 同时针对上面的 patchChildren 我们可以优化以下逻辑: 557 | 558 | ```typescript 559 | // n1是旧节点 n2是新节点 560 | function patchChildren(n1: any, n2: any, container: any, parentComponent: any) { 561 | const prevShapeFlag = n1.shapeFlag 562 | const newShapeFlag = n2.shapeFlag 563 | const c1 = n1.children 564 | const c2 = n2.children 565 | if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) { 566 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 567 | unmountChildren(container) 568 | } 569 | // 判断是否相等,单独把`setElementText`拿出来,兼容了第一种和第三种情况 570 | if (c1 !== c2) { 571 | setElementText(container, c2) 572 | } 573 | } else { 574 | // text to array 575 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 576 | setElementText(container, '') 577 | mountChildren(c2, container, parentComponent) 578 | } 579 | } 580 | } 581 | ``` 582 | 583 | 第四种情况旧节点是数组,新节点是数组,patch 的时候相对复杂,涉及到 vue 的 diff 算法。我将在下一篇带来diff算法的分享。 584 | 585 | ## 最后肝血阅读,栓 Q 586 | 587 | 588 | -------------------------------------------------------------------------------- /src/runtime-dom/docs/2.summary.md: -------------------------------------------------------------------------------- 1 | # Vue3 手写 双端对比的diff算法 2 | 3 | ## 双端对比 diff 算法 4 | 顾名思义,双端对比,左端和右端。通过左端和右端的收缩比较,可以精准的确定旧节点和新节点的改变范围。 5 | ### 左侧对比 6 | 7 | 先来一张图给大家看看,咱们先别看右侧的流程图(那是关于往右侧新增节点的流程) 8 | 9 | `图1` [![jFbS3R.md.png](https://s1.ax1x.com/2022/06/25/jFbS3R.md.png)](https://imgtu.com/i/jFbS3R) 10 | 11 | 解析: 12 | 13 | c1:旧的子节点的数组,比如:有 A、B 两个元素 14 | 15 | c2:新的子节点的数组,比如:有 A、B、C、D 四个元素 16 | 17 | i:头部指针,初始为 0,将会向右移动 18 | 19 | e1:旧节点数组的指针,指针指向数组末尾,初始化值为`c1.length - 1` 20 | 21 | e2:新节点数组的指针,指针指向数组末尾,初始化值为`c2.length - 1` 22 | 23 | 要想知道有哪些节点是需要被新增的,那不得遍历两个数组进行比较嘛,相同就证明不需要新增,不相同就证明旧节点数组没有,新节点数组有的元素,这要新增元素了。这就引出一个索引值`i`,通过索引值可以得出要新增哪些节点以及新增节点的范围。比如上图中,`e1 < i <= e2`这个范围就是需要新增节点的范围。i 在什么条件下会移动呢?当然是`c1[i]`和`c2[i]`有值的时候啊,所以当`i<=e1 && i<=e2`,要判断`c1[i]`和`c2[i]`是否是相同的 vnode,如果相同就`i++`,下一轮循环的时候根据这个已自增的 i 取得 vnode。有进行下一轮的`比较`。 24 | 25 | 而这个比较方法`isSameVNodeType()`的实现就是根据 vnode 的 type 属性和 key 属性进行比较。 26 | 27 | ```typescript 28 | // n1是旧的vnode n2是新的vnode 返回boolean 29 | function isSameVNodeType(n1: any, n2: any) { 30 | return n1.type === n2.type && n1.key === n2.key 31 | } 32 | ``` 33 | 34 | 所以为什么 vue3 里面渲染列表的时候 v-for 后面要顺便定义 key,就是为了能精准确定两个 vnode 节点是不是相同的 vnode,从而提升渲染时的性能。 35 | 36 | 如果发现两个 vnode 节点是相同的节点,那么调用 patch 方法,否则直接跳出循环,拿到这个`i`值,这个 i 值就是要新增节点的开始索引值。 37 | 38 | ```typescript 39 | while (i <= e1 && i <= e2) { 40 | const n1 = c1[i] // 旧的vnode节点 41 | const n2 = c2[i] // 新的vnode节点 42 | if (isSameVNodeType(n1, n2)) { 43 | patch(n1, n2, container, parentComponent, anchor) 44 | } else { 45 | break 46 | } 47 | i++ 48 | } 49 | ``` 50 | 51 | ### 右侧对比 52 | 53 | `图2` [![jkF0LF.md.png](https://s1.ax1x.com/2022/06/25/jkF0LF.md.png)](https://imgtu.com/i/jkF0LF) 54 | 55 | 和左侧对比的方向相反,注意的是:右侧对比不再以 i 为索引获取 vnode 节点,而是用 e1 和 e2 作为索引获取 vnode,并且不再对 i 自增,反而利用 e1 和 e2 自减来获取不同的 vnode 节点。但是判断条件还是一样的`i <= e1 && i <= e2`,i 始终为 0。 56 | 57 | ```typescript 58 | while (i <= e1 && i <= e2) { 59 | const n1 = c1[e1] 60 | const n2 = c2[e2] 61 | if (isSameVNodeType(n1, n2)) { 62 | patch(n1, n2, container, parentComponent, anchor) 63 | } else { 64 | break 65 | } 66 | e1-- 67 | e2-- 68 | } 69 | ``` 70 | 71 | 左侧对比结合右侧对比就能处理以下情况了。 [![jkEMM6.md.png](https://s1.ax1x.com/2022/06/25/jkEMM6.md.png)](https://imgtu.com/i/jkEMM6) 72 | 73 | ### 新增节点 74 | 75 | 新增节点我们使用[`Node.insertBefore()`](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore),第一个入参用于插入的节点,第二个入参用于指定插在哪个节点之前,如果入参为 null,将会插入的节点插入到末尾。 76 | 77 | 我们可以利用第二个入参为 null 来实现右侧添加节点。至于往左侧添加节点我们可以在第二个参数填上对应的引用节点就好。 78 | 79 | `图1`中循环下来 i、e1、e2 的值分别是: 80 | 81 | ``` 82 | i: 2 83 | e1: 1 84 | e2: 3 85 | ``` 86 | 87 | `图2`中循环下来 i、e1、e2 的值分别是: 88 | 89 | ``` 90 | i: 0 91 | e1: -1 92 | e2: 1 93 | ``` 94 | 95 | 可以看出:i 始终大于 e1,始终小于等于 e2 96 | 97 | ```typescript 98 | if (i > e1) { 99 | if (i <= e2) { 100 | const nextPos = e2 + 1 101 | // c2[nextPos].el 将会拿到 Node实例 ,c2[nextPos]是vnode对象 102 | const anchor = nextPos < c2.length ? c2[nextPos].el : null 103 | 104 | while (i <= e2) { 105 | // 新增 106 | patch(null, c2[i], container, parentComponent, anchor) 107 | i++ 108 | } 109 | } 110 | } 111 | 112 | // 核心的新增节点函数 113 | export function insert(child: any, container: any, anchor: any = null) { 114 | container.insertBefore(child, anchor) 115 | } 116 | ``` 117 | 118 | anchor 是作为一个锚点的存在,当右侧添加节点的时候,它将会是 null,会传入 insertBefore 的第二个参数。当需要左边添加节点的时候,他将作为被插入节点的下一个节点。这里可能很难理解,我打个比方: 119 | 120 | `图2`中需要我们插入 C 和 D 到 A 之前,那么这个 anchor 锚点就是 A。前面两个 while 循环(分别是左侧对比和右侧对比)走完之后,i 还是 0,e1 是-1,e2 是 1。最后`nextPos = 2`,`nextPos < c2.length`这个判断自然就走 true 了,而`c2[nextPos].el`就是 A。 121 | 122 | 在下一段代码 patch 的时候,内部调用 insertBefore 就能把 D、C 插入到 A 之前,仔细你会发现其实是先插入 D 再插入 C 的 🤣。 123 | 124 | 😟why?为什么是先插入 D 再插入 C 呢? 125 | 126 | 因为 i 为 0,`c2[i]`取得是 c2 的第一个元素(D)进行 patch,patch 完之后 i 自增加一,这时候才开始取第二个元素(C)进行 patch。 127 | 128 | ```typescript 129 | while (i <= e2) { 130 | // 新增 131 | patch(null, c2[i], container, parentComponent, anchor) 132 | i++ 133 | } 134 | ``` 135 | 136 | ### 删除节点 137 | 138 | [![jk1wVI.md.png](https://s1.ax1x.com/2022/06/25/jk1wVI.md.png)](https://imgtu.com/i/jk1wVI) 左侧删除节点时:i、e1、e2 的值分别是: 139 | 140 | ``` 141 | i: 2 142 | e1: 3 143 | e2: 1 144 | ``` 145 | 146 | 右侧删除节点时:i、e1、e2 的值分别是: 147 | 148 | ``` 149 | i: 0 150 | e1: 1 151 | e2: -1 152 | ``` 153 | 154 | 可以知道规律:c1 的数组长度大于 c2 的数组长度的时候,需要删除节点。删除的范围将会是:`i > e2`的时候 155 | 156 | ```typescript 157 | if (i > e2) { 158 | while (i <= e1) { 159 | remove(c1[i].el) 160 | i++ 161 | } 162 | } 163 | 164 | // 删除节点的核心函数 165 | // 这里可以换成 child.remove() 166 | export function remove(child: HTMLElement) { 167 | const parent = child.parentNode 168 | if (parent) { 169 | parent.removeChild(child) 170 | } 171 | } 172 | ``` 173 | 174 | 右侧删除节点,删除的顺序是:C、D 175 | 176 | 左侧删除节点,删除的顺序是:A、B 177 | 178 | ### 进入真正复杂的 diff 场景 179 | 180 | 181 | diff 有几个场景: 182 | 183 | 1. **删除场景**:旧节点的子节点中间部分中某一个节点在新节点的子节点中不存在。旧节点需要删除该子节点。 184 | 2. **新增场景**:新节点的子节点中间部分中某一个节点在旧节点的子节点中不存在。旧节点需要新增该子节点。 185 | 3. **更新场景**:旧节点的子节点某一个节点在新节点的子节点中状态发生改变了。旧节点需要更新该子节点。 186 | 4. **移动场景**:旧节点的子节点某一个节点在新节点的子节点中位置发生改变了。旧节点需要移动该子节点到目标位置。 187 | 188 | 先考虑删除和更新的逻辑(交换位置和新增节点逻辑先不考虑) 189 | 190 | 这里给一个例子: 191 | 192 | [![jEZVvn.md.png](https://s1.ax1x.com/2022/06/26/jEZVvn.md.png)](https://imgtu.com/i/jEZVvn) 193 | 194 | 我们把之前的左端对比和右端对比结合起来,计算出 i、e1、e2 的值就能知道中间部分的范围在哪了。 195 | 196 | 上图所示:中间部分的范围:i ~ e1、i ~ e2 197 | 198 | 目光聚焦到 c1 和 c2 的中间部分,都有 C,但是 C 的 property 并不相同,主要还是 id 不一样,需要 patch。再看 D,D 在 c2 数组里面是没有的,所以 D 需要被删除。 199 | 200 | 那怎么知道 c1 的元素在 c2 有没有呢?那就需要双重遍历了,先遍历 c1 的中间部分,拿到 c1 的索引值再遍历 c2 的中间部分,拿着这个 c1 的元素依次跟 c2 的元素进行比较,第一轮比较完之后接着拿 c1 的下一个元素跟 c2 的元素进行比较。不过这样做的话时间复杂度是 O(n^2)。要想快速找到 c1 的元素对应再 c2 上有没有,我们可以用映射查找的方式(Map)。 201 | 202 | 先把 c2 的中间部分的每一个元素的索引值都存入一个 Map 中。当遍历 c1 的时候,首先去 Map 查找看看有没有对应的元素。如果有值,直接走 patch,如果没有值,才用上面所说的双重遍历的方式查找,还是没有查找到在 c2 对应的元素呢?说明该元素是需要被删除的。当然如果查找到了还是走 patch。这样时间复杂度就从 O(n^2)变成 O(1)了。这也是为什么列表渲染的时候我们必须给元素添加一个唯一值 key 提升渲染性能 203 | 204 | ```typescript 205 | // 处理中间部分 206 | // a b (c d) e f 207 | // a b (e c) e f 208 | let s1 = i 209 | let s2 = i 210 | // 为了能使用映射查找方式需要的map容器,提高patch的效率 211 | const keyToNewIndexMap = new Map() 212 | // 遍历c2中间部分 213 | for (let i = s2; i <= e2; i++) { 214 | const nextChild = c2[i] 215 | keyToNewIndexMap.set(nextChild.key, i) 216 | } 217 | // 遍历c1中间部分,找出c1在c2对应的元素,如果找不到,就删除,找到了就更新走patch 218 | // 遍历旧节点 219 | for (let i = s1; i <= e1; i++) { 220 | const prevChild = c1[i] 221 | let nextIndex 222 | 223 | if (prevChild.key != null) { 224 | nextIndex = keyToNewIndexMap.get(prevChild.key) 225 | } else { 226 | for (let j = s2; j <= e2; j++) { 227 | if (isSameVNodeType(prevChild, c2[j])) { 228 | nextIndex = j 229 | break 230 | } 231 | } 232 | } 233 | if (nextIndex === undefined) { 234 | hostRemove(prevChild.el) 235 | } else { 236 | patch(prevChild, c2[nextIndex], container, parentComponent, null) 237 | } 238 | } 239 | ``` 240 | 241 | Map 实际上用 vnode.key 作为键去存储 c2 元素的索引值,这个 vnode.key 就是上面代码中的`nextChild.key`和`prevChild.key`,这也是为什么列表渲染的时候我们必须给元素添加一个唯一值 key 提升渲染性能。 242 | 243 | ### 删除逻辑的优化点: 244 | 245 | 上面的代码就是删除节点和更新节点的逻辑了,其中也有一个可以优化的点,那就是当知道 c2 中的元素都已经被比较过了以后,c1 中剩余的元素就没必要处理了,直接删除,提高了性能。 [![jEVbND.md.png](https://s1.ax1x.com/2022/06/26/jEVbND.md.png)](https://imgtu.com/i/jEVbND) 246 | 247 | 如图,红色部分为新增代码。 248 | 249 | 我们需要定义两个变量: 250 | 251 | ``` 252 | toBePatched:表示需要patch的元素的数量 253 | patched:表示已经patch的元素的数量 254 | ``` 255 | 256 | 已经执行了 patch 处理的时候,我们可以把 patched 自增加一,当`patched >= toBePatched`时,也就是说该 patch 的都已经 patch 完了,那剩下 c1 的元素干嘛管他呢,都认定为 c2 中没有的元素,代表要删除的元素,直接 remove 掉就好了。之后的流程不必往下走了直接进入下一轮循环。 257 | 258 | ```typescript 259 | let s1 = i 260 | let s2 = i 261 | // 新增 262 | let toBePatched = e2 - s2 + 1 263 | let patched = 0 264 | const keyToNewIndexMap = new Map() 265 | // 遍历c2中间部分 266 | for (let i = s2; i <= e2; i++) { 267 | const nextChild = c2[i] 268 | keyToNewIndexMap.set(nextChild.key, i) 269 | } 270 | for (let i = s1; i <= e1; i++) { 271 | const prevChild = c1[i] 272 | let newIndex 273 | // 如果该patch的都已经patch完了,之后的元素直接remove掉 274 | if (patched >= toBePatched) { 275 | hostRemove(prevChild.el) 276 | continue 277 | } 278 | if (prevChild.key != null) { 279 | newIndex = keyToNewIndexMap.get(prevChild.key) 280 | } else { 281 | for (let j = s2; j <= e2; j++) { 282 | if (isSameVNodeType(prevChild, c2[j])) { 283 | newIndex = j 284 | break 285 | } 286 | } 287 | } 288 | if (newIndex === undefined) { 289 | hostRemove(prevChild.el) 290 | } else { 291 | patch(prevChild, c2[newIndex], container, parentComponent, null) 292 | // 被patched的数量 +1 293 | patched++ 294 | } 295 | } 296 | ``` 297 | 298 | ### 移动节点逻辑 299 | 300 | [![jMy4oD.md.png](https://s1.ax1x.com/2022/07/01/jMy4oD.md.png)](https://imgtu.com/i/jMy4oD) 301 | 302 | 上图是 diff 算法中需要处理的节点的位置变更场景,通常我们说的 diff 的节点移动逻辑。 303 | 304 | 图中 E 被移动到了 C、D 的前面,而 C、D 也跟着发生位置的变换,如果我们思考移动逻辑该怎么实现的话,我们会很自然的想到,遍历 c2,让 c2 的每一个元素和 c1 的元素进行比较,看看位置是否发生变化,发生变化就移动到新位置。但是!这种移动逻辑真的太消耗性能了,找这样的逻辑,就需要移动三遍,执行三次`insertBefore`。 305 | 306 | 我们不妨换一种逻辑。 307 | 308 | 首先我们可以给 c1 的中间部分的节点添加一个索引(关于怎么确定中间部分就是通过 i,e1,e2 这些指针来确定 i 的变化范围) 309 | 310 | 以下是旧节点的索引(为什么是从 2 开始?因为前面还有 A、B,A 是 0,B 是 1) 311 | 312 | ``` 313 | 索引 2 3 4 314 | C D E 315 | ``` 316 | 317 | 映射到已交换位置的新节点上那就是 318 | 319 | ``` 320 | 索引 4 2 3 321 | E C D 322 | ``` 323 | 324 | 仔细观察,C、D 它们之间的关系并没有发生改变,C、D 排列顺序不变,只是 E 被移动到了 C、D 的前面,E 无论插入到哪个位置,比如插入到 C、D 的前面,还是说插入到 C、D 的中间,都不会改变 C、D 的顺序,那我们可以把 C、D 当成一个稳定序列,只需要对 E 执行一次`insertBefore`就好了啊。这是不是就减少了我们移动元素的个数了呢,从而优化了 diff 的性能。 325 | 326 | > 那怎么确定我们要移动的是 E 呢?而不是 C 或者 D 呢?换言之,哪些是需要被移动的,哪些是不需要移动的? 327 | 328 | 首先我们需要在中间部分生成这个稳定序列,判断中间部分的每个节点在不在这个稳定序列里面,如果不在,那就意味着这个节点是需要移动的,并且这个稳定序列我想要尽可能长,这样的话就能找到更多需要移动的节点和不需要移动的节点,让优化性能最大化。 329 | 330 | 尽可能长的稳定序列,稳定递增的子序列,也就是[最长递增子序列](https://en.wikipedia.org/wiki/Longest_increasing_subsequence) 331 | 332 | 关于怎么实现最长递增子序列,这里略过,具体代码在下方。该实现来自 vue3 源码里面的[renderer.ts](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts)文件。该方法传入的是一个数组,比如[4,2,3],计算出来最长递增子序列是[2,3],返回的是一个索引数组[1,2]。其中 1 2 代表元素在传入的数组中的索引值。 333 | 334 | ```typescript 335 | // 最长递增子序列 336 | function getSequence(arr: number[]): number[] { 337 | const p = arr.slice() 338 | const result = [0] 339 | let i, j, u, v, c 340 | const len = arr.length 341 | for (i = 0; i < len; i++) { 342 | const arrI = arr[i] 343 | if (arrI !== 0) { 344 | j = result[result.length - 1] 345 | if (arr[j] < arrI) { 346 | p[i] = j 347 | result.push(i) 348 | continue 349 | } 350 | u = 0 351 | v = result.length - 1 352 | while (u < v) { 353 | c = (u + v) >> 1 354 | if (arr[result[c]] < arrI) { 355 | u = c + 1 356 | } else { 357 | v = c 358 | } 359 | } 360 | if (arrI < arr[result[u]]) { 361 | if (u > 0) { 362 | p[i] = result[u - 1] 363 | } 364 | result[u] = i 365 | } 366 | } 367 | } 368 | u = result.length 369 | v = result[u - 1] 370 | while (u-- > 0) { 371 | result[u] = v 372 | v = p[v] 373 | } 374 | return result 375 | } 376 | ``` 377 | 378 | 拿到这个索引值数组之后,我们就可以拿 c2 的中间部分组成的索引数组和这个索引数组作比较了。 379 | 380 | 例如: 381 | 382 | ``` 383 | E C D 384 | [0,1,2] c2的中间部分索引值数组 385 | [1,2] 利用最长递增子序列求出的索引值数组 386 | ``` 387 | 388 | 发现 0 并不在索引值数组中,证明 E 是需要被移动的。 389 | 390 | 我们稍微在删除节点的逻辑代码上改动: 391 | 392 | ```typescript 393 | let s1 = i 394 | let s2 = i 395 | let toBePatched = e2 - s2 + 1 396 | let patched = 0 397 | // 为了能使用映射查找方式需要的map容器,提高patch的效率 398 | const keyToNewIndexMap = new Map() 399 | // 定义数组映射,用于查找出需要移动的元素以及移动的位置在哪(这极大的减少了使用insert api的次数) 400 | const newIndexToOldIndexMap = new Array(toBePatched) // 定长的数组比不定长的数组性能更好 401 | for (let i = 0; i < toBePatched; i++) { 402 | newIndexToOldIndexMap[i] = 0 // 初始化为0,表示没有被移动 403 | } 404 | // 遍历c2中间部分 405 | for (let i = s2; i <= e2; i++) { 406 | const nextChild = c2[i] 407 | keyToNewIndexMap.set(nextChild.key, i) 408 | } 409 | for (let i = s1; i <= e1; i++) { 410 | const prevChild = c1[i] 411 | let newIndex 412 | // 这里是针对删除做优化,c2的中间部分都已经被遍历过了,c1剩下的部分就没必要处理了,直接删除就好。 413 | if (patched >= toBePatched) { 414 | hostRemove(prevChild.el) 415 | continue 416 | } 417 | if (prevChild.key != null) { 418 | newIndex = keyToNewIndexMap.get(prevChild.key) 419 | } else { 420 | for (let j = s2; j <= e2; j++) { 421 | if (isSameVNodeType(prevChild, c2[j])) { 422 | newIndex = j 423 | break 424 | } 425 | } 426 | } 427 | if (newIndex === undefined) { 428 | hostRemove(prevChild.el) 429 | } else { 430 | // 能确认新的节点是存在的 431 | newIndexToOldIndexMap[newIndex - s2] = i + 1 // 这里为什么要+1,因为为0另外代表着该元素在c1上没有,需要新增(涉及到下一小节的新增逻辑) 432 | 433 | patch(prevChild, c2[newIndex], container, parentComponent, null) 434 | patched++ 435 | } 436 | } 437 | // 根据映射表生成最长递增子序列 438 | const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap) 439 | let j = 0 // 指针 440 | for (let i = 0; i < toBePatched; i++) { 441 | if (i !== increasingNewIndexSequence[j]) { 442 | console.log('需要移动'); 443 | }else{ 444 | // 不需要移动 指针j++,判断递增子序列的下一个索引是否与 i 相等 445 | j++ 446 | } 447 | } 448 | ``` 449 | 450 | 找到需要移动的节点后,我们考虑怎么把他插入到正确的位置了。 451 | 452 | > `insertBefore`这个 dom api 是一个插入节点的很好的选择。不过第二个参数`锚点`的选择需要注意,怎么找好这个锚点? 453 | 454 | 根据上面的例子,肉眼可见很快就确定了锚点是 C 节点。但是代码层面上怎么实现?我们习惯思维是正序遍历 c2 的中间部分,找 E 的下一个节点作为锚点,在上面例子上或许行得通。但是有没有想过,如果其他情况下,C 也是需要移动位置的节点呢?正序遍历寻找下一个节点,下一个节点未必是一个稳定不变的节点哦。这也得益于 insertBefore 的特性,只能选择下一个节点作为锚点,那如果选择上一个节点作为锚点那就不一样了喔,使用正序遍历就行。 455 | 456 | ``` 457 | A B [E C D] F G 458 | ``` 459 | 460 | 使用倒叙遍历可以解决这个问题。 461 | 462 | 1. 先在 D 节点开始,因为 F 是固定不变的节点,可以使用 F 作为锚点。 463 | 2. D 节点被 patched 上去后,D 就变成固定不变的节点了,便可以使用 D 作为锚点。 464 | 3. 插入 E 的时候,C 已经变成固定不变的节点,可以使用 C 作为锚点。 465 | 466 | ```typescript 467 | // 为什么需要倒序遍历呢?因为需要一个固定的节点作为锚点,正序遍历只能确定一个不稳定的锚点 468 | // looping backwards so that we can use last patched node as anchor 469 | let j = increasingNewIndexSequence.length - 1 // 指针 470 | for (let i = toBePatched - 1; i >= 0 ; i--) { 471 | const nextIndex = i + s2 472 | const nextChild = c2[nextIndex] 473 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null // 防止nextIndex + 1超出 l2 范围 474 | if (i !== increasingNewIndexSequence[j]) { 475 | console.log('需要移动') 476 | hostInsert(nextChild.el, container, anchor) 477 | } else { 478 | // 不需要移动 指针j++,判断递增子序列的下一个索引是否与 i 相等 479 | j-- 480 | } 481 | } 482 | 483 | ``` 484 | ### 新增节点逻辑 485 | 486 | 上一小节留了一个小坑,打算在这补一下。 487 | 488 | `newIndexToOldIndexMap`创建索引值映射表的时候,都初始化为0,然而在patch的时候我偏偏让元素+1,就是为了有一个值能区分该节点需不需要被新建。 489 | 490 | ``` 491 | 没有需要新增的节点 492 | [4,2,3] ==加1后==> [5,3,4] 493 | 有需要新增的节点 494 | [0,4,2,3] ==加1后==> [0,5,3,4] 495 | ``` 496 | 497 | 那么只需要判断`newIndexToOldIndexMap[i]`是否等于0即可判断出是否需要执行`patch()` 498 | 499 | ```typescript 500 | let j = increasingNewIndexSequence.length - 1 // 指针 501 | for (let i = toBePatched - 1; i >= 0 ; i--) { 502 | const nextIndex = i + s2 503 | const nextChild = c2[nextIndex] 504 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null 505 | // 新增节点 506 | if(newIndexToOldIndexMap[i] === 0){ 507 | patch(null, nextChild, container, parentComponent,anchor) 508 | continue 509 | } 510 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 511 | console.log('需要移动') 512 | hostInsert(nextChild.el, container, anchor) 513 | } else { 514 | j-- 515 | } 516 | 517 | } 518 | ``` 519 | 520 | ### 使用 moved 标识符优化性能 521 | 522 | 提高diff的性能对vue的渲染速度来说非常重要,而毫无意义的计算最长递增子序列明显拖慢了diff的速度。 523 | 524 | 这时候我们可以用moved这个布尔值变量去表示c1对比c2来说,c2里面有没有节点被移动,用来判断是否需要执行最长递增子序列的计算。 525 | 526 | ``` 527 | moved ? getSequence() : [] 528 | ``` 529 | > 那应该如何判断c2里面节点需不需要移动了呢? 530 | 531 | [![j8Tusx.md.png](https://s1.ax1x.com/2022/07/03/j8Tusx.md.png)](https://imgtu.com/i/j8Tusx) 532 | 533 | 我们需要定义一个变量`maxNewIndexSoFar`用来记录上一个节点在映射表`keyToNewIndexMap`里的值 534 | ```typescript 535 | // newIndex也是keyToNewIndexMap映射表的映射值哦 536 | if (newIndex >= maxNewIndexSoFar){ 537 | maxNewIndexSoFar = newIndex 538 | }else{ 539 | moved = true 540 | } 541 | ``` 542 | 如果新的映射值比上一个映射值大,那么赋值作为上一个映射值,否则判定为已经移动了。 543 | 544 | 最后生成最长递增子序列的时候加以判断 545 | ```typescript 546 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [] 547 | ``` 548 | 549 | 这样就避免了没有意义的计算,提高了性能。 550 | 551 | ## 结尾 552 | vue3的双端对比diff算法的比vue2的简单粗暴的diff算法性能上更高效,但是实现起来也会更绕,那么下一篇文章我们来聊聊组件的更新逻辑。 553 | 554 | ## 最后肝血阅读,栓 Q 555 | -------------------------------------------------------------------------------- /src/runtime-dom/docs/3.summary.md: -------------------------------------------------------------------------------- 1 | # Vue3 组件的更新逻辑 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '../runtime-core/renderer' 2 | import { isFunction, isOn } from '../shared' 3 | 4 | // 创建元素 5 | export function createElement(type: any) { 6 | return document.createElement(type) 7 | } 8 | // 处理props 9 | export function patchProp(el: any, key: string, oldValue: any, newValue: any) { 10 | if (Array.isArray(newValue)) { 11 | el.setAttribute(key, newValue.join(' ')) 12 | } else if (isOn(key) && isFunction(newValue)) { 13 | // 添加事件 14 | el.addEventListener(key.slice(2).toLowerCase(), newValue) 15 | } else { 16 | // props属性的属性值是undefined或者null,删除该属性 17 | if (newValue === null || newValue === undefined) { 18 | el.removeAttribute(key) 19 | } else { 20 | el.setAttribute(key, newValue) 21 | } 22 | } 23 | } 24 | // 插入元素 anchor锚点 插入哪一个位置之前,如果是null则默认插入到最后 25 | export function insert(child: any, container: any, anchor: any = null) { 26 | container.insertBefore(child, anchor) 27 | // container.append(el) 28 | } 29 | // 删除元素 30 | export function remove(child: HTMLElement){ 31 | const parent = child.parentNode 32 | if(parent){ 33 | parent.removeChild(child) 34 | } 35 | } 36 | // 设置元素文本 37 | export function setElementText(el:HTMLElement, text: string){ 38 | el.textContent = text 39 | } 40 | // 通过对以上函数的抽离,方便实现了自定义渲染器的逻辑 41 | // 以后想自定义渲染器,传入三个函数即可 42 | const render = createRenderer({ 43 | createElement, 44 | patchProp, 45 | insert, 46 | remove, 47 | setElementText 48 | }) 49 | export function createApp(...args: any[]) { 50 | // @ts-ignore 51 | return render.createApp(...args) 52 | } 53 | // 因为runtime-core是更底层的实现,所以应该在runtime-dom里面导出,之后index.ts里面导出runtime-dom 54 | export * from '../runtime-core' 55 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shapeFlags' 2 | export const extend = Object.assign 3 | // 判断value是否object或者array 4 | export const isObject = (value: unknown) => { 5 | return value !== null && typeof value === 'object' 6 | } 7 | export const isString = (value: unknown) => { 8 | return typeof value === 'string' 9 | } 10 | // 类型保护 11 | export const isFunction = (value: unknown): value is Function => { 12 | return typeof value === 'function' 13 | } 14 | export const isArray = Array.isArray 15 | export const hasChanged = (newValue: any, value: any) => { 16 | return !Object.is(newValue, value) 17 | } 18 | export const isOn = (key: string) => /^on[A-Z]/.test(key) 19 | export const hasOwn = (target: Record, key: any) => 20 | Object.prototype.hasOwnProperty.call(target, key) 21 | 22 | // 把kabobCase => camelCase 23 | export const camelCase = (str: string) => { 24 | return str.replace(/-(\w)/g, (_, $1: string) => { 25 | return $1.toUpperCase() 26 | }) 27 | } 28 | // 首字母大写 29 | export const capitalize = (str: string) => { 30 | return str.charAt(0).toUpperCase() + str.slice(1) 31 | } 32 | // 事件前缀追加'on' 33 | export const toHandlerKey = (eventName: string) => { 34 | return eventName ? 'on' + capitalize(eventName) : '' 35 | } 36 | export const EMPTY_OBJ = {} 37 | -------------------------------------------------------------------------------- /src/shared/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | // 为什么不直接用对象然后属性值是1,2,3,4,5.。。。 2 | // export const ShapeFlags = { 3 | // ElEMENT: 1, 4 | // STATEFUL_COMPONENT: 2, 5 | // TEXT_CHILDREN: 3, 6 | // ARRAY_CHILDREN: 4, 7 | // } 8 | export const enum ShapeFlags { 9 | ELEMENT = 1, // 00001 10 | STATEFUL_COMPONENT = 1 << 1, // 00010 11 | TEXT_CHILDREN = 1 << 2, // 00100 12 | ARRAY_CHILDREN = 1 << 3, // 01000 13 | SLOTS_CHILDREN = 1 << 4 // 10000 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["DOM", "DOM.Iterable", "ES2016"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "esnext" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": ["jest"] /* Specify type package names to be included without being referenced in a source file. */, 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------