├── .gitignore ├── @vue └── reactivity.js ├── README.md ├── examples ├── 1.1 Tree-Shaking │ ├── bundle.js │ ├── input.js │ ├── package.json │ ├── pnpm-lock.yaml │ └── utils.js ├── 10 快速 Diff 算法 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── index.html ├── 11 组件的实现原理 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── index.html ├── 12 异步组件与函数式组件 │ └── 1.js ├── 13 内建组件和模块 │ ├── 1.js │ ├── 2.js │ └── 3.js ├── 14 编译器核心技术概览 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── index.html ├── 15 解析器 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── index.html ├── 16 同构渲染 │ ├── 1.js │ ├── 2.js │ ├── ClientOnly.js │ └── index.html ├── 16 编译优化 │ └── 1.js ├── 2.2 初识渲染器 │ ├── 1.html │ ├── 2.html │ └── 3.html ├── 3 响应式系统的实现 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ ├── 4.js │ ├── 5.js │ ├── 6.js │ ├── 7.js │ ├── 8.js │ ├── 9.js │ └── index.html ├── 4 非原始值的响应式方案 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ ├── 4.js │ ├── 5.js │ ├── 6.js │ ├── 7.js │ ├── 8.js │ └── index.html ├── 5 原始值的响应式方案 │ ├── 1.js │ └── index.html ├── 6 渲染器的设计 │ ├── 1.js │ └── index.html ├── 7 挂载与更新 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ ├── 4.js │ ├── 5.js │ ├── 6.js │ ├── 7.js │ └── index.html ├── 8 简单的 Diff 算法 │ ├── 1.js │ └── index.html └── 9 双端 Diff 算法 │ ├── 1.js │ ├── 2.js │ ├── 3.js │ ├── 4.js │ ├── 5.js │ └── index.html ├── imgs ├── DOM properties.png ├── keepAlive.png ├── patchElement.png ├── target-key-effect.png ├── transition.png ├── 事件冒泡-1.png ├── 事件冒泡-2.png ├── 双端diff │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 20.png │ ├── 21.png │ ├── 22.png │ ├── 23.png │ ├── 24.png │ ├── 25.png │ ├── 26.png │ ├── 27.png │ ├── 28.png │ ├── 29.png │ ├── 3.png │ ├── 30.png │ ├── 31.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── 同构渲染 │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png ├── 快速diff │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 20.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── 由内向外的执行方式.png ├── 简单diff │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── 编译器核心 │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 20.png │ ├── 21.png │ ├── 22.png │ ├── 23.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png └── 解析器 │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png └── notes ├── 1.框架设计的核心要素.md ├── 10.快速 Diff 算法.md ├── 11.组件的实现原理.md ├── 12.异步组件和函数式组件.md ├── 13.内建组件和模块.md ├── 14.编译器核心技术概览.md ├── 15.解析器.md ├── 16.编译优化.md ├── 17.同构渲染.md ├── 2.Vue.js 3 的设计思路.md ├── 3.响应系统的作用与实现.md ├── 4.非原始值的响应式方案.md ├── 5.原始值的响应方案.md ├── 6.渲染器的设计.md ├── 7.挂载与更新.md ├── 8.简单的 Diff 算法.md └── 9.双端 Diff 算法.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《Vue.js 设计与实现》阅读笔记 2 | 3 | 本仓库是在阅读《Vue.js 设计与实现》后写下的笔记。 4 | 5 | ## TOC 6 | 7 | 1. [框架设计的核心要素](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/1.%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%A0%B8%E5%BF%83%E8%A6%81%E7%B4%A0.md) 8 | 9 | 2. [Vue.js 3 的设计思路](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/2.Vue.js%203%20%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF.md) 10 | 11 | 3. [响应系统的作用与实现](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/3.%E5%93%8D%E5%BA%94%E7%B3%BB%E7%BB%9F%E7%9A%84%E4%BD%9C%E7%94%A8%E4%B8%8E%E5%AE%9E%E7%8E%B0.md) 12 | 13 | 4. [非原始值的响应式方案](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/4.%E9%9D%9E%E5%8E%9F%E5%A7%8B%E5%80%BC%E7%9A%84%E5%93%8D%E5%BA%94%E5%BC%8F%E6%96%B9%E6%A1%88.md) 14 | 15 | 5. [原始值的响应式方案](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/5.%E5%8E%9F%E5%A7%8B%E5%80%BC%E7%9A%84%E5%93%8D%E5%BA%94%E6%96%B9%E6%A1%88.md) 16 | 17 | 6. [渲染器的设计](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/6.%E6%B8%B2%E6%9F%93%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1.md) 18 | 19 | 7. [挂载与更新](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/7.%E6%8C%82%E8%BD%BD%E4%B8%8E%E6%9B%B4%E6%96%B0.md) 20 | 21 | 8. [简单的 Diff 算法](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/8.%E7%AE%80%E5%8D%95%E7%9A%84%20Diff%20%E7%AE%97%E6%B3%95.md) 22 | 23 | 9. [双端 Diff 算法](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/9.%E5%8F%8C%E7%AB%AF%20Diff%20%E7%AE%97%E6%B3%95.md) 24 | 25 | 10. [快速 Diff 算法](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/10.%E5%BF%AB%E9%80%9F%20Diff%20%E7%AE%97%E6%B3%95.md) 26 | 27 | 11. [组件的实现原理](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/11.%E7%BB%84%E4%BB%B6%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.md) 28 | 29 | 12. [异步组件和函数式组件](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/12.%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6%E5%92%8C%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6.md) 30 | 31 | 13. [内建组件和模块](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/13.%E5%86%85%E5%BB%BA%E7%BB%84%E4%BB%B6%E5%92%8C%E6%A8%A1%E5%9D%97.md) 32 | 33 | 14. [编译器核心技术概览](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/14.%E7%BC%96%E8%AF%91%E5%99%A8%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%A6%82%E8%A7%88.md) 34 | 35 | 15. [解析器](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/15.%E8%A7%A3%E6%9E%90%E5%99%A8.md) 36 | 37 | 16. [编译优化](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/16.%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96.md) 38 | 39 | 17. [同构渲染](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/17.%E5%90%8C%E6%9E%84%E6%B8%B2%E6%9F%93.md) 40 | -------------------------------------------------------------------------------- /examples/1.1 Tree-Shaking/bundle.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/1.1 Tree-Shaking/input.js: -------------------------------------------------------------------------------- 1 | import { foo } from './utils' 2 | 3 | /*#__PURE__*/ foo() 4 | -------------------------------------------------------------------------------- /examples/1.1 Tree-Shaking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1.1-tree-shaking", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "rollup": "^2.75.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/1.1 Tree-Shaking/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | rollup: ^2.75.7 5 | 6 | devDependencies: 7 | rollup: registry.npmmirror.com/rollup/2.75.7 8 | 9 | packages: 10 | 11 | registry.npmmirror.com/fsevents/2.3.2: 12 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz} 13 | name: fsevents 14 | version: 2.3.2 15 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 16 | os: [darwin] 17 | requiresBuild: true 18 | dev: true 19 | optional: true 20 | 21 | registry.npmmirror.com/rollup/2.75.7: 22 | resolution: {integrity: sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/rollup/-/rollup-2.75.7.tgz} 23 | name: rollup 24 | version: 2.75.7 25 | engines: {node: '>=10.0.0'} 26 | hasBin: true 27 | optionalDependencies: 28 | fsevents: registry.npmmirror.com/fsevents/2.3.2 29 | dev: true 30 | -------------------------------------------------------------------------------- /examples/1.1 Tree-Shaking/utils.js: -------------------------------------------------------------------------------- 1 | export function foo (obj) { 2 | obj && obj.foo 3 | } 4 | 5 | export function bar () { 6 | obj && obj.bar 7 | } 8 | -------------------------------------------------------------------------------- /examples/10 快速 Diff 算法/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 快速 Diff 算法 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 49 | 50 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/11 组件的实现原理/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 组件的实现原理 8 | 9 | 10 |
11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /examples/14 编译器核心技术概览/1.js: -------------------------------------------------------------------------------- 1 | // 定义状态机的状态 2 | const State = { 3 | initial: 1, // 初始状态 4 | tagOpen: 2, // 标签开始状态 5 | tagName: 3, // 标签名称状态 6 | text: 4, // 文本状态 7 | tagEnd: 5, // 结束标签状态 8 | tagEndName: 6 // 结束标签名称状态 9 | } 10 | 11 | // 辅助函数,用于判断是否是字符 12 | const isAlpha = char => char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' 13 | 14 | // 辅助函数,用于打印当前 AST 中节点的信息 15 | const dump = (node, indent = 0) => { 16 | // 节点的类型 17 | const { type } = node 18 | // 节点的描述,如果是根节点,则没有描述 19 | // 如果是 Element 类型的节点,则使用 node.tag 作为节点的描述 20 | // 如果是 Text 类型的节点,则使用 node.content 作为节点的描述 21 | const desc = node.type === 'Root' 22 | ? '' 23 | : node.type === 'Element' 24 | ? node.tag 25 | : node.content 26 | 27 | // 打印节点的类型和描述信息 28 | console.log(`${'-'.repeat(indent)}${type}: ${desc}`) 29 | 30 | // 递归地打印子节点 31 | if (node.children) { 32 | node.children.forEach(n => dump(n, indent + 2)) 33 | } 34 | } 35 | 36 | // 转换标签节点 37 | const tranformElement = node => { 38 | if (node.type === 'Element' && node.tag === 'p') { 39 | node.tag = 'h1' 40 | } 41 | } 42 | // 转换文本节点 43 | const tranformText = (node, context) => { 44 | if (node.type === 'Text') { 45 | // 移除文本节点 46 | context.removeNode() 47 | } 48 | } 49 | 50 | // 接收模板字符串作为参数,并将模板切割为 Token 返回 51 | function tokenize (str) { 52 | // 状态机的当前状态:初始状态 53 | let currentState = State.initial 54 | // 用于缓存字符 55 | const chars = [] 56 | // 生成的 Token 会存储到 tokens 数组中,并作为函数的返回值返回 57 | const tokens = [] 58 | 59 | // 使用 while 循环开启自动机,只要模板字符串没有被消费完,自动机就会一直运行 60 | while (str) { 61 | // 查看第一个字符,注意,这里只是查看,没有消费该字符 62 | const char = str[0] 63 | 64 | switch (currentState) { 65 | // 状态机当前处于初始状态 66 | case State.initial: 67 | // 遇到字符 '<' 68 | if (char === '<') { 69 | // 1. 状态机切换到标签开始状态 70 | currentState = State.tagOpen 71 | // 2. 消费字符 '<' 72 | str = str.slice(1) 73 | } else if (isAlpha(char)) { 74 | // 1. 遇到字母,切换到文本状态 75 | currentState = State.text 76 | // 2. 将当前字母缓存到 chars 数组 77 | chars.push(char) 78 | // 3. 消费当前字符 79 | str = str.slice(1) 80 | } 81 | break 82 | // 状态机当前处于标签开始状态 83 | case State.tagOpen: 84 | if (isAlpha(char)) { 85 | // 1. 遇到字母,切换到标签名称状态 86 | currentState = State.tagName 87 | // 2. 将当前字母缓存到 chars 数组 88 | chars.push(char) 89 | // 3. 消费当前字符 90 | str = str.slice(1) 91 | } else if (char === '/') { 92 | // 1. 遇到字符 /,切换到结束标签状态 93 | currentState = State.tagEnd 94 | // 2. 消费字符 / 95 | str = str.slice(1) 96 | } 97 | break 98 | // 状态机当前处于标签名称状态 99 | case State.tagName: 100 | if (isAlpha(char)) { 101 | // 1. 遇到字母,由于当前处理标签名称状态,所以不需要切换状态, 102 | // 但需要将当前字符缓存到 chars 数组中。 103 | chars.push(char) 104 | // 2. 消费当前字符 105 | str = str.slice(1) 106 | } else if (char === '>') { 107 | // 1. 遇到字符 '>',切换到初始状态 108 | currentState = State.initial 109 | // 2. 同时创建一个标签 Token,并添加到 tokens 数组中 110 | // 注意,此时 chars 中的字符就是标签名称 111 | tokens.push({ 112 | type: 'tag', 113 | name: chars.join('') 114 | }) 115 | // 3. chars 数组的内容已经被消费,清空它 116 | chars.length = 0 117 | // 4. 同时消费当前字符 '>' 118 | str = str.slice(1) 119 | } 120 | break 121 | // 状态机当前处于文本状态 122 | case State.text: 123 | if (isAlpha(char)) { 124 | // 1. 遇到字母,保持状态不变,但应该将当前字符缓存到 chars 数组中 125 | chars.push(char) 126 | // 2. 消费当前字符 127 | str = str.slice(1) 128 | } else if (char === '<') { 129 | // 1. 遇到字符 '<',切换到标签开始状态 130 | currentState = State.tagOpen 131 | // 2. 从 文本状态 ---> 标签开始状态,此时应该创建文本 Token,并添加到 tokens 数组中 132 | // 注意,此时 chars 数组中的字符就是文本内容 133 | tokens.push({ 134 | type: 'text', 135 | content: chars.join('') 136 | }) 137 | // 3. chars 数组的内容已经被消费,清空它 138 | chars.length = 0 139 | // 4. 同时消费当前字符 '<' 140 | str = str.slice(1) 141 | } 142 | break 143 | // 状态机处于标签结束状态 144 | case State.tagEnd: 145 | if (isAlpha(char)) { 146 | // 1. 遇到字母,切换到结束标签名称状态 147 | currentState = State.tagEndName 148 | // 2. 将当前字符缓存到 chars 数组中 149 | chars.push(char) 150 | // 3. 消费当前字符 151 | str = str.slice(1) 152 | } 153 | break 154 | // 状态机当前牌结束标签名称状态 155 | case State.tagEndName: 156 | if (isAlpha(char)) { 157 | // 1. 遇到字母,不需要切换状态,但需要将当前字符缓存到 chars 数组中 158 | chars.push(char) 159 | // 2. 消费当前字符 160 | str = str.slice(1) 161 | } else if (char === '>') { 162 | // 1. 遇到字符 '>',切换到初始状态 163 | currentState = State.initial 164 | // 2. 从 结束标签名称状态 ---> 初始状态,应该保存结束标签名称 Token 165 | // 注意,此时 chars 数组中缓存的内容就是标签名称 166 | tokens.push({ 167 | type: 'tagEnd', 168 | name: chars.join('') 169 | }) 170 | // 3. chars 数组的内容已经被消费,清空它 171 | chars.length = 0 172 | // 4. 消费当前字符 173 | str = str.slice(1) 174 | } 175 | break 176 | default: 177 | break 178 | } 179 | } 180 | 181 | // 最后,返回 tokens 182 | return tokens 183 | } 184 | 185 | function parse (str) { 186 | // 获取 tokens 187 | const tokens = tokenize(str) 188 | // 创建 Root 根节点 189 | const root = { 190 | type: 'Root', 191 | children: [] 192 | } 193 | // 创建 elementStack 栈,起初只有 Root 根节点 194 | const elementStack = [root] 195 | 196 | // 开启一个 while 循环扫描 tokens,直到所有 Token 都被扫描完毕为止 197 | while (tokens.length) { 198 | // 获取当前栈顶节点作为父节点 199 | const parent = elementStack[elementStack.length - 1] 200 | // 当前扫描到的 Token 201 | const t = tokens[0] 202 | 203 | switch (t.type) { 204 | case 'tag': 205 | // 如果当前 Token 是开始标签,则创建 Element 类型的 AST 节点 206 | const elementNode = { 207 | type: 'Element', 208 | tag: t.name, 209 | children: [] 210 | } 211 | // 将其添加到父节点的 children 中 212 | parent.children.push(elementNode) 213 | // 将当前节点压入栈 214 | elementStack.push(elementNode) 215 | break 216 | case 'text': 217 | // 如果当前 Token 是文本,则创建 Text 类型的 AST 节点 218 | const textNode = { 219 | type: 'Text', 220 | content: t.content 221 | } 222 | // 将其添加到父节点的 children 中 223 | parent.children.push(textNode) 224 | break 225 | case 'tagEnd': 226 | // 遇到结束标签,将栈顶节点弹出 227 | elementStack.pop() 228 | break 229 | default: 230 | break 231 | } 232 | 233 | // 消费已经扫描过的 token 234 | tokens.shift() 235 | } 236 | 237 | // 最后返回 AST 238 | return root 239 | } 240 | 241 | function traverseNode (ast, context) { 242 | context.currentNode = ast 243 | 244 | // context.nodeTransforms 是一个数组,其中每一个元素都是一个函数 245 | const transforms = context.nodeTransforms 246 | for (let i = 0; i < transforms.length; i++) { 247 | // 将当前节点和 context 都传递给回调函数 248 | transforms[i](context.currentNode, context) 249 | // 由于任何转换函数都可能移除当前节点,因此每个转换函数执行完毕后 250 | // 都应该检查当前节点是否已经被移除,如果被移除了,直接返回即可 251 | if (!context.currentNode) return 252 | } 253 | 254 | // 如果有子节点,则递归调用 traverseNode 函数进行遍历 255 | const { children } = context.currentNode 256 | if (children) { 257 | for (let i = 0; i < children.length; i++) { 258 | // 递归之前,将当前节点设置为父节点 259 | context.parent = context.currentNode 260 | // 设置位置索引 261 | context.childIndex = i 262 | // 递归调用时,将 context 透传 263 | traverseNode(children[i], context) 264 | } 265 | } 266 | } 267 | 268 | // transform 函数用来对 AST 进行转换 269 | function transform (ast) { 270 | // 在 transform 函数内创建 context 对象 271 | const context = { 272 | currentNode: null, // 当前正在转换的节点 273 | childIndex: 0, // 当前节点在父节点的 children 中的位置索引 274 | parent: null, // 用来存储当前转换节点的父节点 275 | 276 | // 用于替换节点的函数,接收新节点作为参数 277 | replaceNode (node) { 278 | // 为了替换节点,我们需要修改 AST 279 | // 找到当前节点在父节点的 children 中的位置 280 | // 然后使用新节点替换即可 281 | context.parent.children[context.childIndex] = node 282 | // 由于当前节点已经被新节点替换掉了,因此我们需要将 currentNode 更新为新节点 283 | context.currentNode = node 284 | }, 285 | 286 | // 用于删除当前节点 287 | removeNode () { 288 | if (context.parent) { 289 | // 调用数组的 splice 方法,根据当前节点的索引删除当前节点 290 | context.parent.children.splice(context.childIndex, 1) 291 | // 将 context.currentNode 置空 292 | context.currentNode = null 293 | } 294 | }, 295 | 296 | // 注册 nodeTransforms 数组 297 | nodeTransforms: [ 298 | tranformElement, // transformElement 函数用来转换标签节点 299 | tranformText // transformText 函数用来转换文本节点 300 | ] 301 | } 302 | 303 | // 调用 traverseNode 完成转换 304 | traverseNode(ast, context) 305 | // 打印 AST 信息 306 | dump(ast) 307 | } 308 | -------------------------------------------------------------------------------- /examples/14 编译器核心技术概览/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 57 | 58 | -------------------------------------------------------------------------------- /examples/15 解析器/1.js: -------------------------------------------------------------------------------- 1 | // 定义文本模式,作为一个状态表 2 | const TextModes = { 3 | /** 能解析标签,支持 HTML 实体 */ 4 | DATA: 'DATA', 5 | /** 不能解析标签,支持 HTML 实体 */ 6 | RCDATA: 'RCDATA', 7 | /** 不能解析标签,不支持 HTML 实体 */ 8 | RAWTEXT: 'RAWTEXT', 9 | /** 不能解析标签,不支持 HTML 实体 */ 10 | CDATA: 'CDATA' 11 | } 12 | 13 | // 解析器函数,接收模板作为参数 14 | function parse (str) { 15 | // 定义上下文对象 16 | const context = { 17 | // source 是模板内容,用于在解析过程中进行消费 18 | source: str, 19 | // 解析器当前处于的文本模式,初始模式为 DATA 20 | mode: TextModes.DATA, 21 | // advanceBy 函数用来消费指定数量的字符,它接收一个数字作为参数 22 | advanceBy (num) { 23 | context.source = context.source.slice(num) 24 | }, 25 | // 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如
26 | advanceSpaces () { 27 | // 匹配空白字符 28 | const match = /^[\t\r\n\f ]+/.exec(context.source) 29 | if (match) { 30 | // 调用 advanceBy 函数消费空白字符 31 | context.advanceBy(match[0].length) 32 | } 33 | } 34 | } 35 | 36 | // 调用 parseChildren 函数开始进行解析,它返回解析后得到的子节点 37 | // parseChildren 函数接收两个参数: 38 | // 1. 上下文对象 context 39 | // 2. 由父节点构成的代码栈,初始时栈为空 40 | const nodes = parseChildren(context, []) 41 | 42 | // 解析器返回 Root 根节点 43 | return { 44 | type: 'Root', 45 | // 使用 nodes 作为根节点的 children 46 | children: nodes 47 | } 48 | } 49 | 50 | function parseChildren (context, ancestors) { 51 | // 定义 nodes 数组存储子节点,它将作为最终的返回值 52 | let nodes = [] 53 | // 从上下文对象中取得当前状态,包括模式 mode 和模板内容 source 54 | const { mode, source } = context 55 | 56 | // 开启 while 循环,只要满足条件就会一直对字符串进行解析 57 | // 关于 isEnd() 后文会详细讲解 58 | while (!isEnd(context, ancestors)) { 59 | let node 60 | // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析 61 | if (mode === TextModes.DATA || mode === TextModes.RCDATA) { 62 | // 只有 DATA 模式才支持标签节点的解析 63 | if (mode === TextModes.DATA && source[0] === '<') { 64 | if (source[1] === '!') { 65 | if (source.starsWith(' 24 |

{{ foo.bar }}

25 |
26 | `) 27 | console.log(ast) 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/16 同构渲染/1.js: -------------------------------------------------------------------------------- 1 | const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr' 2 | 3 | const VNODE_TYPES = { 4 | Text: Symbol(), 5 | Comment: Symbol(), 6 | Fragment: Symbol() 7 | } 8 | 9 | const shouldIgnoreProp = ['key', 'ref'] 10 | 11 | const escapeRE = /["'&<>]/ 12 | 13 | // 判断属性是否是 boolean attribute 14 | const isBooleanAttr = key => ( 15 | 'itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly' + 16 | ',async,autofocus,autoplay,controls,default,defer,disabled,hidden' + 17 | 'loop,open,required,reversed,scoped,seamless,' + 18 | 'checked,muted,multiple,selected' 19 | ).split(',').includes(key) 20 | 21 | // 判断属性名称是否合法且安全 22 | const isSSRSafeAttrName = key => /[>/="'\u0009\u000a\u000c\u0020]/.test(key) 23 | 24 | function renderElementVNode (vnode) { 25 | // 取出标签名称 tag 和标签属性 props,以及标签的子节点 26 | const { type: tag, props, children } = vnode 27 | 28 | // 判断是否是 void element 29 | const isVoidElement = VOID_TAGS.includes(tag) 30 | 31 | // 开始标签的头部 32 | let ret = `<${tag}` 33 | 34 | // 处理标签属性 35 | if (props) { 36 | // 调用 renderAttrs 函数进行严谨处理 37 | ret += renderAttrs(props) 38 | } 39 | 40 | // 如果是 void element,则自闭合 41 | ret += isVoidElement ? '/>' : '>' 42 | // void element 直接返回结果,因为它没有 children 43 | if (isVoidElement) return ret 44 | 45 | // 处理子节点 46 | // 如果子节点是字符串类型,则是文本内容,直接拼接 47 | if (typeof children === 'string') { 48 | ret += children 49 | } else if (Array.isArray(children)) { 50 | children.forEach(child => { 51 | ret += renderElementVNode(child) 52 | }) 53 | } 54 | 55 | // 结束标签 56 | ret += `` 57 | 58 | return ret 59 | } 60 | 61 | function renderAttrs (props) { 62 | let ref = '' 63 | for (const key in props) { 64 | if ( 65 | // 检测属性名称,如果是事件或应该被忽略的属性,则忽略它 66 | shouldIgnoreProp.includes(key) || 67 | /^on[^a-z]/.test(key) 68 | ) { 69 | continue 70 | } 71 | 72 | const value = props[key] 73 | // 调用 renderDynamicAttr 完成属性的渲染 74 | ret += renderDynamicAttr(key, value) 75 | } 76 | } 77 | 78 | function renderDynamicAttr (key, value) { 79 | if (isBooleanAttr(key)) { 80 | // boolean attribute,如果值为 false 则无须渲染内容,否则只需要渲染 key 即可 81 | return value === false ? '' : `${key}` 82 | } else if (isSSRSafeAttrName(key)) { 83 | // 对于其他安全的属性,执行完整的渲染 84 | // 注意:对于属性值,需要对它执行 HTML 转义操作 85 | return value === '' ? ` ${key}` : ` ${key}="${escapeHtml(value)}"` 86 | } else { 87 | // 跳过不安全的属性,并打印警告信息 88 | console.warn( 89 | `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${key}` 90 | ) 91 | return '' 92 | } 93 | } 94 | 95 | function escapeHtml (string) { 96 | const str = '' + string 97 | const match = escapeRE.exec(str) 98 | 99 | if (!match) return str 100 | 101 | let html = '' 102 | let escaped 103 | let index 104 | let lastIndex = 0 105 | 106 | for (index = match.index; index < str.length; index++) { 107 | switch (str.charCodeAt(index)) { 108 | case 34: // " 109 | escaped = '"' 110 | break 111 | case 38: // & 112 | escaped = '&' 113 | break 114 | case 39: // ' 115 | escaped = ''' 116 | break 117 | case 60: // < 118 | escaped = '<' 119 | break 120 | case 62: // > 121 | escaped = '>' 122 | break 123 | default: 124 | continue 125 | } 126 | 127 | if (lastIndex !== index) { 128 | html += str.substring(lastIndex, index) 129 | } 130 | 131 | lastIndex = index + 1 132 | html += escaped 133 | } 134 | 135 | return lastIndex !== index 136 | ? html + str.substring(lastIndex, index) 137 | : html 138 | } 139 | 140 | function renderComponentVNode (vnode) { 141 | const isFunctional = typeof vnode.type === 'function' 142 | let componentOptions = vnode.type 143 | if (isFunctional) { 144 | componentOptions = { 145 | render: vnode.type, 146 | props: vnode.type.props 147 | } 148 | } 149 | 150 | let { 151 | render, 152 | data, 153 | setup, 154 | beforeCreate, 155 | created, 156 | props: propsOption 157 | } = componentOptions 158 | 159 | beforeCreate && beforeCreate() 160 | 161 | // 无须使用 reactive() 创建 data 的响应式版本 162 | const state = data ? data() : null 163 | const [props, attrs] = resolveProps(propsOption, vnode.props) 164 | 165 | const slots = vnode.children || [] 166 | 167 | const instance = { 168 | state, 169 | props, // props 无须 shallowReactive 170 | isMounted: false, 171 | subTree: null, 172 | slots, 173 | mounted: [], 174 | keepAliveCtx: null 175 | } 176 | 177 | function emit(event, ...payload) { 178 | const eventName = `on${event[0].toUpperCase() + event.slice(1)}` 179 | const handler = instance.props[eventName] 180 | if (handler) { 181 | handler(...payload) 182 | } else { 183 | console.error('事件不存在') 184 | } 185 | } 186 | 187 | // setup 188 | let setupState = null 189 | if (setup) { 190 | const setupContext = { attrs, emit, slots } 191 | const prevInstance = setCurrentInstance(instance) 192 | const setupResult = setup(shalloReadonly(instance.props), setupContext) 193 | setCurrentInstance(prevInstance) 194 | if (typeof setupResult === 'function') { 195 | if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略') 196 | render = setupResult 197 | } else { 198 | setupState = setupResult 199 | } 200 | } 201 | 202 | vnode.component = instance 203 | 204 | const renderContext = new Proxy(instance, { 205 | get (t, k, r) { 206 | const { state, props, slots } = t 207 | 208 | if (k === '$slots') return slots 209 | 210 | if (state && k in state) { 211 | return state[k] 212 | } else if (k in props) { 213 | return props[k] 214 | } else if (setupState && k in setupState) { 215 | return setupState[k] 216 | } else { 217 | console.error('不存在') 218 | } 219 | }, 220 | 221 | set (t, k, v, r) { 222 | const { state, props } = t 223 | if (state && k in state) { 224 | state[k] = v 225 | } else if (k in props) { 226 | props[k] = v 227 | } else if (setupState && k in setupState) { 228 | setupState[k] = v 229 | } else { 230 | console.error('不存在') 231 | } 232 | } 233 | }) 234 | 235 | created && created(renderContext) 236 | 237 | const subTree = render.call(renderContext, renderContext) 238 | 239 | return renderVNode(subTree) 240 | } 241 | 242 | function renderVNode (vnode) { 243 | const type = typeof vnode.type 244 | switch (type) { 245 | case 'string': 246 | return renderElementVNode(vnode) 247 | case 'object': 248 | case 'function': 249 | return renderComponentVNode(vnode) 250 | default: 251 | break 252 | } 253 | 254 | switch (vnode.type) { 255 | case VNODE_TYPES.Text: 256 | // 处理文本 ... 257 | break 258 | case VNODE_TYPES.Fragment: 259 | // 处理片段 260 | break 261 | // ... 262 | default: 263 | break 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /examples/16 同构渲染/2.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | function hydrate (vnode, container) { 3 | hydrateNode(container.firstChild, vnode) 4 | } 5 | 6 | function hydrateNode (node, vnode) { 7 | const { type } = vnode 8 | // 1. 让 vnode.el 引用真实 DOM 9 | vnode.el = node 10 | 11 | // 2. 检查虚拟 DOM 的类型,如果是组件,则调用 mountComponent 函数完成激活 12 | if (typeof type === 'object') { 13 | mountComponent(vnode, node.parentNode, null) 14 | } else if (typeof type === 'string') { 15 | // 3. 检查真实 DOM 的类型与虚拟 DOM 的类型是否匹配 16 | if (node.nodeType !== 1) { 17 | console.error('mismatch') 18 | console.error('服务端渲染的真实 DOM 节点是:', node) 19 | console.error('客户端渲染的虚拟 DOM 节点是:', vnode) 20 | } else { 21 | // 4. 如果是普通元素,则调用 hydrateElement 完成激活 22 | hydrateElement(node, vnode) 23 | } 24 | } 25 | 26 | // 5. 重要:hydrateNode 函数需要返回当前节点的下一个兄弟节点,以便继续进行后续的激活操作 27 | return node.nextSibling 28 | } 29 | 30 | function hydrateElement (el, vnode) { 31 | // 1. 为 DOM 元素添加事件 32 | if (vnode.props) { 33 | for (const key in vnode.props) { 34 | // 只有事件类型的 props 需要处理 35 | if (/^on/.test(key)) { 36 | patchProps(el, key, null, vnode.props[key]) 37 | } 38 | } 39 | } 40 | 41 | // 2. 递归地激活子节点 42 | if (Array.isArray(vnode.children)) { 43 | let nextNode = el.firstChild 44 | const len = vnode.children.length 45 | 46 | for (let i = 0; i < len; i++) { 47 | // 激活子节点,注意,每当激活一个子节点,hydrateNode 函数会返回当前子节点的下一个兄弟节点 48 | // 于是可以进行后续的激活 49 | nextNode = hydrateNode(nextNode, vnode.children[i]) 50 | } 51 | } 52 | } 53 | 54 | function mountComponent (vnode, container, anchor) { 55 | // 用于检测是否是函数式组件 56 | const isFunctional = typeof vnode.type === 'function' 57 | 58 | let componentOptions = vnode.type 59 | 60 | if (isFunctional) { 61 | // 如果是函数式组件,则将 vnode.type 作为渲染函数 62 | // 将 vnode.type.props 作为 props 选项定义即可 63 | componentOptions = { 64 | render: vnode.type, 65 | props: vnode.type.props 66 | } 67 | } 68 | 69 | const { 70 | data, 71 | beforeCreate, 72 | created, 73 | beforeMount, 74 | mounted, 75 | beforeUpdate, 76 | updated, 77 | props: propsOption, 78 | setup // 取出 setup() 函数 79 | } = componentOptions 80 | 81 | let { render } = componentOptions 82 | 83 | // 在这里调用 beforeCreate() 钩子 84 | beforeCreate && beforeCreate() 85 | 86 | // 调用 data() 函数得到原始数组,并调用 reactive() 函数将其包装成响应式数组 87 | const state = data ? reactive(data()) : null 88 | // 调用 resolveProps() 函数解析出最终的 props 数据与 attrs 数据 89 | const [props, attrs] = resolveProps(propsOption, vnode.props) 90 | 91 | // 直接使用编译好的 vnode.children 对象作为 slots 对象即可 92 | const slots = vnode.children || {} 93 | 94 | // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息 95 | const instance = { 96 | // 组件自身的状态数据,即 data 97 | state, 98 | // 将 props 数据包装为浅响应并定义到组件实例上 99 | props: shallowReactive(props), 100 | // 一个布尔值,表示组件是否已经被挂载 101 | isMounted: false, 102 | // 组件所渲染的内容,即子树 subTree 103 | subTree: null, 104 | // 将插槽添加到组件实例上 105 | slots, 106 | // 在组件实例中添加 mounted 数组,用来存储通过 onMounted() 函数注册的生命周期钩子函数 107 | mounted: [], 108 | // 只有 KeepAlive 组件的实例下会有 keepAliveCtx 属性 109 | keepAliveCtx: null 110 | } 111 | 112 | // 检测当前要挂载的组件是否是 KeepAlive 组件 113 | const isKeepAlive = vnode.type.__isKeepAlive 114 | if (isKeepAlive) { 115 | // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象 116 | instance.keepAliveCtx = { 117 | // move 函数用于移动一段 vnode 118 | move (vnode, container, anchor) { 119 | // 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中 120 | insert(vnode.component.subTree.el, container, anchor) 121 | }, 122 | createElement 123 | } 124 | } 125 | 126 | // 定义 emit() 函数 127 | function emit (event, ...payload) { 128 | // 根据约定对事件名称进行处理,例如 change --> onChange 129 | // event[0] => 'change'[0] => 'c' 130 | const eventName = `on${event[0].toUpperCase() + event.slice(1)}` 131 | // 根据处理后的事件名称去 props 中寻找对应的事件处理函数 132 | const handler = instance.props[eventName] 133 | if (handler) { 134 | handler(...payload) 135 | } else { 136 | console.error('事件不存在') 137 | } 138 | } 139 | 140 | // setupContext 141 | const setupContext = { attrs, emit, slots } 142 | 143 | // 在调用 setup() 函数之前,设置当前组件实例 144 | setCurrentInstance(instance) 145 | 146 | // 调用 setup() 函数,将只读的 props 作为第一个参数传递,避免用户意外地修改 props 的值, 147 | // 将 setupContext 作为第二个参数传递 148 | const setupResult = setup && setup(shallowReadonly(instance.props), setupContext) 149 | 150 | // 在调用 setup() 函数之后,重置当前组件实例 151 | setCurrentInstance(null) 152 | 153 | // setupState 用来存储由 setup() 返回的数据 154 | let setupState = null 155 | 156 | if (typeof setupResult === 'function') { 157 | // 报告冲突 158 | if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略') 159 | // 将 setupResult 作为渲染函数 160 | render = setupResult 161 | } else { 162 | // 如果返回的不是函数,则作为数据状态赋值给 setupState 163 | setupState = setupResult 164 | } 165 | 166 | // 将组件实例设置到 vnode 上,用于后续更新 167 | vnode.component = instance 168 | 169 | // 创建渲染上下文对象,本质上是组件实例的代理 170 | const renderContext = new Proxy(instance, { 171 | get (t, k, r) { 172 | const { state, props, slots } = t 173 | 174 | // 当 k 值为 $slots 时,直接返回 slots 175 | if (k === '$slots') return slots 176 | 177 | // 先尝试读取自身状态数据 178 | if (state && k in state) { 179 | return state[k] 180 | } else if (k in props) { // 如果组件自身没有该数据,则尝试从 props 中读取 181 | return props[k] 182 | } else if (setupState && k in setupState) { 183 | // 渲染上下文需要增加对 setupState 的支持 184 | return setupState[k] 185 | } else { 186 | console.error('不存在') 187 | } 188 | }, 189 | 190 | set (t, k, v, r) { 191 | const { state, props } = t 192 | if (state && k in state) { 193 | state[k] = v 194 | } else if (k in props) { 195 | props[k] = v 196 | } else if (k in setupState) { 197 | // 渲染上下文需要增加对 setupState 的支持 198 | setupState[k] = v 199 | } else { 200 | console.error('不存在') 201 | } 202 | } 203 | }) 204 | 205 | // 在这里调用 created() 钩子 206 | // 生命周期函数调用时需要绑定渲染上下文 207 | created && created.call(renderContext) 208 | 209 | instance.update = effect(() => { 210 | // 调用 render() 函数时,将其 this 设置为 state, 211 | // 从而 render() 函数内部可以通过 this 访问组件自身状态数据 212 | const subTree = render.call(renderContext, renderContext) 213 | 214 | // 检测组件是否已经被挂载 215 | if (!instance.isMounted) { 216 | // 在这里调用 beforeMount() 钩子 217 | beforeMount && beforeMount.call(renderContext) 218 | 219 | // 如果 vnode.el 存在,则意味着要执行激活 220 | if (vnode.el) { 221 | // 直接调用 hydrateNode 完成激活 222 | hydrateNode(vnode.el, subTree) 223 | } else { 224 | // 正常挂载 225 | patch(null, subTree, container, anchor) 226 | } 227 | // 重点:将组件实例上的 isMounted 标记为 true,这样当更新发生时就不会再次进行挂载操作 228 | // 而是执行更新操作 229 | instance.isMounted = true 230 | 231 | // 在这里调用 mounted() 钩子 232 | mounted && mounted.call(renderContext) 233 | // 遍历 instance.mounted 数组,并逐个执行即可 234 | instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext)) 235 | } else { 236 | // 在这里调用 beforeUpdate() 钩子 237 | beforeUpdate && beforeUpdate.call(renderContext) 238 | 239 | // 当 isMounted 为 true 时,说明组件已经被挂载了,只需要完成自更新即可, 240 | // 所以在调用 patch() 函数时,第一个参数为组件上一次渲染的子树, 241 | // 意思是:使用新的子树与上一次渲染的子树进行打补丁操作 242 | patch(vnode.subTree, subTree, container, anchor) 243 | 244 | // 在这里调用 updated() 钩子 245 | updated && updated.call(renderContext) 246 | } 247 | 248 | // 更新组件实例的子树 249 | instance.subTree = subTree 250 | }, { 251 | // 指定该副作用函数的调度器为 queueJob 即可 252 | scheduler: queueJob 253 | }) 254 | } 255 | 256 | return { 257 | hydrate 258 | } 259 | } -------------------------------------------------------------------------------- /examples/16 同构渲染/ClientOnly.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, defineComponent } from 'vue' 2 | 3 | export const ClientOnly = defineComponent({ 4 | setup (_, { slots }) { 5 | // 标记变量,仅在客户端渲染时为 true 6 | const show = ref(false) 7 | // onMounted 钩子只会在客户端执行 8 | onMounted(() => { 9 | show.value = true 10 | }) 11 | 12 | // 在服务端什么都不渲染,在客户端才会渲染其插槽中的内容 13 | return () => ( 14 | show.value && slots.default 15 | ? slots.default() 16 | : null 17 | ) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /examples/16 同构渲染/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My App 8 | 9 | 10 |
11 | 12 | 13 | 26 | 27 | -------------------------------------------------------------------------------- /examples/16 编译优化/1.js: -------------------------------------------------------------------------------- 1 | const PatchFlags = { 2 | TEXT: 1, 3 | CLASS: 2, 4 | STYLE: 3 5 | } 6 | 7 | // 动态节点栈 8 | const dynamicChildrenStack = [] 9 | // 当前动态节点集合 10 | let currentDynamicChildren = null 11 | // 用来创建一个新的动态节点集合,并将该集合压入栈中 12 | function openBlock () { 13 | dynamicChildrenStack.push((currentDynamicChildren = [])) 14 | } 15 | // 用来将通过 openBlock 创建的动态节点集合从栈中弹出 16 | function closeBlock () { 17 | currentDynamicChildren = dynamicChildrenStack.pop() 18 | } 19 | 20 | // render () { 21 | // // 1. 使用 createBlock 代替 createVNode 来创建 block 22 | // // 2. 每当调用 createBlock 之前,先调用 openBlock 23 | // return (openBlock(), createBlock('div', null, [ 24 | // createVNode('p', { class: 'foo' }, null, 1 /* patch flag */), 25 | // createVNode('p', { class: 'bar' }, null) 26 | // ])) 27 | // } 28 | 29 | function createBlock (tag, props, children) { 30 | // block 本质也是一个 vnode 31 | const block = createVNode(tag, props, children) 32 | // 将当前动态节点集合作为 block.dynamicChildren 33 | block.dynamicChildren = currentDynamicChildren 34 | 35 | // 关闭 block 36 | closeBlock() 37 | 38 | return block 39 | } 40 | 41 | function createVNode (tag, props, children, flags) { 42 | const key = props && props.key 43 | props && delete props.key 44 | 45 | const vnode = { 46 | tag, 47 | props, 48 | children, 49 | key, 50 | patchFlags: flags 51 | } 52 | 53 | if (typeof flags !== undefined && currentDynamicChildren) { 54 | // 动态节点,将其添加到当前动态节点集合中 55 | currentDynamicChildren.push(vnode) 56 | } 57 | 58 | return vnode 59 | } 60 | 61 | function patchElement (n1, n2) { 62 | const el = n2.el = n1.el 63 | const oldProps = n1.props 64 | const newProps = n2.props 65 | 66 | 67 | // 第一步:更新 props 68 | for (const key in newProps) { 69 | if (newProps[key] !== oldProps[key]) { 70 | patchProps(el, key, oldProps[key], newProps[key]) 71 | } 72 | } 73 | for (const key in oldProps) { 74 | if (!key in newProps) { 75 | patchProps(el, key, oldProps[key], null) 76 | } 77 | } 78 | 79 | // 第二步:更新 children 80 | if (n2.dynamicChildren) { 81 | // 调用 patchBlockChildren 函数,这样只会更新动态节点 82 | patchBlockChildren(n1, n2) 83 | } else { 84 | patchChildren(n1, n2, el) 85 | } 86 | } 87 | 88 | function patchBlockChildren (n1, n2) { 89 | // 只更新动态节点即可 90 | for (let i = 0; i < n2.dynamicChildren.length; i++) { 91 | patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i]) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/2.2 初识渲染器/1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 初识渲染器 8 | 9 | 10 |
11 | 12 | 50 | 51 | -------------------------------------------------------------------------------- /examples/2.2 初识渲染器/2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 初识渲染器 8 | 9 | 10 |
11 | 12 | 70 | 71 | -------------------------------------------------------------------------------- /examples/2.2 初识渲染器/3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 初识渲染器 8 | 9 | 10 |
11 | 12 | 72 | 73 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/1.js: -------------------------------------------------------------------------------- 1 | // 存储副作用的“桶” 2 | const bucket = new Set() 3 | 4 | // 原始数据 5 | const data = { text: 'Hello world.' } 6 | // 对原始数据进行代理 7 | const obj = new Proxy(data, { 8 | // 拦截读取操作 9 | get (target, key) { 10 | // 将副作用函数加入 bucket 中 11 | bucket.add(effect) 12 | // 返回属性值 13 | return target[key] 14 | }, 15 | 16 | // 拦截设置操作 17 | set (target, key, value) { 18 | // 设置属性值 19 | target[key] = value 20 | // 把副作用函数从 bucket 中取出并执行 21 | bucket.forEach(fn => fn()) 22 | // 返回 true 表示设置操作成功 23 | return true 24 | } 25 | }) -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/2.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | 4 | // effect 函数用来注册副作用函数 5 | function effect (fn) { 6 | // 当调用 effect() 函数时,将副作用函数 fn() 赋值给 activeEffect 7 | activeEffect = fn 8 | // 执行副作用函数 9 | fn() 10 | } 11 | 12 | // effect( 13 | // // 一个匿名的副作用函数 14 | // () => { 15 | // document.querySelector('#root').textContent = obj.text 16 | // } 17 | // ) 18 | 19 | // 存储副作用的“桶” 20 | const bucket = new Set() 21 | 22 | // 原始数据 23 | const data = { text: 'Hello world.' } 24 | // 对原始数据进行代理 25 | const obj = new Proxy(data, { 26 | // 拦截读取操作 27 | get (target, key) { 28 | // 将 activeEffect 中存储的副作用函数加入 bucket 中 29 | if (activeEffect) { 30 | bucket.add(activeEffect) 31 | } 32 | // 返回属性值 33 | return target[key] 34 | }, 35 | 36 | // 拦截设置操作 37 | set (target, key, value) { 38 | // 设置属性值 39 | target[key] = value 40 | // 把副作用函数从 bucket 中取出并执行 41 | bucket.forEach(fn => fn()) 42 | // 返回 true 表示设置操作成功 43 | return true 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/3.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | 4 | // effect 函数用来注册副作用函数 5 | function effect (fn) { 6 | // 当调用 effect() 函数时,将副作用函数 fn() 赋值给 activeEffect 7 | activeEffect = fn 8 | // 执行副作用函数 9 | fn() 10 | } 11 | 12 | 13 | // // 存储副作用的“桶” 14 | // const bucket = new WeakMap() 15 | 16 | // // 原始数据 17 | // const data = { text: 'Hello world.' } 18 | // // 对原始数据进行代理 19 | // const obj = new Proxy(data, { 20 | // // 拦截读取操作 21 | // get (target, key) { 22 | // // 如果不存在副作用函数,直接返回 23 | // if (!activeEffect) { 24 | // return target[key] 25 | // } 26 | 27 | // // 从 bucket 中取出 depsMap,它是一个 Map 类型 28 | // let depsMap = bucket.get(target) 29 | 30 | // if (!depsMap) { 31 | // bucket.set(target, (depsMap = new Map())) 32 | // } 33 | 34 | // // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 35 | // // 里面存储着所有与当前 key 相当的副作用函数 36 | // let deps = depsMap.get(key) 37 | 38 | // if (!deps) { 39 | // depsMap.set(key, (deps = new Set())) 40 | // } 41 | 42 | // // 最后将副作用函数存储进 deps 里面 43 | // deps.add(activeEffect) 44 | 45 | // // 返回属性值 46 | // return target[key] 47 | // }, 48 | 49 | // // 拦截设置操作 50 | // set (target, key, value) { 51 | // // 设置属性值 52 | // target[key] = value 53 | // // 根据 target 从 bucket 中取出所有的 depsMap 54 | // const depsMap = bucket.get(target) 55 | 56 | // if (!depsMap) return true 57 | 58 | // // 根据 key 从 depsMap 中取出所有的副作用函数 59 | // const effects = depsMap.get(key) 60 | 61 | // effects && effects.forEach(fn => fn()) 62 | // // 返回 true 表示设置操作成功 63 | // return true 64 | // } 65 | // }) 66 | 67 | 68 | // 存储副作用的“桶” 69 | const bucket = new WeakMap() 70 | 71 | // 原始数据 72 | const data = { text: 'Hello world.' } 73 | // 对原始数据进行代理 74 | const obj = new Proxy(data, { 75 | // 拦截读取操作 76 | get (target, key) { 77 | track(target, key) 78 | 79 | // 返回属性值 80 | return target[key] 81 | }, 82 | 83 | // 拦截设置操作 84 | set (target, key, value) { 85 | // 设置属性值 86 | target[key] = value 87 | 88 | trigger(target, key) 89 | 90 | // 返回 true 表示设置操作成功 91 | return true 92 | } 93 | }) 94 | 95 | function track (target, key) { 96 | // 如果不存在副作用函数,直接返回 97 | if (!activeEffect) return 98 | 99 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 100 | let depsMap = bucket.get(target) 101 | 102 | if (!depsMap) { 103 | bucket.set(target, (depsMap = new Map())) 104 | } 105 | 106 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 107 | // 里面存储着所有与当前 key 相当的副作用函数 108 | let deps = depsMap.get(key) 109 | 110 | if (!deps) { 111 | depsMap.set(key, (deps = new Set())) 112 | } 113 | 114 | // 最后将副作用函数存储进 deps 里面 115 | deps.add(activeEffect) 116 | } 117 | 118 | function trigger (target, key) { 119 | // 根据 target 从 bucket 中取出所有的 depsMap 120 | const depsMap = bucket.get(target) 121 | 122 | if (!depsMap) return true 123 | 124 | // 根据 key 从 depsMap 中取出所有的副作用函数 125 | const effects = depsMap.get(key) 126 | 127 | effects && effects.forEach(fn => fn()) 128 | } 129 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/4.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | 4 | // effect 函数用来注册副作用函数 5 | function effect (fn) { 6 | // 当调用 effect() 函数时,将副作用函数 fn() 赋值给 activeEffect 7 | const effectFn = () => { 8 | // 调用 cleanUp() 函数完成清除工作 9 | cleanUp(effectFn) 10 | // 当 effectFn 执行时,将其设置为当前激活的副作用函数 11 | activeEffect = effectFn 12 | fn() 13 | } 14 | 15 | // 使用 effectFn.deps 为缓存所有与该副作用函数相关联的集合 16 | effectFn.deps = [] 17 | // 执行副作用函数 18 | effectFn() 19 | } 20 | 21 | // 存储副作用的“桶” 22 | const bucket = new WeakMap() 23 | 24 | // 原始数据 25 | const data = { ok: true, text: 'Hello world.' } 26 | // 对原始数据进行代理 27 | const obj = new Proxy(data, { 28 | // 拦截读取操作 29 | get (target, key) { 30 | track(target, key) 31 | 32 | // 返回属性值 33 | return target[key] 34 | }, 35 | 36 | // 拦截设置操作 37 | set (target, key, value) { 38 | // 设置属性值 39 | target[key] = value 40 | 41 | trigger(target, key) 42 | 43 | // 返回 true 表示设置操作成功 44 | return true 45 | } 46 | }) 47 | 48 | function track (target, key) { 49 | // 如果不存在副作用函数,直接返回 50 | if (!activeEffect) return 51 | 52 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 53 | let depsMap = bucket.get(target) 54 | 55 | if (!depsMap) { 56 | bucket.set(target, (depsMap = new Map())) 57 | } 58 | 59 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 60 | // 里面存储着所有与当前 key 相当的副作用函数 61 | let deps = depsMap.get(key) 62 | 63 | if (!deps) { 64 | depsMap.set(key, (deps = new Set())) 65 | } 66 | 67 | // 最后将副作用函数存储进 deps 里面 68 | deps.add(activeEffect) 69 | 70 | // deps 就是一个与当前副作用函数存在联系的依赖集合 71 | // 将其添加到 activeEffect.deps 中 72 | activeEffect.deps.push(deps) 73 | } 74 | 75 | function trigger (target, key) { 76 | // 根据 target 从 bucket 中取出所有的 depsMap 77 | const depsMap = bucket.get(target) 78 | 79 | if (!depsMap) return true 80 | 81 | // 根据 key 从 depsMap 中取出所有的副作用函数 82 | const effects = depsMap.get(key) 83 | 84 | // effects && effects.forEach(fn => fn()) 85 | 86 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 87 | const effectsToRun = new Set(effects) 88 | effectsToRun.forEach(effectFn => effectFn()) 89 | } 90 | 91 | function cleanUp (effectFn) { 92 | effectFn.deps.forEach(deps => { 93 | // 将 effectFn 从依赖集合中移除 94 | deps.delete(effectFn) 95 | }) 96 | 97 | // 最后需要重置 effectFn.deps 数组 98 | effectFn.deps.length = 0 99 | } 100 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/5.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | // 用一个 effect 栈来临时存储副作用函数 4 | const effectStack = [] 5 | 6 | function effect (fn) { 7 | const effectFn = () => { 8 | cleanUp(effectFn) 9 | 10 | activeEffect = effectFn 11 | 12 | // 在副作用函数执行之前,将当前的副作用函数压入栈 13 | effectStack.push(effectFn) 14 | 15 | // 执行副作用函数 16 | fn() 17 | 18 | // 将可能的内层嵌套中入栈的副作用函数弹出 19 | effectStack.pop() 20 | 21 | // 恢复之前的副作用函数 22 | activeEffect = effectStack.at(-1) 23 | } 24 | 25 | effectFn.deps = [] 26 | 27 | effectFn() 28 | } 29 | 30 | // 存储副作用的“桶” 31 | const bucket = new WeakMap() 32 | 33 | // 原始数据 34 | // const data = { foo: 1 } 35 | const data = { foo: true, bar: true } 36 | // 对原始数据进行代理 37 | const obj = new Proxy(data, { 38 | // 拦截读取操作 39 | get (target, key) { 40 | track(target, key) 41 | 42 | // 返回属性值 43 | return target[key] 44 | }, 45 | 46 | // 拦截设置操作 47 | set (target, key, value) { 48 | // 设置属性值 49 | target[key] = value 50 | 51 | trigger(target, key) 52 | 53 | // 返回 true 表示设置操作成功 54 | return true 55 | } 56 | }) 57 | 58 | function track (target, key) { 59 | // 如果不存在副作用函数,直接返回 60 | if (!activeEffect) return 61 | 62 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 63 | let depsMap = bucket.get(target) 64 | 65 | if (!depsMap) { 66 | bucket.set(target, (depsMap = new Map())) 67 | } 68 | 69 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 70 | // 里面存储着所有与当前 key 相当的副作用函数 71 | let deps = depsMap.get(key) 72 | 73 | if (!deps) { 74 | depsMap.set(key, (deps = new Set())) 75 | } 76 | 77 | // 最后将副作用函数存储进 deps 里面 78 | deps.add(activeEffect) 79 | 80 | // deps 就是一个与当前副作用函数存在联系的依赖集合 81 | // 将其添加到 activeEffect.deps 中 82 | activeEffect.deps.push(deps) 83 | } 84 | 85 | function trigger (target, key) { 86 | // 根据 target 从 bucket 中取出所有的 depsMap 87 | const depsMap = bucket.get(target) 88 | 89 | if (!depsMap) return true 90 | 91 | // 根据 key 从 depsMap 中取出所有的副作用函数 92 | const effects = depsMap.get(key) 93 | 94 | // effects && effects.forEach(fn => fn()) 95 | 96 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 97 | const effectsToRun = new Set() 98 | 99 | effects && effects.forEach(effectFn => { 100 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 101 | if (effectFn !== activeEffect) { 102 | effectsToRun.add(effectFn) 103 | } 104 | }) 105 | effectsToRun.forEach(effectFn => effectFn()) 106 | } 107 | 108 | function cleanUp (effectFn) { 109 | effectFn.deps.forEach(deps => { 110 | // 将 effectFn 从依赖集合中移除 111 | deps.delete(effectFn) 112 | }) 113 | 114 | // 最后需要重置 effectFn.deps 数组 115 | effectFn.deps.length = 0 116 | } 117 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/6.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | // 用一个 effect 栈来临时存储副作用函数 4 | const effectStack = [] 5 | 6 | function effect (fn, options = {}) { 7 | const effectFn = () => { 8 | cleanUp(effectFn) 9 | 10 | activeEffect = effectFn 11 | 12 | // 在副作用函数执行之前,将当前的副作用函数压入栈 13 | effectStack.push(effectFn) 14 | 15 | // 执行副作用函数 16 | fn() 17 | 18 | // 将可能的内层嵌套中入栈的副作用函数弹出 19 | effectStack.pop() 20 | 21 | // 恢复之前的副作用函数 22 | activeEffect = effectStack.at(-1) 23 | } 24 | 25 | effectFn.deps = [] 26 | 27 | // 将 options 挂载到 effectFn 上 28 | effectFn.options = options 29 | 30 | effectFn() 31 | } 32 | 33 | // 存储副作用的“桶” 34 | const bucket = new WeakMap() 35 | 36 | // 原始数据 37 | const data = { foo: 1 } 38 | // const data = { foo: true, bar: true } 39 | // 对原始数据进行代理 40 | const obj = new Proxy(data, { 41 | // 拦截读取操作 42 | get (target, key) { 43 | track(target, key) 44 | 45 | // 返回属性值 46 | return target[key] 47 | }, 48 | 49 | // 拦截设置操作 50 | set (target, key, value) { 51 | // 设置属性值 52 | target[key] = value 53 | 54 | trigger(target, key) 55 | 56 | // 返回 true 表示设置操作成功 57 | return true 58 | } 59 | }) 60 | 61 | function track (target, key) { 62 | // 如果不存在副作用函数,直接返回 63 | if (!activeEffect) return 64 | 65 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 66 | let depsMap = bucket.get(target) 67 | 68 | if (!depsMap) { 69 | bucket.set(target, (depsMap = new Map())) 70 | } 71 | 72 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 73 | // 里面存储着所有与当前 key 相当的副作用函数 74 | let deps = depsMap.get(key) 75 | 76 | if (!deps) { 77 | depsMap.set(key, (deps = new Set())) 78 | } 79 | 80 | // 最后将副作用函数存储进 deps 里面 81 | deps.add(activeEffect) 82 | 83 | // deps 就是一个与当前副作用函数存在联系的依赖集合 84 | // 将其添加到 activeEffect.deps 中 85 | activeEffect.deps.push(deps) 86 | } 87 | 88 | function trigger (target, key) { 89 | // 根据 target 从 bucket 中取出所有的 depsMap 90 | const depsMap = bucket.get(target) 91 | 92 | if (!depsMap) return true 93 | 94 | // 根据 key 从 depsMap 中取出所有的副作用函数 95 | const effects = depsMap.get(key) 96 | 97 | // effects && effects.forEach(fn => fn()) 98 | 99 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 100 | const effectsToRun = new Set() 101 | 102 | effects && effects.forEach(effectFn => { 103 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 104 | if (effectFn !== activeEffect) { 105 | effectsToRun.add(effectFn) 106 | } 107 | }) 108 | effectsToRun.forEach(effectFn => { 109 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 110 | if (effectFn.options.scheduler) { 111 | effectFn.options.scheduler(effectFn) 112 | } else { 113 | // 否则直接执行副作用函数 114 | effectFn() 115 | } 116 | }) 117 | } 118 | 119 | function cleanUp (effectFn) { 120 | effectFn.deps.forEach(deps => { 121 | // 将 effectFn 从依赖集合中移除 122 | deps.delete(effectFn) 123 | }) 124 | 125 | // 最后需要重置 effectFn.deps 数组 126 | effectFn.deps.length = 0 127 | } 128 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/7.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | // 用一个 effect 栈来临时存储副作用函数 4 | const effectStack = [] 5 | 6 | function effect (fn, options = {}) { 7 | const effectFn = () => { 8 | cleanUp(effectFn) 9 | 10 | activeEffect = effectFn 11 | 12 | // 在副作用函数执行之前,将当前的副作用函数压入栈 13 | effectStack.push(effectFn) 14 | 15 | // 执行副作用函数,并将其返回值交给 res 16 | const res = fn() 17 | 18 | // 将可能的内层嵌套中入栈的副作用函数弹出 19 | effectStack.pop() 20 | 21 | // 恢复之前的副作用函数 22 | activeEffect = effectStack.at(-1) 23 | 24 | // 返回 res 的结果 25 | return res 26 | } 27 | 28 | effectFn.deps = [] 29 | 30 | // 将 options 挂载到 effectFn 上 31 | effectFn.options = options 32 | 33 | // 只有在非 lazy 的情况下,立即执行 34 | if (!options.lazy) { 35 | effectFn() 36 | } 37 | 38 | // 将副作用函数作为返回值返回 39 | return effectFn 40 | } 41 | 42 | // 存储副作用的“桶” 43 | const bucket = new WeakMap() 44 | 45 | // 原始数据 46 | const data = { foo: 1 } 47 | // const data = { foo: true, bar: true } 48 | // 对原始数据进行代理 49 | const obj = new Proxy(data, { 50 | // 拦截读取操作 51 | get (target, key) { 52 | track(target, key) 53 | 54 | // 返回属性值 55 | return target[key] 56 | }, 57 | 58 | // 拦截设置操作 59 | set (target, key, value) { 60 | // 设置属性值 61 | target[key] = value 62 | 63 | trigger(target, key) 64 | 65 | // 返回 true 表示设置操作成功 66 | return true 67 | } 68 | }) 69 | 70 | function track (target, key) { 71 | // 如果不存在副作用函数,直接返回 72 | if (!activeEffect) return 73 | 74 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 75 | let depsMap = bucket.get(target) 76 | 77 | if (!depsMap) { 78 | bucket.set(target, (depsMap = new Map())) 79 | } 80 | 81 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 82 | // 里面存储着所有与当前 key 相当的副作用函数 83 | let deps = depsMap.get(key) 84 | 85 | if (!deps) { 86 | depsMap.set(key, (deps = new Set())) 87 | } 88 | 89 | // 最后将副作用函数存储进 deps 里面 90 | deps.add(activeEffect) 91 | 92 | // deps 就是一个与当前副作用函数存在联系的依赖集合 93 | // 将其添加到 activeEffect.deps 中 94 | activeEffect.deps.push(deps) 95 | } 96 | 97 | function trigger (target, key) { 98 | // 根据 target 从 bucket 中取出所有的 depsMap 99 | const depsMap = bucket.get(target) 100 | 101 | if (!depsMap) return true 102 | 103 | // 根据 key 从 depsMap 中取出所有的副作用函数 104 | const effects = depsMap.get(key) 105 | 106 | // effects && effects.forEach(fn => fn()) 107 | 108 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 109 | const effectsToRun = new Set() 110 | 111 | effects && effects.forEach(effectFn => { 112 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 113 | if (effectFn !== activeEffect) { 114 | effectsToRun.add(effectFn) 115 | } 116 | }) 117 | effectsToRun.forEach(effectFn => { 118 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 119 | if (effectFn.options.scheduler) { 120 | effectFn.options.scheduler(effectFn) 121 | } else { 122 | // 否则直接执行副作用函数 123 | effectFn() 124 | } 125 | }) 126 | } 127 | 128 | function cleanUp (effectFn) { 129 | effectFn.deps.forEach(deps => { 130 | // 将 effectFn 从依赖集合中移除 131 | deps.delete(effectFn) 132 | }) 133 | 134 | // 最后需要重置 effectFn.deps 数组 135 | effectFn.deps.length = 0 136 | } 137 | 138 | function computed (getter) { 139 | // 用来缓存上一次计算的值 140 | let value 141 | // dirty 标志用来标识是否需要重新计算值 142 | let dirty 143 | 144 | const effectFn = effect(getter, { 145 | lazy: true, 146 | // 在调度器中将 dirty 设置为 true 147 | shceduler () { 148 | dirty = true 149 | // 当计算属性的响应式数据变化时,手动调用 trigger() 函数触发响应 150 | trigger(obj, 'value') 151 | } 152 | }) 153 | 154 | const obj = { 155 | get value () { 156 | if (dirty) { 157 | value = effectFn() 158 | dirty = true 159 | } 160 | // 当读取 value 时,手动调用 track() 函数进行追踪 161 | return value 162 | } 163 | } 164 | 165 | return obj 166 | } 167 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/8.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | // 用一个 effect 栈来临时存储副作用函数 4 | const effectStack = [] 5 | 6 | function effect (fn, options = {}) { 7 | const effectFn = () => { 8 | cleanUp(effectFn) 9 | 10 | activeEffect = effectFn 11 | 12 | // 在副作用函数执行之前,将当前的副作用函数压入栈 13 | effectStack.push(effectFn) 14 | 15 | // 执行副作用函数,并将其返回值交给 res 16 | const res = fn() 17 | 18 | // 将可能的内层嵌套中入栈的副作用函数弹出 19 | effectStack.pop() 20 | 21 | // 恢复之前的副作用函数 22 | activeEffect = effectStack.at(-1) 23 | 24 | // 返回 res 的结果 25 | return res 26 | } 27 | 28 | effectFn.deps = [] 29 | 30 | // 将 options 挂载到 effectFn 上 31 | effectFn.options = options 32 | 33 | // 只有在非 lazy 的情况下,立即执行 34 | if (!options.lazy) { 35 | effectFn() 36 | } 37 | 38 | // 将副作用函数作为返回值返回 39 | return effectFn 40 | } 41 | 42 | // 存储副作用的“桶” 43 | const bucket = new WeakMap() 44 | 45 | // 原始数据 46 | const data = { foo: 1 } 47 | // const data = { foo: true, bar: true } 48 | // 对原始数据进行代理 49 | const obj = new Proxy(data, { 50 | // 拦截读取操作 51 | get (target, key) { 52 | track(target, key) 53 | 54 | // 返回属性值 55 | return target[key] 56 | }, 57 | 58 | // 拦截设置操作 59 | set (target, key, value) { 60 | // 设置属性值 61 | target[key] = value 62 | 63 | trigger(target, key) 64 | 65 | // 返回 true 表示设置操作成功 66 | return true 67 | } 68 | }) 69 | 70 | function track (target, key) { 71 | // 如果不存在副作用函数,直接返回 72 | if (!activeEffect) return 73 | 74 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 75 | let depsMap = bucket.get(target) 76 | 77 | if (!depsMap) { 78 | bucket.set(target, (depsMap = new Map())) 79 | } 80 | 81 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 82 | // 里面存储着所有与当前 key 相当的副作用函数 83 | let deps = depsMap.get(key) 84 | 85 | if (!deps) { 86 | depsMap.set(key, (deps = new Set())) 87 | } 88 | 89 | // 最后将副作用函数存储进 deps 里面 90 | deps.add(activeEffect) 91 | 92 | // deps 就是一个与当前副作用函数存在联系的依赖集合 93 | // 将其添加到 activeEffect.deps 中 94 | activeEffect.deps.push(deps) 95 | } 96 | 97 | function trigger (target, key) { 98 | // 根据 target 从 bucket 中取出所有的 depsMap 99 | const depsMap = bucket.get(target) 100 | 101 | if (!depsMap) return true 102 | 103 | // 根据 key 从 depsMap 中取出所有的副作用函数 104 | const effects = depsMap.get(key) 105 | 106 | // effects && effects.forEach(fn => fn()) 107 | 108 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 109 | const effectsToRun = new Set() 110 | 111 | effects && effects.forEach(effectFn => { 112 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 113 | if (effectFn !== activeEffect) { 114 | effectsToRun.add(effectFn) 115 | } 116 | }) 117 | effectsToRun.forEach(effectFn => { 118 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 119 | if (effectFn.options.scheduler) { 120 | effectFn.options.scheduler(effectFn) 121 | } else { 122 | // 否则直接执行副作用函数 123 | effectFn() 124 | } 125 | }) 126 | } 127 | 128 | function cleanUp (effectFn) { 129 | effectFn.deps.forEach(deps => { 130 | // 将 effectFn 从依赖集合中移除 131 | deps.delete(effectFn) 132 | }) 133 | 134 | // 最后需要重置 effectFn.deps 数组 135 | effectFn.deps.length = 0 136 | } 137 | 138 | function computed (getter) { 139 | // 用来缓存上一次计算的值 140 | let value 141 | // dirty 标志用来标识是否需要重新计算值 142 | let dirty 143 | 144 | const effectFn = effect(getter, { 145 | lazy: true, 146 | // 在调度器中将 dirty 设置为 true 147 | shceduler () { 148 | dirty = true 149 | // 当计算属性的响应式数据变化时,手动调用 trigger() 函数触发响应 150 | trigger(obj, 'value') 151 | } 152 | }) 153 | 154 | const obj = { 155 | get value () { 156 | if (dirty) { 157 | value = effectFn() 158 | dirty = true 159 | } 160 | // 当读取 value 时,手动调用 track() 函数进行追踪 161 | return value 162 | } 163 | } 164 | 165 | return obj 166 | } 167 | 168 | function watch (source, cb, options = {}) { 169 | // 定义一个getter 170 | let getter 171 | 172 | if (typeof source === 'function') { 173 | getter = source 174 | } else { 175 | getter = () => traverse(source) 176 | } 177 | 178 | // 定义新值与旧值 179 | let newValue 180 | let oldValue 181 | 182 | // 提取 scheduler 调度函数作为一个独立的 job 函数 183 | const job = () => { 184 | // 在 scheduler 中重新执行副作用函数,拿到新值 185 | newValue = effectFn() 186 | // 将旧值与新值作为回调函数的参数 187 | cb(newValue, oldValue) 188 | // 回调函数执行完毕后 189 | // 将 newValue 的值存到 oldValue 中,下一次就能拿到正确的旧值 190 | oldValue = newValue 191 | } 192 | 193 | const effectFn = effect( 194 | // 执行 getter 195 | () => getter(), 196 | { 197 | lazy: true, 198 | scheduler: () => { 199 | if (options.flush === 'post') { 200 | // 如果 flush 是 'post',则将调度函数放到微任务队列中执行 201 | Promise.resolve().then(job) 202 | } else { 203 | // 这相当于 flush 是 'sync' 的行为 204 | job() 205 | } 206 | } 207 | } 208 | ) 209 | 210 | if (options.immediate) { 211 | // 当 immediate 为 true 时,立即执行 job,从而触发回调执行 212 | job() 213 | } else { 214 | // 手动调用副作用函数,拿到的就是旧值 215 | oldValue = effectFn() 216 | } 217 | } 218 | 219 | function traverse (value, seen = new Set()) { 220 | // 如果要读取的数据是一个原型类型 221 | // 或者已经被读取过了,那么什么都不做 222 | if (typeof value !== 'object' || value === null || seen.has(value)) { 223 | return 224 | } 225 | 226 | // 将数据加入 seen 中,代表已经读取过了,避免死循环 227 | seen.add(value) 228 | 229 | // 暂时不考虑数组等其他结构 230 | // 假设 value 是一个对象,那么我们可以使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理 231 | for (const key in value) { 232 | traverse(value[key], seen) 233 | } 234 | 235 | return value 236 | } 237 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/9.js: -------------------------------------------------------------------------------- 1 | // 用一个全局变量来存储被注册的副作用函数 2 | let activeEffect 3 | // 用一个 effect 栈来临时存储副作用函数 4 | const effectStack = [] 5 | 6 | function effect (fn, options = {}) { 7 | const effectFn = () => { 8 | cleanUp(effectFn) 9 | 10 | activeEffect = effectFn 11 | 12 | // 在副作用函数执行之前,将当前的副作用函数压入栈 13 | effectStack.push(effectFn) 14 | 15 | // 执行副作用函数,并将其返回值交给 res 16 | const res = fn() 17 | 18 | // 将可能的内层嵌套中入栈的副作用函数弹出 19 | effectStack.pop() 20 | 21 | // 恢复之前的副作用函数 22 | activeEffect = effectStack.at(-1) 23 | 24 | // 返回 res 的结果 25 | return res 26 | } 27 | 28 | effectFn.deps = [] 29 | 30 | // 将 options 挂载到 effectFn 上 31 | effectFn.options = options 32 | 33 | // 只有在非 lazy 的情况下,立即执行 34 | if (!options.lazy) { 35 | effectFn() 36 | } 37 | 38 | // 将副作用函数作为返回值返回 39 | return effectFn 40 | } 41 | 42 | // 存储副作用的“桶” 43 | const bucket = new WeakMap() 44 | 45 | // 原始数据 46 | const data = { foo: 1 } 47 | // const data = { foo: true, bar: true } 48 | // 对原始数据进行代理 49 | const obj = new Proxy(data, { 50 | // 拦截读取操作 51 | get (target, key) { 52 | track(target, key) 53 | 54 | // 返回属性值 55 | return target[key] 56 | }, 57 | 58 | // 拦截设置操作 59 | set (target, key, value) { 60 | // 设置属性值 61 | target[key] = value 62 | 63 | trigger(target, key) 64 | 65 | // 返回 true 表示设置操作成功 66 | return true 67 | } 68 | }) 69 | 70 | function track (target, key) { 71 | // 如果不存在副作用函数,直接返回 72 | if (!activeEffect) return 73 | 74 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 75 | let depsMap = bucket.get(target) 76 | 77 | if (!depsMap) { 78 | bucket.set(target, (depsMap = new Map())) 79 | } 80 | 81 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 82 | // 里面存储着所有与当前 key 相当的副作用函数 83 | let deps = depsMap.get(key) 84 | 85 | if (!deps) { 86 | depsMap.set(key, (deps = new Set())) 87 | } 88 | 89 | // 最后将副作用函数存储进 deps 里面 90 | deps.add(activeEffect) 91 | 92 | // deps 就是一个与当前副作用函数存在联系的依赖集合 93 | // 将其添加到 activeEffect.deps 中 94 | activeEffect.deps.push(deps) 95 | } 96 | 97 | function trigger (target, key) { 98 | // 根据 target 从 bucket 中取出所有的 depsMap 99 | const depsMap = bucket.get(target) 100 | 101 | if (!depsMap) return true 102 | 103 | // 根据 key 从 depsMap 中取出所有的副作用函数 104 | const effects = depsMap.get(key) 105 | 106 | // effects && effects.forEach(fn => fn()) 107 | 108 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 109 | const effectsToRun = new Set() 110 | 111 | effects && effects.forEach(effectFn => { 112 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 113 | if (effectFn !== activeEffect) { 114 | effectsToRun.add(effectFn) 115 | } 116 | }) 117 | effectsToRun.forEach(effectFn => { 118 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 119 | if (effectFn.options.scheduler) { 120 | effectFn.options.scheduler(effectFn) 121 | } else { 122 | // 否则直接执行副作用函数 123 | effectFn() 124 | } 125 | }) 126 | } 127 | 128 | function cleanUp (effectFn) { 129 | effectFn.deps.forEach(deps => { 130 | // 将 effectFn 从依赖集合中移除 131 | deps.delete(effectFn) 132 | }) 133 | 134 | // 最后需要重置 effectFn.deps 数组 135 | effectFn.deps.length = 0 136 | } 137 | 138 | function computed (getter) { 139 | // 用来缓存上一次计算的值 140 | let value 141 | // dirty 标志用来标识是否需要重新计算值 142 | let dirty 143 | 144 | const effectFn = effect(getter, { 145 | lazy: true, 146 | // 在调度器中将 dirty 设置为 true 147 | shceduler () { 148 | dirty = true 149 | // 当计算属性的响应式数据变化时,手动调用 trigger() 函数触发响应 150 | trigger(obj, 'value') 151 | } 152 | }) 153 | 154 | const obj = { 155 | get value () { 156 | if (dirty) { 157 | value = effectFn() 158 | dirty = true 159 | } 160 | // 当读取 value 时,手动调用 track() 函数进行追踪 161 | return value 162 | } 163 | } 164 | 165 | return obj 166 | } 167 | 168 | function watch (source, cb, options = {}) { 169 | // 定义一个getter 170 | let getter 171 | 172 | if (typeof source === 'function') { 173 | getter = source 174 | } else { 175 | getter = () => traverse(source) 176 | } 177 | 178 | // 定义新值与旧值 179 | let newValue 180 | let oldValue 181 | 182 | // cleanup 用来存储用户注册的过期回调 183 | let cleanup 184 | // 定义 onInvalidate 函数 185 | const onInvalidate = (fn) => { 186 | // 将过期回调存储到 cleanup 中 187 | cleanup = fn 188 | } 189 | 190 | // 提取 scheduler 调度函数作为一个独立的 job 函数 191 | const job = () => { 192 | // 在 scheduler 中重新执行副作用函数,拿到新值 193 | newValue = effectFn() 194 | // 在调用回调函数 cb() 之前,先调用过期回调 195 | if (cleanup) { 196 | cleanup() 197 | } 198 | // 将旧值与新值作为回调函数的参数 199 | // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用 200 | cb(newValue, oldValue, onInvalidate) 201 | // 回调函数执行完毕后 202 | // 将 newValue 的值存到 oldValue 中,下一次就能拿到正确的旧值 203 | oldValue = newValue 204 | } 205 | 206 | const effectFn = effect( 207 | // 执行 getter 208 | () => getter(), 209 | { 210 | lazy: true, 211 | scheduler: () => { 212 | if (options.flush === 'post') { 213 | // 如果 flush 是 'post',则将调度函数放到微任务队列中执行 214 | Promise.resolve().then(job) 215 | } else { 216 | // 这相当于 flush 是 'sync' 的行为 217 | job() 218 | } 219 | } 220 | } 221 | ) 222 | 223 | if (options.immediate) { 224 | // 当 immediate 为 true 时,立即执行 job,从而触发回调执行 225 | job() 226 | } else { 227 | // 手动调用副作用函数,拿到的就是旧值 228 | oldValue = effectFn() 229 | } 230 | } 231 | 232 | function traverse (value, seen = new Set()) { 233 | // 如果要读取的数据是一个原型类型 234 | // 或者已经被读取过了,那么什么都不做 235 | if (typeof value !== 'object' || value === null || seen.has(value)) { 236 | return 237 | } 238 | 239 | // 将数据加入 seen 中,代表已经读取过了,避免死循环 240 | seen.add(value) 241 | 242 | // 暂时不考虑数组等其他结构 243 | // 假设 value 是一个对象,那么我们可以使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理 244 | for (const key in value) { 245 | traverse(value[key], seen) 246 | } 247 | 248 | return value 249 | } 250 | -------------------------------------------------------------------------------- /examples/3 响应式系统的实现/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | index.html 8 | 9 | 10 |
11 | 12 | 24 | 25 | 39 | 40 | 57 | 58 | 78 | 79 | 112 | 113 | 114 | 130 | 131 | -------------------------------------------------------------------------------- /examples/4 非原始值的响应式方案/1.js: -------------------------------------------------------------------------------- 1 | const TriggerType = { 2 | ADD: 'ADD', 3 | SET: 'SET', 4 | DELETE: 'DELETE' 5 | } 6 | 7 | // 用一个全局变量来存储被注册的副作用函数 8 | let activeEffect 9 | // 用一个 effect 栈来临时存储副作用函数 10 | const effectStack = [] 11 | 12 | function effect (fn, options = {}) { 13 | const effectFn = () => { 14 | cleanUp(effectFn) 15 | 16 | activeEffect = effectFn 17 | 18 | // 在副作用函数执行之前,将当前的副作用函数压入栈 19 | effectStack.push(effectFn) 20 | 21 | // 执行副作用函数,并将其返回值交给 res 22 | const res = fn() 23 | 24 | // 将可能的内层嵌套中入栈的副作用函数弹出 25 | effectStack.pop() 26 | 27 | // 恢复之前的副作用函数 28 | activeEffect = effectStack.at(-1) 29 | 30 | // 返回 res 的结果 31 | return res 32 | } 33 | 34 | effectFn.deps = [] 35 | 36 | // 将 options 挂载到 effectFn 上 37 | effectFn.options = options 38 | 39 | // 只有在非 lazy 的情况下,立即执行 40 | if (!options.lazy) { 41 | effectFn() 42 | } 43 | 44 | // 将副作用函数作为返回值返回 45 | return effectFn 46 | } 47 | 48 | // 存储副作用的“桶” 49 | const bucket = new WeakMap() 50 | 51 | let ITERATE_KEY = Symbol() 52 | const RAW = 'RAW' 53 | 54 | function reactive (obj) { 55 | return new Proxy(obj, { 56 | // 拦截读取操作 57 | get (target, key, receiver) { 58 | // 代理对象可以通过 Symbol.for(RAW) 属性访问原始数据 59 | if (key === Symbol.for(RAW)) { 60 | return target 61 | } 62 | 63 | track(target, key) 64 | 65 | // 返回属性值 66 | return Reflect.get(target, key, receiver) 67 | }, 68 | 69 | // 拦截设置操作 70 | set (target, key, newVal, receiver) { 71 | // 先获取旧值 72 | const oldVal = target[key] 73 | 74 | // 如果属性不存在,则说明是在新增属性 75 | // 否则是修改属性 76 | const type = Object.prototype.hasOwnProperty.call(target, key) 77 | ? TriggerType.SET 78 | : TriggerType.ADD 79 | 80 | // 设置属性值 81 | const res = Reflect.set(target, key, newVal, receiver) 82 | 83 | // target === receiver[Symbol.for(RAW)] 说明 receiver 就是 target 的代理对象 84 | if (target === receiver[Symbol.for(RAW)]) { 85 | // 比较新值与旧值,只有当不全等的时候 86 | // 并且它们都不是 NaN 时才触发响应 87 | if ( 88 | oldVal !== newVal && 89 | ( 90 | oldVal === oldVal || 91 | newVal === newVal 92 | ) 93 | ) { 94 | // 将 type 作为第三个参数传递给 trigger() 函数 95 | trigger(target, key, type) 96 | } 97 | } 98 | 99 | return res 100 | }, 101 | 102 | ownKeys (target) { 103 | // 将副作用函数与 ITERATE_KEY 关联 104 | track(target, ITERATE_KEY) 105 | return Reflect.ownKeys(target) 106 | }, 107 | 108 | deleteProperty (target, key) { 109 | // 检查被操作的属性是否是对象自己的属性 110 | const hadKey = Object.prototype.hasOwnProperty.call(target, key) 111 | 112 | const res = Reflect.deleteProperty(target, key) 113 | 114 | if (res && hadKey) { 115 | // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新 116 | trigger(target, key, 'DELETE') 117 | } 118 | 119 | return res 120 | }, 121 | 122 | // 拦截函数调用 123 | apply (target, thisArg, argsList) { 124 | Reflect.apply(target, thisArg, argsList) 125 | } 126 | }) 127 | } 128 | 129 | function track (target, key) { 130 | // 如果不存在副作用函数,直接返回 131 | if (!activeEffect) return 132 | 133 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 134 | let depsMap = bucket.get(target) 135 | 136 | if (!depsMap) { 137 | bucket.set(target, (depsMap = new Map())) 138 | } 139 | 140 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 141 | // 里面存储着所有与当前 key 相当的副作用函数 142 | let deps = depsMap.get(key) 143 | 144 | if (!deps) { 145 | depsMap.set(key, (deps = new Set())) 146 | } 147 | 148 | // 最后将副作用函数存储进 deps 里面 149 | deps.add(activeEffect) 150 | 151 | // deps 就是一个与当前副作用函数存在联系的依赖集合 152 | // 将其添加到 activeEffect.deps 中 153 | activeEffect.deps.push(deps) 154 | } 155 | 156 | function trigger (target, key, type) { 157 | // 根据 target 从 bucket 中取出所有的 depsMap 158 | const depsMap = bucket.get(target) 159 | 160 | if (!depsMap) return true 161 | 162 | // 根据 key 从 depsMap 中取出所有的副作用函数 163 | const effects = depsMap.get(key) 164 | // 根据 ITERATE_KEY 从 depsMap 中取出所有的副作用函数 165 | const iterateEffects = depsMap.get(ITERATE_KEY) 166 | 167 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 168 | const effectsToRun = new Set() 169 | 170 | effects && effects.forEach(effectFn => { 171 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 172 | if (effectFn !== activeEffect) { 173 | effectsToRun.add(effectFn) 174 | } 175 | }) 176 | 177 | // 只有当操作类型为 'ADD' 或 'DELETE' 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行 178 | if ( 179 | type === TriggerType.ADD || 180 | type === TriggerType.DELETE 181 | ) { 182 | iterateEffects && iterateEffects.forEach(effectFn => { 183 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 184 | if (effectFn !== activeEffect) { 185 | effectsToRun.add(effectFn) 186 | } 187 | }) 188 | } 189 | 190 | effectsToRun.forEach(effectFn => { 191 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 192 | if (effectFn.options.scheduler) { 193 | effectFn.options.scheduler(effectFn) 194 | } else { 195 | // 否则直接执行副作用函数 196 | effectFn() 197 | } 198 | }) 199 | } 200 | 201 | function cleanUp (effectFn) { 202 | effectFn.deps.forEach(deps => { 203 | // 将 effectFn 从依赖集合中移除 204 | deps.delete(effectFn) 205 | }) 206 | 207 | // 最后需要重置 effectFn.deps 数组 208 | effectFn.deps.length = 0 209 | } 210 | 211 | function computed (getter) { 212 | // 用来缓存上一次计算的值 213 | let value 214 | // dirty 标志用来标识是否需要重新计算值 215 | let dirty 216 | 217 | const effectFn = effect(getter, { 218 | lazy: true, 219 | // 在调度器中将 dirty 设置为 true 220 | shceduler () { 221 | dirty = true 222 | // 当计算属性的响应式数据变化时,手动调用 trigger() 函数触发响应 223 | trigger(obj, 'value') 224 | } 225 | }) 226 | 227 | const obj = { 228 | get value () { 229 | if (dirty) { 230 | value = effectFn() 231 | dirty = true 232 | } 233 | // 当读取 value 时,手动调用 track() 函数进行追踪 234 | return value 235 | } 236 | } 237 | 238 | return obj 239 | } 240 | 241 | function watch (source, cb, options = {}) { 242 | // 定义一个getter 243 | let getter 244 | 245 | if (typeof source === 'function') { 246 | getter = source 247 | } else { 248 | getter = () => traverse(source) 249 | } 250 | 251 | // 定义新值与旧值 252 | let newValue 253 | let oldValue 254 | 255 | // cleanup 用来存储用户注册的过期回调 256 | let cleanup 257 | // 定义 onInvalidate 函数 258 | const onInvalidate = (fn) => { 259 | // 将过期回调存储到 cleanup 中 260 | cleanup = fn 261 | } 262 | 263 | // 提取 scheduler 调度函数作为一个独立的 job 函数 264 | const job = () => { 265 | // 在 scheduler 中重新执行副作用函数,拿到新值 266 | newValue = effectFn() 267 | // 在调用回调函数 cb() 之前,先调用过期回调 268 | if (cleanup) { 269 | cleanup() 270 | } 271 | // 将旧值与新值作为回调函数的参数 272 | // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用 273 | cb(newValue, oldValue, onInvalidate) 274 | // 回调函数执行完毕后 275 | // 将 newValue 的值存到 oldValue 中,下一次就能拿到正确的旧值 276 | oldValue = newValue 277 | } 278 | 279 | const effectFn = effect( 280 | // 执行 getter 281 | () => getter(), 282 | { 283 | lazy: true, 284 | scheduler: () => { 285 | if (options.flush === 'post') { 286 | // 如果 flush 是 'post',则将调度函数放到微任务队列中执行 287 | Promise.resolve().then(job) 288 | } else { 289 | // 这相当于 flush 是 'sync' 的行为 290 | job() 291 | } 292 | } 293 | } 294 | ) 295 | 296 | if (options.immediate) { 297 | // 当 immediate 为 true 时,立即执行 job,从而触发回调执行 298 | job() 299 | } else { 300 | // 手动调用副作用函数,拿到的就是旧值 301 | oldValue = effectFn() 302 | } 303 | } 304 | 305 | function traverse (value, seen = new Set()) { 306 | // 如果要读取的数据是一个原始类型 307 | // 或者已经被读取过了,那么什么都不做 308 | if (typeof value !== 'object' || value === null || seen.has(value)) { 309 | return 310 | } 311 | 312 | // 将数据加入 seen 中,代表已经读取过了,避免死循环 313 | seen.add(value) 314 | 315 | // 暂时不考虑数组等其他结构 316 | // 假设 value 是一个对象,那么我们可以使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理 317 | for (const key in value) { 318 | traverse(value[key], seen) 319 | } 320 | 321 | return value 322 | } 323 | -------------------------------------------------------------------------------- /examples/4 非原始值的响应式方案/2.js: -------------------------------------------------------------------------------- 1 | const TriggerType = { 2 | ADD: 'ADD', 3 | SET: 'SET', 4 | DELETE: 'DELETE' 5 | } 6 | 7 | // 用一个全局变量来存储被注册的副作用函数 8 | let activeEffect 9 | // 用一个 effect 栈来临时存储副作用函数 10 | const effectStack = [] 11 | 12 | function effect (fn, options = {}) { 13 | const effectFn = () => { 14 | cleanUp(effectFn) 15 | 16 | activeEffect = effectFn 17 | 18 | // 在副作用函数执行之前,将当前的副作用函数压入栈 19 | effectStack.push(effectFn) 20 | 21 | // 执行副作用函数,并将其返回值交给 res 22 | const res = fn() 23 | 24 | // 将可能的内层嵌套中入栈的副作用函数弹出 25 | effectStack.pop() 26 | 27 | // 恢复之前的副作用函数 28 | activeEffect = effectStack.at(-1) 29 | 30 | // 返回 res 的结果 31 | return res 32 | } 33 | 34 | effectFn.deps = [] 35 | 36 | // 将 options 挂载到 effectFn 上 37 | effectFn.options = options 38 | 39 | // 只有在非 lazy 的情况下,立即执行 40 | if (!options.lazy) { 41 | effectFn() 42 | } 43 | 44 | // 将副作用函数作为返回值返回 45 | return effectFn 46 | } 47 | 48 | // 存储副作用的“桶” 49 | const bucket = new WeakMap() 50 | 51 | let ITERATE_KEY = Symbol() 52 | const RAW = 'RAW' 53 | 54 | // 封装 createReactive() 函数,多接收一个参数 isShallow,代表是否为浅响应,默认为 false 55 | function createReactive (obj, isShallow = false) { 56 | return new Proxy(obj, { 57 | // 拦截读取操作 58 | get (target, key, receiver) { 59 | // 代理对象可以通过 Symbol.for(RAW) 属性访问原始数据 60 | if (key === Symbol.for(RAW)) { 61 | return target 62 | } 63 | 64 | track(target, key) 65 | 66 | // 得到原始值结果 67 | const res = Reflect.get(target, key, receiver) 68 | 69 | // 如果是浅响应,直接返回原始值 70 | if (isShallow) { 71 | return res 72 | } 73 | 74 | if (typeof res === 'object' && res !== null) { 75 | return reactive(res) 76 | } 77 | 78 | return res 79 | }, 80 | 81 | // 拦截设置操作 82 | set (target, key, newVal, receiver) { 83 | // 先获取旧值 84 | const oldVal = target[key] 85 | 86 | // 如果属性不存在,则说明是在新增属性 87 | // 否则是修改属性 88 | const type = Object.prototype.hasOwnProperty.call(target, key) 89 | ? TriggerType.SET 90 | : TriggerType.ADD 91 | 92 | // 设置属性值 93 | const res = Reflect.set(target, key, newVal, receiver) 94 | 95 | // target === receiver[Symbol.for(RAW)] 说明 receiver 就是 target 的代理对象 96 | if (target === receiver[Symbol.for(RAW)]) { 97 | // 比较新值与旧值,只有当不全等的时候 98 | // 并且它们都不是 NaN 时才触发响应 99 | if ( 100 | oldVal !== newVal && 101 | ( 102 | oldVal === oldVal || 103 | newVal === newVal 104 | ) 105 | ) { 106 | // 将 type 作为第三个参数传递给 trigger() 函数 107 | trigger(target, key, type) 108 | } 109 | } 110 | 111 | return res 112 | }, 113 | 114 | ownKeys (target) { 115 | // 将副作用函数与 ITERATE_KEY 关联 116 | track(target, ITERATE_KEY) 117 | return Reflect.ownKeys(target) 118 | }, 119 | 120 | deleteProperty (target, key) { 121 | // 检查被操作的属性是否是对象自己的属性 122 | const hadKey = Object.prototype.hasOwnProperty.call(target, key) 123 | 124 | const res = Reflect.deleteProperty(target, key) 125 | 126 | if (res && hadKey) { 127 | // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新 128 | trigger(target, key, 'DELETE') 129 | } 130 | 131 | return res 132 | }, 133 | 134 | // 拦截函数调用 135 | apply (target, thisArg, argsList) { 136 | Reflect.apply(target, thisArg, argsList) 137 | } 138 | }) 139 | } 140 | 141 | function reactive (obj) { 142 | return createReactive(obj) 143 | } 144 | 145 | function shallowReactive (obj) { 146 | return createReactive(obj, true) 147 | } 148 | 149 | function track (target, key) { 150 | // 如果不存在副作用函数,直接返回 151 | if (!activeEffect) return 152 | 153 | // 从 bucket 中取出 depsMap,它是一个 Map 类型 154 | let depsMap = bucket.get(target) 155 | 156 | if (!depsMap) { 157 | bucket.set(target, (depsMap = new Map())) 158 | } 159 | 160 | // 再根据 key 从 depsMap 中取出 deps,它是一个 Set 类型 161 | // 里面存储着所有与当前 key 相当的副作用函数 162 | let deps = depsMap.get(key) 163 | 164 | if (!deps) { 165 | depsMap.set(key, (deps = new Set())) 166 | } 167 | 168 | // 最后将副作用函数存储进 deps 里面 169 | deps.add(activeEffect) 170 | 171 | // deps 就是一个与当前副作用函数存在联系的依赖集合 172 | // 将其添加到 activeEffect.deps 中 173 | activeEffect.deps.push(deps) 174 | } 175 | 176 | function trigger (target, key, type) { 177 | // 根据 target 从 bucket 中取出所有的 depsMap 178 | const depsMap = bucket.get(target) 179 | 180 | if (!depsMap) return true 181 | 182 | // 根据 key 从 depsMap 中取出所有的副作用函数 183 | const effects = depsMap.get(key) 184 | // 根据 ITERATE_KEY 从 depsMap 中取出所有的副作用函数 185 | const iterateEffects = depsMap.get(ITERATE_KEY) 186 | 187 | // 用一个新的 Set 来完成 forEach 操作,防止添加时进入死循环 188 | const effectsToRun = new Set() 189 | 190 | effects && effects.forEach(effectFn => { 191 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 192 | if (effectFn !== activeEffect) { 193 | effectsToRun.add(effectFn) 194 | } 195 | }) 196 | 197 | // 只有当操作类型为 'ADD' 或 'DELETE' 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行 198 | if ( 199 | type === TriggerType.ADD || 200 | type === TriggerType.DELETE 201 | ) { 202 | iterateEffects && iterateEffects.forEach(effectFn => { 203 | // 如果 trigger 触发执行副作用函数与当前正在执行的副作用函数相同,则不触发 204 | if (effectFn !== activeEffect) { 205 | effectsToRun.add(effectFn) 206 | } 207 | }) 208 | } 209 | 210 | effectsToRun.forEach(effectFn => { 211 | // 如果该副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递 212 | if (effectFn.options.scheduler) { 213 | effectFn.options.scheduler(effectFn) 214 | } else { 215 | // 否则直接执行副作用函数 216 | effectFn() 217 | } 218 | }) 219 | } 220 | 221 | function cleanUp (effectFn) { 222 | effectFn.deps.forEach(deps => { 223 | // 将 effectFn 从依赖集合中移除 224 | deps.delete(effectFn) 225 | }) 226 | 227 | // 最后需要重置 effectFn.deps 数组 228 | effectFn.deps.length = 0 229 | } 230 | 231 | function computed (getter) { 232 | // 用来缓存上一次计算的值 233 | let value 234 | // dirty 标志用来标识是否需要重新计算值 235 | let dirty 236 | 237 | const effectFn = effect(getter, { 238 | lazy: true, 239 | // 在调度器中将 dirty 设置为 true 240 | shceduler () { 241 | dirty = true 242 | // 当计算属性的响应式数据变化时,手动调用 trigger() 函数触发响应 243 | trigger(obj, 'value') 244 | } 245 | }) 246 | 247 | const obj = { 248 | get value () { 249 | if (dirty) { 250 | value = effectFn() 251 | dirty = true 252 | } 253 | // 当读取 value 时,手动调用 track() 函数进行追踪 254 | return value 255 | } 256 | } 257 | 258 | return obj 259 | } 260 | 261 | function watch (source, cb, options = {}) { 262 | // 定义一个getter 263 | let getter 264 | 265 | if (typeof source === 'function') { 266 | getter = source 267 | } else { 268 | getter = () => traverse(source) 269 | } 270 | 271 | // 定义新值与旧值 272 | let newValue 273 | let oldValue 274 | 275 | // cleanup 用来存储用户注册的过期回调 276 | let cleanup 277 | // 定义 onInvalidate 函数 278 | const onInvalidate = (fn) => { 279 | // 将过期回调存储到 cleanup 中 280 | cleanup = fn 281 | } 282 | 283 | // 提取 scheduler 调度函数作为一个独立的 job 函数 284 | const job = () => { 285 | // 在 scheduler 中重新执行副作用函数,拿到新值 286 | newValue = effectFn() 287 | // 在调用回调函数 cb() 之前,先调用过期回调 288 | if (cleanup) { 289 | cleanup() 290 | } 291 | // 将旧值与新值作为回调函数的参数 292 | // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用 293 | cb(newValue, oldValue, onInvalidate) 294 | // 回调函数执行完毕后 295 | // 将 newValue 的值存到 oldValue 中,下一次就能拿到正确的旧值 296 | oldValue = newValue 297 | } 298 | 299 | const effectFn = effect( 300 | // 执行 getter 301 | () => getter(), 302 | { 303 | lazy: true, 304 | scheduler: () => { 305 | if (options.flush === 'post') { 306 | // 如果 flush 是 'post',则将调度函数放到微任务队列中执行 307 | Promise.resolve().then(job) 308 | } else { 309 | // 这相当于 flush 是 'sync' 的行为 310 | job() 311 | } 312 | } 313 | } 314 | ) 315 | 316 | if (options.immediate) { 317 | // 当 immediate 为 true 时,立即执行 job,从而触发回调执行 318 | job() 319 | } else { 320 | // 手动调用副作用函数,拿到的就是旧值 321 | oldValue = effectFn() 322 | } 323 | } 324 | 325 | function traverse (value, seen = new Set()) { 326 | // 如果要读取的数据是一个原始类型 327 | // 或者已经被读取过了,那么什么都不做 328 | if (typeof value !== 'object' || value === null || seen.has(value)) { 329 | return 330 | } 331 | 332 | // 将数据加入 seen 中,代表已经读取过了,避免死循环 333 | seen.add(value) 334 | 335 | // 暂时不考虑数组等其他结构 336 | // 假设 value 是一个对象,那么我们可以使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理 337 | for (const key in value) { 338 | traverse(value[key], seen) 339 | } 340 | 341 | return value 342 | } 343 | -------------------------------------------------------------------------------- /examples/4 非原始值的响应式方案/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 43 | 44 | 59 | 60 | 78 | 79 | 93 | 94 | 106 | 107 | 124 | 125 | 126 | 188 | 189 | -------------------------------------------------------------------------------- /examples/5 原始值的响应式方案/1.js: -------------------------------------------------------------------------------- 1 | // 封装一个 ref 函数 2 | function ref (val) { 3 | // 在 ref 函数内部创建包裹对象 4 | const wrapper = { 5 | value: val 6 | } 7 | 8 | // 使用 Object.defineProperty 给 wrapper 加上一个不可写、不可枚举的属性 9 | Object.defineProperty(wrapper, '__v_isRef', { 10 | value: true 11 | }) 12 | 13 | // 将包裹对象变成响应式数据 14 | return reactive(wrapper) 15 | } 16 | 17 | function toRef (obj, key) { 18 | const wrapper = { 19 | get value () { 20 | return obj[key] 21 | }, 22 | 23 | set value (val) { 24 | obj[key] = val 25 | } 26 | } 27 | 28 | // 使用 Object.defineProperty 给 wrapper 加上一个不可写、不可枚举的属性 29 | Object.defineProperty(wrapper, '__v_isRef', { 30 | value: true 31 | }) 32 | 33 | return wrapper 34 | } 35 | 36 | function toRefs (obj) { 37 | const wrapper = {} 38 | 39 | for (const key in obj) { 40 | wrapper[key] = toRef(obj, key) 41 | } 42 | 43 | return wrapper 44 | } 45 | 46 | function proxyRefs (target) { 47 | return new Proxy(target, { 48 | get (target, key, receiver) { 49 | const value = Reflect.get(target, key, receiver) 50 | 51 | return value.__v_isRef ? value.value : value 52 | }, 53 | 54 | set (target, key, newValue, receiver) { 55 | const value = target[key] 56 | 57 | if (value.__v_isRef) { 58 | value.value = newValue 59 | return true 60 | } 61 | 62 | return Reflect.set(target, key, newValue, receiver) 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /examples/5 原始值的响应式方案/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | 14 | 44 | 45 | -------------------------------------------------------------------------------- /examples/6 渲染器的设计/1.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText 7 | } = options 8 | 9 | function render (vnode, container) { 10 | if (vnode) { 11 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 12 | patch(container._vnode, vnode, container) 13 | } else { 14 | if (container._vnode) { 15 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 16 | // 只需要将 container 内的 DOM 清空即可 17 | container.innerHTML = '' 18 | } 19 | } 20 | 21 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 22 | container._vnode = vnode 23 | } 24 | 25 | function patch (n1, n2, container) { 26 | // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载 27 | if (!n1) { 28 | mountElement(n2, container) 29 | } else { 30 | // n1 存在,意味着更新,暂时省略 31 | } 32 | } 33 | 34 | function mountElement (vnode, container) { 35 | // 创建 DOM 元素 36 | const el = createElement(vnode.type) 37 | 38 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 39 | if (typeof vnode.children === 'string') { 40 | setElementText(el, vnode.children) 41 | } 42 | 43 | // 将元素添加到容器中 44 | insert(el, container) 45 | } 46 | 47 | return { 48 | render 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/6 渲染器的设计/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 渲染器的设计 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 54 | 55 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/1.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText 7 | } = options 8 | 9 | function render (vnode, container) { 10 | if (vnode) { 11 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 12 | patch(container._vnode, vnode, container) 13 | } else { 14 | if (container._vnode) { 15 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 16 | // 只需要将 container 内的 DOM 清空即可 17 | container.innerHTML = '' 18 | } 19 | } 20 | 21 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 22 | container._vnode = vnode 23 | } 24 | 25 | function patch (n1, n2, container) { 26 | // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载 27 | if (!n1) { 28 | mountElement(n2, container) 29 | } else { 30 | // n1 存在,意味着更新,暂时省略 31 | } 32 | } 33 | 34 | function mountElement (vnode, container) { 35 | // 创建 DOM 元素 36 | const el = createElement(vnode.type) 37 | 38 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 39 | if (typeof vnode.children === 'string') { 40 | setElementText(el, vnode.children) 41 | } else if (Array.isArray(vnode.children)) { 42 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 43 | vnode.children.forEach(child => { 44 | patch(null, child, el) 45 | }) 46 | } 47 | 48 | // 如果 vnode.props 存在,则处理 49 | if (vnode.props) { 50 | // 遍历 vnode.props,并将属性设置到元素上 51 | for (const key in vnode.props) { 52 | el[key] = vnode.props[key] 53 | } 54 | } 55 | 56 | // 将元素添加到容器中 57 | insert(el, container) 58 | } 59 | 60 | return { 61 | render 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/2.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText, 7 | patchProps 8 | } = options 9 | 10 | function render (vnode, container) { 11 | if (vnode) { 12 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 13 | patch(container._vnode, vnode, container) 14 | } else { 15 | if (container._vnode) { 16 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 17 | // 只需要将 container 内的 DOM 清空即可 18 | container.innerHTML = '' 19 | } 20 | } 21 | 22 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 23 | container._vnode = vnode 24 | } 25 | 26 | function patch (n1, n2, container) { 27 | // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载 28 | if (!n1) { 29 | mountElement(n2, container) 30 | } else { 31 | // n1 存在,意味着更新,暂时省略 32 | } 33 | } 34 | 35 | function shouldSetAsProps (el, key, value) { 36 | // 特殊处理 37 | if (key === 'form' && el.tagName === 'INPUT') return false 38 | // 兜底 39 | return key in el 40 | } 41 | 42 | function mountElement (vnode, container) { 43 | // 创建 DOM 元素 44 | const el = createElement(vnode.type) 45 | 46 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 47 | if (typeof vnode.children === 'string') { 48 | setElementText(el, vnode.children) 49 | } else if (Array.isArray(vnode.children)) { 50 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 51 | vnode.children.forEach(child => { 52 | patch(null, child, el) 53 | }) 54 | } 55 | 56 | // 如果 vnode.props 存在,则处理 57 | if (vnode.props) { 58 | // 遍历 vnode.props,并将属性设置到元素上 59 | for (const key in vnode.props) { 60 | // 调用 patchProps 即可 61 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 62 | } 63 | } 64 | 65 | // 将元素添加到容器中 66 | insert(el, container) 67 | } 68 | 69 | return { 70 | render 71 | } 72 | } 73 | 74 | const renderer = createRenderer({ 75 | // 用于创建元素 76 | createElement(tag) { 77 | return document.createElement(tag) 78 | }, 79 | // 用于设置元素的文本节点 80 | setElementText (el, text) { 81 | el.textContent = text 82 | }, 83 | // 用于在给定的 parent 下添加指定元素 84 | insert (el, parent, anchor = null) { 85 | parent.insertBefore(el, anchor) 86 | }, 87 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 88 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 89 | // 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties 设置 90 | if (shouldSetAsProps(el, key, nextValue)) { 91 | // 获取该 DOM Properties 的类型 92 | const type = typeof el[key] 93 | 94 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 95 | if (type === 'boolean' && nextValue === '') { 96 | el[key] = true 97 | } else { 98 | el[key] = nextValue 99 | } 100 | } else { 101 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 102 | el.setAttribute(key, nextValue) 103 | } 104 | } 105 | }) 106 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/3.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText, 7 | patchProps 8 | } = options 9 | 10 | function render (vnode, container) { 11 | if (vnode) { 12 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 13 | patch(container._vnode, vnode, container) 14 | } else { 15 | if (container._vnode) { 16 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 17 | // 只需要将 container 内的 DOM 清空即可 18 | container.innerHTML = '' 19 | } 20 | } 21 | 22 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 23 | container._vnode = vnode 24 | } 25 | 26 | function patch (n1, n2, container) { 27 | // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载 28 | if (!n1) { 29 | mountElement(n2, container) 30 | } else { 31 | // n1 存在,意味着更新,暂时省略 32 | } 33 | } 34 | 35 | function shouldSetAsProps (el, key, value) { 36 | // 特殊处理 37 | if (key === 'form' && el.tagName === 'INPUT') return false 38 | // 兜底 39 | return key in el 40 | } 41 | 42 | function mountElement (vnode, container) { 43 | // 创建 DOM 元素 44 | const el = createElement(vnode.type) 45 | 46 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 47 | if (typeof vnode.children === 'string') { 48 | setElementText(el, vnode.children) 49 | } else if (Array.isArray(vnode.children)) { 50 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 51 | vnode.children.forEach(child => { 52 | patch(null, child, el) 53 | }) 54 | } 55 | 56 | // 如果 vnode.props 存在,则处理 57 | if (vnode.props) { 58 | // 遍历 vnode.props,并将属性设置到元素上 59 | for (const key in vnode.props) { 60 | // 调用 patchProps 即可 61 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 62 | } 63 | } 64 | 65 | // 将元素添加到容器中 66 | insert(el, container) 67 | } 68 | 69 | return { 70 | render 71 | } 72 | } 73 | 74 | function normalizeClass(value) { 75 | let res = '' 76 | if (typeof value === 'string') { 77 | res = value 78 | } else if (Array.isArray(value)) { 79 | for (let i = 0; i < value.length; i++) { 80 | const normalized = normalizeClass(value[i]) 81 | if (normalized) { 82 | res += normalized + ' ' 83 | } 84 | } 85 | } else if (Object.prototype.toString.call(value) === '[object Object]') { 86 | for (const name in value) { 87 | if (value[name]) { 88 | res += name + ' ' 89 | } 90 | } 91 | } 92 | return res.trim() 93 | } 94 | 95 | const renderer = createRenderer({ 96 | // 用于创建元素 97 | createElement(tag) { 98 | return document.createElement(tag) 99 | }, 100 | // 用于设置元素的文本节点 101 | setElementText (el, text) { 102 | el.textContent = text 103 | }, 104 | // 用于在给定的 parent 下添加指定元素 105 | insert (el, parent, anchor = null) { 106 | parent.insertBefore(el, anchor) 107 | }, 108 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 109 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 110 | if (key === 'class') { 111 | el.className = nextValue || '' 112 | } else if (shouldSetAsProps(el, key, nextValue)) { 113 | // 获取该 DOM Properties 的类型 114 | const type = typeof el[key] 115 | 116 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 117 | if (type === 'boolean' && nextValue === '') { 118 | el[key] = true 119 | } else { 120 | el[key] = nextValue 121 | } 122 | } else { 123 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 124 | el.setAttribute(key, nextValue) 125 | } 126 | } 127 | }) 128 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/4.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText, 7 | patchProps 8 | } = options 9 | 10 | function render (vnode, container) { 11 | if (vnode) { 12 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 13 | patch(container._vnode, vnode, container) 14 | } else { 15 | if (container._vnode) { 16 | unmount(container._vnode) 17 | } 18 | } 19 | 20 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 21 | container._vnode = vnode 22 | } 23 | 24 | function unmount (vnode) { 25 | // 获取 el 的父元素 26 | const parent = vnode.el.parentNode 27 | // 调用父元素的 removeChild 移除元素 28 | if (parent) { 29 | parent.removeChild(vnode.el) 30 | } 31 | } 32 | 33 | function patch (n1, n2, container) { 34 | // n1 存在,则对比 n1 和 n2 的类型 35 | if (n1 && n1.type !== n2.type) { 36 | // 如果两者类型不一致,则直接将旧 vnode 卸载 37 | unmount(n1) 38 | n1 = null 39 | } 40 | 41 | // 代码运行到这里,证明 n1 和 n2 所描述的内容相同 42 | const { type } = n2 43 | // 如果 n2.type 是字符串类型,则它描述的是普通标签元素 44 | if (typeof type === 'string') { 45 | if (!n1) { 46 | mountElement(n2, container) 47 | } else { 48 | // patchElement(n1, n2) 49 | } 50 | } else if (typeof type === 'object') { 51 | // 如果 n2.type 是对象,则它描述的是组件 52 | } else if (type === 'xxx') { 53 | // 处理其他类型的 vnode 54 | } 55 | } 56 | 57 | function shouldSetAsProps (el, key, value) { 58 | // 特殊处理 59 | if (key === 'form' && el.tagName === 'INPUT') return false 60 | // 兜底 61 | return key in el 62 | } 63 | 64 | function mountElement (vnode, container) { 65 | // 创建 DOM 元素,并让 vnode.el 引用真实 DOM 元素 66 | const el = vnode.el = createElement(vnode.type) 67 | 68 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 69 | if (typeof vnode.children === 'string') { 70 | setElementText(el, vnode.children) 71 | } else if (Array.isArray(vnode.children)) { 72 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 73 | vnode.children.forEach(child => { 74 | patch(null, child, el) 75 | }) 76 | } 77 | 78 | // 如果 vnode.props 存在,则处理 79 | if (vnode.props) { 80 | // 遍历 vnode.props,并将属性设置到元素上 81 | for (const key in vnode.props) { 82 | // 调用 patchProps 即可 83 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 84 | } 85 | } 86 | 87 | // 将元素添加到容器中 88 | insert(el, container) 89 | } 90 | 91 | return { 92 | render 93 | } 94 | } 95 | 96 | function normalizeClass(value) { 97 | let res = '' 98 | if (typeof value === 'string') { 99 | res = value 100 | } else if (Array.isArray(value)) { 101 | for (let i = 0; i < value.length; i++) { 102 | const normalized = normalizeClass(value[i]) 103 | if (normalized) { 104 | res += normalized + ' ' 105 | } 106 | } 107 | } else if (Object.prototype.toString.call(value) === '[object Object]') { 108 | for (const name in value) { 109 | if (value[name]) { 110 | res += name + ' ' 111 | } 112 | } 113 | } 114 | return res.trim() 115 | } 116 | 117 | const renderer = createRenderer({ 118 | // 用于创建元素 119 | createElement(tag) { 120 | return document.createElement(tag) 121 | }, 122 | // 用于设置元素的文本节点 123 | setElementText (el, text) { 124 | el.textContent = text 125 | }, 126 | // 用于在给定的 parent 下添加指定元素 127 | insert (el, parent, anchor = null) { 128 | parent.insertBefore(el, anchor) 129 | }, 130 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 131 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 132 | if (key === 'class') { 133 | el.className = nextValue || '' 134 | } else if (shouldSetAsProps(el, key, nextValue)) { 135 | // 获取该 DOM Properties 的类型 136 | const type = typeof el[key] 137 | 138 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 139 | if (type === 'boolean' && nextValue === '') { 140 | el[key] = true 141 | } else { 142 | el[key] = nextValue 143 | } 144 | } else { 145 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 146 | el.setAttribute(key, nextValue) 147 | } 148 | } 149 | }) 150 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/5.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText, 7 | patchProps 8 | } = options 9 | 10 | function render (vnode, container) { 11 | if (vnode) { 12 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 13 | patch(container._vnode, vnode, container) 14 | } else { 15 | if (container._vnode) { 16 | unmount(container._vnode) 17 | } 18 | } 19 | 20 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 21 | container._vnode = vnode 22 | } 23 | 24 | function unmount (vnode) { 25 | // 获取 el 的父元素 26 | const parent = vnode.el.parentNode 27 | // 调用父元素的 removeChild 移除元素 28 | if (parent) { 29 | parent.removeChild(vnode.el) 30 | } 31 | } 32 | 33 | function patch (n1, n2, container) { 34 | // n1 存在,则对比 n1 和 n2 的类型 35 | if (n1 && n1.type !== n2.type) { 36 | // 如果两者类型不一致,则直接将旧 vnode 卸载 37 | unmount(n1) 38 | n1 = null 39 | } 40 | 41 | // 代码运行到这里,证明 n1 和 n2 所描述的内容相同 42 | const { type } = n2 43 | // 如果 n2.type 是字符串类型,则它描述的是普通标签元素 44 | if (typeof type === 'string') { 45 | if (!n1) { 46 | mountElement(n2, container) 47 | } else { 48 | // patchElement(n1, n2) 49 | } 50 | } else if (typeof type === 'object') { 51 | // 如果 n2.type 是对象,则它描述的是组件 52 | } else if (type === 'xxx') { 53 | // 处理其他类型的 vnode 54 | } 55 | } 56 | 57 | function shouldSetAsProps (el, key, value) { 58 | // 特殊处理 59 | if (key === 'form' && el.tagName === 'INPUT') return false 60 | // 兜底 61 | return key in el 62 | } 63 | 64 | function mountElement (vnode, container) { 65 | // 创建 DOM 元素,并让 vnode.el 引用真实 DOM 元素 66 | const el = vnode.el = createElement(vnode.type) 67 | 68 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 69 | if (typeof vnode.children === 'string') { 70 | setElementText(el, vnode.children) 71 | } else if (Array.isArray(vnode.children)) { 72 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 73 | vnode.children.forEach(child => { 74 | patch(null, child, el) 75 | }) 76 | } 77 | 78 | // 如果 vnode.props 存在,则处理 79 | if (vnode.props) { 80 | // 遍历 vnode.props,并将属性设置到元素上 81 | for (const key in vnode.props) { 82 | // 调用 patchProps 即可 83 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 84 | } 85 | } 86 | 87 | // 将元素添加到容器中 88 | insert(el, container) 89 | } 90 | 91 | return { 92 | render 93 | } 94 | } 95 | 96 | function normalizeClass(value) { 97 | let res = '' 98 | if (typeof value === 'string') { 99 | res = value 100 | } else if (Array.isArray(value)) { 101 | for (let i = 0; i < value.length; i++) { 102 | const normalized = normalizeClass(value[i]) 103 | if (normalized) { 104 | res += normalized + ' ' 105 | } 106 | } 107 | } else if (Object.prototype.toString.call(value) === '[object Object]') { 108 | for (const name in value) { 109 | if (value[name]) { 110 | res += name + ' ' 111 | } 112 | } 113 | } 114 | return res.trim() 115 | } 116 | 117 | const renderer = createRenderer({ 118 | // 用于创建元素 119 | createElement(tag) { 120 | return document.createElement(tag) 121 | }, 122 | // 用于设置元素的文本节点 123 | setElementText (el, text) { 124 | el.textContent = text 125 | }, 126 | // 用于在给定的 parent 下添加指定元素 127 | insert (el, parent, anchor = null) { 128 | parent.insertBefore(el, anchor) 129 | }, 130 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 131 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 132 | if (/^on/.test(key)) { 133 | const invokers = el._vei || (el._vei = {}) 134 | let invoker = invokers[key] 135 | const name = key.slice(2).toLowerCase() 136 | 137 | if (nextValue) { 138 | if (!invoker) { 139 | // 将事件处理函数缓存到 `el._vei[key]` 下,避免覆盖 140 | invoker = el._vei[key] = (e) => { 141 | // 如果 invoker.value 是一个数据,则遍历它并逐个调用事件处理函数 142 | if (Array.isArray(invoker.value)) { 143 | invoker.value.forEach(fn => fn(e)) 144 | } else { 145 | // 否则直接作用函数调用 146 | invoker.value(e) 147 | } 148 | } 149 | // 将真正的事件处理函数赋值给 invoker.value 150 | invoker.value = nextValue 151 | // 绑定 invoker 作为事件处理函数 152 | el.addEventListener(name, invoker) 153 | } else { 154 | // 如果 invoker 存在,意味着更新,只需要更新 invoker.value 的值即可 155 | invoker.value = nextValue 156 | } 157 | } else if (invoker) { 158 | // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定 159 | el.removeEventListener(name, invoker) 160 | } 161 | } else if (key === 'class') { 162 | el.className = nextValue || '' 163 | } else if (shouldSetAsProps(el, key, nextValue)) { 164 | // 获取该 DOM Properties 的类型 165 | const type = typeof el[key] 166 | 167 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 168 | if (type === 'boolean' && nextValue === '') { 169 | el[key] = true 170 | } else { 171 | el[key] = nextValue 172 | } 173 | } else { 174 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 175 | el.setAttribute(key, nextValue) 176 | } 177 | } 178 | }) 179 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/6.js: -------------------------------------------------------------------------------- 1 | function createRenderer (options) { 2 | // 通过 options 取得操作 DOM 的 API 3 | const { 4 | createElement, 5 | insert, 6 | setElementText, 7 | patchProps 8 | } = options 9 | 10 | function render (vnode, container) { 11 | if (vnode) { 12 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 13 | patch(container._vnode, vnode, container) 14 | } else { 15 | if (container._vnode) { 16 | unmount(container._vnode) 17 | } 18 | } 19 | 20 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 21 | container._vnode = vnode 22 | } 23 | 24 | function unmount (vnode) { 25 | // 获取 el 的父元素 26 | const parent = vnode.el.parentNode 27 | // 调用父元素的 removeChild 移除元素 28 | if (parent) { 29 | parent.removeChild(vnode.el) 30 | } 31 | } 32 | 33 | function patch (n1, n2, container) { 34 | // n1 存在,则对比 n1 和 n2 的类型 35 | if (n1 && n1.type !== n2.type) { 36 | // 如果两者类型不一致,则直接将旧 vnode 卸载 37 | unmount(n1) 38 | n1 = null 39 | } 40 | 41 | // 代码运行到这里,证明 n1 和 n2 所描述的内容相同 42 | const { type } = n2 43 | // 如果 n2.type 是字符串类型,则它描述的是普通标签元素 44 | if (typeof type === 'string') { 45 | if (!n1) { 46 | mountElement(n2, container) 47 | } else { 48 | patchElement(n1, n2) 49 | } 50 | } else if (typeof type === 'object') { 51 | // 如果 n2.type 是对象,则它描述的是组件 52 | } else if (type === 'xxx') { 53 | // 处理其他类型的 vnode 54 | } 55 | } 56 | 57 | function shouldSetAsProps (el, key, value) { 58 | // 特殊处理 59 | if (key === 'form' && el.tagName === 'INPUT') return false 60 | // 兜底 61 | return key in el 62 | } 63 | 64 | function mountElement (vnode, container) { 65 | // 创建 DOM 元素,并让 vnode.el 引用真实 DOM 元素 66 | const el = vnode.el = createElement(vnode.type) 67 | 68 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 69 | if (typeof vnode.children === 'string') { 70 | setElementText(el, vnode.children) 71 | } else if (Array.isArray(vnode.children)) { 72 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 73 | vnode.children.forEach(child => { 74 | patch(null, child, el) 75 | }) 76 | } 77 | 78 | // 如果 vnode.props 存在,则处理 79 | if (vnode.props) { 80 | // 遍历 vnode.props,并将属性设置到元素上 81 | for (const key in vnode.props) { 82 | // 调用 patchProps 即可 83 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 84 | } 85 | } 86 | 87 | // 将元素添加到容器中 88 | insert(el, container) 89 | } 90 | 91 | function patchElement (n1, n2) { 92 | const el = n2.el = n1.el 93 | const oldProps = n1.props 94 | const newProps = n2.props 95 | 96 | // 第一步:更新 props 97 | for (const key in newProps) { 98 | if (newProps[key] !== oldProps[key]) { 99 | patchProps(el, key, oldProps[key], newProps[key]) 100 | } 101 | } 102 | for (const key in oldProps) { 103 | if (!key in newProps) { 104 | patchProps(el, key, oldProps[key], null) 105 | } 106 | } 107 | 108 | // 第二步:更新 children 109 | patchChildren(n1, n2, el) 110 | } 111 | 112 | function patchChildren (n1, n2, container) { 113 | // 判断新子节点的类型是否是文本节点 114 | if (typeof n2.children === 'string') { 115 | // 旧子节点的类型有三种可能 116 | // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况什么都不需要做 117 | if (Array.isArray(n1.children)) { 118 | n1.children.forEach(c => unmount(c)) 119 | } 120 | // 最后将新的文本节点内容设置给容器元素 121 | setElementText(container, n2.children) 122 | } else if (Array.isArray(n2.children)) { 123 | // 如果新子节点的类型是一组子节点 124 | // 判断旧子节点是否也是一组子节点 125 | if (Array.isArray(n1.children)) { 126 | // **这里涉及到核心的 diff 算法** 127 | 128 | // 我们可以先用一种傻方式来处理 129 | // 就是将旧节点全卸载,重新挂载新的一组子节点 130 | n1.children.forEach(c => unmount(c)) 131 | n2.children.forEach(c => patch(null, c, container)) 132 | } else { 133 | // 此时: 134 | // 旧子节点要么是文本子节,要么不存在 135 | // 无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载即可 136 | setElementText(container, '') 137 | n2.children.forEach(c => patch(null, c, container)) 138 | } 139 | } else { 140 | // 代码运行到这里,说明新的子节点不存在 141 | // 如果旧的子节点是一组子节点,只需要逐个卸载即可 142 | if (Array.isArray(n1.children)) { 143 | n1.children.forEach(c => unmount(c)) 144 | } else if (typeof n1.children === 'string') { 145 | // 旧子节点是文本节点,清空内容即可 146 | setElementText(container, '') 147 | } 148 | // 如果也没有旧子节点,那么什么都不需要做 149 | } 150 | } 151 | 152 | return { 153 | render 154 | } 155 | } 156 | 157 | function normalizeClass(value) { 158 | let res = '' 159 | if (typeof value === 'string') { 160 | res = value 161 | } else if (Array.isArray(value)) { 162 | for (let i = 0; i < value.length; i++) { 163 | const normalized = normalizeClass(value[i]) 164 | if (normalized) { 165 | res += normalized + ' ' 166 | } 167 | } 168 | } else if (Object.prototype.toString.call(value) === '[object Object]') { 169 | for (const name in value) { 170 | if (value[name]) { 171 | res += name + ' ' 172 | } 173 | } 174 | } 175 | return res.trim() 176 | } 177 | 178 | const renderer = createRenderer({ 179 | // 用于创建元素 180 | createElement(tag) { 181 | return document.createElement(tag) 182 | }, 183 | // 用于设置元素的文本节点 184 | setElementText (el, text) { 185 | el.textContent = text 186 | }, 187 | // 用于在给定的 parent 下添加指定元素 188 | insert (el, parent, anchor = null) { 189 | parent.insertBefore(el, anchor) 190 | }, 191 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 192 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 193 | if (/^on/.test(key)) { 194 | const invokers = el._vei || (el._vei = {}) 195 | let invoker = invokers[key] 196 | const name = key.slice(2).toLowerCase() 197 | 198 | if (nextValue) { 199 | if (!invoker) { 200 | // 将事件处理函数缓存到 `el._vei[key]` 下,避免覆盖 201 | invoker = el._vei[key] = (e) => { 202 | // 如果事件发生的时间 早于 事件处理函数被绑定的时间 203 | // 则不执行事件处理函数 204 | if (e.timeStamp < invoker.attached) return 205 | 206 | // 如果 invoker.value 是一个数组,则遍历它并逐个调用事件处理函数 207 | if (Array.isArray(invoker.value)) { 208 | invoker.value.forEach(fn => fn(e)) 209 | } else { 210 | // 否则直接作用函数调用 211 | invoker.value(e) 212 | } 213 | } 214 | // 将真正的事件处理函数赋值给 invoker.value 215 | invoker.value = nextValue 216 | // 添加 invoker.attached 属性,存储事件处理函数被绑定的时间 217 | invoker.attached = performance.now() 218 | // 绑定 invoker 作为事件处理函数 219 | el.addEventListener(name, invoker) 220 | } else { 221 | // 如果 invoker 存在,意味着更新,只需要更新 invoker.value 的值即可 222 | invoker.value = nextValue 223 | } 224 | } else if (invoker) { 225 | // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定 226 | el.removeEventListener(name, invoker) 227 | } 228 | } else if (key === 'class') { 229 | el.className = nextValue || '' 230 | } else if (shouldSetAsProps(el, key, nextValue)) { 231 | // 获取该 DOM Properties 的类型 232 | const type = typeof el[key] 233 | 234 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 235 | if (type === 'boolean' && nextValue === '') { 236 | el[key] = true 237 | } else { 238 | el[key] = nextValue 239 | } 240 | } else { 241 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 242 | el.setAttribute(key, nextValue) 243 | } 244 | } 245 | }) 246 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/7.js: -------------------------------------------------------------------------------- 1 | const VNODE_TYPES = { 2 | Text: Symbol(), 3 | Comment: Symbol(), 4 | Fragment: Symbol() 5 | } 6 | 7 | function createRenderer (options) { 8 | // 通过 options 取得操作 DOM 的 API 9 | const { 10 | createElement, 11 | insert, 12 | setElementText, 13 | patchProps, 14 | createText, 15 | setText, 16 | createComment, 17 | setComment 18 | } = options 19 | 20 | function render (vnode, container) { 21 | if (vnode) { 22 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 23 | patch(container._vnode, vnode, container) 24 | } else { 25 | if (container._vnode) { 26 | unmount(container._vnode) 27 | } 28 | } 29 | 30 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 31 | container._vnode = vnode 32 | } 33 | 34 | function unmount (vnode) { 35 | if (vnode.type === VNODE_TYPES.Fragment) { 36 | vnode.children.forEach(c => unmount(c)) 37 | return 38 | } 39 | 40 | // 获取 el 的父元素 41 | const parent = vnode.el.parentNode 42 | // 调用父元素的 removeChild 移除元素 43 | if (parent) { 44 | parent.removeChild(vnode.el) 45 | } 46 | } 47 | 48 | function patch (n1, n2, container) { 49 | // n1 存在,则对比 n1 和 n2 的类型 50 | if (n1 && n1.type !== n2.type) { 51 | // 如果两者类型不一致,则直接将旧 vnode 卸载 52 | unmount(n1) 53 | n1 = null 54 | } 55 | 56 | // 代码运行到这里,证明 n1 和 n2 所描述的内容相同 57 | const { type } = n2 58 | // 如果 n2.type 是字符串类型,则它描述的是普通标签元素 59 | if (typeof type === 'string') { 60 | if (!n1) { 61 | mountElement(n2, container) 62 | } else { 63 | patchElement(n1, n2) 64 | } 65 | } else if (typeof type === 'object') { 66 | // 如果 n2.type 是对象,则它描述的是组件 67 | } else if (type === VNODE_TYPES.Text) { 68 | // 处理文本节点 69 | if (!n1) { 70 | // 如果没有旧节点,则进行挂载 71 | const el = n2.el = createText(n2.children) 72 | // 将文本节点插入到容器中 73 | insert(el, container) 74 | } else { 75 | // 如果旧 vnode 存在,只需要使用新文本节点的内容替换更新旧文本节点即可 76 | const el = n2.el = n1.el 77 | if (n2.children !== n1.children) { 78 | setText(el, n2.children) 79 | } 80 | } 81 | } else if (type === VNODE_TYPES.Comment) { 82 | if (!n1) { 83 | const el = n2.el = createComment(n2.children) 84 | insert(el, container) 85 | } else { 86 | const el = n2.el = n1.el 87 | if (n2.children !== n1.children) { 88 | setComment(el, n2.children) 89 | } 90 | } 91 | } else if (type === VNODE_TYPES.Fragment) { 92 | // 处理 Fragment 类型的 vnode 93 | if (!n1) { 94 | n2.children.forEach(child => patch(null, child, container)) 95 | } else { 96 | // 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可 97 | patchChildren(n1, n2, container) 98 | } 99 | } 100 | } 101 | 102 | function shouldSetAsProps (el, key, value) { 103 | // 特殊处理 104 | if (key === 'form' && el.tagName === 'INPUT') return false 105 | // 兜底 106 | return key in el 107 | } 108 | 109 | function mountElement (vnode, container) { 110 | // 创建 DOM 元素,并让 vnode.el 引用真实 DOM 元素 111 | const el = vnode.el = createElement(vnode.type) 112 | 113 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 114 | if (typeof vnode.children === 'string') { 115 | setElementText(el, vnode.children) 116 | } else if (Array.isArray(vnode.children)) { 117 | // 如果 children 是一个数组,则遍历每一个子节点,并调用 patch 函数挂载它们 118 | vnode.children.forEach(child => { 119 | patch(null, child, el) 120 | }) 121 | } 122 | 123 | // 如果 vnode.props 存在,则处理 124 | if (vnode.props) { 125 | // 遍历 vnode.props,并将属性设置到元素上 126 | for (const key in vnode.props) { 127 | // 调用 patchProps 即可 128 | patchProps(el, key, null, vnode.props[key], shouldSetAsProps) 129 | } 130 | } 131 | 132 | // 将元素添加到容器中 133 | insert(el, container) 134 | } 135 | 136 | function patchElement (n1, n2) { 137 | const el = n2.el = n1.el 138 | const oldProps = n1.props 139 | const newProps = n2.props 140 | 141 | // 第一步:更新 props 142 | for (const key in newProps) { 143 | if (newProps[key] !== oldProps[key]) { 144 | patchProps(el, key, oldProps[key], newProps[key]) 145 | } 146 | } 147 | for (const key in oldProps) { 148 | if (!key in newProps) { 149 | patchProps(el, key, oldProps[key], null) 150 | } 151 | } 152 | 153 | // 第二步:更新 children 154 | patchChildren(n1, n2, el) 155 | } 156 | 157 | function patchChildren (n1, n2, container) { 158 | // 判断新子节点的类型是否是文本节点 159 | if (typeof n2.children === 'string') { 160 | // 旧子节点的类型有三种可能 161 | // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况什么都不需要做 162 | if (Array.isArray(n1.children)) { 163 | n1.children.forEach(c => unmount(c)) 164 | } 165 | // 最后将新的文本节点内容设置给容器元素 166 | setElementText(container, n2.children) 167 | } else if (Array.isArray(n2.children)) { 168 | // 如果新子节点的类型是一组子节点 169 | // 判断旧子节点是否也是一组子节点 170 | if (Array.isArray(n1.children)) { 171 | // **这里涉及到核心的 diff 算法** 172 | 173 | // 我们可以先用一种傻方式来处理 174 | // 就是将旧节点全卸载,重新挂载新的一组子节点 175 | n1.children.forEach(c => unmount(c)) 176 | n2.children.forEach(c => patch(null, c, container)) 177 | } else { 178 | // 此时: 179 | // 旧子节点要么是文本子节,要么不存在 180 | // 无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载即可 181 | setElementText(container, '') 182 | n2.children.forEach(c => patch(null, c, container)) 183 | } 184 | } else { 185 | // 代码运行到这里,说明新的子节点不存在 186 | // 如果旧的子节点是一组子节点,只需要逐个卸载即可 187 | if (Array.isArray(n1.children)) { 188 | n1.children.forEach(c => unmount(c)) 189 | } else if (typeof n1.children === 'string') { 190 | // 旧子节点是文本节点,清空内容即可 191 | setElementText(container, '') 192 | } 193 | // 如果也没有旧子节点,那么什么都不需要做 194 | } 195 | } 196 | 197 | return { 198 | render 199 | } 200 | } 201 | 202 | function normalizeClass(value) { 203 | let res = '' 204 | if (typeof value === 'string') { 205 | res = value 206 | } else if (Array.isArray(value)) { 207 | for (let i = 0; i < value.length; i++) { 208 | const normalized = normalizeClass(value[i]) 209 | if (normalized) { 210 | res += normalized + ' ' 211 | } 212 | } 213 | } else if (Object.prototype.toString.call(value) === '[object Object]') { 214 | for (const name in value) { 215 | if (value[name]) { 216 | res += name + ' ' 217 | } 218 | } 219 | } 220 | return res.trim() 221 | } 222 | 223 | const renderer = createRenderer({ 224 | // 用于创建元素 225 | createElement(tag) { 226 | return document.createElement(tag) 227 | }, 228 | // 用于设置元素的文本节点 229 | setElementText (el, text) { 230 | el.textContent = text 231 | }, 232 | // 用于在给定的 parent 下添加指定元素 233 | insert (el, parent, anchor = null) { 234 | parent.insertBefore(el, anchor) 235 | }, 236 | // 将属性设置相关的操作封装到 patchProps 函数中,并作为渲染器选项传递 237 | patchProps (el, key, prevValue, nextValue, shouldSetAsProps) { 238 | if (/^on/.test(key)) { 239 | const invokers = el._vei || (el._vei = {}) 240 | let invoker = invokers[key] 241 | const name = key.slice(2).toLowerCase() 242 | 243 | if (nextValue) { 244 | if (!invoker) { 245 | // 将事件处理函数缓存到 `el._vei[key]` 下,避免覆盖 246 | invoker = el._vei[key] = (e) => { 247 | // 如果事件发生的时间 早于 事件处理函数被绑定的时间 248 | // 则不执行事件处理函数 249 | if (e.timeStamp < invoker.attached) return 250 | 251 | // 如果 invoker.value 是一个数组,则遍历它并逐个调用事件处理函数 252 | if (Array.isArray(invoker.value)) { 253 | invoker.value.forEach(fn => fn(e)) 254 | } else { 255 | // 否则直接作用函数调用 256 | invoker.value(e) 257 | } 258 | } 259 | // 将真正的事件处理函数赋值给 invoker.value 260 | invoker.value = nextValue 261 | // 添加 invoker.attached 属性,存储事件处理函数被绑定的时间 262 | invoker.attached = performance.now() 263 | // 绑定 invoker 作为事件处理函数 264 | el.addEventListener(name, invoker) 265 | } else { 266 | // 如果 invoker 存在,意味着更新,只需要更新 invoker.value 的值即可 267 | invoker.value = nextValue 268 | } 269 | } else if (invoker) { 270 | // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定 271 | el.removeEventListener(name, invoker) 272 | } 273 | } else if (key === 'class') { 274 | el.className = nextValue || '' 275 | } else if (shouldSetAsProps(el, key, nextValue)) { 276 | // 获取该 DOM Properties 的类型 277 | const type = typeof el[key] 278 | 279 | // 如果是布尔类型,并且值是空字符串,则将值矫正为 true 280 | if (type === 'boolean' && nextValue === '') { 281 | el[key] = true 282 | } else { 283 | el[key] = nextValue 284 | } 285 | } else { 286 | // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性 287 | el.setAttribute(key, nextValue) 288 | } 289 | }, 290 | // 创建文本节点 291 | createText (text) { 292 | return document.createTextNode(text) 293 | }, 294 | // 设置文本节点的内容 295 | setText(el, text) { 296 | el.nodeValue = text 297 | }, 298 | // 创建注释节点 299 | createComment (comment) { 300 | return document.createComment(comment) 301 | }, 302 | // 设置注释节点的内容 303 | setComment (el, text) { 304 | el.nodeValue = text 305 | } 306 | }) 307 | -------------------------------------------------------------------------------- /examples/7 挂载与更新/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 挂载与更新 8 | 9 | 10 |
11 | 12 | 13 | 14 | 33 | 34 | 52 | 53 | 84 | 85 | 86 | 118 | 119 | -------------------------------------------------------------------------------- /examples/8 简单的 Diff 算法/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 简单的 Diff 算法 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 55 | 56 | -------------------------------------------------------------------------------- /examples/9 双端 Diff 算法/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 简单的 Diff 算法 8 | 9 | 10 |
11 | 12 | 13 | 42 | 43 | 72 | 73 | 101 | 102 | 103 | 104 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /imgs/DOM properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/DOM properties.png -------------------------------------------------------------------------------- /imgs/keepAlive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/keepAlive.png -------------------------------------------------------------------------------- /imgs/patchElement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/patchElement.png -------------------------------------------------------------------------------- /imgs/target-key-effect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/target-key-effect.png -------------------------------------------------------------------------------- /imgs/transition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/transition.png -------------------------------------------------------------------------------- /imgs/事件冒泡-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/事件冒泡-1.png -------------------------------------------------------------------------------- /imgs/事件冒泡-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/事件冒泡-2.png -------------------------------------------------------------------------------- /imgs/双端diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/1.png -------------------------------------------------------------------------------- /imgs/双端diff/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/10.png -------------------------------------------------------------------------------- /imgs/双端diff/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/11.png -------------------------------------------------------------------------------- /imgs/双端diff/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/12.png -------------------------------------------------------------------------------- /imgs/双端diff/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/13.png -------------------------------------------------------------------------------- /imgs/双端diff/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/14.png -------------------------------------------------------------------------------- /imgs/双端diff/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/15.png -------------------------------------------------------------------------------- /imgs/双端diff/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/16.png -------------------------------------------------------------------------------- /imgs/双端diff/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/17.png -------------------------------------------------------------------------------- /imgs/双端diff/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/18.png -------------------------------------------------------------------------------- /imgs/双端diff/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/19.png -------------------------------------------------------------------------------- /imgs/双端diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/2.png -------------------------------------------------------------------------------- /imgs/双端diff/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/20.png -------------------------------------------------------------------------------- /imgs/双端diff/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/21.png -------------------------------------------------------------------------------- /imgs/双端diff/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/22.png -------------------------------------------------------------------------------- /imgs/双端diff/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/23.png -------------------------------------------------------------------------------- /imgs/双端diff/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/24.png -------------------------------------------------------------------------------- /imgs/双端diff/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/25.png -------------------------------------------------------------------------------- /imgs/双端diff/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/26.png -------------------------------------------------------------------------------- /imgs/双端diff/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/27.png -------------------------------------------------------------------------------- /imgs/双端diff/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/28.png -------------------------------------------------------------------------------- /imgs/双端diff/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/29.png -------------------------------------------------------------------------------- /imgs/双端diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/3.png -------------------------------------------------------------------------------- /imgs/双端diff/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/30.png -------------------------------------------------------------------------------- /imgs/双端diff/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/31.png -------------------------------------------------------------------------------- /imgs/双端diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/4.png -------------------------------------------------------------------------------- /imgs/双端diff/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/5.png -------------------------------------------------------------------------------- /imgs/双端diff/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/6.png -------------------------------------------------------------------------------- /imgs/双端diff/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/7.png -------------------------------------------------------------------------------- /imgs/双端diff/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/8.png -------------------------------------------------------------------------------- /imgs/双端diff/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/双端diff/9.png -------------------------------------------------------------------------------- /imgs/同构渲染/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/同构渲染/1.png -------------------------------------------------------------------------------- /imgs/同构渲染/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/同构渲染/2.png -------------------------------------------------------------------------------- /imgs/同构渲染/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/同构渲染/3.png -------------------------------------------------------------------------------- /imgs/同构渲染/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/同构渲染/4.png -------------------------------------------------------------------------------- /imgs/同构渲染/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/同构渲染/5.png -------------------------------------------------------------------------------- /imgs/快速diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/1.png -------------------------------------------------------------------------------- /imgs/快速diff/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/10.png -------------------------------------------------------------------------------- /imgs/快速diff/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/11.png -------------------------------------------------------------------------------- /imgs/快速diff/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/12.png -------------------------------------------------------------------------------- /imgs/快速diff/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/13.png -------------------------------------------------------------------------------- /imgs/快速diff/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/14.png -------------------------------------------------------------------------------- /imgs/快速diff/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/15.png -------------------------------------------------------------------------------- /imgs/快速diff/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/16.png -------------------------------------------------------------------------------- /imgs/快速diff/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/17.png -------------------------------------------------------------------------------- /imgs/快速diff/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/18.png -------------------------------------------------------------------------------- /imgs/快速diff/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/19.png -------------------------------------------------------------------------------- /imgs/快速diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/2.png -------------------------------------------------------------------------------- /imgs/快速diff/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/20.png -------------------------------------------------------------------------------- /imgs/快速diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/3.png -------------------------------------------------------------------------------- /imgs/快速diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/4.png -------------------------------------------------------------------------------- /imgs/快速diff/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/5.png -------------------------------------------------------------------------------- /imgs/快速diff/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/6.png -------------------------------------------------------------------------------- /imgs/快速diff/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/7.png -------------------------------------------------------------------------------- /imgs/快速diff/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/8.png -------------------------------------------------------------------------------- /imgs/快速diff/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/快速diff/9.png -------------------------------------------------------------------------------- /imgs/由内向外的执行方式.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/由内向外的执行方式.png -------------------------------------------------------------------------------- /imgs/简单diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/1.png -------------------------------------------------------------------------------- /imgs/简单diff/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/10.png -------------------------------------------------------------------------------- /imgs/简单diff/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/11.png -------------------------------------------------------------------------------- /imgs/简单diff/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/12.png -------------------------------------------------------------------------------- /imgs/简单diff/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/13.png -------------------------------------------------------------------------------- /imgs/简单diff/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/14.png -------------------------------------------------------------------------------- /imgs/简单diff/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/15.png -------------------------------------------------------------------------------- /imgs/简单diff/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/16.png -------------------------------------------------------------------------------- /imgs/简单diff/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/17.png -------------------------------------------------------------------------------- /imgs/简单diff/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/18.png -------------------------------------------------------------------------------- /imgs/简单diff/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/19.png -------------------------------------------------------------------------------- /imgs/简单diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/2.png -------------------------------------------------------------------------------- /imgs/简单diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/3.png -------------------------------------------------------------------------------- /imgs/简单diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/4.png -------------------------------------------------------------------------------- /imgs/简单diff/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/5.png -------------------------------------------------------------------------------- /imgs/简单diff/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/6.png -------------------------------------------------------------------------------- /imgs/简单diff/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/7.png -------------------------------------------------------------------------------- /imgs/简单diff/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/8.png -------------------------------------------------------------------------------- /imgs/简单diff/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/简单diff/9.png -------------------------------------------------------------------------------- /imgs/编译器核心/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/1.png -------------------------------------------------------------------------------- /imgs/编译器核心/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/10.png -------------------------------------------------------------------------------- /imgs/编译器核心/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/11.png -------------------------------------------------------------------------------- /imgs/编译器核心/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/12.png -------------------------------------------------------------------------------- /imgs/编译器核心/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/13.png -------------------------------------------------------------------------------- /imgs/编译器核心/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/14.png -------------------------------------------------------------------------------- /imgs/编译器核心/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/15.png -------------------------------------------------------------------------------- /imgs/编译器核心/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/16.png -------------------------------------------------------------------------------- /imgs/编译器核心/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/17.png -------------------------------------------------------------------------------- /imgs/编译器核心/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/18.png -------------------------------------------------------------------------------- /imgs/编译器核心/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/19.png -------------------------------------------------------------------------------- /imgs/编译器核心/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/2.png -------------------------------------------------------------------------------- /imgs/编译器核心/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/20.png -------------------------------------------------------------------------------- /imgs/编译器核心/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/21.png -------------------------------------------------------------------------------- /imgs/编译器核心/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/22.png -------------------------------------------------------------------------------- /imgs/编译器核心/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/23.png -------------------------------------------------------------------------------- /imgs/编译器核心/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/3.png -------------------------------------------------------------------------------- /imgs/编译器核心/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/4.png -------------------------------------------------------------------------------- /imgs/编译器核心/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/5.png -------------------------------------------------------------------------------- /imgs/编译器核心/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/6.png -------------------------------------------------------------------------------- /imgs/编译器核心/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/7.png -------------------------------------------------------------------------------- /imgs/编译器核心/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/8.png -------------------------------------------------------------------------------- /imgs/编译器核心/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/编译器核心/9.png -------------------------------------------------------------------------------- /imgs/解析器/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/1.png -------------------------------------------------------------------------------- /imgs/解析器/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/10.png -------------------------------------------------------------------------------- /imgs/解析器/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/11.png -------------------------------------------------------------------------------- /imgs/解析器/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/12.png -------------------------------------------------------------------------------- /imgs/解析器/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/13.png -------------------------------------------------------------------------------- /imgs/解析器/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/14.png -------------------------------------------------------------------------------- /imgs/解析器/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/15.png -------------------------------------------------------------------------------- /imgs/解析器/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/16.png -------------------------------------------------------------------------------- /imgs/解析器/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/17.png -------------------------------------------------------------------------------- /imgs/解析器/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/18.png -------------------------------------------------------------------------------- /imgs/解析器/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/19.png -------------------------------------------------------------------------------- /imgs/解析器/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/2.png -------------------------------------------------------------------------------- /imgs/解析器/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/3.png -------------------------------------------------------------------------------- /imgs/解析器/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/4.png -------------------------------------------------------------------------------- /imgs/解析器/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/5.png -------------------------------------------------------------------------------- /imgs/解析器/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/6.png -------------------------------------------------------------------------------- /imgs/解析器/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/7.png -------------------------------------------------------------------------------- /imgs/解析器/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/8.png -------------------------------------------------------------------------------- /imgs/解析器/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humandetail/VueJS-design-and-implementation/fe32a8ab9e45643ea0ae352f79f1c13c825838a3/imgs/解析器/9.png -------------------------------------------------------------------------------- /notes/1.框架设计的核心要素.md: -------------------------------------------------------------------------------- 1 | # 框架设计的核心要素 2 | 3 | ## 提升用户的开发体验 4 | 5 | 1. 必要的警告信息 6 | 7 | 合理的警告信息,能让用户更清晰且快速地定位问题。 8 | 9 | ```js 10 | const mount = function (el) { 11 | document.querySelector(el).appendChild(document.createTextNode('Hello world.')) 12 | } 13 | 14 | mount('#not-exists') // Uncaught TypeError: Cannot read properties of null (reading 'appendChild') 15 | ``` 16 | 17 | 根据此信息,我们可能很难去定位问题出在哪里。所以在设计框架时,需要提供更为有用的信息来帮助用户定位问题。 18 | 19 | ```js 20 | const mount = function (el) { 21 | const container = document.querySelector(el) 22 | 23 | if (!container) { 24 | throw new Error( 25 | `Target selector "${el}" returned null.` 26 | ) 27 | } 28 | container.appendChild(document.createTextNode('Hello world.')) 29 | } 30 | 31 | mount('#not-exists') 32 | ``` 33 | 34 | 2. 直观的输出内容 35 | 36 | 在 Vue.js 3 中,当我们在控制台打印一个 Ref 数据时: 37 | 38 | ```js 39 | const count = Vue.ref(0) 40 | console.log(count) // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0} 41 | ``` 42 | 43 | 这样的数据阅读起来是不友好的。 44 | 45 | 所以在 Vue.js 3 的源码中提供了 `initCustomFormatter` 的函数用于在开发环境初始化自定义的 formatter。 46 | 47 | 在 Chrome 浏览器中可以通过 `设置 -> 控制台 -> 启用自定义格式设置工具` 来开启。 48 | 49 | ```js 50 | const count = Vue.ref(0) 51 | console.log(count) // Ref<0> 52 | ``` 53 | 54 | ## 控制框架代码的体积 55 | 56 | 在 Vue.js 3 的源码中,通过一些环境常量来决定某些代码只会在特定的环境中生效: 57 | 58 | ```js 59 | if (__DEV__ && !res) { 60 | warn( 61 | `Failed to mount app: mount target selector "${container}" returned null.` 62 | ) 63 | } 64 | ``` 65 | 66 | 这里的 `__DEV__` 常量实际上是通过 rollup.js 的插件配置来预定义的。 67 | 68 | 在开发环境的版本中 `__DEV__` 会被设置为 true,上面的代码就相当于: 69 | 70 | ```js 71 | if (true && !res) { 72 | warn( 73 | `Failed to mount app: mount target selector "${container}" returned null.` 74 | ) 75 | } 76 | ``` 77 | 78 | 而在生产版本中 `__DEV__` 会被设置为 false,上面的代码等价于: 79 | 80 | ```js 81 | if (false && !res) { 82 | warn( 83 | `Failed to mount app: mount target selector "${container}" returned null.` 84 | ) 85 | } 86 | ``` 87 | 88 | 我们可以发现,这段代码它的判断条件假,所以它永远不会被执行,这种代码称为 dead code,在构建资源的时候会被移除。 89 | 90 | ## 良好的 Tree-Shaking 91 | 92 | 仅仅通过环境常量的形式来排除 dead code 是不够的。 93 | 94 | ```js 95 | // utils.js 96 | export function foo (obj) { 97 | obj && obj.foo 98 | } 99 | 100 | export function bar () { 101 | obj && obj.bar 102 | } 103 | 104 | // input.js 105 | import { foo } from 'utils' 106 | foo() 107 | ``` 108 | 109 | 像上面的这种情况,bar() 并未被使用到,那么它就不应该出现在打包后的代码中。要做到这一点,我们就需要 Tree-Shaking。 110 | 111 | 简单地说,Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是 dead code。 112 | 113 | 想要实现 Tree-Shaking,必须要满足一个条件:模块必须是 ESM(ES Module)。因为 Tree-Shaking 依赖 ESM 的静态结构。我们以 rollup.js 为例看看 Tree-Shaking 如何工作: 114 | 115 | 还是上面的代码,我们通过 rollup.js 进行构建 116 | 117 | ```shell 118 | npx rollup input.js -f esm -o bundle.js 119 | ``` 120 | 121 | 我们可以看到 bundle.js 中并未包含 `bar()` 函数的内容: 122 | 123 | ```js 124 | // bundle.js 125 | function foo (obj) { 126 | obj && obj.foo; 127 | } 128 | 129 | foo(); 130 | ``` 131 | 132 | 这就说明 Tree-Shaking 起了作用。由于我们并未使用到 `bar()` 函数的内容,因此它被作为 dead code 删除了。但是我们通过代码可以发现,`foo()` 函数似乎没什么作用:它仅仅是读取了对象中的值。把这段代码删了也不会对我们的程序产生影响,那么 rollup.js 为什么不把这段代码也作为 dead code 删除掉呢? 133 | 134 | 我们把 input.js 中的代码改造一下: 135 | 136 | ```js 137 | import { foo } from './utils' 138 | 139 | const log = { 140 | count: 0 141 | } 142 | 143 | const obj = Proxy({}, { 144 | get (target, prop) { 145 | log.count++ 146 | return target[prop] 147 | } 148 | }) 149 | 150 | foo(obj) 151 | ``` 152 | 153 | 在我们读取 obj 中的某个属性时,会记录它的读取次数。 154 | 155 | 这也就是 Tree-Shaking 中的第二个关键点——副作用。`foo()` 函数的调用,会产生副作用,那么就不能将其移除。 156 | 157 | 会不会产生副作用,我们只有在代码运行的时候才会知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 是非常困难的。 158 | 159 | 因此,像 rollup.js 这类工具提供了一个机制,让我们开发明确地告诉构建工具某些代码是不会产生副作用,你可以放心地移除它。我们修改一下 input.js: 160 | 161 | ```js 162 | import { foo } from './utils' 163 | 164 | /*#__PURE__*/ foo() 165 | 166 | ``` 167 | 168 | 通过 `/*#__PURE__*/` 注释告诉构建工具,我这段代码是纯的,不会产生副作用。此时我们再执行构建命令,会发现 `bundle.js` 里面是空的,这说明 Tree-Shaking 生效了。 169 | 170 | ## 框架应输出什么样的构建产物 171 | 172 | 我们需要针对不同的运行环境提供不同的构建产物。通过在 rollup.config.js 中配置 173 | 174 | 1. `iife`:script 标签直接引用 175 | 2. `esm`:` 308 | ``` 309 | 310 | 其中 `` 中的内容就是模板内容,编译器会把该内容编译成渲染函数并添加到 ` 114 | ``` 115 | 116 | 这样做会导致响应丢失。其表现是,当我们修改响应式数据的值时,不会触发组件重新渲染。 117 | 118 | 那么为什么会导致响应式丢失呢?这是由展开运算符导致的: 119 | 120 | ```js 121 | return { 122 | ...obj 123 | } 124 | // 等价于 125 | return { 126 | foo: 1, 127 | bar: 2 128 | } 129 | ``` 130 | 131 | 可以看到,这其实就是返回了一个普通对象,它不具有任何响应式能力。 132 | 133 | 我们可以用另一个方式来描述响应丢失问题: 134 | 135 | ```js 136 | const obj = reactive({ foo: 1, bar: 2 }) 137 | 138 | const newObj = { 139 | ...obj 140 | } 141 | 142 | effect(() => { 143 | console.log(newObj.foo) 144 | }) 145 | 146 | obj.foo = 10 147 | ``` 148 | 149 | 很显然,我们在修改 obj.foo 的值时,不会触发副作用函数重新执行。 150 | 151 | 如何解决这个问题呢?换句话说,有没有办法能够帮助我们实现:在副作用函数内,即使通过普通对象 newObj 来访问属性值,也能够建立响应联系?其实是可以的: 152 | 153 | ```js 154 | const obj = reactive({ foo: 1, bar: 2 }) 155 | 156 | const newObj = { 157 | foo: { 158 | get value () { 159 | return obj.foo 160 | } 161 | }, 162 | 163 | var: { 164 | get value () { 165 | return obj.bar 166 | } 167 | } 168 | } 169 | 170 | effect(() => { 171 | console.log(newObj.foo) 172 | }) 173 | 174 | obj.foo = 10 175 | ``` 176 | 177 | 如此一来,我们访问 newObj.foo 时间接访问了 obj.foo 的值,从而实现了修改 obj.foo 时会触发副作用函数重新执行。 178 | 179 | 观察一下 newObj 对象,可以发现它的结构存在相似之处:foo 和 bar 这两个属性的结构非常相似,所以我们可以把这种结构抽象出来并封装成通用函数: 180 | 181 | ```js 182 | function toRef (obj, key) { 183 | const wrapper = { 184 | get value () { 185 | return obj[key] 186 | } 187 | } 188 | 189 | return wrapper 190 | } 191 | ``` 192 | 193 | 这样一来,我们就可以重新实现 newObj 对象了: 194 | 195 | ```js 196 | const newObj = { 197 | foo: toRef(obj, 'foo'), 198 | bar: toRef(obj, 'bar') 199 | } 200 | ``` 201 | 202 | 可以看到,代码变得非常简洁。但如果响应式数据 obj 的键非常多,我们还是要花费很大力气来做这一层转换。为此,我们可以封装一个 toRefs 函数,来批量地完成转换: 203 | 204 | ```js 205 | function toRefs (obj) { 206 | const wrapper = {} 207 | 208 | for (const key in obj) { 209 | wrapper[key] = toRef(obj, key) 210 | } 211 | 212 | return wrapper 213 | } 214 | ``` 215 | 216 | 现在,响应丢失问题就被我们彻底解决了。但为了概念上的统一,我们将通过 toRef 或 toRefs 转换后得到的结果视为直接的 ref 数据,为此我们还需要为 toRef 函数增加一段代码: 217 | 218 | ```js 219 | function toRef (obj, key) { 220 | const wrapper = { 221 | get value () { 222 | return obj[key] 223 | } 224 | } 225 | 226 | // 使用 Object.defineProperty 给 wrapper 加上一个不可写、不可枚举的属性 227 | Object.defineProperty(wrapper, '__v_isRef', { 228 | value: true 229 | }) 230 | 231 | return wrapper 232 | } 233 | ``` 234 | 235 | 但上面的 toRef 函数还是有缺陷的,它创建的 ref 是只读的,无法修改值。所以我们还需要为它加上 setter 函数: 236 | 237 | ```js 238 | function toRef (obj, key) { 239 | const wrapper = { 240 | get value () { 241 | return obj[key] 242 | }, 243 | 244 | set value (val) { 245 | obj[key] = val 246 | } 247 | } 248 | 249 | // 使用 Object.defineProperty 给 wrapper 加上一个不可写、不可枚举的属性 250 | Object.defineProperty(wrapper, '__v_isRef', { 251 | value: true 252 | }) 253 | 254 | return wrapper 255 | } 256 | ``` 257 | 258 | ## 自动脱 ref 259 | 260 | toRefs 函数的确解决了响应丢失问题,但同时也带来了新的问题,因为 ref 必须通过 value 属性来访问值。这会增加用户的心智负担,因为通常用户是在模板中访问数据的: 261 | 262 | ```vue 263 | 266 | ``` 267 | 268 | 用户肯定不希望编写下面这样的代码: 269 | 270 | ```vue 271 | 274 | ``` 275 | 276 | 所以我们需要自动脱 ref 的能力。所谓自动脱 ref,指的是属性的访问行为,即如果读取的属性是一个 ref,则直接将该 ref 对应的 value 属性返回,例如: 277 | 278 | ```js 279 | newObj.foo // 1 280 | ``` 281 | 282 | 可以看到,即使 newObj.foo 是一个 ref,也无须通过 newObj.foo.value 来访问它的值。要实现此功能,需要使用 Proxy 为 newObj 创建一个代理对象,通过代理来实现最终目标,这时就用到了之前介绍的 __v_isRef 属性: 283 | 284 | ```js 285 | function proxyRefs (target) { 286 | return new Proxy(target, { 287 | get (target, key, receiver) { 288 | const value = Reflect.get(target, key, receiver) 289 | 290 | return value.__v_isRef ? value.value : value 291 | } 292 | }) 293 | } 294 | 295 | // 通过 proxyRefs 函数创建代码 296 | const newObj = proxyRefs({ ...toRefs(obj) }) 297 | ``` 298 | 299 | 这样我们就实现了自动脱 ref 能力。实际上,在 Vue.js 组件中的 setup 函数返回的数据都会传递给 proxyRefs 函数进行处理,这也是为什么我们可以在模板直接访问一个 ref 的值,而不需要通过 value 属性来访问。 300 | 301 | 既然读取属性的值有自动脱 ref 的能力,对应地,设置属性的值也应该有自动为 ref 设置的能力,例如: 302 | 303 | ```js 304 | newObj.foo = 10 // 应该生效 305 | ``` 306 | 307 | 实现此功能很简单,只需要添加对应的 `set()` 拦截函数即可: 308 | 309 | ```js 310 | function proxyRefs (target) { 311 | return new Proxy(target, { 312 | get (target, key, receiver) { 313 | const value = Reflect.get(target, key, receiver) 314 | 315 | return value.__v_isRef ? value.value : value 316 | }, 317 | 318 | set (target, key, newValue, receiver) { 319 | const value = target[key] 320 | 321 | if (value.__v_isRef) { 322 | value.value = newValue 323 | return true 324 | } 325 | 326 | return Reflect.set(target, key, newValue, receiver) 327 | } 328 | }) 329 | } 330 | ``` 331 | 332 | 实际上,自动脱 ref 不仅存在于上述场景。在 Vue.js 中,`reactive()` 函数也有自动脱 ref 的能力: 333 | 334 | ```js 335 | const count = ref(0) 336 | const obj = reactive(count) 337 | 338 | obj.count = 0 339 | ``` 340 | 341 | 这样设计旨在减轻用户的心智负担,因为在大部分情况下,用户并不知道一个值到底是不是 ref。有了自动脱 ref 的能力后,用户在模板中使用响应式数据时将不再需要关心哪些是 ref,哪些不是 ref。 342 | 343 | ## 🚀 章节链接 344 | 345 | - 上一章:[非原始值的响应式方案](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/4.%E9%9D%9E%E5%8E%9F%E5%A7%8B%E5%80%BC%E7%9A%84%E5%93%8D%E5%BA%94%E5%BC%8F%E6%96%B9%E6%A1%88.md) 346 | 347 | - 下一章: [渲染器的设计](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/6.%E6%B8%B2%E6%9F%93%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1.md) -------------------------------------------------------------------------------- /notes/6.渲染器的设计.md: -------------------------------------------------------------------------------- 1 | # 渲染器的设计 2 | 3 | ## 渲染器与响应系统的结合 4 | 5 | 顾名思义,渲染器是用来执行渲染任务的。在浏览器平台上,用它来渲染其中的真实DOM元素。渲染器不仅能够渲染真实 DOM 元素,它还是框架跨平台能力的关键。因此,在设计渲染器的时候一定要考虑好可自定义的能力。 6 | 7 | 本节,我们暂时将渲染器限定在 DOM 平台。既然渲染器用来渲染真实 DOM 元素,严格地来说,下面的函数就是一个合格的渲染器: 8 | 9 | ```js 10 | function renderer (domString, container) { 11 | container.innerHTML = domString 12 | } 13 | ``` 14 | 15 | 利用响应系统,我们可以让整个渲染过程自动化: 16 | 17 | ```js 18 | const count = ref(1) 19 | 20 | effect(() => { 21 | renderer(`

${count.value}

`, document.getElementById('app')) 22 | }) 23 | 24 | count.value++ 25 | ``` 26 | 27 | 我们利用响应系统的能力,自动调用渲染器完成页面的渲染和更新。这个过程与渲染器的具体实现无关,在上面给出的渲染器的实现中,仅仅设置了元素的 innerHTML 内容。 28 | 29 | 从现在开始,我们将使用 `@vue/reactivity` 包提供的响应式 API 进行讲解。 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | ```js 36 | function renderer (domString, container) { 37 | container.innerHTML = domString 38 | } 39 | 40 | const { effect, ref } = VueReactivity 41 | 42 | const count = ref(1) 43 | 44 | effect(() => { 45 | renderer(`

${count.value}

`, document.getElementById('app')) 46 | }) 47 | 48 | count.value++ 49 | ``` 50 | 51 | ## 渲染器的基本概念 52 | 53 | 通过,我们使用 renderer 来表达“渲染器”。渲染器的作用是把虚拟 DOM 渲染为特定平台上的真实元素。 54 | 55 | 虚拟 DOM 通宵用 virtual DOM 来表示,有时会简写成 vdom。虚拟 DOM 和 真实 DOM 的结构一样,都是由一个个节点组成的树形结构。所以,我们经常能听到“虚拟节点”这样的词,即 virtual node,有时会简写成 vnode。 56 | 57 | 虚拟 DOM 是树形结构,这棵树中的任何一个 vnode 节点都可以是一棵子树,因此 vnode 和 vdom 有时可以替换使用。为了避免造成困扰,我们将统一使用 vnode。 58 | 59 | 渲染器把虚拟 DOM 节点渲染为真实 DOM 切点的过程叫作**挂载**,通过用 mount 来表示。另外,渲染器通常需要接收一个挂载点作为参数,用于指定具体的挂载位置,这里的“挂载点”其实就是一个 DOM 元素,通常使用 container 来表示。举个例子: 60 | 61 | ```js 62 | function createRenderer () { 63 | function render (vnode, container) { 64 | // ... 65 | } 66 | 67 | return render 68 | } 69 | ``` 70 | 71 | 为什么需要一个 createRenderer 函数呢?渲染器与渲染是不同的,渲染器是更加宽泛的概念,它不仅包含了渲染,还可以用来激活已有的 DOM 元素,这个过程通常发生在同结构渲染的情况下: 72 | 73 | ```js 74 | function createRenderer () { 75 | function render (vnode, container) { 76 | // ... 77 | } 78 | 79 | function hydrate (vnode, container) { 80 | // ... 81 | } 82 | 83 | return { 84 | render, 85 | hydrate 86 | } 87 | } 88 | ``` 89 | 90 | 这个例子说明,渲染器的内容非常广泛,而用来把 vnode 渲染为真实 DOM 的 render 函数只是其中一部分。实际上,在 Vue.js 3 中,甚至连创建应用的 createApp 函数也是渲染器的一部分。 91 | 92 | 有了渲染器,我们就可以用它来执行渲染任务了,如下面的代码所示: 93 | 94 | ```js 95 | const renderer = createRenderer() 96 | // 首次渲染 97 | renderer.render(vnode, document.querySelector('#app')) 98 | ``` 99 | 100 | 当首次调用 renderer.render 函数时,只需要创建新的 DOM 元素即可,这个过程只涉及**挂载**。 101 | 102 | 而当多次在同一个 container 上调用了 renderer.render 函数进行渲染时,渲染器除了要执行挂载动作外,还要执行更新动作: 103 | 104 | ```js 105 | const renderer = createRenderer() 106 | // 首次渲染 107 | renderer.render(oldVnode, document.querySelector('#app')) 108 | // 第二次渲染 109 | renderer.render(newVnode, document.querySelector('#app')) 110 | ``` 111 | 112 | 由于首次渲染时已经把 oldVnode 渲染到 container 内了,所以当再次调用 renderer.render 函数并尝试渲染 newVnode 时,就不能简单地执行挂载动作了。 113 | 114 | 在这种情况下,渲染器会使用 newVnode 与上一次渲染的 oldVnode 进行比较,试图找到并更新变更点。这个过程叫作**打补丁(或更新)**,通常使用 patch 表示。 115 | 116 | 实际上,挂载动作也可以看作一种特殊的打补丁,它的特殊之处在于旧的 vnode 是不存在的。我们看下示例: 117 | 118 | ```js 119 | function createRenderer () { 120 | function render (vnode, container) { 121 | if (vnode) { 122 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 123 | patch(container._vnode, vnode, container) 124 | } else { 125 | if (container._vnode) { 126 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 127 | // 只需要将 container 内的 DOM 清空即可 128 | container.innerHTML = '' 129 | } 130 | } 131 | 132 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 133 | container._vnode = vnode 134 | } 135 | 136 | return { 137 | render 138 | } 139 | } 140 | 141 | ``` 142 | 143 | 上面这段代码给出了 render() 函数的基本实现。我们可以配合下面的代码分析其执行流程,从而更好地理解 render() 函数的实现思路。假设我们连续三次调用 renderer.render() 函数来执行渲染: 144 | 145 | ```js 146 | const renderer = createRenderer() 147 | 148 | // 首次渲染 149 | renderer.render(vnode1, document.querySelector('#app')) 150 | // 第二次渲染 151 | renderer.render(vnode2, document.querySelector('#app')) 152 | // 第三次渲染 153 | renderer.render(null, document.querySelector('#app')) 154 | ``` 155 | 156 | + 在首次渲染时,渲染器会将 vnode1 渲染为真实 DOM。渲染完成后,vnode1 会存在到容器元素的 `container._vnode` 中,它会在后续渲染中作为旧 vnode 使用; 157 | + 第二次渲染时,旧 vnode 存在,此时渲染器会把 vnode2 作为新 vnode, 并将新旧 vnode 一同传递给 `patch()` 函数进行更新操作; 158 | + 第三次渲染时,新 vnode 的值为 null,即什么都不需要渲染。但此时容器中渲染的是 vnode2 所描述的内容,所以渲染器需要清空容器。我们暂时使用 `container.innerHTML = ''` 来实现清空操作。 159 | 160 | 另外,我们注意到了 `patch()` 函数的签名: 161 | 162 | ```js 163 | patch(container._vnode, vnode, container) 164 | ``` 165 | 166 | 虽然我们并没有给出 `patch()` 函数的具体实现,但从上面的代码中,仍然可以窥探 `patch()` 函数的部分细节。实际上,`patch()` 函数是整个渲染器的核心入口,它承载了最重要的渲染逻辑,我们会花费大量篇幅来详细讲解它,但这里仍然有必要对它做一些初步的解释。 167 | 168 | `patch()` 函数至少接收三个参数: 169 | 170 | ```js 171 | function (oldVnode, newVnode, container) { 172 | // ... 173 | } 174 | ``` 175 | 176 | 它不仅可以用来完成更新,也可以用来执行挂载。 177 | 178 | ## 自定义渲染器 179 | 180 | 本节,我们将以浏览器作为渲染的目标平台,编写一个渲染嘎嘎,在这个过程中,看看哪些内容是可以抽象的,然后通过抽象,将浏览器特定的 API 抽离,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,我们再为那些被抽离的 API 提供可配置的接口,从而实现跨平台的能力。 181 | 182 | 我们先从渲染一个普通的 `

` 标签开始: 183 | 184 | ```js 185 | const vnode = { 186 | type: 'h1', 187 | children: 'hello' 188 | } 189 | ``` 190 | 191 | 使用上面的 vnode 来描述一个 `

` 标签,type 描述一个 vnode 的类型。当 type 为一个字符串类型的值时,可以认为它描述的是普通标签,并使用该 type 属性的值作为标签的名称。对于这样一个 vnode,我们可以使用 `render()` 函数渲染它: 192 | 193 | ```js 194 | const vnode = { 195 | type: 'h1', 196 | children: 'hello' 197 | } 198 | 199 | const renderer = createRenderer() 200 | 201 | renderer.render(vnode, document.querySelector('#app')) 202 | ``` 203 | 204 | 为了完成渲染工作,我们需要补充 `patch()` 函数,我们将 `patch()` 函数也编写在 `createRenderer()` 函数内。在后续的讲解中,如无特殊声明,我们编写的函数都定义在 `createRenderer()` 函数内: 205 | 206 | ```js 207 | function createRenderer () { 208 | function render (vnode, container) { 209 | if (vnode) { 210 | // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行更新 211 | patch(container._vnode, vnode, container) 212 | } else { 213 | if (container._vnode) { 214 | // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作 215 | // 只需要将 container 内的 DOM 清空即可 216 | container.innerHTML = '' 217 | } 218 | } 219 | 220 | // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode 221 | container._vnode = vnode 222 | } 223 | 224 | function patch (n1, n2, container) { 225 | // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载 226 | if (!n1) { 227 | mountElement(n2, container) 228 | } else { 229 | // n1 存在,意味着更新,暂时省略 230 | } 231 | } 232 | 233 | return { 234 | render 235 | } 236 | } 237 | ``` 238 | 239 | `mountElement()` 函数的实现如下: 240 | 241 | ```js 242 | function mountElement (vnode, container) { 243 | // 创建 DOM 元素 244 | const el = document.createElement(vnode.type) 245 | 246 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 247 | if (typeof vnode.children === 'string') { 248 | el.textContent = vnode.children 249 | } 250 | 251 | // 将元素添加到容器中 252 | container.appendChild(el) 253 | } 254 | ``` 255 | 256 | 挂载一个普通标签元素的工作已经完成。接下来,我们分析下这段代码存在的问题。我们的目标是设计一个不依赖于特定平台的通用渲染器,但很明显,`mountElement()` 函数内调用了大量依赖于浏览器的 API。所以我们需要抽离这些 API,做法也很简单,我们只需要把这些操作 DOM 的 API 作为配置项,该配置项可以作为 `createRenderer()` 函数的参数: 257 | 258 | ```js 259 | // 在创建 renderer 时传入配置项 260 | const renderer = createRenderer({ 261 | // 用于创建元素 262 | createElement(tag) { 263 | return document.createElement(tag) 264 | }, 265 | // 用于设置元素的文本节点 266 | setElementText (el, text) { 267 | el.textContent = text 268 | }, 269 | // 用于在给定的 parent 下添加指定元素 270 | insert (el, parent, anchor = null) { 271 | parent.insertBefore(el, anchor) 272 | } 273 | }) 274 | ``` 275 | 276 | 在样,在 `mountElement()` 等函数内就可以通过配置项来取得操作 DOM 的 API 了: 277 | 278 | ```js 279 | function createRenderer (options) { 280 | // 通过 options 取得操作 DOM 的 API 281 | const { 282 | createElement, 283 | insert, 284 | setElementText 285 | } = options 286 | 287 | function render (vnode, container) { 288 | // ... 289 | } 290 | 291 | function patch (n1, n2, container) { 292 | // ... 293 | } 294 | 295 | function mountElement (vnode, container) { 296 | // ... 297 | } 298 | 299 | return { 300 | render 301 | } 302 | } 303 | ``` 304 | 305 | 接着,我们就可以使用从配置项中取得的 API 来改造 `mountElement()` 函数: 306 | 307 | ```js 308 | function mountElement (vnode, container) { 309 | // 创建 DOM 元素 310 | const el = createElement(vnode.type) 311 | 312 | // 处理子节点,如果子节点是字符串,代表元素具有文本节点 313 | if (typeof vnode.children === 'string') { 314 | setElementText(el, vnode.children) 315 | } 316 | 317 | // 将元素添加到容器中 318 | insert(el, container) 319 | } 320 | ``` 321 | 322 | ## 🚀 章节链接 323 | 324 | - 上一章:[原始值的响应式方案](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/5.%E5%8E%9F%E5%A7%8B%E5%80%BC%E7%9A%84%E5%93%8D%E5%BA%94%E6%96%B9%E6%A1%88.md) 325 | 326 | - 下一章: [挂载与更新](https://github.com/humandetail/VueJS-design-and-implementation/blob/master/notes/7.%E6%8C%82%E8%BD%BD%E4%B8%8E%E6%9B%B4%E6%96%B0.md) --------------------------------------------------------------------------------