├── .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 |