├── .gitignore ├── .vscode └── settings.json ├── babel.config.js ├── docs └── getSequence.md ├── examples ├── apiInject │ ├── App.js │ ├── index.html │ └── main.js ├── compiler-base │ ├── App.js │ ├── index.html │ └── main.js ├── componentEmit │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlots │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentUpdate │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── customRenderer │ ├── App.js │ ├── index.html │ └── main.js ├── getCurrentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── hello-world │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── nextTicker │ ├── App.js │ ├── index.html │ └── main.js ├── patchChildren │ ├── App.js │ ├── ArrayToArray.js │ ├── ArrayToText.js │ ├── TextToArray.js │ ├── TextToText.js │ ├── index.html │ └── main.js └── update │ ├── App.js │ ├── index.html │ └── main.js ├── lib ├── mini-vue.cjs.js └── mini-vue.esm.js ├── package.json ├── rollup.config.js ├── src ├── compiler-core │ ├── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── compile.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ ├── transforms │ │ │ ├── transformElement.ts │ │ │ ├── transformExpression.ts │ │ │ └── transformText.ts │ │ └── utils.ts │ └── tests │ │ ├── __snapshots__ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parse.spec.ts │ │ └── transform.spec.ts ├── index.ts ├── reactivity │ ├── baseHandler.ts │ ├── computed.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── tests │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentRenderUtils.ts │ ├── componentSlots.ts │ ├── createApp.ts │ ├── h.ts │ ├── helpers │ │ └── renderSlot.ts │ ├── index.ts │ ├── renderer.ts │ ├── scheduler.ts │ └── vnode.ts ├── runtime-dom │ └── index.ts └── shared │ ├── index.ts │ └── toDisplayString.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.ignoreWords": [ 3 | "vnode" 4 | ] 5 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /docs/getSequence.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | 手撕最长递增子序列以及 vue3 里的实现 4 | 5 | # 前言 6 | 7 | 此处不讨论到 vue3 的 快速 diff 算法,只要记住三个步骤,顺着思路展开细节即可: 8 | 9 | 1. 双端预处理 10 | 11 | 2. 理想状态(新旧节点序列总有一个被处理掉) 12 | - 根据剩下的那个序列做 *新增* 或 *移除* 13 | 14 | 3. 非理想状态(中间对比),只要记住两点: 15 | - 如何*找到*需要移动的节点以及如何*移动* 16 | - *找到*需要被添加或移除的节点并做对应操作 17 | 18 | 具体的设计思路可以去参考 HCY 的书籍,这里不做过多阐述。 19 | 20 | 但是很可能有人需要考验你的代码及算法功底,此时就会让你手撕一个 LIS(最长递增子序列)。 21 | 22 | 因此我这里对 LIS 的算法做一个补充笔记。 23 | 24 | # dp 算法 25 | 26 | ## 问题简化 27 | 28 | 我们先进行问题简化,不要求出 LIS,而是求出 LIS 的*长度*,可以参考 leetcode 的 [300 题](https://leetcode-cn.com/problems/longest-increasing-subsequence/) 29 | 30 | 后面的实现可以丢到 leetcode 中进行测试 31 | 32 | ## 算法 33 | 34 | 这里定义 `dp[i]` 为考虑前 `i` 个元素,以第 `i` 个数字结尾的 LIS 的长度,因此动转方程也就很好写了 35 | 36 | dp[i] = max(dp[j]) + 1, `j` 区间是 [0, i) 且 nums[j] < nums[i] 37 | 38 | ```js 39 | var lengthOfLIS = function(nums) { 40 | let dp = new Array(nums.length).fill(1) 41 | for (let i = 0; i < nums.length; i++) { 42 | for (let j = 0; j < i; j++) { 43 | if (nums[j] < nums[i]) { 44 | dp[i] = Math.max(dp[i],dp[j]+1) 45 | } 46 | } 47 | } 48 | return Math.max(...dp) 49 | }; 50 | ``` 51 | 52 | 但是这个和 vue3 的 LIS 算法还相差甚远,而且它还是 O(n^2) 的肯定不能让人满意 53 | 54 | # 贪心算法 55 | 56 | ## 前言 57 | 58 | 在想办法降低时间复杂度前,我们先想想能不能换个思路,用 贪心 来解决这个问题 59 | 60 | 贪心思路就是,如果我们要使 LIS 尽可能的长,那么就要让序列上升得尽可能慢,因此希望每次在子序列的末尾数字要尽可能小。 61 | 62 | 所以这里维护一个数组 `d[i]`,保存 `nums` 中的数字。 `i` 索引表示,在 `i + 1` 长度时候的 LIS 的末尾*最小值* 63 | 64 | 看不懂这个解释没关系,只需要记住一点,贪心算法中的数组,并没有保存*完整的* LIS 序列,它只关心某个长度下末尾的最小值。具体可以看下面的例子理解 65 | 66 | ## 例子 67 | 68 | 以数组 ` [0,8,4,12,2]` 为例,贪心会得到一个序列 `[0, 2, 12]` 69 | 70 | d 序列存储的并非是最长递增子序列,而是对应 len 的末尾最小值,如果约束: 71 | 72 | - 最长递增子序列的 len 为 1 时,找到对应的 0 索引:`[0,2,12]` -> d[0] = 0,即如果 LIS 最长只能为 1,那么它的末尾元素一定为 0。对应的 LIS 为 `[0]` 73 | - 最长递增子序列的 len 为 2 时,找到对应的 1 索引:`[0,2,12]` -> d[1] = 2,即如果 LIS 最长只能为 2,那么它的末尾元素一定为 2。对应的 LIS 为 `[0,2]` 74 | - 最长递增子序列的 len 为 3 时,找到对应的 2 索引:`[0,2,12]` -> d[2] = 12,即如果 LIS 最长只能为 3,那么它的末尾元素一定为 12。对应的 LIS 为 `[0,4,12]` 75 | 76 | 即贪心算法没有保留子序列,它只是保留了对应长度的最后一个数字 77 | 78 | ## 算法 79 | 80 | 算法的思路就是遍历 nums: 81 | 82 | - 如果 nums[i] 大于 d的末尾元素,那么直接将 nums[i] push 进来 83 | - 反之 nums[i] 一定可以替换掉 d 里的一个数字,找到那个数字并做替换 84 | 85 | ```js 86 | var lengthOfLIS = function(nums) { 87 | if (!nums.length) return 0 88 | let d = [nums[0]] 89 | for (let i = 1; i < nums.length; i++) { 90 | const numsI = nums[i] 91 | if (numsI > d[d.length - 1]) { 92 | // 塞进 d 末尾 93 | d.push(numsI) 94 | } else { 95 | let j 96 | for (j = 0; j < d.length; j++) { 97 | if (numsI <= d[j]) { 98 | // 找到可以替换的值 99 | break 100 | } 101 | } 102 | d[j] = numsI 103 | } 104 | } 105 | return d.length 106 | }; 107 | ``` 108 | 109 | 但是此时贪心算法的时间复杂度还是 O(n^2) 并没有减少,别急,这时候我们看看有没有可以优化的点。我们发现在 d 序列中找可以替换的值时候,用了一个循环遍历了 d 序列,但是在之前例子中我们可以隐约感觉到 d 是 *单调递增* 的。 110 | 111 | 没错,d 确实是单调递增的,下个小节我给一个数学证明,不想看的跳过即可。利用 d 的 *单调递增* 性质可以使用 二分 找到想要替换的值。 112 | 113 | ## 数学证明 114 | 115 | 证明当 j < i 时, d[j] < d[i] 116 | 117 | 证明: 118 | 119 | 题设为 d[i] 表示一个长度为 i 的 LIS 的末尾**最小**元素 120 | 121 | 假设存在 j < i 时,d[j] >= d[i] 122 | 123 | 此时创造一个长度为 j 的 LIS 命名为序列 B, 124 | 125 | 该序列 B 由长度为 i 的 LIS 从末尾删除 i-j 个元素所构成 126 | 127 | 并设序列 B 的末尾元素为 x 128 | 129 | 由 LIS 特性可知: x < d[i] 130 | 131 | 又由假设可知: x < d[i] <= d[j] 即 x < d[j] 132 | 133 | 因此存在一个长度为 j 的序列 B, 其末尾元素 x < d[j] 134 | 135 | 与题设相矛盾, 得证 d[i] 具有单调性 136 | 137 | # 贪心+二分 138 | 139 | ## 前言 140 | 141 | 这个算法中,我们仅仅是将贪心中 对 `j` 索引的搜索从遍历变成了二分,因此时间复杂度最终会变成 O(nlogn)。 142 | 143 | 但是注意一点,这次我将不再使用 d 数组来存储 nums 的值,而是使用 d 数组存储 nums 对应的 index 值,方便后续把 LIS 还原出来。思考一个问题,此时的 d[i] 将不再具备 *单调递增* 的性质,那还可以用二分搜索么?其实是没有问题的,第二层搜索的时候,实际变成了对 nums[d[i]] 的搜索,而 nums[d[i]] 依旧具备 *单调递增* 性质 144 | 145 | ## 算法 146 | 147 | ```js 148 | var lengthOfLIS = function (nums) { 149 | if (!nums.length) return 0 150 | let d = [0] 151 | for (let i = 1; i < nums.length; i++) { 152 | const numsI = nums[i] 153 | if (numsI > nums[d[d.length - 1]]) { 154 | d.push(i) 155 | } else { 156 | // 将搜索换成二分 157 | // 选一个自己喜欢的二分即可 158 | let l = 0 159 | let r = d.length - 1 160 | while (l <= r) { 161 | let mid = (l + r) >> 1 162 | if (numsI > nums[d[mid]]) { 163 | l = mid + 1 164 | } else { 165 | r = mid - 1 166 | } 167 | } 168 | d[l] = i 169 | } 170 | } 171 | return d.length 172 | } 173 | ``` 174 | 175 | # 贪心+二分+路径回溯 176 | 177 | ## 前言 178 | 179 | 由于*贪心*算法只会保留当前长度的 LIS 下的末尾*最小*元素,因此我们需要使用一个 path 辅助数组 180 | 181 | `path[i]` 存放了到达当前的 `numsI` 的 `prevIndex` 182 | 183 | 并且此时的实现,需要用 ts 来写,之后丢到 vue3 源码中使用尤大的测试来检验 184 | 185 | ## 实现 186 | 187 | ```typescript 188 | function getSequence(nums: number[]): number[] { 189 | const path = nums.slice() 190 | let d = [0] 191 | for (let i = 1; i < nums.length; i++) { 192 | const numsI = nums[i] 193 | if (numsI > nums[d[d.length - 1]]) { 194 | // 记录路径 195 | // 是由当前的 d 末尾索引指向的元素到达的 numsI 196 | path[i] = d[d.length - 1] 197 | d.push(i) 198 | } else { 199 | let l = 0 200 | let r = d.length - 1 201 | while (l <= r) { 202 | let mid = (l + r) >> 1 203 | if (numsI > nums[d[mid]]) { 204 | l = mid + 1 205 | } else { 206 | r = mid - 1 207 | } 208 | } 209 | // 记录路径 210 | // 使用 i 覆盖 d[l] 211 | // 因此记录 path[i] 上一个索引为 d[l - 1] 212 | path[i] = d[l - 1] 213 | d[l] = i 214 | } 215 | } 216 | // 反向恢复出正确的 LIS 索引数组 217 | let prev = d[d.length - 1] 218 | for (let i = d.length - 1; i >= 0; i--) { 219 | d[i] = prev 220 | // 通过 path 返回上一个 index 221 | prev = path[prev] 222 | } 223 | return d 224 | } 225 | ``` 226 | 227 | ## edge case 228 | 229 | 到这里 `getSequence` 已经算实现的差不多了,我们回顾一下最开始的问题: 230 | 231 | 手撕最长递增子序列以及 vue3 里的实现 232 | 233 | 手撕已经完成了,但是 vue3 的实现和我们的手撕有什么区别么?可以看下面这个例子: 234 | 235 | `[2,0,1,3,4,5]` 的 LIS 应该是 `[0,1,3,4,5]` 而对应的 index 结果应该为 `[1,2,3,4,5]` 236 | 237 | 但是如果你使用 vue3 的源码来执行上面的例子,就会发现结果为 `[2,3,4,5]` 238 | 239 | ```typescript 240 | function getSequence(arr: number[]): number[] { 241 | const p = arr.slice() 242 | // 凡事总有例外 243 | // 由于 result 初始化会把 index 为 0 塞进去 244 | // 如果 arr[0] === 0 的话会照样进入 result 245 | const result = [0] 246 | let i, j, u, v, c 247 | const len = arr.length 248 | for (i = 0; i < len; i++) { 249 | const arrI = arr[i] 250 | // 但是除了第一个元素为 0 的情况 251 | // arr 中的 其他 0 是不参与 result 的构造的 252 | if (arrI !== 0) { 253 | // 省略构造 result 代码 254 | } 255 | } 256 | // 省略恢复 result 数组代码 257 | return result 258 | } 259 | ``` 260 | 261 | 为什么构造 LIS 的时候不考虑 0 的情况呢?我们看下调用 `getSequence` 的情况在哪里 262 | 263 | ```typescript 264 | // moved 标记了存在需要移动的节点 265 | // 如果存在那么就从 newIndexToOldIndexMap 中生成 LIS 266 | // newIndexToOldIndexMap 是 新节点序列 index 和 旧节点序列 index 的索引 267 | const increasingNewIndexSequence = moved 268 | ? getSequence(newIndexToOldIndexMap) 269 | : EMPTY_ARR 270 | ``` 271 | 272 | 参考尤大的注释可以得知,0 是一种特殊的标记,标记了新增的节点 273 | 274 | 同时由于 offset +1 的存在,后面的代码也都会带有这个 offset,可以参见[这一行](https://github.com/vuejs/core/blob/0683a022ec83694e29636f64aaf3c04012e9a7f0/packages/runtime-core/src/renderer.ts#L1931) 275 | 276 | 277 | ```typescript 278 | // works as Map 279 | // Note that oldIndex is offset by +1 280 | // and oldIndex = 0 is a special value indicating the new node has 281 | // no corresponding old node. 282 | // used for determining longest stable subsequence 283 | const newIndexToOldIndexMap = new Array(toBePatched) 284 | for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 285 | ``` 286 | 287 | 我们求出 LIS 的目的是为了找到哪些节点无需移动,而新增的节点根本不在节点移动的讨论范畴之内。 288 | 289 | 因此在 LIS 算法中,也无需考虑 0 的情况了。 290 | 291 | -------------------------------------------------------------------------------- /examples/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | import { provide, inject } from '../../lib/mini-vue.esm.js' 3 | 4 | export const ProvideOne = { 5 | name: 'ProvideOne', 6 | setup() { 7 | provide('foo', 'foo') 8 | provide('bar', 'bar') 9 | }, 10 | render() { 11 | return h(ProvideTwo) 12 | }, 13 | } 14 | 15 | const ProvideTwo = { 16 | name: 'ProvideTwo', 17 | setup() { 18 | provide('foo', 'fooOverride') 19 | provide('baz', 'baz') 20 | const foo = inject('foo') 21 | return { 22 | foo, 23 | } 24 | }, 25 | render() { 26 | return h('Fragment', {}, [h('p', {}, `ProvideTwo: ${this.foo}`), h(Foo)]) 27 | }, 28 | } 29 | 30 | const Foo = { 31 | name: 'Foo', 32 | setup() { 33 | const foo = inject('foo') 34 | const bar = inject('bar') 35 | const baz = inject('baz') 36 | const bax = inject('bax', 'baxDefault') 37 | const func = inject('func', () => 'func') 38 | return { 39 | foo, 40 | bar, 41 | baz, 42 | bax, 43 | func, 44 | } 45 | }, 46 | render() { 47 | return h( 48 | 'p', 49 | {}, 50 | `Foo: ${this.foo} - ${this.bar} - ${this.baz} - ${this.bax} - ${this.func}` 51 | ) 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /examples/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/apiInject/main.js: -------------------------------------------------------------------------------- 1 | import { ProvideOne } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(ProvideOne).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/compiler-base/App.js: -------------------------------------------------------------------------------- 1 | import { ref } from '../../lib/mini-vue.esm.js' 2 | export const App = { 3 | name: 'App', 4 | template: `
hi, {{message}}, {{count}}
`, 5 | setup() { 6 | const message = 'mini-vue' 7 | const count = (window.count = ref(0)) 8 | return { 9 | message, 10 | count, 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /examples/compiler-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/compiler-base/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | setup() { 6 | return { 7 | count: 0, 8 | } 9 | }, 10 | render() { 11 | return h('div', {}, [ 12 | h('p', {}, `App: ${this.count}`), 13 | h(Foo, { 14 | count: this.count, 15 | // emit: onEvent 16 | onAdd() { 17 | console.log('onAdd: App Component') 18 | // TODO: 完成子组件修改父组件 data 19 | // 几个问题: 20 | // 涉及到了组件更新 21 | // 如果将 emit 改写为 handler && handler.call(instance) 22 | // 那么此时的 instance 实际为 子组件,即这里的 this 指向的是子组件 23 | // console.log(this.props.count) 24 | }, 25 | }), 26 | ]) 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /examples/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | console.log('props: ', props) 6 | const emitAdd = () => { 7 | console.log('Foo Component') 8 | // event 发射到父组件 -> 执行父组件的 onEvent 9 | emit('add') 10 | } 11 | return { 12 | emitAdd, 13 | } 14 | }, 15 | render() { 16 | const foo = h('p', {}, `Foo: ${this.count}`) 17 | const btn = h( 18 | 'button', 19 | { 20 | onClick: this.emitAdd, 21 | }, 22 | 'emit add' 23 | ) 24 | return h('div', {}, [foo, btn]) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /examples/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from '../../lib/mini-vue.esm.js' 2 | import Foo from './Foo.js' 3 | 4 | export default { 5 | setup() {}, 6 | 7 | render() { 8 | // TODO: 字符串字面量 9 | // TODO: slots 传递组件 10 | let foo 11 | // case 1: slots 为 single element 12 | // foo = h('p', {}, 'this is a single element') 13 | 14 | // case 2: slots 为 element 数组 15 | // foo = [h('p', {}, 'element-01'), h('p', {}, 'element-02')] 16 | 17 | // case 3: 具名插槽 18 | // 从 case 3 开始,slots 的数据结构亦发生了变化: array -> object 19 | // case2 变为 20 | // foo = { 21 | // default: [h('p', {}, 'element-01'), h('p', {}, 'element-02')], 22 | // } 23 | // foo = { 24 | // header: [h('p', {}, 'element-01'), h('p', {}, 'element-02')], 25 | // footer: h('p', {}, 'this is footer'), 26 | // } 27 | 28 | // case4: 作用域插槽 29 | // slots 依旧是 obj 30 | // slot 从 array 变成了 返回 array 的 function 31 | // slot() 后才会得到对应的 array 32 | foo = { 33 | header: ({ num_1, num_2 }) => [ 34 | h('p', {}, 'element-' + num_1), 35 | h('p', {}, 'element-' + num_2), 36 | ], 37 | footer: () => [ 38 | h('p', {}, 'this is footer'), 39 | createTextVNode(' And a TextNode'), 40 | ], 41 | } 42 | return h('div', {}, [h('div', {}, 'Parent Component'), h(Foo, {}, foo)]) 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /examples/componentSlots/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlot } from '../../lib/mini-vue.esm.js' 2 | // renderSlot 会返回一个 vnode 3 | // 其本质和 h 是一样的 4 | // 第三个参数给出数据 5 | export default { 6 | setup() {}, 7 | render() { 8 | // case1: single element 9 | // this.$slots -> instance.slots -> instance.vnode.children 10 | // return h('div', {}, [h('p', {}, 'Child Component'), this.$slots]) 11 | 12 | // case2: elements array 13 | // 粗暴解法1: 如果把 this.$slots 直接解构似乎也是可以的 14 | // return h('div', {}, [h('p', {}, 'Child Component'), ...this.$slots]) 15 | // 粗暴解法2: 但是实际上需要一个 vnode 来接住 slots,即类似 h('div',{},this.$slots) 16 | // return h('div', {}, [ 17 | // h('p', {}, 'Child Component'), 18 | // renderSlot(this.$slots), 19 | // ]) 20 | 21 | // case3: 具名插槽 22 | // return h('div', {}, [ 23 | // renderSlot(this.$slots, 'header'), 24 | // h('p', {}, 'Child Component'), 25 | // renderSlot(this.$slots, 'footer'), 26 | // ]) 27 | // case4: 作用域插槽 28 | 29 | return h('div', {}, [ 30 | renderSlot(this.$slots, 'header', { num_1: 1, num_2: 2 }), 31 | h('p', {}, 'Child Component'), 32 | renderSlot(this.$slots, 'footer'), 33 | ]) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /examples/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue.esm.js' 2 | import Child from './Child.js' 3 | 4 | export default { 5 | setup() { 6 | let count = ref(0) 7 | let msg = ref('Child props') 8 | window.count = count 9 | window.msg = msg 10 | return { 11 | msg, 12 | count, 13 | } 14 | }, 15 | render() { 16 | return h('div', {}, [ 17 | h('p', {}, `parent count: ${this.count}`), 18 | h(Child, { msg: this.msg }), 19 | ]) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /examples/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | 3 | export default { 4 | setup() {}, 5 | render() { 6 | return h('p', {}, this.$props.msg) 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /examples/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | 3 | export const App = { 4 | setup() { 5 | return { 6 | x: 100, 7 | y: 100, 8 | } 9 | }, 10 | render() { 11 | return h('rect', { x: this.x, y: this.y }) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /examples/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '../../lib/mini-vue.esm.js' 2 | import { App } from './App.js' 3 | 4 | const game = new PIXI.Application({ 5 | width: 500, 6 | height: 500, 7 | }) 8 | 9 | document.body.append(game.view) 10 | 11 | const renderer = createRenderer({ 12 | createElement(type) { 13 | if (type === 'rect') { 14 | const rect = new PIXI.Graphics() 15 | rect.beginFill(0xff0000) 16 | rect.drawRect(0, 0, 100, 100) 17 | rect.endFill() 18 | 19 | return rect 20 | } 21 | }, 22 | patchProp(el, key, val) { 23 | el[key] = val 24 | }, 25 | insert(el, parent) { 26 | parent.addChild(el) 27 | }, 28 | }) 29 | 30 | renderer.createApp(App).mount(game.stage) 31 | -------------------------------------------------------------------------------- /examples/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | window.self = null 5 | export const App = { 6 | name: 'App', 7 | setup() { 8 | console.log('App: ', getCurrentInstance()) 9 | }, 10 | render() { 11 | return h( 12 | 'div', 13 | { 14 | id: 'root', 15 | }, 16 | [h('p', {}, 'App'), h(Foo)] 17 | ) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue.esm.js' 2 | 3 | export const Foo = { 4 | name: 'Foo', 5 | setup() { 6 | console.log('Foo: ', getCurrentInstance()) 7 | }, 8 | render() { 9 | return h('div', {}, 'Foo') 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /examples/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/hello-world/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | // 测试代码 5 | window.self = null 6 | export const App = { 7 | setup() { 8 | return { 9 | msg: 'this is a msg from setup', 10 | } 11 | }, 12 | render() { 13 | // 测试代码 14 | window.self = this 15 | return h( 16 | 'div', 17 | { 18 | id: 'root', 19 | onClick: () => { 20 | alert('click!') 21 | }, 22 | }, 23 | [ 24 | h('p', { class: 'red' }, 'hello'), 25 | h('p', { class: 'green' }, 'mini-vue'), 26 | // this.setupState.msg -> this.msg 27 | // this.$el -> root element 实例, 给用户直接操作 dom 的方法 28 | // this.$data 29 | // 上述通过 代理模式 统一交给用户 30 | h('p', { class: 'blue' }, this.msg), 31 | h(Foo, { count: 1 }), 32 | ] 33 | ) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /examples/hello-world/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | 3 | // props 4 | // 1. 参数接收 props 5 | // 2. render 通过 this 可以获取到 props 6 | // 3. props 是 shallowReadonly 7 | // TODO: props 是否合法,非法需要加入 attrs 8 | export const Foo = { 9 | setup(props) { 10 | // props = { count: 1 } 11 | console.log(props) 12 | // props.count should still be 1 13 | // 同时应该抛出一个 warning 14 | props.count++ 15 | }, 16 | render() { 17 | // props.count should still be 1 18 | // 由于 PublicInstanceProxyHandlers 没有 set 所以并不会奏效 19 | this.count = 100 20 | return h('div', {}, `props: ${this.count}`) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/hello-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/hello-world/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, nextTick } from '../../lib/mini-vue.esm.js' 2 | export default { 3 | name: 'App', 4 | setup() { 5 | const count = ref(0) 6 | const onClick = async () => { 7 | console.log('onClick') 8 | for (let i = 0; i < 5; i++) { 9 | count.value = i 10 | } 11 | // 此时视图由于异步还没有更新 -> 0 12 | console.log('count: ', document.getElementById('counter').textContent) 13 | nextTick(() => { 14 | console.log('count: ', document.getElementById('counter').textContent) 15 | }) 16 | // await nextTick() 17 | // console.log('count: ', document.getElementById('counter').textContent) 18 | } 19 | return { 20 | count, 21 | onClick, 22 | } 23 | }, 24 | render() { 25 | return h('div', {}, [ 26 | h('p', { id: 'counter' }, `${this.count}`), 27 | h( 28 | 'button', 29 | { 30 | onClick: this.onClick, 31 | }, 32 | `start loop` 33 | ), 34 | ]) 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /examples/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue.esm.js' 2 | import App from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue.esm.js' 2 | import { ArrayToText } from './ArrayToText.js' 3 | import { TextToText } from './TextToText.js' 4 | import { TextToArray } from './TextToArray.js' 5 | import { ArrayToArray } from './ArrayToArray.js' 6 | 7 | export const App = { 8 | setup() {}, 9 | render() { 10 | return h('div', {}, [ 11 | h('p', {}, 'App Component'), 12 | // h(ArrayToText), 13 | // h(TextToText), 14 | // h(TextToArray), 15 | h(ArrayToArray), 16 | ]) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /examples/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import { ref, h } from '../../lib/mini-vue.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 | // 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: 'C' }, 'C'), 54 | // h('p', { key: 'A' }, 'A'), 55 | // h('p', { key: 'B' }, 'B'), 56 | // ] 57 | 58 | // 4. 老的比新的长 59 | // 删除老的 60 | // 左侧 61 | // (a b) c 62 | // (a b) 63 | // i = 2, e1 = 2, e2 = 1 64 | // const prevChildren = [ 65 | // h("p", { key: "A" }, "A"), 66 | // h("p", { key: "B" }, "B"), 67 | // h("p", { key: "C" }, "C"), 68 | // ]; 69 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 70 | 71 | // 右侧 72 | // a (b c) 73 | // (b c) 74 | // i = 0, e1 = 0, e2 = -1 75 | 76 | // const prevChildren = [ 77 | // h('p', { key: 'A' }, 'A'), 78 | // h('p', { key: 'B' }, 'B'), 79 | // h('p', { key: 'C' }, 'C'), 80 | // ] 81 | // const nextChildren = [h('p', { key: 'B' }, 'B'), h('p', { key: 'C' }, 'C')] 82 | 83 | // 5. 对比中间的部分 84 | // 删除老的 (在老的里面存在,新的里面不存在) 85 | // 5.1 86 | // a,b,(c,d),f,g 87 | // a,b,(e,c),f,g 88 | // D 节点在新的里面是没有的 - 需要删除掉 89 | // C 节点 props 也发生了变化 90 | 91 | // const prevChildren = [ 92 | // h('p', { key: 'A' }, 'A'), 93 | // h('p', { key: 'B' }, 'B'), 94 | // h('p', { key: 'C', id: 'c-prev' }, 'C'), 95 | // h('p', { key: 'D' }, 'D'), 96 | // h('p', { key: 'F' }, 'F'), 97 | // h('p', { key: 'G' }, 'G'), 98 | // ] 99 | 100 | // const nextChildren = [ 101 | // h('p', { key: 'A' }, 'A'), 102 | // h('p', { key: 'B' }, 'B'), 103 | // h('p', { key: 'E' }, 'E'), 104 | // h('p', { key: 'C', id: 'c-next' }, 'C'), 105 | // h('p', { key: 'F' }, 'F'), 106 | // h('p', { key: 'G' }, 'G'), 107 | // ] 108 | 109 | // 5.1.1 110 | // a,b,(c,e,d),f,g 111 | // a,b,(e,c),f,g 112 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 113 | // const prevChildren = [ 114 | // h('p', { key: 'A' }, 'A'), 115 | // h('p', { key: 'B' }, 'B'), 116 | // h('p', { key: 'C', id: 'c-prev' }, 'C'), 117 | // h('p', { key: 'E' }, 'E'), 118 | // h('p', { key: 'D' }, 'D'), 119 | // h('p', { key: 'F' }, 'F'), 120 | // h('p', { key: 'G' }, 'G'), 121 | // ] 122 | 123 | // const nextChildren = [ 124 | // h('p', { key: 'A' }, 'A'), 125 | // h('p', { key: 'B' }, 'B'), 126 | // h('p', { key: 'E' }, 'E'), 127 | // h('p', { key: 'C', id: 'c-next' }, 'C'), 128 | // h('p', { key: 'F' }, 'F'), 129 | // h('p', { key: 'G' }, 'G'), 130 | // ] 131 | 132 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 133 | 134 | // 2.1 135 | // a,b,(c,d,e),f,g 136 | // a,b,(e,c,d),f,g 137 | // 最长子序列: [1,2] 138 | 139 | // const prevChildren = [ 140 | // h('p', { key: 'A' }, 'A'), 141 | // h('p', { key: 'B' }, 'B'), 142 | // h('p', { key: 'C' }, 'C'), 143 | // h('p', { key: 'D' }, 'D'), 144 | // h('p', { key: 'E' }, 'E'), 145 | // h('p', { key: 'F' }, 'F'), 146 | // h('p', { key: 'G' }, 'G'), 147 | // ] 148 | 149 | // const nextChildren = [ 150 | // h('p', { key: 'A' }, 'A'), 151 | // h('p', { key: 'B' }, 'B'), 152 | // h('p', { key: 'E' }, 'E'), 153 | // h('p', { key: 'C' }, 'C'), 154 | // h('p', { key: 'D' }, 'D'), 155 | // h('p', { key: 'F' }, 'F'), 156 | // h('p', { key: 'G' }, 'G'), 157 | // ] 158 | 159 | // 3. 创建新的节点 160 | // a,b,(c,e),f,g 161 | // a,b,(e,c,d),f,g 162 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 163 | // const prevChildren = [ 164 | // h('p', { key: 'A' }, 'A'), 165 | // h('p', { key: 'B' }, 'B'), 166 | // h('p', { key: 'C' }, 'C'), 167 | // h('p', { key: 'E' }, 'E'), 168 | // h('p', { key: 'F' }, 'F'), 169 | // h('p', { key: 'G' }, 'G'), 170 | // ] 171 | 172 | // const nextChildren = [ 173 | // h('p', { key: 'A' }, 'A'), 174 | // h('p', { key: 'B' }, 'B'), 175 | // h('p', { key: 'E' }, 'E'), 176 | // h('p', { key: 'C' }, 'C'), 177 | // h('p', { key: 'D' }, 'D'), 178 | // h('p', { key: 'F' }, 'F'), 179 | // h('p', { key: 'G' }, 'G'), 180 | // ] 181 | 182 | // 综合例子 183 | // a,b,(c,d,e,z),f,g 184 | // a,b,(d,c,y,e),f,g 185 | 186 | // const prevChildren = [ 187 | // h('p', { key: 'A' }, 'A'), 188 | // h('p', { key: 'B' }, 'B'), 189 | // h('p', { key: 'C' }, 'C'), 190 | // h('p', { key: 'D' }, 'D'), 191 | // h('p', { key: 'E' }, 'E'), 192 | // h('p', { key: 'Z' }, 'Z'), 193 | // h('p', { key: 'F' }, 'F'), 194 | // h('p', { key: 'G' }, 'G'), 195 | // ] 196 | 197 | // const nextChildren = [ 198 | // h('p', { key: 'A' }, 'A'), 199 | // h('p', { key: 'B' }, 'B'), 200 | // h('p', { key: 'D' }, 'D'), 201 | // h('p', { key: 'C' }, 'C'), 202 | // h('p', { key: 'Y' }, 'Y'), 203 | // h('p', { key: 'E' }, 'E'), 204 | // h('p', { key: 'F' }, 'F'), 205 | // h('p', { key: 'G' }, 'G'), 206 | // ] 207 | 208 | // fix c 节点应该是 move 而不是删除之后重新创建的 209 | const prevChildren = [ 210 | h('p', { key: 'A' }, 'A'), 211 | h('p', {}, 'C'), 212 | h('p', { key: 'B' }, 'B'), 213 | h('p', { key: 'D' }, 'D'), 214 | ] 215 | 216 | const nextChildren = [ 217 | h('p', { key: 'A' }, 'A'), 218 | h('p', { key: 'B' }, 'B'), 219 | h('p', {}, 'C'), 220 | h('p', { key: 'D' }, 'D'), 221 | ] 222 | 223 | export const ArrayToArray = { 224 | name: 'ArrayToArray', 225 | setup() { 226 | const isChange = ref(false) 227 | window.isChange = isChange 228 | 229 | return { 230 | isChange, 231 | } 232 | }, 233 | render() { 234 | const self = this 235 | 236 | return self.isChange === true 237 | ? h('div', {}, nextChildren) 238 | : h('div', {}, prevChildren) 239 | }, 240 | } 241 | -------------------------------------------------------------------------------- /examples/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue.esm.js' 2 | 3 | export const ArrayToText = { 4 | name: 'ArrayToText', 5 | setup() { 6 | const switcher = ref(false) 7 | window.switcher = switcher 8 | return { 9 | switcher, 10 | } 11 | }, 12 | render() { 13 | const prevChildren = [ 14 | h('p', {}, 'arrayChildOne'), 15 | h('p', {}, 'arrayChildTwo'), 16 | ] 17 | const nextChildren = 'next Text Children' 18 | return this.switcher 19 | ? h('div', {}, nextChildren) 20 | : h('div', {}, prevChildren) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue.esm.js' 2 | 3 | export const TextToArray = { 4 | name: 'TextToArray', 5 | setup() { 6 | const switcher = ref(false) 7 | window.switcher = switcher 8 | return { 9 | switcher, 10 | } 11 | }, 12 | render() { 13 | const prevChildren = 'prev Text Children' 14 | const nextChildren = [ 15 | h('p', {}, 'arrayChildOne'), 16 | h('p', {}, 'arrayChildTwo'), 17 | ] 18 | return this.switcher 19 | ? h('div', {}, nextChildren) 20 | : h('div', {}, prevChildren) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue.esm.js' 2 | 3 | export const TextToText = { 4 | name: 'TextToText', 5 | setup() { 6 | const switcher = ref(false) 7 | window.switcher = switcher 8 | return { 9 | switcher, 10 | } 11 | }, 12 | render() { 13 | const prevChildren = 'prev Text Children' 14 | const nextChildren = 'next Text Children' 15 | return this.switcher 16 | ? h('div', {}, nextChildren) 17 | : h('div', {}, prevChildren) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /examples/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue.esm.js' 2 | 3 | export const App = { 4 | setup() { 5 | const count = ref(0) 6 | // 这里如果使用类似 foo = ref('foo') 放入到 props 中也是可以实现对应效果的 7 | // 同样的用 reactive 包装也可以,但是这样会无法检测到 整个变动 8 | 9 | // 首先在 render 中使用 ...this.props 先到代理模式中 PublicInstanceProxyHandlers 的 get 中 10 | // 返回了 setupState.props 到了 proxyRefs 的 get 中进行 unRef 操作 11 | // 脱ref 会返回 .value 又进入了 RefImpl 的 get value() 会返回 this._value 12 | // 而 this._value 是一个 object, 在创建的时候已经被 reactive 包装了 13 | // 展开运算符会到 reactive 的 get 14 | const props = ref({ 15 | foo: 'foo', 16 | bar: 'bar', 17 | }) 18 | const onClick = () => { 19 | count.value++ 20 | } 21 | const onChangePropsDemo1 = () => { 22 | props.value.foo = 'newFoo' 23 | } 24 | const onChangePropsDemo2 = () => { 25 | props.value.foo = undefined 26 | } 27 | const onChangePropsDemo3 = () => { 28 | props.value.foo = null 29 | } 30 | const onChangePropsDemo4 = () => { 31 | props.value = { foo: 'foo' } 32 | } 33 | const onChangePropsDemo5 = () => { 34 | props.value = { foo: 'foo', bar: 'bar', baz: 'baz' } 35 | } 36 | return { 37 | count, 38 | props, 39 | onClick, 40 | onChangePropsDemo1, 41 | onChangePropsDemo2, 42 | onChangePropsDemo3, 43 | onChangePropsDemo4, 44 | onChangePropsDemo5, 45 | } 46 | }, 47 | render() { 48 | return h( 49 | 'div', 50 | { 51 | ...this.props, 52 | }, 53 | [ 54 | h('p', {}, `count: ${this.count}`), 55 | h( 56 | 'button', 57 | { 58 | onClick: this.onClick, 59 | }, 60 | 'count++' 61 | ), 62 | h( 63 | 'button', 64 | { 65 | onClick: this.onChangePropsDemo1, 66 | }, 67 | 'change prop: foo to newFoo' 68 | ), 69 | h( 70 | 'button', 71 | { 72 | onClick: this.onChangePropsDemo2, 73 | }, 74 | 'change prop: foo to undefined' 75 | ), 76 | h( 77 | 'button', 78 | { 79 | onClick: this.onChangePropsDemo3, 80 | }, 81 | 'change prop: foo to null' 82 | ), 83 | h( 84 | 'button', 85 | { 86 | onClick: this.onChangePropsDemo4, 87 | }, 88 | 'delete prop: bar' 89 | ), 90 | h( 91 | 'button', 92 | { 93 | onClick: this.onChangePropsDemo5, 94 | }, 95 | 'add prop: baz' 96 | ), 97 | ] 98 | ) 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/update/main.js: -------------------------------------------------------------------------------- 1 | import { App } from './App.js' 2 | import { createApp } from '../../lib/mini-vue.esm.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /lib/mini-vue.esm.js: -------------------------------------------------------------------------------- 1 | function toDisplayString(value) { 2 | return String(value); 3 | } 4 | 5 | const extend = Object.assign; 6 | const EMPTY_OBJ = {}; 7 | const EMPTY_ARR = []; 8 | const isObject = (val) => val !== null && typeof val === 'object'; 9 | const isFunction = (val) => typeof val === 'function'; 10 | const isString = (val) => typeof val === 'string'; 11 | const isArray = (val) => Array.isArray(val); 12 | const isOn = (val) => /^on[A-Z]/.test(val); 13 | // NaN 算作变更 14 | const hasChanged = (newVal, oldVal) => !Object.is(newVal, oldVal); 15 | // tips: in vs. hasOwnProperty 16 | // | in | hasOwnProperty 17 | // Symbol | yes | yes 18 | // inherited properties | yes | no 19 | // ES6 getters/setters | yes | no 20 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 21 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); 22 | 23 | const TO_DISPLAY_STRING = Symbol('toDisplayString'); 24 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 25 | const helperMapName = { 26 | [TO_DISPLAY_STRING]: 'toDisplayString', 27 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 28 | }; 29 | 30 | function generate(ast) { 31 | const context = generateContext(); 32 | const { push } = context; 33 | genFunctionPreamble(ast, context); 34 | const functionName = 'render'; 35 | const args = ['_ctx', '_cache']; 36 | const signature = args.join(', '); 37 | push(`function ${functionName}(${signature}) {`); 38 | push('return '); 39 | genNode(ast.codegenNode, context); 40 | push('}'); 41 | return { 42 | code: context.code, 43 | }; 44 | } 45 | function genFunctionPreamble(ast, context) { 46 | const { push } = context; 47 | const VueBinging = 'Vue'; 48 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}`; 49 | if (ast.helpers.length > 0) { 50 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`); 51 | } 52 | push('\n'); 53 | push('return '); 54 | } 55 | function genNode(node, context) { 56 | switch (node.type) { 57 | case 4 /* TEXT */: 58 | genText(node, context); 59 | break; 60 | case 1 /* INTERPOLATION */: 61 | genInterpolation(node, context); 62 | break; 63 | case 2 /* SIMPLE_EXPRESSION */: 64 | genExpression(node, context); 65 | break; 66 | case 3 /* ELEMENT */: 67 | genElement(node, context); 68 | break; 69 | case 5 /* COMPOUND_EXPRESSION */: 70 | genCompoundExpression(node, context); 71 | break; 72 | } 73 | } 74 | function genCompoundExpression(node, context) { 75 | const { children } = node; 76 | const { push } = context; 77 | for (let i = 0; i < children.length; i++) { 78 | const child = children[i]; 79 | if (isString(child)) { 80 | push(child); 81 | } 82 | else { 83 | genNode(child, context); 84 | } 85 | } 86 | } 87 | function genElement(node, context) { 88 | const { push, helper } = context; 89 | const { tag, props, children } = node; 90 | push(`${helper(CREATE_ELEMENT_VNODE)}(`); 91 | genNodeList(genNullableArgs([tag, props, children]), context); 92 | push(')'); 93 | } 94 | function genNodeList(nodes, context) { 95 | const { push } = context; 96 | for (let i = 0; i < nodes.length; i++) { 97 | const node = nodes[i]; 98 | if (isString(node)) { 99 | push(node); 100 | } 101 | else { 102 | genNode(node, context); 103 | } 104 | if (i < nodes.length - 1) { 105 | push(', '); 106 | } 107 | } 108 | } 109 | function genNullableArgs(args) { 110 | return args.map((arg) => arg || 'null'); 111 | } 112 | function genExpression(node, context) { 113 | const { push } = context; 114 | push(`${node.content}`); 115 | } 116 | function genInterpolation(node, context) { 117 | const { push, helper } = context; 118 | push(`${helper(TO_DISPLAY_STRING)}(`); 119 | genNode(node.content, context); 120 | push(')'); 121 | } 122 | function genText(node, context) { 123 | const { push } = context; 124 | push(`"${node.content}"`); 125 | } 126 | function generateContext() { 127 | const context = { 128 | code: '', 129 | push(source) { 130 | context.code += source; 131 | }, 132 | helper(key) { 133 | return `_${helperMapName[key]}`; 134 | }, 135 | }; 136 | return context; 137 | } 138 | 139 | function baseParse(content, options = {}) { 140 | // 将 content 包装至 ctx 中 141 | const context = createParserContext(content, options); 142 | return createRoot(parseChildren(context, [])); 143 | } 144 | function parseChildren(context, ancestors) { 145 | const nodes = []; 146 | // parseChildren 应该使用循环来处理 147 | while (!isEnd(context, ancestors)) { 148 | const s = context.source; 149 | let node; 150 | if (s.startsWith(context.options.delimiters[0])) { 151 | node = parseInterpolation(context); 152 | } 153 | else if (s[0] === '<') { 154 | // TODO: 判断条件 startsWith 和 s[0] 的区别是什么 155 | if (/[a-z]/i.test(s[1])) { 156 | node = parseElement(context, ancestors); 157 | } 158 | } 159 | if (!node) { 160 | node = parseText(context); 161 | } 162 | nodes.push(node); 163 | } 164 | return nodes; 165 | } 166 | function isEnd(context, ancestors) { 167 | // 结束标签 168 | let s = context.source; 169 | if (s.startsWith('= 0; i--) { 171 | const tag = ancestors[i].tag; 172 | if (startsWithEndTagOpen(s, tag)) { 173 | return true; 174 | } 175 | } 176 | } 177 | // context.source 为空 178 | return !s; 179 | } 180 | function parseText(context) { 181 | let endTokens = ['<', context.options.delimiters[0]]; 182 | let endIndex = context.source.length; 183 | for (let i = 0; i < endTokens.length; i++) { 184 | const index = context.source.indexOf(endTokens[i]); 185 | // endIndex 应该尽量小 186 | if (index !== -1 && index < endIndex) { 187 | endIndex = index; 188 | } 189 | } 190 | const content = parseTextData(context, endIndex); 191 | return { 192 | type: 4 /* TEXT */, 193 | content, 194 | }; 195 | } 196 | function parseTextData(context, length) { 197 | const content = context.source.slice(0, length); 198 | advanceBy(context, length); 199 | return content; 200 | } 201 | function parseElement(context, ancestors) { 202 | // StartTag 203 | const element = parseTag(context, 0 /* Start */); 204 | ancestors.push(element); 205 | // parseEl 的时候应该也要 递归 parseChildren 206 | // 否则就变成只解析一个 tag 了 207 | element.children = parseChildren(context, ancestors); 208 | ancestors.pop(); 209 | // EndTag 210 | if (startsWithEndTagOpen(context.source, element.tag)) { 211 | parseTag(context, 1 /* End */); 212 | } 213 | else { 214 | throw new Error(`缺少结束标签: ${element.tag}`); 215 | } 216 | return element; 217 | } 218 | function parseTag(context, type) { 219 | const match = /^<\/?([a-z]*)/i.exec(context.source); 220 | const tag = match[1]; 221 | advanceBy(context, match[0].length + 1); 222 | if (type === 1 /* End */) 223 | return; 224 | return { 225 | type: 3 /* ELEMENT */, 226 | tag, 227 | }; 228 | } 229 | function parseInterpolation(context) { 230 | const openDelimiter = context.options.delimiters[0]; 231 | const closeDelimiter = context.options.delimiters[1]; 232 | // context -> content 233 | // 注意 indexOf 是带一个 [, fromIndex] 可选参数的 234 | // 并且 closeIndex 需要在 advanceBy 先保留下来 235 | const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length); 236 | // slice 是 非 破坏性方法 因此需要 赋值 237 | advanceBy(context, openDelimiter.length); 238 | // {{msg}} 239 | // openDelimiter.length: 2, closeIndex: 5 240 | const rawContentLength = closeIndex - openDelimiter.length; 241 | const rawContent = parseTextData(context, rawContentLength); 242 | const content = rawContent.trim(); 243 | advanceBy(context, closeDelimiter.length); 244 | return { 245 | type: 1 /* INTERPOLATION */, 246 | content: { 247 | type: 2 /* SIMPLE_EXPRESSION */, 248 | content: content, 249 | }, 250 | }; 251 | } 252 | function advanceBy(context, length) { 253 | context.source = context.source.slice(length); 254 | } 255 | function createRoot(children) { 256 | return { 257 | children, 258 | type: 0 /* ROOT */, 259 | }; 260 | } 261 | function createParserContext(content, rawOptions) { 262 | const options = extend({}, defaultParserOptions); 263 | for (const key in rawOptions) { 264 | options[key] = 265 | rawOptions[key] === undefined 266 | ? defaultParserOptions[key] 267 | : rawOptions[key]; 268 | } 269 | return { 270 | options, 271 | source: content, 272 | }; 273 | } 274 | function startsWithEndTagOpen(source, tag) { 275 | return (source.startsWith(' { 356 | // tag 357 | const vnodeTag = `"${node.tag}"`; 358 | // props 359 | let vnodeProps; 360 | // children 361 | const { children } = node; 362 | let vnodeChildren; 363 | if (children.length > 0) { 364 | if (children.length === 1) { 365 | vnodeChildren = children[0]; 366 | } 367 | } 368 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 369 | }; 370 | } 371 | } 372 | 373 | /* 374 | * root = { 375 | * children: [ 376 | * { 377 | * type: 0 378 | * content: {type: 1, content: 'message'} 379 | * }, 380 | * ], 381 | * } 382 | */ 383 | function transformExpression(node) { 384 | if (node.type === 1 /* INTERPOLATION */) { 385 | node.content = processExpression(node.content); 386 | } 387 | } 388 | function processExpression(node) { 389 | const raw = node.content; 390 | node.content = `_ctx.${raw}`; 391 | return node; 392 | } 393 | 394 | function isText(node) { 395 | return node.type === 4 /* TEXT */ || node.type === 1 /* INTERPOLATION */; 396 | } 397 | 398 | function transformText(node, context) { 399 | if (node.type === 3 /* ELEMENT */) { 400 | return () => { 401 | const { children } = node; 402 | let currentContainer; 403 | for (let i = 0; i < children.length; i++) { 404 | const child = children[i]; 405 | if (isText(child)) { 406 | for (let j = i + 1; j < children.length; j++) { 407 | const next = children[j]; 408 | if (isText(next)) { 409 | // init 410 | if (!currentContainer) { 411 | currentContainer = children[i] = { 412 | type: 5 /* COMPOUND_EXPRESSION */, 413 | children: [child], 414 | }; 415 | } 416 | currentContainer.children.push(' + '); 417 | currentContainer.children.push(next); 418 | children.splice(j, 1); 419 | j--; 420 | } 421 | else { 422 | currentContainer = undefined; 423 | break; 424 | } 425 | } 426 | } 427 | } 428 | }; 429 | } 430 | } 431 | 432 | function baseCompile(template) { 433 | const ast = baseParse(template); 434 | transform(ast, { 435 | nodeTransforms: [transformExpression, transformElement, transformText], 436 | }); 437 | return generate(ast); 438 | } 439 | 440 | const Fragment = Symbol('Fragment'); 441 | const Text = Symbol('Text'); 442 | function createVNode(type, props, children) { 443 | const vnode = { 444 | type, 445 | props, 446 | component: null, 447 | key: props && props.key, 448 | children, 449 | shapeFlag: getShapeFlag(type), 450 | }; 451 | if (isString(children)) { 452 | vnode.shapeFlag |= 8 /* TEXT_CHILDREN */; 453 | } 454 | else if (isArray(children)) { 455 | vnode.shapeFlag |= 16 /* ARRAY_CHILDREN */; 456 | } 457 | // 如何判断是一个 slots 458 | // vnode是一个组件 且 children 是 object 459 | if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) { 460 | if (isObject(vnode.children)) { 461 | vnode.shapeFlag |= 32 /* SLOTS_CHILDREN */; 462 | } 463 | } 464 | return vnode; 465 | } 466 | function getShapeFlag(type) { 467 | return isString(type) ? 1 /* ELEMENT */ : 4 /* STATEFUL_COMPONENT */; 468 | } 469 | function createTextVNode(text) { 470 | return createVNode(Text, {}, text); 471 | } 472 | function isSameVNodeType(n1, n2) { 473 | return n1.type === n2.type && n1.key === n2.key; 474 | } 475 | 476 | function h(type, props, children) { 477 | return createVNode(type, props, children); 478 | } 479 | 480 | function renderSlot(slots, name = 'default', props) { 481 | // TODO: default 具名 482 | const slot = slots[name]; 483 | if (slot) { 484 | // slot: (props) => h(el, {}, props) 485 | if (isFunction(slot)) { 486 | // 需要使用 Fragment 487 | return createVNode(Fragment, {}, slot(props)); 488 | } 489 | } 490 | } 491 | 492 | // target -> key -> dep -> effect 实例 493 | const targetMap = new Map(); 494 | // activeEffect 保存了激活的 effect,便于在 track 的时候使用 495 | let activeEffect; 496 | // 在 run 函数中开关,在 track 中进行判断 497 | let shouldTrack; 498 | class ReactiveEffect { 499 | constructor(fn, scheduler) { 500 | this.deps = []; // 反向依赖的数据结构 501 | this.active = true; // active 标识位 502 | this._fn = fn; 503 | // effect(fn, options?) 存在两个参数且内部使用了 extend(_effect,options) 504 | // 所以 _effect 可从 options 中拿到 scheduler 505 | // 而 computed(getter) 只有一个参数,内部只 new constructor 506 | // 所以必须在 constructor 这里接受两个参数,并传给实例的 scheduler 507 | this.scheduler = scheduler; 508 | } 509 | run() { 510 | // 手动执行 runner 的分支 511 | if (!this.active) { 512 | // 为什么不 activeEffect = this?理由可能是手动执行意味着 activeEffect 当前并非是 this 513 | // 其实后续 activeEffect 会变为 栈 结构以便于 effect 嵌套执行 514 | return this._fn(); 515 | } 516 | // 响应式触发 517 | shouldTrack = true; 518 | // activeEffect 保存的是实例化对象 519 | activeEffect = this; 520 | const result = this._fn(); 521 | shouldTrack = false; 522 | return result; 523 | } 524 | stop() { 525 | // 如果用户多次调用 stop,即使已经 cleanup 过,effect 实际不存在于 dep中了 526 | // 但是 cleanupEffect 依旧会执行循环 527 | // 性能优化:使用 active 标识位 528 | if (this.active) { 529 | cleanupEffect(this); 530 | // onStop 的回调函数 531 | if (this.onStop) { 532 | this.onStop(); 533 | } 534 | this.active = false; 535 | } 536 | } 537 | } 538 | function cleanupEffect(effect) { 539 | // 负责通过反向依赖把 effectFn 从依赖收集的 Set 中解除 540 | effect.deps.forEach((dep) => { 541 | dep.delete(effect); 542 | }); 543 | // 清空 deps 544 | effect.deps.length = 0; 545 | } 546 | // 1. 边界判断 547 | // 2. 找到 dep: targetMap -> depsMap -> dep 548 | // 3. 依赖收集 549 | function track(target, key) { 550 | // 边界判断 551 | if (!isTracking()) 552 | return; 553 | // 核心是 targetMap -> depsMap -> dep -> dep.add 554 | // 两个 if 用于 init 555 | let depsMap = targetMap.get(target); 556 | if (!depsMap) { 557 | targetMap.set(target, (depsMap = new Map())); 558 | } 559 | let dep = depsMap.get(key); 560 | if (!dep) { 561 | depsMap.set(key, (dep = new Set())); 562 | } 563 | // 依赖收集 564 | trackEffects(dep); 565 | } 566 | function isTracking() { 567 | // 边界,注意不要让 undefined 进入 dep 568 | // 边界,!shouldTrack 时直接返回 569 | return shouldTrack && activeEffect !== undefined; 570 | } 571 | function trackEffects(dep) { 572 | // 常见于 wrapped.foo = 2, set 后还会执行一次 get 573 | // 而此时的 effect 已经在 dep 中了,其实对于 Set 来说无所谓 574 | // 但是 deps 就很吃力了,因为它是个 Array 并不判重,会持续增长 575 | // 到了 cleanup 的部分,就会多出来很多性能消耗 576 | if (dep.has(activeEffect)) 577 | return; 578 | dep.add(activeEffect); 579 | // 反向依赖收集 580 | activeEffect.deps.push(dep); 581 | } 582 | // 1. 找到 dep 583 | // 2. 触发依赖 584 | function trigger(target, key) { 585 | // 需要对 depsMap 和 dep 是否存在做出判断 586 | const depsMap = targetMap.get(target); 587 | if (!depsMap) 588 | return; 589 | const dep = depsMap.get(key); 590 | triggerEffects(dep); 591 | } 592 | function triggerEffects(dep) { 593 | dep && 594 | dep.forEach((effect) => { 595 | if (effect.scheduler) { 596 | effect.scheduler(); 597 | } 598 | else { 599 | effect.run(); 600 | } 601 | }); 602 | } 603 | // 1. 实例化对象 604 | // 2. 接受 options 605 | // 3. 执行 effectFn 606 | // 4. return runner 607 | function effect(fn, options = {}) { 608 | // 使用 _effect 实例化对象来处理逻辑 609 | const _effect = new ReactiveEffect(fn); 610 | // 接收 options 611 | extend(_effect, options); 612 | // 通过实例执行 613 | _effect.run(); 614 | const runner = _effect.run.bind(_effect); 615 | // 返回前保存当前的 effect 616 | runner.effect = _effect; 617 | return runner; 618 | } 619 | 620 | const get = createGetter(); 621 | const set = createSetter(); 622 | const readonlyGet = createGetter(true); 623 | const shallowReadonlyGet = createGetter(true, true); 624 | function createGetter(isReadonly = false, isShallow = false) { 625 | // 两个出口 626 | return function (target, key) { 627 | // IS_REACTIVE| IS_READONLY 628 | // 判断是否为内部的属性,进行拦截 629 | if (key === ReactiveFlags.IS_REACTIVE) { 630 | return !isReadonly; 631 | } 632 | else if (key === ReactiveFlags.IS_READONLY) { 633 | return isReadonly; 634 | } 635 | // 普通响应式数据的逻辑 636 | const res = Reflect.get(target, key); 637 | // XXX: isShallow 直接返回 res,不判断深响应和深只读。但是 track 是否应该执行? 638 | if (isShallow) { 639 | return res; 640 | } 641 | // 深响应 && 深只读 642 | if (isObject(res)) { 643 | return isReadonly ? readonly(res) : reactive(res); 644 | } 645 | if (!isReadonly) { 646 | track(target, key); 647 | } 648 | return res; 649 | }; 650 | } 651 | function createSetter() { 652 | return function (target, key, value) { 653 | const res = Reflect.set(target, key, value); 654 | trigger(target, key); 655 | return res; 656 | }; 657 | } 658 | const mutableHandlers = { 659 | get: get, 660 | set: set, 661 | }; 662 | const readonlyHandlers = { 663 | get: readonlyGet, 664 | set(target, key) { 665 | console.warn(`fail to set key "${String(key)}", because target is readonly: `, target); 666 | return true; 667 | }, 668 | }; 669 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 670 | get: shallowReadonlyGet, 671 | }); 672 | 673 | var ReactiveFlags; 674 | (function (ReactiveFlags) { 675 | ReactiveFlags["IS_REACTIVE"] = "__v_isReactive"; 676 | ReactiveFlags["IS_READONLY"] = "__v_isReadonly"; 677 | })(ReactiveFlags || (ReactiveFlags = {})); 678 | function reactive(raw) { 679 | return createReactiveObject(raw, mutableHandlers); 680 | } 681 | function readonly(raw) { 682 | return createReactiveObject(raw, readonlyHandlers); 683 | } 684 | function shallowReadonly(raw) { 685 | return createReactiveObject(raw, shallowReadonlyHandlers); 686 | } 687 | function createReactiveObject(raw, baseHandles) { 688 | return new Proxy(raw, baseHandles); 689 | } 690 | 691 | // dep 692 | class RefImpl { 693 | constructor(value) { 694 | this.__v_isRef = true; 695 | // 保存原始值,便于后续比较 696 | this._rawValue = value; 697 | this._value = convert(value); 698 | this.dep = new Set(); 699 | } 700 | get value() { 701 | // 依赖收集 702 | trackRefValue(this); 703 | return this._value; 704 | } 705 | set value(newValue) { 706 | // 合理触发依赖 707 | if (hasChanged(newValue, this._rawValue)) { 708 | this._rawValue = newValue; 709 | this._value = convert(newValue); 710 | // 依赖触发 711 | triggerRefValue(this); 712 | } 713 | } 714 | } 715 | function trackRefValue(ref) { 716 | if (isTracking()) { 717 | trackEffects(ref.dep); 718 | } 719 | } 720 | function triggerRefValue(ref) { 721 | triggerEffects(ref.dep); 722 | } 723 | function convert(value) { 724 | // 判断 原始值 还是 引用 进行转换 725 | return isObject(value) ? reactive(value) : value; 726 | } 727 | function ref(value) { 728 | return new RefImpl(value); 729 | } 730 | function isRef(ref) { 731 | return !!ref.__v_isRef; 732 | } 733 | function unRef(ref) { 734 | // unRef 主要就是为了暴露给 proxyRefs 使用的 735 | // 读取到值的内容的时候,会触发 unRef 736 | // 而 unRef 里应该触发 .value 而不是 ._value 737 | // 否则不能触发依赖收集 738 | return isRef(ref) ? ref.value : ref; 739 | } 740 | // proxyRefs 用于包装一个 obj(一般为 setupResult) 741 | // setupResult 可能为这种形式 742 | // { 743 | // ref(原始值) 744 | // reactive(obj) 写个测试用例测试一下 745 | // function 746 | // 原始值 747 | // } 748 | function proxyRefs(objectWithRefs) { 749 | // TODO: proxyRefs handler 750 | return new Proxy(objectWithRefs, { 751 | get(target, key) { 752 | return unRef(Reflect.get(target, key)); 753 | }, 754 | set(target, key, newVal) { 755 | const oldVal = target[key]; 756 | // newVal is not Ref && oldVal is Ref 757 | if (!isRef(newVal) && isRef(oldVal)) { 758 | oldVal.value = newVal; 759 | return true; 760 | } 761 | else { 762 | return Reflect.set(target, key, newVal); 763 | } 764 | }, 765 | }); 766 | } 767 | 768 | // 1. 拿到 onEvent 769 | // 2. onEvent && onEvent() 770 | function emit(instance, event) { 771 | // 在子组件实例的 props 中应该存在 onEvent 事件 772 | const { props } = instance; 773 | const toHandleKey = (str) => (str ? `on${capitalize(event)}` : ''); 774 | const handleName = toHandleKey(event); 775 | const handler = props[handleName]; 776 | handler && handler(); 777 | } 778 | 779 | function initProps(instance, rawProps) { 780 | // instance.vnode.props 可能为 props 或者 undefined -> {} 781 | instance.props = rawProps || {}; 782 | // TODO: attrs 783 | } 784 | 785 | // key -> function(instance) 786 | const publicPropertiesMap = { 787 | $el: (i) => i.vnode.el, 788 | $slots: (i) => i.slots, 789 | $props: (i) => i.props, 790 | }; 791 | const PublicInstanceProxyHandlers = { 792 | get({ _: instance }, key) { 793 | const { setupState, props } = instance; 794 | // 类似 this.count 795 | // 需要检查 count 是 setupResult 里的,还是 props 里的 796 | if (hasOwn(setupState, key)) { 797 | return setupState[key]; 798 | } 799 | else if (hasOwn(props, key)) { 800 | return props[key]; 801 | } 802 | // 类似 this.$el 803 | const publicGetter = publicPropertiesMap[key]; 804 | if (publicGetter) { 805 | return publicGetter(instance); 806 | } 807 | // TODO: 不在 setupState | props | $ 中,需要做处理 808 | }, 809 | }; 810 | 811 | function initSlots(instance, children) { 812 | // 判断 children 是否是一个 object 813 | // 判断任务加入到 shapeFlags 中 814 | const { vnode } = instance; 815 | if (vnode.shapeFlag & 32 /* SLOTS_CHILDREN */) { 816 | normalizeObjectSlots(children, instance.slots); 817 | } 818 | } 819 | function normalizeObjectSlots(children, slots) { 820 | for (const key in children) { 821 | const value = children[key]; 822 | // value 或者说 slot 此时是一个 function 823 | slots[key] = (props) => normalizeSlotValue(value(props)); 824 | } 825 | } 826 | // 需要判断 children 是 single element 还是 数组 827 | function normalizeSlotValue(value) { 828 | return isArray(value) ? value : [value]; 829 | } 830 | 831 | function createComponentInstance(vnode, parent) { 832 | const component = { 833 | vnode, 834 | next: null, 835 | type: vnode.type, 836 | setupState: {}, 837 | props: {}, 838 | slots: {}, 839 | provides: parent ? parent.provides : {}, 840 | parent, 841 | isMounted: false, 842 | subTree: {}, 843 | emit: null, 844 | update: null, 845 | }; 846 | // bind 除了可以处理 this 丢失的问题 847 | // 还可以隐藏参数 848 | // XXX: as any 需要在 ts 的学习中解决 849 | component.emit = emit.bind(null, component); 850 | return component; 851 | } 852 | function setupComponent(instance) { 853 | const { props, children } = instance.vnode; 854 | // 将 props 接收到 instance 中 855 | // instance.vnode.props -> instance.props 856 | initProps(instance, props); 857 | initSlots(instance, children); 858 | setupStatefulComponent(instance); 859 | // TODO: 函数组件(无状态) 860 | } 861 | // 1. instance.proxy 862 | // 2. instance.setupState 判断是否有 setup -> setupResult 863 | // 3. instance.render 判断是否有 setup -> setupResult -> render 864 | function setupStatefulComponent(instance) { 865 | // 代理模式, 使用 proxy 866 | // 这里的解构是为了保持源码一致,源码后续第一个参数会是 instance.ctx 867 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 868 | // instance -> vnode -> type === component -> setupResult = setup() 869 | // instance: {vnode, type} 870 | // instance -> type === component -> setupResult = setup() 871 | const { props, type: Component } = instance; 872 | const { setup } = Component; 873 | if (setup) { 874 | setCurrentInstance(instance); 875 | // setup 接收 props 参数 876 | const setupResult = setup(shallowReadonly(props), { emit: instance.emit }); 877 | setCurrentInstance(null); 878 | handleSetupResult(instance, setupResult); 879 | } 880 | } 881 | // 1. setupResult 是 function 882 | // 2. setupResult 是 object 883 | // 3. finishComponentSetup 884 | function handleSetupResult(instance, setupResult) { 885 | // TODO: function 886 | if (isObject(setupResult)) { 887 | // render 中要拿到自动脱 ref 所以使用 proxyRefs 包装 setupResult 的内容 888 | instance.setupState = proxyRefs(setupResult); 889 | } 890 | finishComponentSetup(instance); 891 | } 892 | function finishComponentSetup(instance) { 893 | const Component = instance.type; 894 | // 如果 instance 还没有 render 895 | if (!instance.render) { 896 | if (compile && !Component.render) { 897 | const template = Component.template; 898 | if (template) { 899 | Component.render = compile(template); 900 | } 901 | } 902 | instance.render = Component.render; 903 | } 904 | } 905 | let currentInstance = null; 906 | function getCurrentInstance() { 907 | return currentInstance; 908 | } 909 | function setCurrentInstance(instance) { 910 | currentInstance = instance; 911 | } 912 | let compile; 913 | function registerRuntimeCompiler(_compiler) { 914 | compile = _compiler; 915 | } 916 | 917 | function provide(key, value) { 918 | const currentInstance = getCurrentInstance(); 919 | let provides = currentInstance.provides; 920 | const parentProvides = currentInstance.parent && currentInstance.parent.provides; 921 | // init 922 | if (parentProvides === provides) { 923 | provides = currentInstance.provides = Object.create(parentProvides); 924 | } 925 | provides[key] = value; 926 | } 927 | function inject(key, defaultValue) { 928 | // TODO: not self-inject 929 | const currentInstance = getCurrentInstance(); 930 | if (currentInstance) { 931 | const parentProvides = currentInstance.parent.provides; 932 | // XXX: 为什么使用 in 而不是 hasOwn 933 | if (key in parentProvides) { 934 | return parentProvides[key]; 935 | } 936 | else if (defaultValue) { 937 | return isFunction(defaultValue) ? defaultValue() : defaultValue; 938 | } 939 | } 940 | } 941 | 942 | function hasPropsChanged(prevProps, nextProps) { 943 | const nextKeys = Object.keys(nextProps); 944 | if (Object.keys(prevProps).length !== nextKeys.length) { 945 | return true; 946 | } 947 | for (let i = 0; i < nextKeys.length; i++) { 948 | const key = nextKeys[i]; 949 | if (nextProps[key] !== prevProps[key]) { 950 | return true; 951 | } 952 | } 953 | return false; 954 | } 955 | 956 | function createAppAPI(render) { 957 | return function createApp(rootComponent) { 958 | return { 959 | mount(rootContainer) { 960 | // 1. 创建 vnode: rootComponent -> vnode 961 | // vnode: {type, props?, children?} 962 | const vnode = createVNode(rootComponent); 963 | // 2. 渲染 vnode: render(vnode, rootContainer) 964 | render(vnode, convertContainer(rootContainer)); 965 | }, 966 | }; 967 | }; 968 | } 969 | function convertContainer(container) { 970 | if (isString(container)) { 971 | const result = document.querySelector(container); 972 | return result; 973 | } 974 | else { 975 | // TODO: 考虑 container 为空,需要 document.createElement('div') 976 | return container; 977 | } 978 | } 979 | 980 | const queue = []; 981 | let isFlushPending = false; 982 | const p = Promise.resolve(); 983 | function nextTick(fn) { 984 | return fn ? p.then(fn) : p; 985 | } 986 | function queueJobs(job) { 987 | if (!queue.includes(job)) { 988 | queue.push(job); 989 | } 990 | queueFlush(); 991 | } 992 | function queueFlush() { 993 | if (isFlushPending) 994 | return; 995 | isFlushPending = true; 996 | Promise.resolve().then(() => { 997 | // 如果在这里有 log 的话会发现 then 执行了 循环的次数 998 | // 是因为微任务队列塞进了 循环次数 的 promise 999 | // 第一次 queue 有内容, 但是后面的 queue 是空 1000 | // 所以创建如此多的 promise 是没有必要的 1001 | // 开关重新初始化 1002 | isFlushPending = false; 1003 | let job; 1004 | while ((job = queue.shift())) { 1005 | job && job(); 1006 | } 1007 | }); 1008 | } 1009 | 1010 | function createRenderer(options) { 1011 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 1012 | function render(vnode, rootContainer) { 1013 | // patch 递归 1014 | patch(null, vnode, rootContainer, null, null); 1015 | } 1016 | function patch(n1, n2, container, parentComponent, anchor) { 1017 | const { type, shapeFlag } = n2; 1018 | switch (type) { 1019 | case Fragment: 1020 | processFragment(n1, n2, container, parentComponent, anchor); 1021 | break; 1022 | case Text: 1023 | processText(n1, n2, container); 1024 | break; 1025 | default: 1026 | // TODO: vnode 不合法就没有出口了 1027 | if (shapeFlag & 1 /* ELEMENT */) { 1028 | // isString -> processElement 1029 | processElement(n1, n2, container, parentComponent, anchor); 1030 | } 1031 | else if (shapeFlag & 4 /* STATEFUL_COMPONENT */) { 1032 | // isObj ->processComponent 1033 | processComponent(n1, n2, container, parentComponent, anchor); 1034 | } 1035 | break; 1036 | } 1037 | } 1038 | function processFragment(n1, n2, container, parentComponent, anchor) { 1039 | const { children } = n2; 1040 | mountChildren(children, container, parentComponent, anchor); 1041 | } 1042 | function processText(n1, n2, container) { 1043 | const { children } = n2; 1044 | // TODO: 这里使用了 DOM 平台,需要抽离逻辑 1045 | const el = (n2.el = document.createTextNode(children)); 1046 | container.append(el); 1047 | } 1048 | function processElement(n1, n2, container, parentComponent, anchor) { 1049 | // 判断是 mount 还是 update 1050 | if (!n1) { 1051 | mountElement(n2, container, parentComponent, anchor); 1052 | } 1053 | else { 1054 | patchElement(n1, n2, container, parentComponent, anchor); 1055 | } 1056 | } 1057 | // 1. 创建 type === tag 的 el 1058 | // 2. el.props 是 attribute 还是 event 1059 | // 3. children 是否为 string 或者 array 1060 | // 4. 挂载 container.append 1061 | function mountElement(vnode, container, parentComponent, anchor) { 1062 | const { type, props, children, shapeFlag } = vnode; 1063 | // 这里的 vnode 是 tag, 通过 vnode.el 把 el 传递出来 1064 | const el = (vnode.el = hostCreateElement(type)); 1065 | if (props) { 1066 | for (const key in props) { 1067 | const val = props[key]; 1068 | hostPatchProp(el, key, null, val); 1069 | } 1070 | } 1071 | if (shapeFlag & 8 /* TEXT_CHILDREN */) { 1072 | el.innerText = children; 1073 | } 1074 | else if (shapeFlag & 16 /* ARRAY_CHILDREN */) { 1075 | mountChildren(children, el, parentComponent, anchor); 1076 | } 1077 | hostInsert(el, container, anchor); 1078 | } 1079 | function mountChildren(children, container, parentComponent, anchor) { 1080 | children.forEach((child) => { 1081 | patch(null, child, container, parentComponent, anchor); 1082 | }); 1083 | } 1084 | function patchElement(n1, n2, container, parentComponent, anchor) { 1085 | const el = (n2.el = n1.el); 1086 | console.log('patchElement'); 1087 | console.log('n1: ', n1); 1088 | console.log('n2: ', n2); 1089 | // children 1090 | // 注意这里传入的是 el 而不是 container 1091 | // container 是整个容器 1092 | // 此时更新的仅仅是需要更新节点的 el 1093 | patchChildren(n1, n2, el, parentComponent, anchor); 1094 | // props 1095 | const oldProps = n1.props || EMPTY_OBJ; 1096 | const newProps = n2.props || EMPTY_OBJ; 1097 | patchProps(el, oldProps, newProps); 1098 | } 1099 | // 此处的 container 是需要更新的容器 即 n1 n2 的 el 1100 | function patchChildren(n1, n2, container, parentComponent, anchor) { 1101 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 1102 | const { shapeFlag, children: c2 } = n2; 1103 | if (shapeFlag & 8 /* TEXT_CHILDREN */) { 1104 | if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) { 1105 | // remove all children 1106 | unmountChildren(c1); 1107 | } 1108 | if (c1 !== c2) { 1109 | // (ArrayToText | TextToText) -> insert text element 1110 | hostSetElementText(container, c2); 1111 | } 1112 | } 1113 | else { 1114 | if (prevShapeFlag & 8 /* TEXT_CHILDREN */) { 1115 | // TextToArray 1116 | // 清空 textContent 1117 | hostSetElementText(container, null); 1118 | // mountChildren 1119 | mountChildren(c2, container, parentComponent, anchor); 1120 | } 1121 | else { 1122 | // ArrayToArray 1123 | patchKeyedChildren(c1, c2, container, parentComponent); 1124 | } 1125 | } 1126 | } 1127 | // 快速 diff 1128 | function patchKeyedChildren(c1, c2, container, parentComponent) { 1129 | // 双端预处理 => 步骤 1 和 2 1130 | let i = 0; 1131 | // 长度可能不同 1132 | let e1 = c1.length - 1; 1133 | let e2 = c2.length - 1; 1134 | // 记录 c2 长度 1135 | const l2 = c2.length; 1136 | // 1. sync from start 1137 | // (a b) c 1138 | // (a b) d e 1139 | while (i <= e1 && i <= e2) { 1140 | // 实则此处 while 的条件是边界 1141 | let n1 = c1[i]; 1142 | let n2 = c2[i]; 1143 | if (isSameVNodeType(n1, n2)) { 1144 | patch(n1, n2, container, parentComponent, null); 1145 | } 1146 | else { 1147 | // 当 n1 与 n2 不相等时为普通出口 1148 | break; 1149 | } 1150 | i++; 1151 | } 1152 | // 2. sync from end 1153 | // a (b c) 1154 | // d e (b c) 1155 | while (i <= e1 && i <= e2) { 1156 | let n1 = c1[e1]; 1157 | let n2 = c2[e2]; 1158 | if (isSameVNodeType(n1, n2)) { 1159 | patch(n1, n2, container, parentComponent, null); 1160 | } 1161 | else { 1162 | break; 1163 | } 1164 | e1--; 1165 | e2--; 1166 | } 1167 | // 预处理结束 理想状态下总有一个 children 处理完毕 1168 | // 3. common sequence + mount 1169 | // (a b) 1170 | // (a b) c 1171 | // i = 2, e1 = 1, e2 = 2 1172 | // (a b) 1173 | // c (a b) 1174 | // i = 0, e1 = -1, e2 = 0 1175 | // oldChildren 处理完毕 说明还有新的节点需要 mount 1176 | // 特征是 i > oldEnd && i <= newEnd 而 [i,newEnd] 区间的内容即为 mount 内容 1177 | if (i > e1) { 1178 | if (i <= e2) { 1179 | // mount 1180 | while (i <= e2) { 1181 | // anchor index -> newEnd + 1 1182 | const anchorIndex = e2 + 1; 1183 | // anchorIndex < c2.length -> anchor 在 新的子节点中 -> c2[anchorIndex].el 1184 | // 否则 anchor -> null 1185 | const anchor = anchorIndex < l2 ? c2[anchorIndex].el : null; 1186 | patch(null, c2[i], container, parentComponent, anchor); 1187 | i++; 1188 | } 1189 | } 1190 | } 1191 | // 4. common sequence + unmount 1192 | // (a b) c 1193 | // (a b) 1194 | // i = 2, e1 = 2, e2 = 1 1195 | // a (b c) 1196 | // (b c) 1197 | // i = 0, e1 = 0, e2 = -1 1198 | // newChildren 处理完毕 说明还有旧的节点需要 unmount 1199 | // 特征是 i > newEnd && i <= oldEnd 而 [i, oldEnd] 区间内容即为 unmount 内容 1200 | else if (i > e2) { 1201 | while (i <= e1) { 1202 | hostRemove(c1[i].el); 1203 | i++; 1204 | } 1205 | } 1206 | // 5. unknown sequence 1207 | // [i ... e1 + 1]: a b [c d e] f g 1208 | // [i ... e2 + 1]: a b [e d c h] f g 1209 | // i = 2, e1 = 4, e2 = 5 1210 | // 非理想状态要 LIS 找移动节点 1211 | else { 1212 | const s1 = i; 1213 | const s2 = i; 1214 | // 1. 先完成 patch 和 unmount 逻辑 1215 | // 建立索引 1216 | // 遍历 c1 的 [s1,e1] -> 在索引中找到 newIndex || 没有索引需要遍历寻找 O(n^2) 1217 | // 如果 newIndex === undefined -> unmount 1218 | // 否则 patch 并且记录 source 方便后面 LIS 1219 | const keyToNewIndexMap = new Map(); 1220 | for (let i = s2; i <= e2; i++) { 1221 | const nextChild = c2[i]; 1222 | keyToNewIndexMap.set(nextChild.key, i); 1223 | } 1224 | // 当 patch >= toBePatched 时可以直接 unmount 并 continue 1225 | let patched = 0; 1226 | const toBePatched = e2 - s2 + 1; 1227 | // source 数组 -> LIS 1228 | // 0 代表新节点 offset = +1 1229 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); 1230 | // 判断是否存在需要移动的节点 1231 | let moved = false; 1232 | let maxNewIndexSoFar = 0; 1233 | for (let i = s1; i <= e1; i++) { 1234 | const prevChild = c1[i]; 1235 | // 当 patched >= toBePatched 时可以 unmount 并跳过 1236 | if (patched >= toBePatched) { 1237 | hostRemove(prevChild.el); 1238 | continue; 1239 | } 1240 | let newIndex; 1241 | if (prevChild.key != null) { 1242 | newIndex = keyToNewIndexMap.get(prevChild.key); 1243 | } 1244 | else { 1245 | // undefined || null 1246 | for (let j = s2; j <= e2; j++) { 1247 | if (isSameVNodeType(prevChild, c2[j])) { 1248 | newIndex = j; 1249 | break; 1250 | } 1251 | } 1252 | } 1253 | if (newIndex === undefined) { 1254 | hostRemove(prevChild.el); 1255 | } 1256 | else { 1257 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 1258 | if (newIndex >= maxNewIndexSoFar) { 1259 | maxNewIndexSoFar = newIndex; 1260 | } 1261 | else { 1262 | moved = true; 1263 | } 1264 | patch(prevChild, c2[newIndex], container, parentComponent, null); 1265 | patched++; 1266 | } 1267 | } 1268 | // 2. 然后再完成移动以及新增逻辑 1269 | const increasingNewIndexSequence = moved 1270 | ? getSequence(newIndexToOldIndexMap) 1271 | : EMPTY_ARR; 1272 | let j = increasingNewIndexSequence.length - 1; 1273 | for (let i = toBePatched - 1; i >= 0; i--) { 1274 | const nextIndex = s2 + i; 1275 | const nextChild = c2[nextIndex]; 1276 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 1277 | if (newIndexToOldIndexMap[i] === 0) { 1278 | // 新增的节点 1279 | patch(null, nextChild, container, parentComponent, anchor); 1280 | } 1281 | else if (moved) { 1282 | // 存在需要移动的节点 1283 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 1284 | // j < 0: LIS处理结束剩下的均为需要移动的节点 1285 | // i !== increasingNewIndexSequence[j]: 不在 LIS 中需要移动 1286 | hostInsert(nextChild.el, container, anchor); 1287 | } 1288 | else { 1289 | // 不是新增的节点也无需移动 1290 | // LIS 的索引向前移动 1291 | j--; 1292 | } 1293 | } 1294 | } 1295 | } 1296 | } 1297 | function unmountChildren(children) { 1298 | // XXX: 这里为什么用 for 而不是 forEach 1299 | // 并且vue3源码中的remove是把parentComponent也传递了过去 1300 | // 按理来说传递后就不需要使用 Node.parentNode 来找 parent 了 1301 | // 多次找 parentNode 也是一个消耗因为可能是同一个 1302 | for (let i = 0; i < children.length; i++) { 1303 | // 注意这里需要传入 el 1304 | // children[i] 只是一个 vnode 1305 | hostRemove(children[i].el); 1306 | } 1307 | } 1308 | function patchProps(el, oldProps, newProps) { 1309 | // #5857 1310 | if (oldProps !== newProps) { 1311 | for (const key in newProps) { 1312 | const prevProp = oldProps[key]; 1313 | const nextProp = newProps[key]; 1314 | if (nextProp !== prevProp) { 1315 | hostPatchProp(el, key, prevProp, nextProp); 1316 | } 1317 | } 1318 | if (oldProps !== EMPTY_OBJ) { 1319 | for (const key in oldProps) { 1320 | if (!(key in newProps)) { 1321 | const prevProp = oldProps[key]; 1322 | hostPatchProp(el, key, prevProp, null); 1323 | } 1324 | } 1325 | } 1326 | } 1327 | } 1328 | function processComponent(n1, n2, container, parentComponent, anchor) { 1329 | if (!n1) { 1330 | mountComponent(n2, container, parentComponent, anchor); 1331 | } 1332 | else { 1333 | updateComponent(n1, n2); 1334 | } 1335 | } 1336 | function updateComponent(n1, n2) { 1337 | const instance = (n2.component = n1.component); 1338 | // 将 n2 传递给 instance 1339 | if (hasPropsChanged(n1.props, n2.props)) { 1340 | instance.next = n2; 1341 | instance.update(); 1342 | } 1343 | else { 1344 | n2.el = n1.el; 1345 | instance.vnode = n2; 1346 | } 1347 | } 1348 | function mountComponent(initialVNode, container, parentComponent, anchor) { 1349 | // 1. 创建 componentInstance 1350 | // 数据类型: vnode -> component 1351 | // component: {vnode, type} 1352 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 1353 | // 2. setupComponent(instance) 1354 | setupComponent(instance); 1355 | // 3. setupRenderEffect(instance) 1356 | // 此时 instance 通过 setupComponent 拿到了 render 1357 | setupRenderEffect(instance, initialVNode, container, anchor); 1358 | } 1359 | function setupRenderEffect(instance, initialVNode, container, anchor) { 1360 | instance.update = effect(() => { 1361 | // mount 流程 1362 | if (!instance.isMounted) { 1363 | // setupState | $el | $data 的代理 1364 | const { proxy } = instance; 1365 | // render 的 this 指向的是 proxy 1366 | // proxy 读取 setup 返回值的时通过 handler 处理掉了 setupState 1367 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 1368 | patch(null, subTree, container, instance, anchor); 1369 | // 递归结束, subTree 是 root element, 即最外层的 tag 1370 | // 而这个方法里的 vnode 是一个 componentInstance 1371 | // vnode.el = subTree.el 将 el 传递给了 component 1372 | initialVNode.el = subTree.el; 1373 | // 更新 isMounted 状态 1374 | instance.isMounted = true; 1375 | } 1376 | else { 1377 | const { proxy, vnode, next } = instance; 1378 | // updateComponent 的逻辑 1379 | // vnode: n1, next: n2 1380 | if (next) { 1381 | // updateComponent 的 el 传递 1382 | next.el = vnode.el; 1383 | updateComponentPreRender(instance, next); 1384 | } 1385 | const subTree = instance.render.call(proxy, proxy); 1386 | const preSubTree = instance.subTree; 1387 | // 更新 instance 的 subTree 1388 | instance.subTree = subTree; 1389 | patch(preSubTree, subTree, container, instance, anchor); 1390 | // update 流程中 el 是否会被更新? 1391 | // 答案是会的, 在 patchElement 第一步就是 el = n2.el = n1.el 1392 | // 但是注意这里是 element 更新逻辑里的 el 1393 | // 而 Component 的 el 更新逻辑在上面的那个 if 判断里 1394 | // 感觉这里写的不是很好 二者没有归一起来 1395 | } 1396 | }, { 1397 | scheduler() { 1398 | queueJobs(instance.update); 1399 | }, 1400 | }); 1401 | } 1402 | function updateComponentPreRender(instance, nextVNode) { 1403 | // 传递 props 1404 | instance.props = nextVNode.props; 1405 | // 更新 instance 中的 vnode 1406 | instance.vnode = nextVNode; 1407 | nextVNode = null; 1408 | } 1409 | return { 1410 | createApp: createAppAPI(render), 1411 | }; 1412 | } 1413 | // 注意 arrI 的 edge case: 1414 | // [2,0,1,3,4,5] 的 LIS index 是 [2,3,4,5] 1415 | function getSequence(arr) { 1416 | const p = arr.slice(); 1417 | const result = [0]; 1418 | let i, j, u, v, c; 1419 | const len = arr.length; 1420 | for (i = 0; i < len; i++) { 1421 | const arrI = arr[i]; 1422 | if (arrI !== 0) { 1423 | j = result[result.length - 1]; 1424 | if (arr[j] < arrI) { 1425 | p[i] = j; 1426 | result.push(i); 1427 | continue; 1428 | } 1429 | u = 0; 1430 | v = result.length - 1; 1431 | while (u < v) { 1432 | c = (u + v) >> 1; 1433 | if (arr[result[c]] < arrI) { 1434 | u = c + 1; 1435 | } 1436 | else { 1437 | v = c; 1438 | } 1439 | } 1440 | if (arrI < arr[result[u]]) { 1441 | if (u > 0) { 1442 | p[i] = result[u - 1]; 1443 | } 1444 | result[u] = i; 1445 | } 1446 | } 1447 | } 1448 | u = result.length; 1449 | v = result[u - 1]; 1450 | while (u-- > 0) { 1451 | result[u] = v; 1452 | v = p[v]; 1453 | } 1454 | return result; 1455 | } 1456 | 1457 | function createElement(type) { 1458 | return document.createElement(type); 1459 | } 1460 | function patchProp(el, key, prevVal, nextVal) { 1461 | if (isOn(key)) { 1462 | const event = key.substring(2).toLowerCase(); 1463 | el.addEventListener(event, nextVal); 1464 | } 1465 | else if (nextVal === undefined || nextVal === null) { 1466 | el.removeAttribute(key); 1467 | } 1468 | else { 1469 | el.setAttribute(key, nextVal); 1470 | } 1471 | } 1472 | function insert(el, parent, anchor) { 1473 | parent.insertBefore(el, anchor || null); 1474 | } 1475 | function remove(el) { 1476 | const parentNode = el.parentNode; 1477 | if (parentNode) { 1478 | parentNode.removeChild(el); 1479 | } 1480 | } 1481 | function setElementText(container, children) { 1482 | // XXX: textContent v. innerText 1483 | container.textContent = children; 1484 | } 1485 | const renderer = createRenderer({ 1486 | createElement, 1487 | patchProp, 1488 | insert, 1489 | remove, 1490 | setElementText, 1491 | }); 1492 | function createApp(...args) { 1493 | return renderer.createApp(...args); 1494 | } 1495 | 1496 | var runtimeDom = /*#__PURE__*/Object.freeze({ 1497 | __proto__: null, 1498 | createApp: createApp, 1499 | h: h, 1500 | renderSlot: renderSlot, 1501 | createTextVNode: createTextVNode, 1502 | createElementVNode: createVNode, 1503 | getCurrentInstance: getCurrentInstance, 1504 | registerRuntimeCompiler: registerRuntimeCompiler, 1505 | provide: provide, 1506 | inject: inject, 1507 | createRenderer: createRenderer, 1508 | nextTick: nextTick, 1509 | toDisplayString: toDisplayString, 1510 | ref: ref 1511 | }); 1512 | 1513 | function compileToFunction(template) { 1514 | const { code } = baseCompile(template); 1515 | // tmpl : const { toDisplayString: _toDisplayString} = Vue -> runtimeDom 1516 | const render = new Function('Vue', code)(runtimeDom); 1517 | return render; 1518 | } 1519 | registerRuntimeCompiler(compileToFunction); 1520 | 1521 | export { createApp, createVNode as createElementVNode, createRenderer, createTextVNode, getCurrentInstance, h, inject, nextTick, provide, ref, registerRuntimeCompiler, renderSlot, toDisplayString }; 1522 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vue", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "rollup -c rollup.config.js" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.17.8", 12 | "@babel/preset-env": "^7.16.11", 13 | "@babel/preset-typescript": "^7.16.7", 14 | "@rollup/plugin-typescript": "^8.3.1", 15 | "@types/jest": "^27.4.1", 16 | "babel-jest": "^27.5.1", 17 | "jest": "^27.5.1", 18 | "rollup": "^2.70.1", 19 | "tslib": "^2.3.1", 20 | "typescript": "^4.6.3" 21 | }, 22 | "dependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | export default { 3 | input: './src/index.ts', 4 | output: [ 5 | // cjs 6 | { 7 | format: 'cjs', 8 | file: 'lib/mini-vue.cjs.js', 9 | }, 10 | // esm 11 | { 12 | format: 'es', 13 | file: 'lib/mini-vue.esm.js', 14 | }, 15 | ], 16 | plugins: [typescript()], 17 | } 18 | -------------------------------------------------------------------------------- /src/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | import { CREATE_ELEMENT_VNODE } from './runtimeHelpers' 2 | 3 | export const enum NodeTypes { 4 | ROOT, 5 | INTERPOLATION, 6 | SIMPLE_EXPRESSION, 7 | ELEMENT, 8 | TEXT, 9 | COMPOUND_EXPRESSION, 10 | } 11 | 12 | export function createVNodeCall(context, tag, props, children) { 13 | context.helper(CREATE_ELEMENT_VNODE) 14 | return { 15 | type: NodeTypes.ELEMENT, 16 | tag, 17 | props, 18 | children, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '../../shared' 2 | import { NodeTypes } from './ast' 3 | import { 4 | CREATE_ELEMENT_VNODE, 5 | helperMapName, 6 | TO_DISPLAY_STRING, 7 | } from './runtimeHelpers' 8 | 9 | export function generate(ast) { 10 | const context = generateContext() 11 | const { push } = context 12 | 13 | genFunctionPreamble(ast, context) 14 | const functionName = 'render' 15 | const args = ['_ctx', '_cache'] 16 | const signature = args.join(', ') 17 | 18 | push(`function ${functionName}(${signature}) {`) 19 | push('return ') 20 | genNode(ast.codegenNode, context) 21 | push('}') 22 | return { 23 | code: context.code, 24 | } 25 | } 26 | 27 | function genFunctionPreamble(ast, context) { 28 | const { push } = context 29 | const VueBinging = 'Vue' 30 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}` 31 | if (ast.helpers.length > 0) { 32 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`) 33 | } 34 | push('\n') 35 | push('return ') 36 | } 37 | 38 | function genNode(node, context) { 39 | switch (node.type) { 40 | case NodeTypes.TEXT: 41 | genText(node, context) 42 | break 43 | case NodeTypes.INTERPOLATION: 44 | genInterpolation(node, context) 45 | break 46 | case NodeTypes.SIMPLE_EXPRESSION: 47 | genExpression(node, context) 48 | break 49 | case NodeTypes.ELEMENT: 50 | genElement(node, context) 51 | break 52 | case NodeTypes.COMPOUND_EXPRESSION: 53 | genCompoundExpression(node, context) 54 | break 55 | default: 56 | break 57 | } 58 | } 59 | 60 | function genCompoundExpression(node: any, context: any) { 61 | const { children } = node 62 | const { push } = context 63 | for (let i = 0; i < children.length; i++) { 64 | const child = children[i] 65 | if (isString(child)) { 66 | push(child) 67 | } else { 68 | genNode(child, context) 69 | } 70 | } 71 | } 72 | 73 | function genElement(node, context) { 74 | const { push, helper } = context 75 | const { tag, props, children } = node 76 | push(`${helper(CREATE_ELEMENT_VNODE)}(`) 77 | genNodeList(genNullableArgs([tag, props, children]), context) 78 | push(')') 79 | } 80 | 81 | function genNodeList(nodes, context) { 82 | const { push } = context 83 | for (let i = 0; i < nodes.length; i++) { 84 | const node = nodes[i] 85 | if (isString(node)) { 86 | push(node) 87 | } else { 88 | genNode(node, context) 89 | } 90 | if (i < nodes.length - 1) { 91 | push(', ') 92 | } 93 | } 94 | } 95 | 96 | function genNullableArgs(args) { 97 | return args.map((arg) => arg || 'null') 98 | } 99 | 100 | function genExpression(node: any, context: any) { 101 | const { push } = context 102 | push(`${node.content}`) 103 | } 104 | 105 | function genInterpolation(node: any, context: any) { 106 | const { push, helper } = context 107 | push(`${helper(TO_DISPLAY_STRING)}(`) 108 | genNode(node.content, context) 109 | push(')') 110 | } 111 | 112 | function genText(node, context) { 113 | const { push } = context 114 | push(`"${node.content}"`) 115 | } 116 | 117 | function generateContext() { 118 | const context = { 119 | code: '', 120 | push(source) { 121 | context.code += source 122 | }, 123 | helper(key) { 124 | return `_${helperMapName[key]}` 125 | }, 126 | } 127 | return context 128 | } 129 | -------------------------------------------------------------------------------- /src/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { generate } from './codegen' 2 | import { baseParse } from './parse' 3 | import { transform } from './transform' 4 | import { transformElement } from './transforms/transformElement' 5 | import { transformExpression } from './transforms/transformExpression' 6 | import { transformText } from './transforms/transformText' 7 | 8 | export function baseCompile(template) { 9 | const ast: any = baseParse(template) 10 | transform(ast, { 11 | nodeTransforms: [transformExpression, transformElement, transformText], 12 | }) 13 | return generate(ast) 14 | } 15 | -------------------------------------------------------------------------------- /src/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' 2 | -------------------------------------------------------------------------------- /src/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '../../shared' 2 | import { NodeTypes } from './ast' 3 | 4 | const enum TagType { 5 | Start, 6 | End, 7 | } 8 | 9 | export function baseParse(content: string, options = {}) { 10 | // 将 content 包装至 ctx 中 11 | const context = createParserContext(content, options) 12 | return createRoot(parseChildren(context, [])) 13 | } 14 | 15 | function parseChildren(context, ancestors) { 16 | const nodes: any = [] 17 | // parseChildren 应该使用循环来处理 18 | while (!isEnd(context, ancestors)) { 19 | const s = context.source 20 | let node 21 | if (s.startsWith(context.options.delimiters[0])) { 22 | node = parseInterpolation(context) 23 | } else if (s[0] === '<') { 24 | // TODO: 判断条件 startsWith 和 s[0] 的区别是什么 25 | if (/[a-z]/i.test(s[1])) { 26 | node = parseElement(context, ancestors) 27 | } 28 | } 29 | if (!node) { 30 | node = parseText(context) 31 | } 32 | nodes.push(node) 33 | } 34 | 35 | return nodes 36 | } 37 | 38 | function isEnd(context: any, ancestors) { 39 | // 结束标签 40 | let s = context.source 41 | if (s.startsWith('= 0; i--) { 43 | const tag = ancestors[i].tag 44 | if (startsWithEndTagOpen(s, tag)) { 45 | return true 46 | } 47 | } 48 | } 49 | // context.source 为空 50 | return !s 51 | } 52 | 53 | function parseText(context: any): any { 54 | let endTokens = ['<', context.options.delimiters[0]] 55 | let endIndex = context.source.length 56 | for (let i = 0; i < endTokens.length; i++) { 57 | const index = context.source.indexOf(endTokens[i]) 58 | // endIndex 应该尽量小 59 | if (index !== -1 && index < endIndex) { 60 | endIndex = index 61 | } 62 | } 63 | const content = parseTextData(context, endIndex) 64 | return { 65 | type: NodeTypes.TEXT, 66 | content, 67 | } 68 | } 69 | 70 | function parseTextData(context: any, length: number): any { 71 | const content = context.source.slice(0, length) 72 | advanceBy(context, length) 73 | return content 74 | } 75 | 76 | function parseElement(context: any, ancestors): any { 77 | // StartTag 78 | const element = parseTag(context, TagType.Start) 79 | ancestors.push(element) 80 | // parseEl 的时候应该也要 递归 parseChildren 81 | // 否则就变成只解析一个 tag 了 82 | element.children = parseChildren(context, ancestors) 83 | ancestors.pop() 84 | // EndTag 85 | if (startsWithEndTagOpen(context.source, element.tag)) { 86 | parseTag(context, TagType.End) 87 | } else { 88 | throw new Error(`缺少结束标签: ${element.tag}`) 89 | } 90 | return element 91 | } 92 | 93 | function parseTag(context, type: TagType): any { 94 | const match: any = /^<\/?([a-z]*)/i.exec(context.source) 95 | const tag = match[1] 96 | advanceBy(context, match[0].length + 1) 97 | if (type === TagType.End) return 98 | 99 | return { 100 | type: NodeTypes.ELEMENT, 101 | tag, 102 | } 103 | } 104 | 105 | function parseInterpolation(context) { 106 | const openDelimiter = context.options.delimiters[0] 107 | const closeDelimiter = context.options.delimiters[1] 108 | // context -> content 109 | 110 | // 注意 indexOf 是带一个 [, fromIndex] 可选参数的 111 | // 并且 closeIndex 需要在 advanceBy 先保留下来 112 | const closeIndex = context.source.indexOf( 113 | closeDelimiter, 114 | openDelimiter.length 115 | ) 116 | // slice 是 非 破坏性方法 因此需要 赋值 117 | advanceBy(context, openDelimiter.length) 118 | // {{msg}} 119 | // openDelimiter.length: 2, closeIndex: 5 120 | const rawContentLength = closeIndex - openDelimiter.length 121 | 122 | const rawContent = parseTextData(context, rawContentLength) 123 | const content = rawContent.trim() 124 | advanceBy(context, closeDelimiter.length) 125 | 126 | return { 127 | type: NodeTypes.INTERPOLATION, 128 | content: { 129 | type: NodeTypes.SIMPLE_EXPRESSION, 130 | content: content, 131 | }, 132 | } 133 | } 134 | 135 | function advanceBy(context, length) { 136 | context.source = context.source.slice(length) 137 | } 138 | 139 | function createRoot(children) { 140 | return { 141 | children, 142 | type: NodeTypes.ROOT, 143 | } 144 | } 145 | 146 | function createParserContext(content: string, rawOptions) { 147 | const options = extend({}, defaultParserOptions) 148 | for (const key in rawOptions) { 149 | options[key] = 150 | rawOptions[key] === undefined 151 | ? defaultParserOptions[key] 152 | : rawOptions[key] 153 | } 154 | return { 155 | options, 156 | source: content, 157 | } 158 | } 159 | 160 | function startsWithEndTagOpen(source, tag) { 161 | return ( 162 | source.startsWith(' { 6 | // tag 7 | const vnodeTag = `"${node.tag}"` 8 | // props 9 | let vnodeProps 10 | // children 11 | const { children } = node 12 | let vnodeChildren 13 | if (children.length > 0) { 14 | if (children.length === 1) { 15 | vnodeChildren = children[0] 16 | } 17 | } 18 | node.codegenNode = createVNodeCall( 19 | context, 20 | vnodeTag, 21 | vnodeProps, 22 | vnodeChildren 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/compiler-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * root = { 3 | * children: [ 4 | * { 5 | * type: 0 6 | * content: {type: 1, content: 'message'} 7 | * }, 8 | * ], 9 | * } 10 | */ 11 | 12 | import { NodeTypes } from '../ast' 13 | 14 | export function transformExpression(node) { 15 | if (node.type === NodeTypes.INTERPOLATION) { 16 | node.content = processExpression(node.content) 17 | } 18 | } 19 | 20 | function processExpression(node) { 21 | const raw = node.content 22 | node.content = `_ctx.${raw}` 23 | return node 24 | } 25 | -------------------------------------------------------------------------------- /src/compiler-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | import { isText } from '../utils' 3 | 4 | export function transformText(node, context) { 5 | if (node.type === NodeTypes.ELEMENT) { 6 | return () => { 7 | const { children } = node 8 | let currentContainer 9 | for (let i = 0; i < children.length; i++) { 10 | const child = children[i] 11 | if (isText(child)) { 12 | for (let j = i + 1; j < children.length; j++) { 13 | const next = children[j] 14 | if (isText(next)) { 15 | // init 16 | if (!currentContainer) { 17 | currentContainer = children[i] = { 18 | type: NodeTypes.COMPOUND_EXPRESSION, 19 | children: [child], 20 | } 21 | } 22 | currentContainer.children.push(' + ') 23 | currentContainer.children.push(next) 24 | children.splice(j, 1) 25 | j-- 26 | } else { 27 | currentContainer = undefined 28 | break 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from './ast' 2 | 3 | export function isText(node) { 4 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION 5 | } 6 | -------------------------------------------------------------------------------- /src/compiler-core/tests/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`code generate should generate interpolation 1`] = ` 4 | "const { toDisplayString: _toDisplayString } = Vue 5 | return function render(_ctx, _cache) {return _toDisplayString(_ctx.message)}" 6 | `; 7 | 8 | exports[`code generate should generate nested element 1`] = ` 9 | "const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue 10 | return function render(_ctx, _cache) {return _createElementVNode(\\"div\\", null, \\"hi, \\" + _toDisplayString(_ctx.message))}" 11 | `; 12 | 13 | exports[`code generate should generate string 1`] = ` 14 | " 15 | return function render(_ctx, _cache) {return \\"hi\\"}" 16 | `; 17 | -------------------------------------------------------------------------------- /src/compiler-core/tests/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { baseCompile } from '../src/compile' 2 | 3 | describe('code generate', () => { 4 | it('should generate string', () => { 5 | const { code } = baseCompile('hi') 6 | expect(code).toMatchSnapshot() 7 | }) 8 | 9 | it('should generate interpolation', () => { 10 | const { code } = baseCompile('{{message}}') 11 | expect(code).toMatchSnapshot() 12 | }) 13 | 14 | it('should generate nested element', () => { 15 | const { code } = baseCompile('
hi, {{message}}
') 16 | expect(code).toMatchSnapshot() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/compiler-core/tests/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast' 2 | import { baseParse } from '../src/parse' 3 | 4 | describe('Parse', () => { 5 | describe('interpolation', () => { 6 | it('interpolation happy path', () => { 7 | const ast = baseParse('{{message}}') 8 | expect(ast.children[0]).toStrictEqual({ 9 | type: NodeTypes.INTERPOLATION, 10 | content: { 11 | type: NodeTypes.SIMPLE_EXPRESSION, 12 | content: 'message', 13 | }, 14 | }) 15 | }) 16 | }) 17 | describe('element', () => { 18 | it('element div', () => { 19 | const ast = baseParse('
') 20 | expect(ast.children[0]).toStrictEqual({ 21 | type: NodeTypes.ELEMENT, 22 | tag: 'div', 23 | children: [], 24 | }) 25 | }) 26 | }) 27 | describe('text', () => { 28 | it('simple text', () => { 29 | const ast = baseParse('simple text') 30 | expect(ast.children[0]).toStrictEqual({ 31 | type: NodeTypes.TEXT, 32 | content: 'simple text', 33 | }) 34 | }) 35 | }) 36 | 37 | test('hello', () => { 38 | const ast = baseParse('
hello
') 39 | expect(ast.children[0]).toStrictEqual({ 40 | type: NodeTypes.ELEMENT, 41 | tag: 'div', 42 | children: [ 43 | { 44 | type: NodeTypes.TEXT, 45 | content: 'hello', 46 | }, 47 | ], 48 | }) 49 | }) 50 | 51 | test('element interpolation', () => { 52 | const ast = baseParse('
{{ message }}
') 53 | expect(ast.children[0]).toStrictEqual({ 54 | type: NodeTypes.ELEMENT, 55 | tag: 'div', 56 | children: [ 57 | { 58 | type: NodeTypes.INTERPOLATION, 59 | content: { 60 | type: NodeTypes.SIMPLE_EXPRESSION, 61 | content: 'message', 62 | }, 63 | }, 64 | ], 65 | }) 66 | }) 67 | 68 | test('hello world', () => { 69 | const ast = baseParse('
hello,{{ message }}
') 70 | expect(ast.children[0]).toStrictEqual({ 71 | type: NodeTypes.ELEMENT, 72 | tag: 'div', 73 | children: [ 74 | { 75 | type: NodeTypes.TEXT, 76 | content: 'hello,', 77 | }, 78 | { 79 | type: NodeTypes.INTERPOLATION, 80 | content: { 81 | type: NodeTypes.SIMPLE_EXPRESSION, 82 | content: 'message', 83 | }, 84 | }, 85 | ], 86 | }) 87 | }) 88 | 89 | test('Nested element', () => { 90 | const ast = baseParse('

hello

,{{ message }}
') 91 | expect(ast.children[0]).toStrictEqual({ 92 | type: NodeTypes.ELEMENT, 93 | tag: 'div', 94 | children: [ 95 | { 96 | type: NodeTypes.ELEMENT, 97 | tag: 'p', 98 | children: [ 99 | { 100 | type: NodeTypes.TEXT, 101 | content: 'hello', 102 | }, 103 | ], 104 | }, 105 | { 106 | type: NodeTypes.TEXT, 107 | content: ',', 108 | }, 109 | { 110 | type: NodeTypes.INTERPOLATION, 111 | content: { 112 | type: NodeTypes.SIMPLE_EXPRESSION, 113 | content: 'message', 114 | }, 115 | }, 116 | ], 117 | }) 118 | }) 119 | 120 | test('should throw error when lack tag', () => { 121 | expect(() => { 122 | const ast = baseParse('

') 123 | }).toThrow('缺少结束标签: p') 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /src/compiler-core/tests/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast' 2 | import { baseParse } from '../src/parse' 3 | import { transform } from '../src/transform' 4 | 5 | describe('transform', () => { 6 | test('happy path', () => { 7 | const plugin = (node) => { 8 | if (node.type === NodeTypes.TEXT) { 9 | node.content += ' mini-vue' 10 | } 11 | } 12 | const ast = baseParse('
hi,{{message}}
') 13 | transform(ast, { 14 | nodeTransforms: [plugin], 15 | }) 16 | expect(ast.children[0].children[0].content).toBe('hi, mini-vue') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { baseCompile } from './compiler-core/src' 2 | import * as runtimeDom from './runtime-dom' 3 | import { registerRuntimeCompiler } from './runtime-core' 4 | 5 | export * from './runtime-dom' 6 | 7 | function compileToFunction(template) { 8 | const { code } = baseCompile(template) 9 | // tmpl : const { toDisplayString: _toDisplayString} = Vue -> runtimeDom 10 | const render = new Function('Vue', code)(runtimeDom) 11 | return render 12 | } 13 | 14 | registerRuntimeCompiler(compileToFunction) 15 | -------------------------------------------------------------------------------- /src/reactivity/baseHandler.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from '../shared' 2 | import { track, trigger } from './effect' 3 | import { reactive, ReactiveFlags, readonly } from './reactive' 4 | 5 | const get = createGetter() 6 | const set = createSetter() 7 | const readonlyGet = createGetter(true) 8 | const shallowReadonlyGet = createGetter(true, true) 9 | 10 | function createGetter(isReadonly: boolean = false, isShallow: boolean = false) { 11 | // 两个出口 12 | return function (target, key) { 13 | // IS_REACTIVE| IS_READONLY 14 | // 判断是否为内部的属性,进行拦截 15 | if (key === ReactiveFlags.IS_REACTIVE) { 16 | return !isReadonly 17 | } else if (key === ReactiveFlags.IS_READONLY) { 18 | return isReadonly 19 | } 20 | // 普通响应式数据的逻辑 21 | const res = Reflect.get(target, key) 22 | // XXX: isShallow 直接返回 res,不判断深响应和深只读。但是 track 是否应该执行? 23 | if (isShallow) { 24 | return res 25 | } 26 | // 深响应 && 深只读 27 | if (isObject(res)) { 28 | return isReadonly ? readonly(res) : reactive(res) 29 | } 30 | if (!isReadonly) { 31 | track(target, key) 32 | } 33 | return res 34 | } 35 | } 36 | 37 | function createSetter() { 38 | return function (target, key, value) { 39 | const res = Reflect.set(target, key, value) 40 | trigger(target, key) 41 | return res 42 | } 43 | } 44 | 45 | export const mutableHandlers = { 46 | get: get, 47 | set: set, 48 | } 49 | 50 | export const readonlyHandlers = { 51 | get: readonlyGet, 52 | set(target, key) { 53 | console.warn( 54 | `fail to set key "${String(key)}", because target is readonly: `, 55 | target 56 | ) 57 | return true 58 | }, 59 | } 60 | 61 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 62 | get: shallowReadonlyGet, 63 | }) 64 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from './effect' 2 | 3 | class ComputedRefImpl { 4 | private _dirty: boolean = true 5 | private _effect: ReactiveEffect 6 | constructor(getter) { 7 | this._effect = new ReactiveEffect(getter, () => { 8 | if (!this._dirty) { 9 | this._dirty = true 10 | } 11 | }) 12 | } 13 | 14 | get value(): any { 15 | if (this._dirty) { 16 | this._dirty = false 17 | return this._effect.run() 18 | } 19 | } 20 | } 21 | 22 | export function computed(getter) { 23 | return new ComputedRefImpl(getter) 24 | } 25 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '../shared' 2 | 3 | // target -> key -> dep -> effect 实例 4 | const targetMap = new Map() 5 | // activeEffect 保存了激活的 effect,便于在 track 的时候使用 6 | let activeEffect 7 | // 在 run 函数中开关,在 track 中进行判断 8 | let shouldTrack 9 | 10 | export class ReactiveEffect { 11 | private _fn: any // effectFn 12 | public scheduler: Function | undefined 13 | deps = [] // 反向依赖的数据结构 14 | active: boolean = true // active 标识位 15 | onStop?: () => void 16 | constructor(fn, scheduler?) { 17 | this._fn = fn 18 | // effect(fn, options?) 存在两个参数且内部使用了 extend(_effect,options) 19 | // 所以 _effect 可从 options 中拿到 scheduler 20 | // 而 computed(getter) 只有一个参数,内部只 new constructor 21 | // 所以必须在 constructor 这里接受两个参数,并传给实例的 scheduler 22 | this.scheduler = scheduler 23 | } 24 | run() { 25 | // 手动执行 runner 的分支 26 | if (!this.active) { 27 | // 为什么不 activeEffect = this?理由可能是手动执行意味着 activeEffect 当前并非是 this 28 | // 其实后续 activeEffect 会变为 栈 结构以便于 effect 嵌套执行 29 | return this._fn() 30 | } 31 | // 响应式触发 32 | shouldTrack = true 33 | // activeEffect 保存的是实例化对象 34 | activeEffect = this 35 | const result = this._fn() 36 | shouldTrack = false 37 | activeEffect = undefined 38 | return result 39 | } 40 | stop() { 41 | // 如果用户多次调用 stop,即使已经 cleanup 过,effect 实际不存在于 dep中了 42 | // 但是 cleanupEffect 依旧会执行循环 43 | // 性能优化:使用 active 标识位 44 | if (this.active) { 45 | cleanupEffect(this) 46 | // onStop 的回调函数 47 | if (this.onStop) { 48 | this.onStop() 49 | } 50 | this.active = false 51 | } 52 | } 53 | } 54 | 55 | function cleanupEffect(effect) { 56 | // 负责通过反向依赖把 effectFn 从依赖收集的 Set 中解除 57 | effect.deps.forEach((dep) => { 58 | dep.delete(effect) 59 | }) 60 | // 清空 deps 61 | effect.deps.length = 0 62 | } 63 | 64 | // 1. 边界判断 65 | // 2. 找到 dep: targetMap -> depsMap -> dep 66 | // 3. 依赖收集 67 | export function track(target, key) { 68 | // 边界判断 69 | if (!isTracking()) return 70 | 71 | // 核心是 targetMap -> depsMap -> dep -> dep.add 72 | // 两个 if 用于 init 73 | let depsMap = targetMap.get(target) 74 | if (!depsMap) { 75 | targetMap.set(target, (depsMap = new Map())) 76 | } 77 | let dep = depsMap.get(key) 78 | if (!dep) { 79 | depsMap.set(key, (dep = new Set())) 80 | } 81 | // 依赖收集 82 | trackEffects(dep) 83 | } 84 | 85 | export function isTracking() { 86 | // 边界,注意不要让 undefined 进入 dep 87 | // 边界,!shouldTrack 时直接返回 88 | return shouldTrack && activeEffect !== undefined 89 | } 90 | 91 | export function trackEffects(dep) { 92 | // 常见于 wrapped.foo = 2, set 后还会执行一次 get 93 | // 而此时的 effect 已经在 dep 中了,其实对于 Set 来说无所谓 94 | // 但是 deps 就很吃力了,因为它是个 Array 并不判重,会持续增长 95 | // 到了 cleanup 的部分,就会多出来很多性能消耗 96 | if (dep.has(activeEffect)) return 97 | dep.add(activeEffect) 98 | // 反向依赖收集 99 | activeEffect.deps.push(dep) 100 | } 101 | 102 | // 1. 找到 dep 103 | // 2. 触发依赖 104 | export function trigger(target, key) { 105 | // 需要对 depsMap 和 dep 是否存在做出判断 106 | const depsMap = targetMap.get(target) 107 | if (!depsMap) return 108 | const dep = depsMap.get(key) 109 | triggerEffects(dep) 110 | } 111 | 112 | export function triggerEffects(dep) { 113 | dep && 114 | dep.forEach((effect) => { 115 | triggerEffect(effect) 116 | }) 117 | } 118 | 119 | function triggerEffect(effect: any) { 120 | if (effect !== activeEffect) { 121 | if (effect.scheduler) { 122 | effect.scheduler() 123 | } else { 124 | effect.run() 125 | } 126 | } 127 | } 128 | 129 | // 1. 实例化对象 130 | // 2. 接受 options 131 | // 3. 执行 effectFn 132 | // 4. return runner 133 | export function effect(fn, options: any = {}) { 134 | // 使用 _effect 实例化对象来处理逻辑 135 | const _effect = new ReactiveEffect(fn) 136 | // 接收 options 137 | extend(_effect, options) 138 | // 通过实例执行 139 | _effect.run() 140 | const runner: any = _effect.run.bind(_effect) 141 | // 返回前保存当前的 effect 142 | runner.effect = _effect 143 | return runner 144 | } 145 | 146 | export function stop(runner) { 147 | // runner 是一个 function 148 | // 通过 runner -> effect,effect.stop() 149 | // 通过在 effect 中返回 runner 前,在 runner 里塞入当前的 _effect 来解决 150 | runner.effect.stop() 151 | } 152 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ref } from './ref' 2 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mutableHandlers, 3 | readonlyHandlers, 4 | shallowReadonlyHandlers, 5 | } from './baseHandler' 6 | 7 | export enum ReactiveFlags { 8 | IS_REACTIVE = '__v_isReactive', 9 | IS_READONLY = '__v_isReadonly', 10 | } 11 | 12 | export function reactive(raw) { 13 | return createReactiveObject(raw, mutableHandlers) 14 | } 15 | 16 | export function readonly(raw) { 17 | return createReactiveObject(raw, readonlyHandlers) 18 | } 19 | 20 | export function shallowReadonly(raw) { 21 | return createReactiveObject(raw, shallowReadonlyHandlers) 22 | } 23 | 24 | function createReactiveObject(raw, baseHandles) { 25 | return new Proxy(raw, baseHandles) 26 | } 27 | 28 | export function isProxy(value) { 29 | return isReactive(value) || isReadonly(value) 30 | } 31 | 32 | export function isReactive(value): boolean { 33 | // 如果使用字面量字符串传递,则是魔数 34 | return !!value[ReactiveFlags.IS_REACTIVE] 35 | } 36 | export function isReadonly(value): boolean { 37 | return !!value[ReactiveFlags.IS_READONLY] 38 | } 39 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from '../shared' 2 | import { isTracking, trackEffects, triggerEffects } from './effect' 3 | import { reactive } from './reactive' 4 | 5 | // dep 6 | class RefImpl { 7 | private _value: any 8 | private _rawValue: any 9 | public readonly __v_isRef = true 10 | dep: Set 11 | constructor(value) { 12 | // 保存原始值,便于后续比较 13 | this._rawValue = value 14 | this._value = convert(value) 15 | this.dep = new Set() 16 | } 17 | 18 | get value(): any { 19 | // 依赖收集 20 | trackRefValue(this) 21 | return this._value 22 | } 23 | 24 | set value(newValue: any) { 25 | // 合理触发依赖 26 | if (hasChanged(newValue, this._rawValue)) { 27 | this._rawValue = newValue 28 | this._value = convert(newValue) 29 | // 依赖触发 30 | triggerRefValue(this) 31 | } 32 | } 33 | } 34 | 35 | function trackRefValue(ref) { 36 | if (isTracking()) { 37 | trackEffects(ref.dep) 38 | } 39 | } 40 | 41 | function triggerRefValue(ref) { 42 | triggerEffects(ref.dep) 43 | } 44 | 45 | function convert(value) { 46 | // 判断 原始值 还是 引用 进行转换 47 | return isObject(value) ? reactive(value) : value 48 | } 49 | 50 | export function ref(value) { 51 | return new RefImpl(value) 52 | } 53 | 54 | export function isRef(ref) { 55 | return !!ref.__v_isRef 56 | } 57 | 58 | export function unRef(ref) { 59 | // unRef 主要就是为了暴露给 proxyRefs 使用的 60 | // 读取到值的内容的时候,会触发 unRef 61 | // 而 unRef 里应该触发 .value 而不是 ._value 62 | // 否则不能触发依赖收集 63 | return isRef(ref) ? ref.value : ref 64 | } 65 | 66 | // proxyRefs 用于包装一个 obj(一般为 setupResult) 67 | // setupResult 可能为这种形式 68 | // { 69 | // ref(原始值) 70 | // reactive(obj) 写个测试用例测试一下 71 | // function 72 | // 原始值 73 | // } 74 | export function proxyRefs(objectWithRefs) { 75 | // TODO: proxyRefs handler 76 | return new Proxy(objectWithRefs, { 77 | get(target, key) { 78 | return unRef(Reflect.get(target, key)) 79 | }, 80 | set(target, key, newVal) { 81 | const oldVal = target[key] 82 | // newVal is not Ref && oldVal is Ref 83 | if (!isRef(newVal) && isRef(oldVal)) { 84 | oldVal.value = newVal 85 | return true 86 | } else { 87 | return Reflect.set(target, key, newVal) 88 | } 89 | }, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/reactivity/tests/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '../computed' 2 | import { reactive } from '../reactive' 3 | 4 | describe('computed', () => { 5 | it('happy path', () => { 6 | const user = reactive({ 7 | age: 1, 8 | }) 9 | const age = computed(() => { 10 | return user.age 11 | }) 12 | expect(age.value).toBe(1) 13 | }) 14 | 15 | it('should compute lazily', () => { 16 | // new Proxy 17 | const value = reactive({ 18 | foo: 1, 19 | }) 20 | const getter = jest.fn(() => { 21 | return value.foo 22 | }) 23 | // new ComputedRefImpl -> new ReactiveEffect 24 | const cValue = computed(getter) 25 | 26 | // lazy 27 | expect(getter).not.toHaveBeenCalled() 28 | 29 | // get value -> 脏 -> run -> _fn -> get -> track -> 干净 30 | expect(cValue.value).toBe(1) 31 | // _fn 执行过了一次 32 | expect(getter).toHaveBeenCalledTimes(1) 33 | 34 | // should not compute again 35 | // 干净 -> _fn 不执行,因此还是一次 36 | cValue.value // get 37 | expect(getter).toHaveBeenCalledTimes(1) 38 | 39 | // should not compute until needed 40 | // set -> trigger -> scheduler -> 标记为 脏 41 | value.foo = 2 42 | // 懒计算 43 | expect(getter).toHaveBeenCalledTimes(1) 44 | 45 | // now it should compute 46 | // 读取 计算属性 -> 脏 -> run -> _fn -> get -> track -> 干净 47 | expect(cValue.value).toBe(2) 48 | // 真正计算 49 | expect(getter).toHaveBeenCalledTimes(2) 50 | 51 | // should not compute again 52 | cValue.value 53 | expect(getter).toHaveBeenCalledTimes(2) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/reactivity/tests/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect, stop } from '../effect' 2 | import { reactive } from '../reactive' 3 | 4 | describe('effect', () => { 5 | it('should be happy path', () => { 6 | let num = reactive({ value: 1 }) 7 | let res 8 | effect(() => { 9 | res = num.value + 1 10 | }) 11 | expect(res).toBe(2) 12 | num.value = 2 13 | expect(res).toBe(3) 14 | }) 15 | 16 | it('should observe nested properties', () => { 17 | let dummy 18 | const counter = reactive({ nested: { num: 0 } }) 19 | effect(() => (dummy = counter.nested.num)) 20 | 21 | expect(dummy).toBe(0) 22 | counter.nested.num = 8 23 | expect(dummy).toBe(8) 24 | }) 25 | 26 | it('should return runner and runner res when call it', () => { 27 | // effect(fn) -> return runner 28 | // runner -> fn return 29 | let foo = 1 30 | const runner = effect(() => { 31 | foo++ 32 | return 'foo' 33 | }) 34 | expect(foo).toBe(2) 35 | const r = runner() 36 | expect(foo).toBe(3) 37 | expect(r).toBe('foo') 38 | }) 39 | 40 | it('scheduler', () => { 41 | let dummy 42 | let run: any 43 | const scheduler = jest.fn(() => { 44 | run = runner 45 | }) 46 | const obj = reactive({ foo: 1 }) 47 | const runner = effect( 48 | () => { 49 | dummy = obj.foo 50 | }, 51 | { scheduler } 52 | ) 53 | expect(scheduler).not.toHaveBeenCalled() 54 | expect(dummy).toBe(1) 55 | // should be called on first trigger 56 | obj.foo++ 57 | expect(scheduler).toHaveBeenCalledTimes(1) 58 | // should not run yet 59 | expect(dummy).toBe(1) 60 | // manually run 61 | run() 62 | // should have run 63 | expect(dummy).toBe(2) 64 | }) 65 | 66 | it('stop', () => { 67 | let dummy 68 | const obj = reactive({ prop: 1 }) 69 | const runner = effect(() => { 70 | dummy = obj.prop 71 | }) 72 | obj.prop = 2 73 | expect(dummy).toBe(2) 74 | stop(runner) 75 | obj.prop = 3 76 | expect(dummy).toBe(2) 77 | 78 | // stopped effect should still be manually callable 79 | runner() 80 | expect(dummy).toBe(3) 81 | }) 82 | 83 | it('stop after runner', () => { 84 | let dummy 85 | const obj = reactive({ prop: 1 }) 86 | const runner = effect(() => { 87 | dummy = obj.prop 88 | }) 89 | obj.prop = 2 90 | expect(dummy).toBe(2) 91 | stop(runner) 92 | obj.prop = 3 93 | expect(dummy).toBe(2) 94 | 95 | // stopped effect should still be manually callable 96 | runner() 97 | expect(dummy).toBe(3) 98 | 99 | obj.prop = 4 100 | expect(dummy).toBe(3) 101 | }) 102 | 103 | it('stop auto increment', () => { 104 | let dummy 105 | const obj = reactive({ prop: 1 }) 106 | const runner = effect(() => { 107 | dummy = obj.prop 108 | }) 109 | obj.prop = 2 110 | expect(dummy).toBe(2) 111 | stop(runner) 112 | obj.prop++ 113 | expect(dummy).toBe(2) 114 | 115 | // stopped effect should still be manually callable 116 | runner() 117 | expect(dummy).toBe(3) 118 | }) 119 | 120 | it('events: onStop', () => { 121 | const onStop = jest.fn() 122 | const runner = effect(() => {}, { 123 | onStop, 124 | }) 125 | 126 | stop(runner) 127 | expect(onStop).toHaveBeenCalled() 128 | }) 129 | 130 | it('should avoid implicit infinite recursive loops with itself', () => { 131 | const counter = reactive({ num: 0 }) 132 | const counterSpy = jest.fn(() => counter.num++) 133 | effect(counterSpy) 134 | expect(counter.num).toBe(1) 135 | expect(counterSpy).toHaveBeenCalledTimes(1) 136 | counter.num = 4 137 | expect(counter.num).toBe(5) 138 | expect(counterSpy).toHaveBeenCalledTimes(2) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, isReadonly, reactive } from '../reactive' 2 | 3 | describe('reactive', () => { 4 | it('should be happy path', () => { 5 | const original: any = { 6 | foo: 1, 7 | } 8 | const observed = reactive(original) 9 | // proxy 10 | expect(observed).not.toBe(original) 11 | // get 12 | expect(observed.foo).toBe(1) 13 | // set 14 | observed.foo = 2 15 | expect(observed.foo).toBe(2) 16 | expect(original.foo).toBe(2) 17 | observed.bar = 1 18 | expect(observed.bar).toBe(1) 19 | expect(original.bar).toBe(1) 20 | // isReactive 21 | expect(isReactive(original)).toBe(false) 22 | expect(isReactive(observed)).toBe(true) 23 | // isReadonly 24 | expect(isReadonly(observed)).toBe(false) 25 | expect(isReadonly(original)).toBe(false) 26 | // isProxy 27 | expect(isProxy(observed)).toBe(true) 28 | expect(isProxy(original)).toBe(false) 29 | }) 30 | 31 | it('nested reactives', () => { 32 | const original = { 33 | nested: { 34 | foo: 1, 35 | }, 36 | array: [{ bar: 2 }], 37 | } 38 | const observed = reactive(original) 39 | expect(isReactive(observed.nested)).toBe(true) 40 | expect(isReactive(observed.array)).toBe(true) 41 | expect(isReactive(observed.array[0])).toBe(true) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, isReadonly, readonly } from '../reactive' 2 | 3 | describe('readonly', () => { 4 | it('should be happy path', () => { 5 | const original = { foo: 1, bar: { baz: 2 } } 6 | const wrapped = readonly(original) 7 | expect(wrapped).not.toBe(original) 8 | expect(wrapped.foo).toBe(1) 9 | // isReadonly 10 | expect(isReactive(wrapped)).toBe(false) 11 | expect(isReadonly(wrapped)).toBe(true) 12 | expect(isReactive(original)).toBe(false) 13 | expect(isReadonly(original)).toBe(false) 14 | expect(isReactive(wrapped.bar)).toBe(false) 15 | expect(isReadonly(wrapped.bar)).toBe(true) 16 | expect(isReactive(original.bar)).toBe(false) 17 | expect(isReadonly(original.bar)).toBe(false) 18 | // isProxy 19 | expect(isProxy(wrapped)).toBe(true) 20 | expect(isProxy(original)).toBe(false) 21 | // get 22 | expect(wrapped.foo).toBe(1) 23 | // has 24 | expect('foo' in wrapped).toBe(true) 25 | // ownKeys 26 | expect(Object.keys(wrapped)).toEqual(['foo', 'bar']) 27 | }) 28 | 29 | it('should call console.warn when set', () => { 30 | // mock 31 | console.warn = jest.fn() 32 | const wrapped = readonly({ 33 | foo: 1, 34 | }) 35 | wrapped.foo = 2 36 | expect(console.warn).toHaveBeenCalled() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/reactivity/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../effect' 2 | import { reactive } from '../reactive' 3 | import { isRef, proxyRefs, ref, unRef } from '../ref' 4 | describe('ref', () => { 5 | it('happy path', () => { 6 | const a = ref(1) 7 | expect(a.value).toBe(1) 8 | }) 9 | 10 | it('should be reactive', () => { 11 | const a = ref(1) 12 | let dummy 13 | let calls = 0 14 | effect(() => { 15 | calls++ 16 | dummy = a.value 17 | }) 18 | expect(calls).toBe(1) 19 | expect(dummy).toBe(1) 20 | a.value = 2 21 | expect(calls).toBe(2) 22 | expect(dummy).toBe(2) 23 | // same value should not trigger 24 | a.value = 2 25 | expect(calls).toBe(2) 26 | expect(dummy).toBe(2) 27 | }) 28 | 29 | it('should make nested properties reactive', () => { 30 | const a = ref({ 31 | count: 1, 32 | }) 33 | let dummy 34 | effect(() => { 35 | dummy = a.value.count 36 | }) 37 | expect(dummy).toBe(1) 38 | a.value.count = 2 39 | expect(dummy).toBe(2) 40 | }) 41 | 42 | it('should not trigger when nested properties reactive is same', () => { 43 | let calls = 0 44 | const original = { count: 1 } 45 | const a = ref(original) 46 | let dummy 47 | effect(() => { 48 | calls++ 49 | dummy = a.value.count 50 | }) 51 | expect(dummy).toBe(1) 52 | expect(calls).toBe(1) 53 | // same value should not trigger 54 | a.value = original 55 | expect(dummy).toBe(1) 56 | expect(calls).toBe(1) 57 | }) 58 | 59 | it('isRef', () => { 60 | const a = ref(1) 61 | const b = reactive({ value: 1 }) 62 | expect(isRef(a)).toBe(true) 63 | expect(isRef(1)).toBe(false) 64 | expect(isRef(b)).toBe(false) 65 | expect(a.__v_isRef).toBe(true) 66 | }) 67 | 68 | it('unRef', () => { 69 | const a = ref(1) 70 | expect(unRef(a)).toBe(1) 71 | expect(unRef(1)).toBe(1) 72 | }) 73 | 74 | it('proxyRefs', () => { 75 | const obj = { 76 | foo: ref(10), 77 | bar: 'test', 78 | } 79 | 80 | // ref 是 RefImpl,是需要使用 .value 来读取的 81 | // 无需使用 .value 读取的应该使用 Proxy 来拦截 get 以及 set 82 | // 所以 newObj 应该是return new Proxy 83 | const newObj = proxyRefs(obj) 84 | expect(obj.foo.value).toBe(10) 85 | 86 | // get 拦截时,使用 unRef 来脱 ref 87 | expect(newObj.foo).toBe(10) 88 | expect(newObj.bar).toBe('test') 89 | 90 | // set 拦截时 91 | // | newVal | oldVal | 处理 92 | // | !isRef | isRef | ref.value = newVal 93 | // | !isRef | !isRef | Reflect.set 94 | // | isRef | isRef | Reflect.set 95 | // | isRef | !isRef | Reflect.set 96 | newObj.foo = 20 97 | expect(newObj.foo).toBe(20) 98 | expect(obj.foo.value).toBe(20) 99 | 100 | newObj.foo = ref(10) 101 | expect(newObj.foo).toBe(10) 102 | expect(obj.foo.value).toBe(10) 103 | }) 104 | 105 | it('proxyRefs reactive', () => { 106 | const foo = ref(0) 107 | const addFoo = () => { 108 | foo.value++ 109 | } 110 | const barObj = reactive({ num: 0 }) 111 | const addBar = () => { 112 | barObj.num++ 113 | } 114 | // setupResult 115 | const obj = { 116 | foo, 117 | addFoo, 118 | barObj, 119 | addBar, 120 | } 121 | // handleSetupResult 122 | const newObj = proxyRefs(obj) 123 | 124 | let count = 0 125 | effect(() => { 126 | // render 127 | newObj.foo 128 | newObj.barObj.num 129 | count++ 130 | }) 131 | expect(count).toBe(1) 132 | newObj.addFoo() 133 | expect(count).toBe(2) 134 | newObj.addBar() 135 | expect(count).toBe(3) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from '../reactive' 2 | 3 | describe('shallowReadonly', () => { 4 | it('should not make non-reactive properties readonly', () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }) 6 | expect(isReadonly(props)).toBe(true) 7 | expect(isReadonly(props.n)).toBe(false) 8 | }) 9 | 10 | it('should call console.warn when set', () => { 11 | // mock 12 | console.warn = jest.fn() 13 | const wrapped = shallowReadonly({ 14 | foo: 1, 15 | }) 16 | wrapped.foo = 2 17 | expect(console.warn).toHaveBeenCalled() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../shared' 2 | import { getCurrentInstance } from './component' 3 | 4 | export function provide(key, value) { 5 | const currentInstance: any = getCurrentInstance() 6 | let provides = currentInstance.provides 7 | const parentProvides = 8 | currentInstance.parent && currentInstance.parent.provides 9 | // init 10 | if (parentProvides === provides) { 11 | provides = currentInstance.provides = Object.create(parentProvides) 12 | } 13 | provides[key] = value 14 | } 15 | 16 | export function inject(key, defaultValue) { 17 | // TODO: not self-inject 18 | const currentInstance: any = getCurrentInstance() 19 | if (currentInstance) { 20 | const parentProvides = currentInstance.parent.provides 21 | // XXX: 为什么使用 in 而不是 hasOwn 22 | if (key in parentProvides) { 23 | return parentProvides[key] 24 | } else if (defaultValue) { 25 | return isFunction(defaultValue) ? defaultValue() : defaultValue 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { shallowReadonly } from '../reactivity/reactive' 2 | import { proxyRefs } from '../reactivity/ref' 3 | import { isObject, NOOP } 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 function createComponentInstance(vnode: any, parent: any) { 10 | const component = { 11 | vnode, 12 | next: null, 13 | type: vnode.type, 14 | setupState: {}, 15 | props: {}, 16 | slots: {}, 17 | provides: parent ? parent.provides : {}, 18 | parent, 19 | isMounted: false, 20 | subTree: {}, 21 | emit: null, 22 | update: null, 23 | } 24 | // bind 除了可以处理 this 丢失的问题 25 | // 还可以隐藏参数 26 | // XXX: as any 需要在 ts 的学习中解决 27 | component.emit = emit.bind(null, component) as any 28 | return component 29 | } 30 | 31 | export function setupComponent(instance: any) { 32 | const { props, children } = instance.vnode 33 | // 将 props 接收到 instance 中 34 | // instance.vnode.props -> instance.props 35 | initProps(instance, props) 36 | initSlots(instance, children) 37 | setupStatefulComponent(instance) 38 | // TODO: 函数组件(无状态) 39 | } 40 | 41 | // 1. instance.proxy 42 | // 2. instance.setupState 判断是否有 setup -> setupResult 43 | // 3. instance.render 判断是否有 setup -> setupResult -> render 44 | function setupStatefulComponent(instance: any) { 45 | // 代理模式, 使用 proxy 46 | // 这里的解构是为了保持源码一致,源码后续第一个参数会是 instance.ctx 47 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers) 48 | // instance -> vnode -> type === component -> setupResult = setup() 49 | // instance: {vnode, type} 50 | // instance -> type === component -> setupResult = setup() 51 | const { props, type: Component } = instance 52 | const { setup } = Component 53 | if (setup) { 54 | setCurrentInstance(instance) 55 | // setup 接收 props 参数 56 | const setupResult = setup(shallowReadonly(props), { emit: instance.emit }) 57 | setCurrentInstance(null) 58 | handleSetupResult(instance, setupResult) 59 | } 60 | } 61 | 62 | // 1. setupResult 是 function 63 | // 2. setupResult 是 object 64 | // 3. finishComponentSetup 65 | function handleSetupResult(instance, setupResult: any) { 66 | // TODO: function 67 | 68 | if (isObject(setupResult)) { 69 | // render 中要拿到自动脱 ref 所以使用 proxyRefs 包装 setupResult 的内容 70 | instance.setupState = proxyRefs(setupResult) 71 | } 72 | 73 | finishComponentSetup(instance) 74 | } 75 | function finishComponentSetup(instance: any) { 76 | const Component = instance.type 77 | 78 | // 如果 instance 还没有 render 79 | if (!instance.render) { 80 | if (compile && !Component.render) { 81 | const template = Component.template 82 | if (template) { 83 | Component.render = compile(template) 84 | } 85 | } 86 | instance.render = Component.render 87 | } 88 | } 89 | 90 | let currentInstance = null 91 | export function getCurrentInstance() { 92 | return currentInstance 93 | } 94 | 95 | function setCurrentInstance(instance) { 96 | currentInstance = instance 97 | } 98 | 99 | let compile 100 | export function registerRuntimeCompiler(_compiler) { 101 | compile = _compiler 102 | } 103 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from '../shared' 2 | // 1. 拿到 onEvent 3 | // 2. onEvent && onEvent() 4 | export function emit(instance, event) { 5 | // 在子组件实例的 props 中应该存在 onEvent 事件 6 | const { props } = instance 7 | 8 | const toHandleKey = (str) => (str ? `on${capitalize(event)}` : '') 9 | const handleName = toHandleKey(event) 10 | 11 | const handler = props[handleName] 12 | handler && handler() 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | // instance.vnode.props 可能为 props 或者 undefined -> {} 3 | instance.props = rawProps || {} 4 | // TODO: attrs 5 | } 6 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '../shared' 2 | 3 | // key -> function(instance) 4 | const publicPropertiesMap = { 5 | $el: (i) => i.vnode.el, 6 | $slots: (i) => i.slots, 7 | $props: (i) => i.props, 8 | } 9 | 10 | export const PublicInstanceProxyHandlers = { 11 | get({ _: instance }, key) { 12 | const { setupState, props } = instance 13 | // 类似 this.count 14 | // 需要检查 count 是 setupResult 里的,还是 props 里的 15 | if (hasOwn(setupState, key)) { 16 | return setupState[key] 17 | } else if (hasOwn(props, key)) { 18 | return props[key] 19 | } 20 | 21 | // 类似 this.$el 22 | const publicGetter = publicPropertiesMap[key] 23 | if (publicGetter) { 24 | return publicGetter(instance) 25 | } 26 | // TODO: 不在 setupState | props | $ 中,需要做处理 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/runtime-core/componentRenderUtils.ts: -------------------------------------------------------------------------------- 1 | export function hasPropsChanged(prevProps, nextProps) { 2 | const nextKeys = Object.keys(nextProps) 3 | if (Object.keys(prevProps).length !== nextKeys.length) { 4 | return true 5 | } 6 | for (let i = 0; i < nextKeys.length; i++) { 7 | const key = nextKeys[i] 8 | if (nextProps[key] !== prevProps[key]) { 9 | return true 10 | } 11 | } 12 | return false 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { isArray, ShapeFlags } from '../shared' 2 | 3 | export function initSlots(instance, children) { 4 | // 判断 children 是否是一个 object 5 | // 判断任务加入到 shapeFlags 中 6 | const { vnode } = instance 7 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 8 | normalizeObjectSlots(children, instance.slots) 9 | } 10 | } 11 | 12 | function normalizeObjectSlots(children: any, slots: any) { 13 | for (const key in children) { 14 | const value = children[key] 15 | // value 或者说 slot 此时是一个 function 16 | slots[key] = (props) => normalizeSlotValue(value(props)) 17 | } 18 | } 19 | 20 | // 需要判断 children 是 single element 还是 数组 21 | function normalizeSlotValue(value) { 22 | return isArray(value) ? value : [value] 23 | } 24 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '../shared' 2 | import { createVNode } from './vnode' 3 | 4 | export function createAppAPI(render) { 5 | return function createApp(rootComponent) { 6 | return { 7 | mount(rootContainer) { 8 | // 1. 创建 vnode: rootComponent -> vnode 9 | // vnode: {type, props?, children?} 10 | const vnode = createVNode(rootComponent) 11 | // 2. 渲染 vnode: render(vnode, rootContainer) 12 | render(vnode, convertContainer(rootContainer)) 13 | }, 14 | } 15 | } 16 | } 17 | 18 | function convertContainer(container: any) { 19 | if (isString(container)) { 20 | const result = document.querySelector(container) 21 | return result 22 | } else { 23 | // TODO: 考虑 container 为空,需要 document.createElement('div') 24 | return container 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | export function h(type, props?, children?) { 4 | return createVNode(type, props, children) 5 | } 6 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlot.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../../shared' 2 | import { createVNode, Fragment } from '../vnode' 3 | 4 | export function renderSlot(slots, name = 'default', props) { 5 | // TODO: default 具名 6 | const slot = slots[name] 7 | if (slot) { 8 | // slot: (props) => h(el, {}, props) 9 | if (isFunction(slot)) { 10 | // 需要使用 Fragment 11 | return createVNode(Fragment, {}, slot(props)) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from './h' 2 | export { renderSlot } from './helpers/renderSlot' 3 | export { createTextVNode, createElementVNode } from './vnode' 4 | export { getCurrentInstance, registerRuntimeCompiler } from './component' 5 | export { provide, inject } from './apiInject' 6 | export { createRenderer } from './renderer' 7 | export { nextTick } from './scheduler' 8 | export { toDisplayString } from '../shared' 9 | export * from '../reactivity' 10 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../reactivity/effect' 2 | import { EMPTY_ARR, EMPTY_OBJ, ShapeFlags } from '../shared' 3 | import { createComponentInstance, setupComponent } from './component' 4 | import { hasPropsChanged } from './componentRenderUtils' 5 | import { createAppAPI } from './createApp' 6 | import { queueJobs } from './scheduler' 7 | import { Fragment, isSameVNodeType, Text } from './vnode' 8 | 9 | export function createRenderer(options) { 10 | const { 11 | createElement: hostCreateElement, 12 | patchProp: hostPatchProp, 13 | insert: hostInsert, 14 | remove: hostRemove, 15 | setElementText: hostSetElementText, 16 | } = options 17 | 18 | function render(vnode: any, rootContainer: any) { 19 | // patch 递归 20 | patch(null, vnode, rootContainer, null, null) 21 | } 22 | 23 | function patch(n1, n2: any, container: any, parentComponent, anchor) { 24 | const { type, shapeFlag } = n2 25 | 26 | switch (type) { 27 | case Fragment: 28 | processFragment(n1, n2, container, parentComponent, anchor) 29 | break 30 | case Text: 31 | processText(n1, n2, container) 32 | break 33 | default: 34 | // TODO: vnode 不合法就没有出口了 35 | if (shapeFlag & ShapeFlags.ELEMENT) { 36 | // isString -> processElement 37 | processElement(n1, n2, container, parentComponent, anchor) 38 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 39 | // isObj ->processComponent 40 | processComponent(n1, n2, container, parentComponent, anchor) 41 | } 42 | break 43 | } 44 | } 45 | 46 | function processFragment( 47 | n1, 48 | n2: any, 49 | container: any, 50 | parentComponent, 51 | anchor 52 | ) { 53 | const { children } = n2 54 | mountChildren(children, container, parentComponent, anchor) 55 | } 56 | 57 | function processText(n1, n2: any, container: any) { 58 | const { children } = n2 59 | // TODO: 这里使用了 DOM 平台,需要抽离逻辑 60 | const el = (n2.el = document.createTextNode(children)) 61 | container.append(el) 62 | } 63 | 64 | function processElement( 65 | n1, 66 | n2: any, 67 | container: any, 68 | parentComponent, 69 | anchor 70 | ) { 71 | // 判断是 mount 还是 update 72 | if (!n1) { 73 | mountElement(n2, container, parentComponent, anchor) 74 | } else { 75 | patchElement(n1, n2, container, parentComponent, anchor) 76 | } 77 | } 78 | 79 | // 1. 创建 type === tag 的 el 80 | // 2. el.props 是 attribute 还是 event 81 | // 3. children 是否为 string 或者 array 82 | // 4. 挂载 container.append 83 | function mountElement(vnode: any, container: any, parentComponent, anchor) { 84 | const { type, props, children, shapeFlag } = vnode 85 | // 这里的 vnode 是 tag, 通过 vnode.el 把 el 传递出来 86 | const el = (vnode.el = hostCreateElement(type)) 87 | 88 | if (props) { 89 | for (const key in props) { 90 | const val = props[key] 91 | hostPatchProp(el, key, null, val) 92 | } 93 | } 94 | 95 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 96 | el.innerText = children 97 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 98 | mountChildren(children, el, parentComponent, anchor) 99 | } 100 | hostInsert(el, container, anchor) 101 | } 102 | 103 | function mountChildren(children, container, parentComponent, anchor) { 104 | children.forEach((child) => { 105 | patch(null, child, container, parentComponent, anchor) 106 | }) 107 | } 108 | 109 | function patchElement(n1, n2, container, parentComponent, anchor) { 110 | const el = (n2.el = n1.el) 111 | console.log('patchElement') 112 | console.log('n1: ', n1) 113 | console.log('n2: ', n2) 114 | // children 115 | // 注意这里传入的是 el 而不是 container 116 | // container 是整个容器 117 | // 此时更新的仅仅是需要更新节点的 el 118 | patchChildren(n1, n2, el, parentComponent, anchor) 119 | // props 120 | const oldProps = n1.props || EMPTY_OBJ 121 | const newProps = n2.props || EMPTY_OBJ 122 | patchProps(el, oldProps, newProps) 123 | } 124 | 125 | // 此处的 container 是需要更新的容器 即 n1 n2 的 el 126 | function patchChildren(n1, n2, container, parentComponent, anchor) { 127 | const { shapeFlag: prevShapeFlag, children: c1 } = n1 128 | const { shapeFlag, children: c2 } = n2 129 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 130 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 131 | // remove all children 132 | unmountChildren(c1) 133 | } 134 | if (c1 !== c2) { 135 | // (ArrayToText | TextToText) -> insert text element 136 | hostSetElementText(container, c2) 137 | } 138 | } else { 139 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 140 | // TextToArray 141 | // 清空 textContent 142 | hostSetElementText(container, null) 143 | // mountChildren 144 | mountChildren(c2, container, parentComponent, anchor) 145 | } else { 146 | // ArrayToArray 147 | patchKeyedChildren(c1, c2, container, parentComponent) 148 | } 149 | } 150 | } 151 | 152 | // 快速 diff 153 | function patchKeyedChildren(c1, c2, container, parentComponent) { 154 | // 双端预处理 => 步骤 1 和 2 155 | let i = 0 156 | // 长度可能不同 157 | let e1 = c1.length - 1 158 | let e2 = c2.length - 1 159 | // 记录 c2 长度 160 | const l2 = c2.length 161 | 162 | // 1. sync from start 163 | // (a b) c 164 | // (a b) d e 165 | while (i <= e1 && i <= e2) { 166 | // 实则此处 while 的条件是边界 167 | let n1 = c1[i] 168 | let n2 = c2[i] 169 | if (isSameVNodeType(n1, n2)) { 170 | patch(n1, n2, container, parentComponent, null) 171 | } else { 172 | // 当 n1 与 n2 不相等时为普通出口 173 | break 174 | } 175 | i++ 176 | } 177 | // 2. sync from end 178 | // a (b c) 179 | // d e (b c) 180 | while (i <= e1 && i <= e2) { 181 | let n1 = c1[e1] 182 | let n2 = c2[e2] 183 | if (isSameVNodeType(n1, n2)) { 184 | patch(n1, n2, container, parentComponent, null) 185 | } else { 186 | break 187 | } 188 | e1-- 189 | e2-- 190 | } 191 | // 预处理结束 理想状态下总有一个 children 处理完毕 192 | 193 | // 3. common sequence + mount 194 | // (a b) 195 | // (a b) c 196 | // i = 2, e1 = 1, e2 = 2 197 | // (a b) 198 | // c (a b) 199 | // i = 0, e1 = -1, e2 = 0 200 | 201 | // oldChildren 处理完毕 说明还有新的节点需要 mount 202 | // 特征是 i > oldEnd && i <= newEnd 而 [i,newEnd] 区间的内容即为 mount 内容 203 | if (i > e1) { 204 | if (i <= e2) { 205 | // mount 206 | while (i <= e2) { 207 | // anchor index -> newEnd + 1 208 | const anchorIndex = e2 + 1 209 | // anchorIndex < c2.length -> anchor 在 新的子节点中 -> c2[anchorIndex].el 210 | // 否则 anchor -> null 211 | const anchor = anchorIndex < l2 ? c2[anchorIndex].el : null 212 | patch(null, c2[i], container, parentComponent, anchor) 213 | i++ 214 | } 215 | } 216 | } 217 | // 4. common sequence + unmount 218 | // (a b) c 219 | // (a b) 220 | // i = 2, e1 = 2, e2 = 1 221 | // a (b c) 222 | // (b c) 223 | // i = 0, e1 = 0, e2 = -1 224 | 225 | // newChildren 处理完毕 说明还有旧的节点需要 unmount 226 | // 特征是 i > newEnd && i <= oldEnd 而 [i, oldEnd] 区间内容即为 unmount 内容 227 | else if (i > e2) { 228 | while (i <= e1) { 229 | hostRemove(c1[i].el) 230 | i++ 231 | } 232 | } 233 | // 5. unknown sequence 234 | // [i ... e1 + 1]: a b [c d e] f g 235 | // [i ... e2 + 1]: a b [e d c h] f g 236 | // i = 2, e1 = 4, e2 = 5 237 | 238 | // 非理想状态要 LIS 找移动节点 239 | else { 240 | const s1 = i 241 | const s2 = i 242 | // 1. 先完成 patch 和 unmount 逻辑 243 | // 建立索引 244 | // 遍历 c1 的 [s1,e1] -> 在索引中找到 newIndex || 没有索引需要遍历寻找 O(n^2) 245 | // 如果 newIndex === undefined -> unmount 246 | // 否则 patch 并且记录 source 方便后面 LIS 247 | const keyToNewIndexMap = new Map() 248 | for (let i = s2; i <= e2; i++) { 249 | const nextChild = c2[i] 250 | keyToNewIndexMap.set(nextChild.key, i) 251 | } 252 | // 当 patch >= toBePatched 时可以直接 unmount 并 continue 253 | let patched = 0 254 | const toBePatched = e2 - s2 + 1 255 | // source 数组 -> LIS 256 | // 0 代表新节点 offset = +1 257 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0) 258 | // 判断是否存在需要移动的节点 259 | let moved = false 260 | let maxNewIndexSoFar = 0 261 | 262 | for (let i = s1; i <= e1; i++) { 263 | const prevChild = c1[i] 264 | // 当 patched >= toBePatched 时可以 unmount 并跳过 265 | if (patched >= toBePatched) { 266 | hostRemove(prevChild.el) 267 | continue 268 | } 269 | let newIndex 270 | if (prevChild.key != null) { 271 | newIndex = keyToNewIndexMap.get(prevChild.key) 272 | } else { 273 | // undefined || null 274 | for (let j = s2; j <= e2; j++) { 275 | if (isSameVNodeType(prevChild, c2[j])) { 276 | newIndex = j 277 | break 278 | } 279 | } 280 | } 281 | if (newIndex === undefined) { 282 | hostRemove(prevChild.el) 283 | } else { 284 | newIndexToOldIndexMap[newIndex - s2] = i + 1 285 | if (newIndex >= maxNewIndexSoFar) { 286 | maxNewIndexSoFar = newIndex 287 | } else { 288 | moved = true 289 | } 290 | patch(prevChild, c2[newIndex], container, parentComponent, null) 291 | patched++ 292 | } 293 | } 294 | // 2. 然后再完成移动以及新增逻辑 295 | const increasingNewIndexSequence = moved 296 | ? getSequence(newIndexToOldIndexMap) 297 | : EMPTY_ARR 298 | let j = increasingNewIndexSequence.length - 1 299 | for (let i = toBePatched - 1; i >= 0; i--) { 300 | const nextIndex = s2 + i 301 | const nextChild = c2[nextIndex] 302 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null 303 | if (newIndexToOldIndexMap[i] === 0) { 304 | // 新增的节点 305 | patch(null, nextChild, container, parentComponent, anchor) 306 | } else if (moved) { 307 | // 存在需要移动的节点 308 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 309 | // j < 0: LIS处理结束剩下的均为需要移动的节点 310 | // i !== increasingNewIndexSequence[j]: 不在 LIS 中需要移动 311 | hostInsert(nextChild.el, container, anchor) 312 | } else { 313 | // 不是新增的节点也无需移动 314 | // LIS 的索引向前移动 315 | j-- 316 | } 317 | } 318 | } 319 | } 320 | } 321 | 322 | function unmountChildren(children) { 323 | // XXX: 这里为什么用 for 而不是 forEach 324 | // 并且vue3源码中的remove是把parentComponent也传递了过去 325 | // 按理来说传递后就不需要使用 Node.parentNode 来找 parent 了 326 | // 多次找 parentNode 也是一个消耗因为可能是同一个 327 | for (let i = 0; i < children.length; i++) { 328 | // 注意这里需要传入 el 329 | // children[i] 只是一个 vnode 330 | hostRemove(children[i].el) 331 | } 332 | } 333 | 334 | function patchProps(el, oldProps, newProps) { 335 | // #5857 336 | if (oldProps !== newProps) { 337 | for (const key in newProps) { 338 | const prevProp = oldProps[key] 339 | const nextProp = newProps[key] 340 | if (nextProp !== prevProp) { 341 | hostPatchProp(el, key, prevProp, nextProp) 342 | } 343 | } 344 | if (oldProps !== EMPTY_OBJ) { 345 | for (const key in oldProps) { 346 | if (!(key in newProps)) { 347 | const prevProp = oldProps[key] 348 | hostPatchProp(el, key, prevProp, null) 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | function processComponent( 356 | n1, 357 | n2: any, 358 | container: any, 359 | parentComponent, 360 | anchor 361 | ) { 362 | if (!n1) { 363 | mountComponent(n2, container, parentComponent, anchor) 364 | } else { 365 | updateComponent(n1, n2) 366 | } 367 | } 368 | 369 | function updateComponent(n1, n2) { 370 | const instance = (n2.component = n1.component) 371 | // 将 n2 传递给 instance 372 | if (hasPropsChanged(n1.props, n2.props)) { 373 | instance.next = n2 374 | instance.update() 375 | } else { 376 | n2.el = n1.el 377 | instance.vnode = n2 378 | } 379 | } 380 | 381 | function mountComponent( 382 | initialVNode: any, 383 | container: any, 384 | parentComponent, 385 | anchor 386 | ) { 387 | // 1. 创建 componentInstance 388 | // 数据类型: vnode -> component 389 | // component: {vnode, type} 390 | const instance = (initialVNode.component = createComponentInstance( 391 | initialVNode, 392 | parentComponent 393 | )) 394 | // 2. setupComponent(instance) 395 | setupComponent(instance) 396 | // 3. setupRenderEffect(instance) 397 | // 此时 instance 通过 setupComponent 拿到了 render 398 | setupRenderEffect(instance, initialVNode, container, anchor) 399 | } 400 | 401 | function setupRenderEffect(instance, initialVNode, container, anchor) { 402 | instance.update = effect( 403 | () => { 404 | // mount 流程 405 | if (!instance.isMounted) { 406 | // setupState | $el | $data 的代理 407 | const { proxy } = instance 408 | // render 的 this 指向的是 proxy 409 | // proxy 读取 setup 返回值的时通过 handler 处理掉了 setupState 410 | const subTree = (instance.subTree = instance.render.call( 411 | proxy, 412 | proxy 413 | )) 414 | patch(null, subTree, container, instance, anchor) 415 | // 递归结束, subTree 是 root element, 即最外层的 tag 416 | // 而这个方法里的 vnode 是一个 componentInstance 417 | // vnode.el = subTree.el 将 el 传递给了 component 418 | initialVNode.el = subTree.el 419 | // 更新 isMounted 状态 420 | instance.isMounted = true 421 | } else { 422 | const { proxy, vnode, next } = instance 423 | // updateComponent 的逻辑 424 | // vnode: n1, next: n2 425 | if (next) { 426 | // updateComponent 的 el 传递 427 | next.el = vnode.el 428 | updateComponentPreRender(instance, next) 429 | } 430 | const subTree = instance.render.call(proxy, proxy) 431 | const preSubTree = instance.subTree 432 | // 更新 instance 的 subTree 433 | instance.subTree = subTree 434 | patch(preSubTree, subTree, container, instance, anchor) 435 | // update 流程中 el 是否会被更新? 436 | // 答案是会的, 在 patchElement 第一步就是 el = n2.el = n1.el 437 | // 但是注意这里是 element 更新逻辑里的 el 438 | // 而 Component 的 el 更新逻辑在上面的那个 if 判断里 439 | // 感觉这里写的不是很好 二者没有归一起来 440 | } 441 | }, 442 | { 443 | scheduler() { 444 | queueJobs(instance.update) 445 | }, 446 | } 447 | ) 448 | } 449 | 450 | function updateComponentPreRender(instance, nextVNode) { 451 | // 传递 props 452 | instance.props = nextVNode.props 453 | // 更新 instance 中的 vnode 454 | instance.vnode = nextVNode 455 | nextVNode = null 456 | } 457 | 458 | return { 459 | createApp: createAppAPI(render), 460 | } 461 | } 462 | 463 | // 注意 arrI 的 edge case: 464 | // [2,0,1,3,4,5] 的 LIS index 是 [2,3,4,5] 465 | function getSequence(arr: number[]): number[] { 466 | const p = arr.slice() 467 | const result = [0] 468 | let i, j, u, v, c 469 | const len = arr.length 470 | for (i = 0; i < len; i++) { 471 | const arrI = arr[i] 472 | if (arrI !== 0) { 473 | j = result[result.length - 1] 474 | if (arr[j] < arrI) { 475 | p[i] = j 476 | result.push(i) 477 | continue 478 | } 479 | u = 0 480 | v = result.length - 1 481 | while (u < v) { 482 | c = (u + v) >> 1 483 | if (arr[result[c]] < arrI) { 484 | u = c + 1 485 | } else { 486 | v = c 487 | } 488 | } 489 | if (arrI < arr[result[u]]) { 490 | if (u > 0) { 491 | p[i] = result[u - 1] 492 | } 493 | result[u] = i 494 | } 495 | } 496 | } 497 | u = result.length 498 | v = result[u - 1] 499 | while (u-- > 0) { 500 | result[u] = v 501 | v = p[v] 502 | } 503 | return result 504 | } 505 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = [] 2 | let isFlushPending = false 3 | const p = Promise.resolve() 4 | export function nextTick(fn?) { 5 | return fn ? p.then(fn) : p 6 | } 7 | 8 | export function queueJobs(job) { 9 | if (!queue.includes(job)) { 10 | queue.push(job) 11 | } 12 | queueFlush() 13 | } 14 | 15 | function queueFlush() { 16 | if (isFlushPending) return 17 | isFlushPending = true 18 | Promise.resolve().then(() => { 19 | // 如果在这里有 log 的话会发现 then 执行了 循环的次数 20 | // 是因为微任务队列塞进了 循环次数 的 promise 21 | // 第一次 queue 有内容, 但是后面的 queue 是空 22 | // 所以创建如此多的 promise 是没有必要的 23 | 24 | // 开关重新初始化 25 | isFlushPending = false 26 | let job 27 | while ((job = queue.shift())) { 28 | job && job() 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isObject, isString, ShapeFlags } from '../shared' 2 | export const Fragment = Symbol('Fragment') 3 | export const Text = Symbol('Text') 4 | export { createVNode as createElementVNode } 5 | export function createVNode(type, props?, children?) { 6 | const vnode = { 7 | type, 8 | props, 9 | component: null, 10 | key: props && props.key, 11 | children, 12 | shapeFlag: getShapeFlag(type), 13 | } 14 | 15 | if (isString(children)) { 16 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 17 | } else if (isArray(children)) { 18 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN 19 | } 20 | 21 | // 如何判断是一个 slots 22 | // vnode是一个组件 且 children 是 object 23 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 24 | if (isObject(vnode.children)) { 25 | vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN 26 | } 27 | } 28 | return vnode 29 | } 30 | function getShapeFlag(type: any) { 31 | return isString(type) ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT 32 | } 33 | 34 | export function createTextVNode(text: string) { 35 | return createVNode(Text, {}, text) 36 | } 37 | 38 | export function isSameVNodeType(n1, n2) { 39 | return n1.type === n2.type && n1.key === n2.key 40 | } 41 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '../runtime-core' 2 | import { isOn } from '../shared' 3 | 4 | function createElement(type) { 5 | return document.createElement(type) 6 | } 7 | 8 | function patchProp(el, key, prevVal, nextVal) { 9 | if (isOn(key)) { 10 | const event = key.substring(2).toLowerCase() 11 | el.addEventListener(event, nextVal) 12 | } else if (nextVal === undefined || nextVal === null) { 13 | el.removeAttribute(key) 14 | } else { 15 | el.setAttribute(key, nextVal) 16 | } 17 | } 18 | 19 | function insert(el, parent, anchor) { 20 | parent.insertBefore(el, anchor || null) 21 | } 22 | 23 | function remove(el) { 24 | const parentNode = el.parentNode 25 | if (parentNode) { 26 | parentNode.removeChild(el) 27 | } 28 | } 29 | 30 | function setElementText(container, children) { 31 | // XXX: textContent v. innerText 32 | container.textContent = children 33 | } 34 | 35 | const renderer: any = createRenderer({ 36 | createElement, 37 | patchProp, 38 | insert, 39 | remove, 40 | setElementText, 41 | }) 42 | 43 | export function createApp(...args) { 44 | return renderer.createApp(...args) 45 | } 46 | 47 | export * from '../runtime-core' 48 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toDisplayString' 2 | export const NOOP = () => {} 3 | export const extend = Object.assign 4 | export const EMPTY_OBJ = {} 5 | export const EMPTY_ARR = [] 6 | 7 | export const isObject = (val) => val !== null && typeof val === 'object' 8 | export const isFunction = (val) => typeof val === 'function' 9 | export const isString = (val) => typeof val === 'string' 10 | export const isArray = (val) => Array.isArray(val) 11 | export const isOn = (val) => /^on[A-Z]/.test(val) 12 | 13 | // NaN 算作变更 14 | export const hasChanged = (newVal, oldVal) => !Object.is(newVal, oldVal) 15 | 16 | // tips: in vs. hasOwnProperty 17 | // | in | hasOwnProperty 18 | // Symbol | yes | yes 19 | // inherited properties | yes | no 20 | // ES6 getters/setters | yes | no 21 | export const hasOwn = (val, key) => 22 | Object.prototype.hasOwnProperty.call(val, key) 23 | 24 | export const enum ShapeFlags { 25 | ELEMENT = 1, 26 | FUNCTIONAL_COMPONENT = 1 << 1, 27 | STATEFUL_COMPONENT = 1 << 2, 28 | TEXT_CHILDREN = 1 << 3, 29 | ARRAY_CHILDREN = 1 << 4, 30 | SLOTS_CHILDREN = 1 << 5, 31 | TELEPORT = 1 << 6, 32 | SUSPENSE = 1 << 7, 33 | COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, 34 | COMPONENT_KEPT_ALIVE = 1 << 9, 35 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, 36 | } 37 | 38 | export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1) 39 | -------------------------------------------------------------------------------- /src/shared/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(value) { 2 | return String(value) 3 | } 4 | -------------------------------------------------------------------------------- /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": [], /* 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": false, /* 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 | --------------------------------------------------------------------------------