upload courseware

This commit is contained in:
xie jie 2024-09-11 10:55:44 +08:00
commit faef9237f9
151 changed files with 29198 additions and 0 deletions

View File

@ -0,0 +1,35 @@
# 课程导读-必看
**Vue3面试题分类**
1. Vue使用相关的
- Vue中如何自定义指令
- 说一下Vue中的生命周期
- Vue中组件通信的方式有哪些
- v-if 和 v-show 的区别?
- Vue2 OptionsAPI 和 CompositionAPI 的区别?
- 常见的修饰符有哪些?
- Vue 中data 为什么必须是一个函数?
- ....
2. Vue原理相关的
1. 非八股文:确实需要搞懂原理,不然影响开发
1. 说一下Vue响应式渲染原理
2. 说一下什么是虚拟DOM
3. key有什么作用
4. ....
2. 八股文:没弄懂其实也不影响实际开发
1. diff算法
2. Vue2的diff和Vue3的diff区别是什么
**课程设置**
1. 讲什么:讲一些比较典型的,有代表意义的面试题目
2. 不讲什么
1. 和Vue2相关的面试题
2. Vue3课程讲过的知识相关的面试题
---
-EOF-

View File

@ -0,0 +1,197 @@
# Vue3整体变化
> 面试题:说一下 Vue3 相比 Vue2 有什么新的变化?
主要有这么几类变化:
1. 源码上的变化
2. 性能的变化
3. 语法API的变化
4. 引入RFC
## 源码优化
**1. Monorepo**
Vue2 的源码是托管在 src 目录下面的,然后依据功能拆分出了:
- compiler编译器
- core和平台无关的通用运行时代码
- platforms平台专有代码
- server服务端渲染相关代码
- sfc单文件组件解析相关代码
- shared共享工具库代码
但是各个模块**无法单独抽离**出来下载安装,也无法针对单个模块进行发布。
Vue3 源码工程的搭建改为了 Monorepo 的形式,将模块拆分到了不同的包里面,每个包有各自的 API、类型定义以及测试。这样一类粒度更细责任划分更加明确。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-21-073731.png" alt="image-20240821153730971" style="zoom:50%;" />
**2. Typescript**
- Vue1.x纯 JS 开发,无类型系统
- Vue2.xFlow.js这是 Facebook 推出的类型系统
- Vue3.xTypeScript
## 性能优化
**1. 源码体积缩小**
- 移除冷门功能filter、inline-template 这些特性被去除掉了
- 生产环境采用 rollup 进行构建,利用 tree-shaking 减少用户代码打包的体积
**2. 数据劫持优化**
- Vue2.xObject.defineProperty
- Vue3.xProxy
**3. 编译优化**
模板本质上是语法糖,最终会被编译器编译为渲染函数。
1. 静态提升
2. 预字符串化
3. 缓存事件处理函数
4. Block Tree
5. PatchFlag
**4. diff算法优化**
- Vue2.x: 双端 diff 算法
- Vue3.x: 快速 diff 算法
## 语法API优化
**1. 优化逻辑组织**
- Vue2.x: OptionsAPI逻辑代码按照 data、methods、computed、props 进行分类
- Vue3.x: OptionsAPI + CompositionAPI推荐
- CompositionAPI优点查看一个功能的实现时候不需要在文件跳来跳去并且这种风格代码可复用的粒度更细
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-26-033538.jpg" alt="16028271652994" style="zoom:50%;" />
**2. 优化逻辑复用**
Vue2.x: 复用逻辑使用 mixin但是 mixin 本身有一些缺点:
- 不清晰的数据来源
- 命名空间冲突
- 隐式的跨mixin交流
参阅Vue课程《细节补充 - 组合式函数》
**3. 其他变化**
Vue2
```js
import App from './App.vue'
// 通过实例化 Vue 来创建应用
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
// or
new Vue({
render: h => h(App),
}).$mount('#app')
```
思考🤔:这种方式存在什么问题?
答案:这种方式缺点在于一个页面如果存在多个 Vue 应用,部分配置会影响所有的 Vue 应用
```vue
<!-- vue2 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
Vue.use(...); // 此代码会影响所有的vue应用
Vue.mixin(...); // 此代码会影响所有的vue应用
Vue.component(...); // 此代码会影响所有的vue应用
new Vue({
// 配置
}).$mount("#app1")
new Vue({
// 配置
}).$mount("#app2")
</script>
```
Vue3
```js
import { createApp } from 'vue';
import App from './App.vue'
createApp(App).mount('#app');
```
这种方式就能很好的规避上面的问题:
```vue
<!-- vue3 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
createApp(根组件).mount("#app2")
</script>
```
> 面试题:为什么 Vue3 中去掉了 Vue 构造函数?
>
> 参考答案:
>
> Vue2 的全局构造函数带来了诸多问题:
>
> 1. 调用构造函数的静态方法会对所有vue应用生效不利于隔离不同应用
> 2. Vue2 的构造函数集成了太多功能,不利于 tree shakingVue3 把这些功能使用普通函数导出,能够充分利用 tree shaking 优化打包体积
> 3. Vue2 没有把组件实例和 Vue 应用两个概念区分开,在 Vue2 中,通过 new Vue 创建的对象,既是一个 Vue 应用,同时又是一个特殊的 Vue 组件。Vue3 中,把两个概念区别开来,通过 createApp 创建的对象,是一个 Vue 应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。
## 引入RFC
RFC 全称是 Request For Comments. 这是一种在软件开发和开源项目中常用的提案流程,用于收集社区对某个新功能、改动或标准的意见和建议。
RFC 是一种文档格式它详细描述了某个特性或更改的提议讨论其动机、设计选择、实现细节以及潜在的影响。在通过讨论和反馈达成共识后RFC 会被采纳或拒绝。一份 RFC 主要的组成部分有:
1. 标题:简短描述提案的目的。
2. 摘要:简要说明提案的内容和动机。
3. 动机:解释为什么需要这个提案,解决了什么问题。
4. 详细设计:深入描述提案的设计和实现细节。
5. 潜在问题和替代方案:讨论可能存在的问题和可以考虑的替代方案。
6. 不兼容的变更:描述提案是否会引入不兼容的变更,以及这些变更的影响。
通过 RFCVue 核心团队能够更好地倾听用户的需求和建议,从而开发出更加符合社区期待的功能和特性。
> 面试题:说一下 Vue3 相比 Vue2 有什么新的变化?
>
> 参考答案:
>
> Vue3 相比 Vue2 的整体变化,可以分为好几大类:
>
> 1. 源码优化
> 2. 性能优化
> 3. 语法 API 优化
> 4. 引入 RFC
>
> **源码优化**体现在使用 typescript 重构整个 Vue 源码,对冷门的功能进行了删除,并且整个源码的结构变为了使用 Monorepo 进行管理,这样粒度更细,不同的包可以独立测试发布。用户也可以单独引入某一个包使用,而不用必须引入 Vue.
>
> **性能上的优化**是整个 Vue3 最核心的变化通过优化响应式、diff算法、模板编译Vue3 的性能相比 Vue2 有质的飞跃,基本上将性能这一块儿做到了极致。所以 Vue 的新项目建议都使用 Vue3 来搭建。
>
> 不过性能层面的优化,开发者无法直接的感知,开发者能够直接感知的,是**语法上的优化**,例如 Vue3 提出的 CompositionAPI用于替代 Vue2 时期的 OptionsAPI. 这样能够让功能逻辑更加集中,无论是在阅读还是修改都更加方便。另外 CompositionAPI 让代码复用的粒度上更细,不需要再像以前一样使用 mixin 复用逻辑,而是推荐使用组合式函数来复用逻辑。
>
> 不过 Vue3 也不是完全废弃了 OptionsAPI在 Vue3 中OptionsAPI 成为了一种编码风格。
>
> 最后就是引入 RFC尤雨溪和核心团队广泛采用了 RFC 的流程来处理新功能和重大更改。
---
-EOF-

View File

@ -0,0 +1,112 @@
# Vue2响应式回顾
>面试题:说一说 Vue3 响应式相较于 Vue2 是否有改变?如果有,那么说一下具体有哪些改变?
**观察者模式**
生活中的观察者模式:
假设顾客对新型号的手机感兴趣,但是目前商店还没到货,那么顾客及时如何买到新型号的手机?
1. 顾客每天去一趟商场 🙅
2. 商品到货后没所有顾客发出通知 🙅
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-03-22-074632.png" alt="image-20240322154631735" style="zoom:50%;" />
我们似乎遇到了一个矛盾:要么让顾客浪费时间检查产品是否到货,要么让商店浪费资源去通知没有需求的顾客。
解决方案其实很简单让有需求的顾客watcher主动订阅即可之后商店dep只需要给订阅了用户发送通知。
**Vue2响应式工作机制**
1. data 中的数据会被 Vue 遍历生成 getter 和 setter这样一来当访问或设置属性时Vue 就有机会做一些别的事情。
2. 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher从而使它关联的组件重新渲染。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-074111.png" alt="image-20240901154111493" style="zoom:40%;" />
几个比较重要的点:
1. 劫持数据:通过 Object.defineProperty 方法来做数据劫持,生成 getter 和 setter 从而让获取/设置值的时候可以做一些其他的事情。
2. 发布者:记录依赖,也就是数据和 watcher 之间的映射关系
3. 观察者watcher 会被发布者记录,数据发生变化的时候,发布者会会通知 watcher之后 watcher 执行相应的处理
**劫持数据**
劫持数据对象,是 Observer 的工作,它的目标很简单,就是把一个普通的对象转换为响应式的对象
为了实现这一点Observer 把对象的每个属性通过 Object.defineProperty 转换为带有 getter 和 setter 的属性这样一来当访问或设置属性时Vue 就有机会做一些别的事情。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-052809.png" alt="20210226153448" style="zoom:67%;" />
Observer 是 Vue 内部的构造器,我们可以通过 Vue 提供的静态方法 Vue.observable( object ) 间接的使用该功能。
在组件生命周期中,这件事发生在 beforeCreate 之后created 之前。
具体实现上,**它会递归遍历对象的所有属性,以完成深度的属性转换**。由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此 Vue 提供了 $set 和 $delete 两个实例方法让开发者通过这两个实例方法对已有响应式对象添加或删除属性。对于数组Vue 会更改它的隐式原型,之所以这样做,是因为 Vue 需要监听那些可能改变数组内容的方法。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-052949.png" alt="20210226154624" style="zoom:67%;" />
总之Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 Vue 感知到。
**发布者(商店)**
发布者,也被称之为依赖管理器,对应英文 Dependency简称 Dep.
其中最核心的两个功能:
- 能够添加观察者:当读取响应式对象的某个属性时,它会进行依赖收集
- 能够通知观察者:当改变某个属性时(商品发售了),它会派发更新(通知所有顾客)
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-053233.png" alt="20210226155852" style="zoom:67%;" />
**观察者**
当依赖的数据发生变化时,发布者会通知每一个观察者,而观察者需要调用 update 来更新数据。
**scheduler**
Vue2 内部实现中,还存在一个 Scheduler因为Dep 通知 watcher 之后,如果 watcher 执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下
试想,如果一个交给 watcher 的函数,它里面用到了属性 a、b、c、d那么 a、b、c、d 属性都会记录依赖,于是下面的代码将触发 4 次更新:
```js
state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
```
这样显然是不合适的因此watcher 收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西
调度器维护一个执行队列,该**队列同一个 watcher 仅会存在一次,队列中的 watcher 不是立即执行,它会通过一个叫做 nextTick 的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列中**也就是说当响应式数据变化时render 函数的执行是**异步**的,并且在**微队列**中。
**Vue2响应式整体流程**
![20210226163936](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-053804.png)
几个核心部件:
1. Observer用于劫持数据对象把对象的每个属性通过 Object.defineProperty 转换为带有 getter 和 setter 的属性
2. Dep(商店):发布者,也被称之为依赖管理器
- 能够添加观察者:当读取响应式对象的某个属性时,它会进行依赖收集
- 能够通知观察者:当改变某个属性时,它会派发更新
3. Watcher顾客负责具体的更新操作可以理解为用户收到商场的邮件后自身要做什么事情
4. Scheduler负责调度。
---
-EOF-

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<div>{{msg}}</div>
<input type="text" v-model="msg" />
{{msg}}
</div>
<script type="module">
import Vue from "./js/Vue.js";
const options = {
el: "#app",
data: {
msg: "hello vue",
},
};
new Vue(options);
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
// 发布者
export default class Dep {
constructor() {
this.subs = []; // 搜集所有的watcher
}
// 添加watcher
addSub(sub) {
this.subs.push(sub);
}
// 通知watcher更新
notify() {
this.subs.forEach((sub) => {
// 具体如何更新,发布者是不管的,它只负责通知
sub.update();
});
}
}
Dep.target = null; // 一会儿用于记录当前 watcher

View File

@ -0,0 +1,9 @@
import { observer, compile } from "./utils.js";
export default class Vue {
constructor(options) {
this.$el = options.el;
// 需要对数据进行劫持
observer(this, options.data);
compile(this);
}
}

View File

@ -0,0 +1,20 @@
import Dep from "./Dep.js";
export default class Watcher {
constructor(vm, el, vmKey) {
// 做一些信息的初始化
this.vm = vm;
this.el = el;
this.vmKey = vmKey;
Dep.target = this;
this.update();
Dep.target = null;
}
update() {
if (this.el.nodeType === Node.TEXT_NODE) {
this.el.nodeValue = this.vm[this.vmKey];
} else if (this.el.nodeType === Node.ELEMENT_NODE) {
this.el.innerHTML = this.vm[this.vmKey];
}
}
}

View File

@ -0,0 +1,58 @@
import Dep from "./Dep.js";
import Watcher from "./Watcher.js";
export function observer(vm, obj) {
const dep = new Dep(); // 创建一个发布者实例
Object.keys(obj).forEach((key) => {
let internalValue = obj[key]; // 保存内部值
Object.defineProperty(vm, key, {
get() {
// 在获取响应式数据的时候,要做依赖的收集
if (Dep.target) {
dep.addSub(Dep.target);
}
return internalValue;
},
set(newVal) {
// 在设置响应式数据的时候,要做依赖的通知
internalValue = newVal;
dep.notify();
},
});
});
}
export function compile(vm) {
const el = document.querySelector(vm.$el);
if (!el) {
throw new Error(`Element with selector "${vm.$el}" not found.`);
}
const documentFragment = document.createDocumentFragment();
const reg = /\{\{(.*)\}\}/;
while (el.firstChild) {
const child = el.firstChild;
if (child.nodeType === Node.ELEMENT_NODE) {
const element = child;
if (reg.test(element.innerHTML || "")) {
const vmKey = RegExp.$1.trim();
new Watcher(vm, child, vmKey);
} else {
Array.from(element.attributes).forEach((attr) => {
if (attr.name === "v-model") {
const vmKey = attr.value;
element.addEventListener("input", (event) => {
const target = event.target;
vm[vmKey] = target.value;
});
}
});
}
} else if (
child.nodeType === Node.TEXT_NODE &&
reg.test(child.nodeValue || "")
) {
const vmKey = RegExp.$1.trim();
new Watcher(vm, child, vmKey);
}
documentFragment.appendChild(child);
}
el.appendChild(documentFragment);
}

View File

@ -0,0 +1,137 @@
# Vue3响应式变化
>面试题:说一说 Vue3 响应式相较于 Vue2 是否有改变?如果有,那么说一下具体有哪些改变?
**1. 变化一**
首当其冲的就是数据拦截的变化:
- Vue2: 使用 Object.defineProperty 进行拦截
- Vue3: 使用 Proxy + Object.defineProperty 进行拦截
**两者的共同点**
- 都可以针对对象成员拦截
- 都可以实现深度拦截
**两者的差异点**
- 拦截的广度
- Object.defineProperty 是针对对象特定**属性**的**读写**操作进行拦截,这意味着之后新增加/删除的属性是侦测不到的
- Proxy 则是针对**一整个对象**的多种操作,包括属性的读取、赋值、属性的删除、属性描述符的获取和设置、原型的查看、函数调用等行为能够进行拦截。
- 性能上的区别在大多数场景下Proxy 比 Object.defineProperty 效率更高,拦截方式更加灵活。
**2. 变化二**
创建响应式数据上面的变化:
- Vue2: 通过 data 来创建响应式数据
- Vue3: 通过 ref、reactvie 等方法来创建响应式数据
- ref使用 Object.defineProperty + Proxy 方式
- reactive使用 Proxy 方式
**对应源码**
```js
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 有可能是原始值,有可能是 reactive 返回的 proxy
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 收集依赖 略
return this._value
}
set value(newVal) {
// 略
}
}
// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
```
```js
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// ...
// 创建 Proxy 代理对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
export function reactive(target: object) {
// ...
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
```
**3. 变化三**
依赖收集上面的变化:
- Vue2Watcher + Dep
- 每个响应式属性都有一个 Dep 实例,用于做依赖收集,内部包含了一个数组,存储依赖这个属性的所有 watcher
- 当属性值发生变化dep 就会通知所有的 watcher 去做更新操作
- Vue3WeakMap + Map + Set
- Vue3 的依赖收集粒度更细
- WeakMap 键对应的是响应式对象,值是一个 Map这个 Map 的键是该对象的属性,值是一个 SetSet 里面存储了所有依赖于这个属性的 effect 函数
总结起来Vue3相比Vue2的依赖追踪粒度更细Vue2依赖收集收集的是具体的Watcher组件Vue3依赖收集收集的是对应的副作用函数。
> 面试题:说一说 Vue3 响应式相较于 Vue2 是否有改变?如果有,那么说一下具体有哪些改变?
>
> 参考答案:
>
> 相比较 Vue2Vue3 在响应式的实现方面有这么一些方面的改变:
>
> 1. 数据拦截从 Object.defineProperty 改为了 Proxy + Object.defineProperty 的拦截方式,其中
> - ref使用 ObjectdefineProperty + Proxy 方式
> - reactive使用 Proxy 方式
> 2. 创建响应式数据在语法层面有了变化:
> - Vue2: 通过 data 来创建响应式数据
> - Vue3: 通过 ref、reactvie 等方法来创建响应式数据
> 3. 依赖收集上面的变化
> - Vue2Watcher + Dep
> - Vue3WeakMap + Map + Set
> - 这种实现方式可以实现更细粒度的依赖追踪和更新控制
---
-EOF-

View File

@ -0,0 +1,185 @@
# nextTick实现原理
>面试题Vue 的 nextTick 是如何实现的?
```vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
for (let i = 1; i <= 1000; i++) {
count.value = i
}
}
</script>
```
思考🤔:点击按钮后,页面会渲染几次?
答案:只会渲染一次,同步代码中多次对响应式数据做了修改,多次修改会被**合并**为一次,之后根据最终的修改结果**异步**的去更新 DOM.
思考🤔倘若不合并并且同步的去修改DOM会有什么样的问题
答案如果不进行合并并且数据一变就同步更新DOM会导致频繁的重绘和重排这非常耗费性能。
思考🤔:异步更新会带来问题
答案无法及时获取到更新后的DOM值
原因因为获取DOM数据是同步代码DOM的更新是异步的同步代码会先于异步代码执行。
解决方案将获取DOM数据的同步任务包装成一个微任务浏览器在完成一次渲染后就会立即执行微任务。
当前我们自己的解决方案:
```js
const increment = () => {
count.value++
Promise.resolve().then(() => {
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据', counterRef.value.textContent)
console.log('通过DOM拿textContent数据', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据', document.getElementById('counter').innerHTML)
})
}
```
nextTick 帮我们做的就是上面的事情,将一个任务包装成一个微任务。
```js
const increment = () => {
count.value++
nextTick(() => {
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据', counterRef.value.textContent)
console.log('通过DOM拿textContent数据', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据', document.getElementById('counter').innerHTML)
})
}
```
nextTick 返回的是一个 Promise
```js
const increment = async () => {
count.value++
await nextTick()
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据', counterRef.value.textContent)
console.log('通过DOM拿textContent数据', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据', document.getElementById('counter').innerHTML)
}
```
$nextTick首先这是一个方法是 Vue 组件实例的方法,用于 OptionsAPI 风格的。
```js
export default {
data() {
return {
count: 1,
counterRef: null
}
},
methods: {
increment() {
this.count++
this.$nextTick(() => {
// 在下一个 DOM 更新循环后执行的回调函数
console.log('最新数据为:', this.count)
console.log('拿到的DOM:', document.getElementById('counter'))
console.log('拿到的DOM:', this.$refs.counterRef)
console.log('通过DOM拿数据:', document.getElementById('counter').textContent)
console.log('通过DOM拿数据:', document.getElementById('counter').innerHTML)
console.log('通过DOM拿数据:', this.$refs.counterRef.textContent)
console.log('通过DOM拿数据:', this.$refs.counterRef.innerHTML)
})
}
}
}
```
[nextTick源码](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts)
```js
// 创建一个已经解析的 Promise 对象,这个 Promise 会立即被解决,
// 用于创建一个微任务microtask
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
// 一个全局变量,用于跟踪当前的刷新 Promise。
// 初始状态为 null表示当前没有刷新任务。
let currentFlushPromise: Promise<void> | null = null
// queueFlush 函数负责将刷新任务flushJobs放入微任务队列。
// 这是 Vue 的异步更新机制的核心部分,用于优化性能。
function queueFlush() {
// 检查是否已经在刷新isFlushing或者刷新任务是否已被挂起isFlushPending
if (!isFlushing && !isFlushPending) {
// 设置 isFlushPending 为 true表示刷新任务已被挂起正在等待执行。
isFlushPending = true
// 将 currentFlushPromise 设置为 resolvedPromise.then(flushJobs)
// 这将创建一个微任务,当 resolvedPromise 被解决时,执行 flushJobs 函数。
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
// nextTick 函数用于在下一个 DOM 更新循环之后执行一个回调函数。
// 它返回一个 Promise这个 Promise 会在 DOM 更新完成后解决。
export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R, // 可选的回调函数,在 DOM 更新之后执行
): Promise<Awaited<R>> {
// 如果 currentFlushPromise 不为 null使用它否则使用 resolvedPromise。
// 这样可以确保在 DOM 更新之后再执行回调。
const p = currentFlushPromise || resolvedPromise
// 如果传入了回调函数 fn返回一个新的 Promise在 p 解决之后执行 fn。
// 使用 this 绑定来确保回调函数的上下文正确。
return fn ? p.then(this ? fn.bind(this) : fn) : p
// 如果没有传入回调函数 fn直接返回 Promise p这样外部代码可以使用 await 等待 DOM 更新完成。
}
```
>面试题Vue 的 nextTick 是如何实现的?
>
>参考答案:
>
>nextTick 的本质将回调函数包装为一个微任务放入到微任务队列,这样浏览器在完成渲染任务后会优先执行微任务。
>
>nextTick 在 Vue2 和 Vue3 里的实现有一些不同:
>
>1. Vue2 为了兼容旧浏览器,会根据不同的环境选择不同包装策略:
>
> - 优先使用 Promise因为它是现代浏览器中最有效的微任务实现。
>
> - 如果不支持 Promise则使用 MutationObserver这是另一种微任务机制。
>
> - 在 IE 环境下,使用 setImmediate这是一种表现接近微任务的宏任务。
>
> - 最后是 setTimeout(fn, 0) 作为兜底方案,这是一个宏任务,但会在下一个事件循环中尽快执行。
>
>
>2. Vue3 则是只考虑现代浏览器环境,直接使用 Promise 来实现微任务的包装,这样做的好处在于代码更加简洁,性能更高,因为不需要处理多种环境的兼容性问题。
>
>整体来讲Vue3 的 nextTick 实现更加简洁和高效,是基于现代浏览器环境的优化版本,而 Vue2 则为了兼容性考虑,实现层面存在更多的兼容性代码。

View File

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
// 添加自定义规则
'vue/multi-word-component-names': 'off'
}
}

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@ -0,0 +1,35 @@
# demo
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"prettier": "^3.2.5",
"vite": "^5.3.1",
"vite-plugin-vue-devtools": "^7.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,67 @@
<template>
<div class="counter-container">
<h2>计数器示例</h2>
<p id="counter" ref="counterRef">{{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const counterRef = ref(1)
const increment = async () => {
count.value++
await nextTick()
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据', counterRef.value.textContent)
console.log('通过DOM拿textContent数据', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据', document.getElementById('counter').innerHTML)
}
</script>
<style scoped>
.counter-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
width: 300px;
margin: 50px auto;
background-color: #f9f9f9;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h2 {
font-size: 1.5em;
margin-bottom: 10px;
color: #333;
}
p {
font-size: 1.2em;
margin-bottom: 20px;
color: #666;
}
button {
padding: 10px 20px;
font-size: 1em;
color: white;
background-color: #42b983;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #36a572;
}
</style>

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,8 @@
// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

View File

@ -0,0 +1,109 @@
# 两道代码题
面试题1
完成 Component 类代码的书写,要求:
1. 修改数据时能够触发 render 方法的执行
2. 同步变更时需要合并,仅触发一次 render 方法
```js
class Component {
data = {
name: "",
};
constructor() {
}
render() {
console.log(`render - name: ${this.data.name}`);
}
}
const com = new Component();
// 要求以下代码需要触发 render 方法,并且同步变更需要合并
com.data.name = "张三";
com.data.name = "李四";
com.data.name = "王五";
setTimeout(() => {
com.data.name = "渡一";
}, 0);
```
面试题2
以下两段代码在 Vue 中分别渲染几次?为什么?
代码一:
```vue
<template>
<div>{{rCount}}</div>
</template>
<script setup>
import { ref } from 'vue';
const count = 0;
const rCount = ref(count);
for(let i = 1; i <= 5; ++i){
rCount.value = i;
}
</script>
```
代码二:
```vue
<template>
<div>{{rCount}}</div>
</template>
<script setup>
import { ref } from 'vue';
const count = 0;
const rCount = ref(count);
for(let i = 1; i <= 5; ++i){
setTimeout(()=>{
rCount.value = i;
}, 0);
}
</script>
```
- 代码一2次初始化渲染1次之后虽然在 for 循环中修改了 5 次响应式数据,但是会被合并,因此之后只会渲染 1次。
- 代码二6次初始化渲染1次之后每一个 setTimeout 中修改一次响应式数据就会渲染1次。
>参考答案:
>
>**代码一(同步赋值)**
>
>会渲染两次:
>
>1. 初始化渲染一次在组件挂载时Vue 会进行一次初始渲染,将 rCount 的初始值 0 渲染到 DOM 中。
>
>2. 响应式数据更新和批处理:
> - 在 for 循环中rCount.value 被依次赋值为 1, 2, 3, 4, 5. 每次赋值时Vue 的响应式系统会检测到数据的变化。
> - 然而这些变化发生在同一个同步代码块内Vue 会将这些变化推入异步更新队列中。因为这些赋值操作是同步执行的Vue 会在当前事件循环结束时对这些变化进行批处理batching
> - Vue 的批处理机制会将这些同步的更改**合并为一次更新**,因此,无论有多少次对 rCount.value 的赋值,最终只会在异步队列中触发一次渲染更新。
>
>3. 最终渲染一次:由于 Vue 的批处理机制,这段代码最终只会触发 一次 DOM 更新,渲染出 rCount 的最终值 5.
>
>总计渲染次数2 次(初始化渲染 1 次 + 批处理渲染 1 次)
>
>**代码二(异步赋值)**
>
>会渲染六次:
>
>1. 初始化渲染一次:同样,组件挂载时会进行一次初始渲染,将 rCount 的初始值 0 渲染到 DOM 中。
>
>2. 异步更新渲染:
> - 在 for 循环中,每次迭代都会创建一个 setTimeout每个 setTimeout 会在 0 毫秒后异步执行。在每个 setTimeout 的回调中rCount.value 被依次赋值为 1, 2, 3, 4, 5
> - 由于每次赋值都发生在一个独立的异步回调中Vue 的响应式系统会在每个异步回调执行后,立即触发相应的更新流程。每次 setTimeout 回调都会使 rCount.value 发生变化,因此每次都需要进行一次渲染更新。
>
>3. 每个异步回调导致一次渲染:因此,这段代码会触发 5 次 DOM 更新,每次将 rCount 渲染为 1 到 5.
>
>总计渲染次数6 次(初始化渲染 1 次 + 5 次异步更新渲染)
---
-EOF-

View File

@ -0,0 +1,36 @@
class Component {
_data = {
name: "",
};
pending = false; // 开关
constructor() {
this.data = new Proxy(this._data, {
set: (target, key, value) => {
// 1. 设置值
this._data[key] = value;
// this.render();
// 2. 判断是否开启
if (!this.pending) {
this.pending = true;
Promise.resolve().then(() => {
this.render();
this.pending = false;
});
}
},
});
}
render() {
console.log(`render - name: ${this.data.name}`);
}
}
const com = new Component();
// 要求以下代码需要触发 render 方法,并且同步变更需要合并
com.data.name = "张三";
com.data.name = "李四";
com.data.name = "王五";
setTimeout(() => {
com.data.name = "渡一";
}, 0);

View File

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
// 添加自定义规则
'vue/multi-word-component-names': 'off'
}
}

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@ -0,0 +1,35 @@
# demo
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"prettier": "^3.2.5",
"vite": "^5.3.1",
"vite-plugin-vue-devtools": "^7.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,17 @@
<template>
<div>{{ rCount }}</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const count = 0
const rCount = ref(count)
watchEffect(() => {
console.log('渲染', rCount.value)
})
for (let i = 1; i <= 5; ++i) {
// rCount.value = i
setTimeout(() => {
rCount.value = i
}, 0)
}
</script>

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,8 @@
// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

View File

@ -0,0 +1,261 @@
# Vue运行机制
>面试题:介绍一下 Vue3 内部的运行机制是怎样的?
Vue3 整体可以分为几大核心模块:
- 响应式系统
- 编译器
- 渲染器
**如何描述UI**
思考🤔UI涉及到的信息有哪些
1. DOM元素
2. 属性
3. 事件
4. 元素的层次结构
思考🤔:如何在 JS 中描述这些信息?
考虑使用对象来描述上面的信息
```html
<h1 id='title' @click=handler><span>hello</span></h1>
```
```js
const obj = {
tag: 'h1',
props: {
id: 'title',
onClick: handler
},
children: [
{
tag: 'span',
children: 'hello'
}
]
}
```
虽然这种方式能够描述出来 UI但是非常麻烦因此 Vue 提供了模板的方式。
用户书写模板----> 编译器 ----> 渲染函数 ----> 渲染函数执行得到上面的 JS 对象虚拟DOM
虽然大多数时候,模板比 JS 对象更加直观但是偶尔有一些场景JS 的方式更加灵活
```vue
<h1 v-if="level === 1"></h1>
<h2 v-else-if="level === 2"></h2>
<h3 v-else-if="level === 3"></h3>
<h4 v-else-if="level === 4"></h4>
<h5 v-else-if="level === 5"></h5>
<h6 v-else-if="level === 6"></h6>
```
```js
let level = 1;
const title = {
tag: `h${level}`
}
```
**编译器**
主要负责将开发者所书写的**模板转换为渲染函数**。例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
编译后的结果为:
```js
function render(){
return h('div', [
h('h1', {id: someId}, 'Hello')
])
}
```
执行渲染函数,就会得到 JS 对象形式的 UI 表达。
整体来讲,整个编译过程如下图所示:
![image-20231113095532166](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-015532.png)
可以看到,在编译器的内部,实际上又分为了:
- 解析器:负责将模板解析为对应的模板 AST抽象语法树
- 转换器负责将模板AST转换为 JS AST
- 生成器:将 JS AST 生成对应的 JS 代码(渲染函数)
Vue3 的编译器,除了最基本的编译以外,还做了很多的优化:
1. 静态提升
2. 预字符串化
3. 缓存事件处理函数
4. Block Tree
5. PatchFlag
**渲染器**
执行渲染函数得到的就是虚拟 DOM也就是像这样的 JS 对象,里面包含着 UI 的描述信息
```html
<div>点击</div>
```
```js
const vnode = {
tag: 'div',
props: {
onClick: ()=> alert('hello')
},
children: '点击'
}
```
渲染器拿到这个虚拟 DOM 后,就会将其转换为真实的 DOM
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-01-094219.png" alt="image-20240901174218998" style="zoom:50%;" />
一个简易版渲染器的实现思路:
1. 创建元素
2. 为元素添加属性和事件
3. 处理children
```js
function renderer(vnode, container){
// 1. 创建元素
const el = document.createElement(vnode.tag);
// 2. 遍历 props为元素添加属性
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick --->click
vnode.props[key] // 事件处理函数
);
}
}
// 3. 处理children
if(typeof vnode.children === 'string'){
el.appendChild(document.createTextNode(vnode.children))
} else if(Array.isArray(vnode.children)) {
// 递归的调用 renderer
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
```
**组件的本质**
组件本质就是**一组 DOM 元素**的封装。
假设函数代表一个组件:
```js
// 这个函数就可以当作是一个组件
const MyComponent = function () {
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
};
```
vnode 的 tag 就不再局限于 html 元素,而是可以写作这个函数名:
```js
const vnode = {
tag: MyComponent
}
```
渲染器需要新增针对这种 tag 类型的处理:
```js
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === "function") {
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
```
组件也可以使用对象的形式:
```js
const MyComponent = {
render(){
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
}
}
```
```js
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === "object") {
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
```
**响应式系统**
总结:当模板编译成的渲染函数执行时,渲染函数内部用到的响应式数据会和渲染函数本身构成依赖关系,之后只要响应式数据发生变化,渲染函数就会重新执行。
> 面试题:介绍一下 Vue3 内部的运行机制是怎样的?
>
> 参考答案:
>
> Vue3 是一个声明式的框架。声明式的好处在于它直接描述结果用户不需要关注过程。Vue.js 采用模板的方式来描述 UI但它同样支持使用虚拟 DOM 来描述 UI。**虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观**。
>
> 当用户使用模板来描述 UI 的时候,内部的 **编译器** 会将其编译为渲染函数,渲染函数执行后能够确定响应式数据和渲染函数之间的依赖关系,之后响应式数据一变化,渲染函数就会重新执行。
>
> 渲染函数执行的结果是得到虚拟 DOM之后就需要 **渲染器** 来将虚拟 DOM 对象渲染为真实 DOM 元素。它的工作原理是,递归地遍历虚拟 DOM 对象,并调用原生 DOM API 来完成真实 DOM 的创建。渲染器的精髓在于后续的更新,它会通过 Diff 算法找出变更点,并且只会更新需要更新的内容。
>
> 编译器、渲染器、响应式系统都是 Vue 内部的核心模块,它们共同构成一个有机的整体,不同模块之间互相配合,进一步提升框架性能。
---
-EOF-

View File

@ -0,0 +1,118 @@
# computed面试题
>面试题:谈谈 computed 的机制,缓存了什么?为什么 computed 不支持异步?
响应式系统:
- track进行依赖收集建立数据和函数的映射关系
- trigger触发更新重新执行数据所映射的所有函数
computed开发者使用
```js
const state = reactive({
a: 1,
b: 2
})
const sum = computed(() => {
return state.a + state.b
})
```
```js
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
;[firstName.value, lastName.value] = newValue.split(' ')
}
})
```
computed核心实现
1. 参数归一化,统一成对象的形式
2. 返回一个存取器对象
```js
import { effect } from "./effect/effect.js";
import track from "./effect/track.js";
import trigger from "./effect/trigger.js";
import { TriggerOpTypes, TrackOpTypes } from "./utils.js";
// 参数归一化
function normalizeParameter(getterOrOptions) {
// 代码略
}
/**
*
* @param {*} getterOrOptions 可能是函数,也可能是对象
*/
export function computed(getterOrOptions) {
// 1. 参数归一化
const {getter, setter} = normalizeParameter(getterOrOptions);
// value 用于存储计算结果, dirty 负责控制从缓存中获取值还是重新计算新的值dirty为true就代表要重新计算
let value, dirty = true;
// 让getter内部的响应式数据和getter建立映射关系
// 回头getter内部的响应式数据发生变化后重新执行getter
const effectFn = effect(getter, {
lazy: true,
scheduler(){
dirty = true;
trigger(obj, TriggerOpTypes.SET, "value")
}
})
// 2. 返回一个存取器对象
const obj = {
get value(){
// 需要将 value 和渲染函数建立映射关系
track(obj, TrackOpTypes.GET, "value")
if(dirty){
value = effectFn()
dirty = false;
}
return value;
},
set value(newValue){
setter(newValue)
}
}
return obj;
}
```
> 面试题:谈谈 computed 的机制,缓存了什么?为什么 computed 不支持异步?
>
> 参考答案:
>
> **谈谈 computed 的机制,缓存了什么?**
>
> 缓存的是上一次 getter 计算出来的值。
>
> **为什么 computed 不支持异步?**
>
> computed 属性在 Vue 中不支持异步操作的主要原因是设计上的理念和使用场景的考虑。**computed 属性的初衷是用于计算并缓存一个基于响应式依赖的同步计算结果**当其依赖的响应式数据发生变化时Vue 会自动重新计算 computed 的值,并将其缓存,以提高性能。
>
> computed 不支持异步的几个具体原因:
>
> 1. 缓存机制与同步计算computed 属性的一个核心特性是缓存。当依赖的响应式数据没有变化时computed 的计算结果会被缓存并直接返回而不会重新执行计算。这种缓存机制是基于同步计算的假如允许异步计算那么在异步操作完成之前computed 属性无法提供有效的返回值,这与它的同步缓存理念相违背。
> 2. 数据一致性computed 属性通常用于模板中的绑定,它的计算结果需要在渲染期间是稳定且可用的。如果 computed 支持异步操作,渲染过程中的数据可能不一致,会导致模板渲染时无法确定使用什么数据,从而可能造成视图的闪烁或数据错误。
> 3. 调试与依赖追踪困难:如果 computed 属性是异步的,那么在调试和依赖追踪时就会变得非常复杂。异步操作的完成时间不确定,会使得依赖追踪的过程变得不直观,也难以预期。
>
> 如果需要进行异步操作,通常推荐使用 watch 来实现。
---
-EOF-

View File

@ -0,0 +1,87 @@
# watch面试题
>面试题watch 和 computed 的区别是什么?说一说各自的使用场景?
watch的使用
```js
const count = ref('');
watch(count, async (newVal, oldVal)=>{})
watch(()=>{
count...
}, (newVal, oldVal)=>{
// ...
})
```
watch核心实现
```js
import { effect, cleanup } from "./effect/effect.js";
// 遍历对象
function traverse(value, seen = new Set()) {
// ...
}
/**
* @param {*} source
* @param {*} cb 要执行的回调函数
* @param {*} options 选项对象
* @returns
*/
export function watch(source, cb, options = {}) {
// 1. 参数归一化,统一成一个函数
let getter;
if(typeof source === 'function'){
getter = source;
} else {
getter = () => traverse(source)
}
// 2. 保存新值和旧值
let oldValue, newValue;
const effectFn = effect(()=>getter(), {
lazy: true,
scheduler: ()=>{
newValue = effectFn();
cb(newValue, oldValue)
oldValue = newValue
}
})
oldValue = effectFn()
return ()=>{
cleanup(effectFn)
}
}
```
> 面试题watch 和 computed 的区别是什么?说一说各自的使用场景?
>
> 参考答案:
>
> **computed**
>
> - 作用:用于创建计算属性,依赖于 Vue 的响应式系统来做数据追踪。当依赖的数据发生变化时,会自动重新计算。
> - 无副作用:计算属性内部的计算应当是没有副作用的,也就是说仅仅基于数据做二次计算。
> - 缓存:计算属性具备缓存机制,如果响应式数据没变,每次获取计算属性时,内部直接返回的是上一次计算值。
> - 用处:通常用于模板当中,以便在模板中显示二次计算后的结构。
> - 同步:计算属性的一个核心特性是缓存,而这种缓存机制是基于同步计算的,假如允许异步计算,那么在异步操作完成之前,计算属性无法提供有效的返回值,这与它的缓存设计理念相违背。
>
> **watch**
>
> - 作用:用于监听数据的变化,可以监听一个或者多个数据,当数据发生改变时,执行一些用户指定的操作。
> - 副作用:监听器中的回调函数可以执行副作用操作,例如发送网络请求、手动操作 DOM 等。
> - 无缓存:监听器中的回调函数执行结果不会被缓存,也没办法缓存,因为不知道用户究竟要执行什么操作,有可能是包含副作用的操作,有可能是不包含副作用的操作。
> - 用处:常用于响应式数据发生变化后,重新发送网络请求,或者修改 DOM 元素等场景。
> - 支持异步:在监听到响应式数据发生变化后,可以进行同步或者异步的操作。
---
-EOF-

View File

@ -0,0 +1,323 @@
# 模板编译器
>面试题:说一下 Vue 中 Compiler 的实现原理是什么?
**Vue中的编译器**
Vue 里面的编译器,主要负责将开发者所书写的模板转换为渲染函数。例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
编译后的结果为:
```js
function render(){
return h('div', [
h('h1', {id: someId}, 'Hello')
])
}
```
这里整个过程并非一触而就的,而是经历一个又一个步骤一点一点转换而来的。
整体来讲,整个编译过程如下图所示:
![image-20231113095532166](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-015532.png)
可以看到,在编译器的内部,实际上又分为了:
- 解析器:负责将模板解析为所对应的 AST
- 转换器:负责将模板 AST 转换为 JavaScript AST
- 生成器:根据 JavaScript 的 AST 生成最终的渲染函数
**解析器**
解析器的核心作用是负责将模板解析为所对应的模板 AST。
首先用户所书写的模板,例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
对于解析器来讲仍然就是一段字符串而已,类似于:
```js
'<template><div><h1 :id="someId">Hello</h1></div></template>'
```
那么解析器是如何进行解析的呢?这里涉及到一个 <u>有限状态机</u> 的概念。
### FSM
FSM英语全称为 Finite State Machine翻译成中文就是有限状态机它首先定义了**一组状态**,然后还定义了状态之间的转移以及触发这些转移的事件。然后就会去解析字符串里面的每一个字符,根据字符做状态的转换。
举一个例子,假设我们要解析的模板内容为:
```js
'<p>Vue</p>'
```
那么整个状态的迁移过程如下:
1. 状态机一开始处于 **初始状态**
2. 在 **初始状态** 下,读取字符串的第一个字符 < ,然后状态机的状态会更新为 **标签开始状态**
3. 接下来继续读取下一个字符 p由于 p 是字母,所以状态机的状态会更新为 **标签名称开始状态**
4. 接下来读取的下一个字符为 >,状态机的状态会回到 **初始状态**,并且会记录在标签状态下产生的标签名称 p。
5. 读取下一个字符 V此时状态机会进入到 **文本状态**
6. 读取下一个字符 u状态机仍然是 **文本状态**
7. 读取下一个字符 e状态机仍然是 **文本状态**
8. 读取下一个字符 <,此时状态机会进入到 **标签开始状态**
9. 读取下一个字符 / ,状态机会进入到 **标签结束状态**
10. 读取下一个字符 p状态机进入 **标签名称结束状态**
11. 读取下一个字符 >,状态机进重新回到 **初始状态**
具体如下图所示:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-060437.png" alt="image-20231113140436969" style="zoom:60%;" />
```js
let x = 10 + 5;
```
```
token:
let(关键字) x(标识符) =(运算符) 10(数字) +(运算符) 5(数字) ;(分号)
```
对应代码:
```js
const template = '<p>Vue</p>';
// 首先定义一些状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始状态
tagName: 3, // 标签名称开始状态
text: 4, // 文本状态
tagEnd: 5, // 标签结束状态
tagEndName: 6 // 标签名称结束状态
}
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
// 将字符串解析为 token
function tokenize(str){
// 初始化当前状态
let currentState = State.initial;
// 用于缓存字符
const chars = [];
// 存储解析出来的 token
const tokens = [];
while(str){
const char = str[0]; // 获取字符串里面的第一个字符
switch(currentState){
case State.initial:{
if(char === '<'){
currentState = State.tagOpen;
// 消费一个字符
str = str.slice(1);
} else if(isAlpha(char)){
// 判断是否为字母
currentState = State.text;
chars.push(char);
// 消费一个字符
str = str.slice(1);
}
break;
}
case State.tagOpen: {
// 相应的状态处理
}
case State.tagName: {
// 相应的状态处理
}
}
}
return tokens;
}
tokenize(template);
```
最终解析出来的 token:
```js
[
{type: 'tag', name: 'p'}, // 开始标签
{type: 'text', content: 'Vue'}, // 文本节点
{type: 'tagEnd', name: 'p'}, // 结束标签
]
```
**构造模板AST**
根据 token 列表创建模板 AST 的过程,其实就是对 token 列表进行扫描的过程。从列表的第一个 token 开始,按照顺序进行扫描,直到列表中所有的 token 处理完毕。
在这个过程中,我们需**要维护一个栈**,这个栈将用于维护元素间的父子关系。每遇到一个开始标签节点,就构造一个 Element 类型的 AST 节点,并将其压入栈中。
类似的,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。
举个例子,假设我们有如下的模板内容:
```vue
'<div><p>Vue</p><p>React</p></div>'
```
经过上面的 tokenize 后能够得到如下的数组:
```js
[
{"type": "tag","name": "div"},
{"type": "tag","name": "p"},
{"type": "text","content": "Vue"},
{"type": "tagEnd","name": "p"},
{"type": "tag","name": "p"},
{"type": "text","content": "React"},
{"type": "tagEnd","name": "p"},
{"type": "tagEnd","name": "div"}
]
```
那么接下来会遍历这个数组(也就是扫描 tokens 列表)
1. 一开始有一个 elementStack 栈,刚开始有一个 Root 节点,[ Root ]
2. 首先是一个 **div tag**,创建一个 Element 类型的 AST 节点,并将其压栈到 elementStack当前的栈为 `[ Root, div ]`div 会作为 Root 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070249.png" alt="image-20231113150248725" style="zoom:50%;" />
3. 接下来是 **p tag**,创建一个 Element 类型的 AST 节点,同样会压栈到 elementStack当前的栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070335.png" alt="image-20231113150335866" style="zoom:50%;" />
4. 接下来是 **Vue text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070356.png" alt="image-20231113150356416" style="zoom:50%;" />
5. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
6. 接下来是 **p tag**,同样创建一个 Element 类型的 AST 节点,压栈后栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070442.png" alt="image-20231113150442450" style="zoom:50%;" />
7. 接下来是 **React text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070537.png" alt="image-20231113150537351" style="zoom:50%;" />
8. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
9. 最后是 **div tagEnd**,发现是一个结束标签,将其弹出,栈区重新为 `[ Root ]`,至此整个 AST 构建完毕
落地到具体的代码,大致就是这样的:
```js
// 解析器
function parse(str){
const tokens = tokenize(str);
// 创建Root根AST节点
const root = {
type: 'Root',
children: []
}
// 创建一个栈
const elementStack = [root]
while(tokens.length){
// 获取当前栈顶点作为父节点,也就是栈数组最后一项
const parent = elementStack[elementStack.length - 1];
// 从 tokens 列表中依次取出第一个 token
const t = tokens[0];
switch(t.type){
// 根据不同的type做不同的处理
case 'tag':{
// 创建一个Element类型的AST节点
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
// 将其添加为父节点的子节点
parent.children.push(elementNode)
// 将当前节点压入栈里面
elementStack.push(elementNode)
break;
}
case 'text':
// 创建文本类型的 AST 节点
const textNode = {
type: 'Text',
content: t.content
}
// 将其添加到父级节点的 children 中
parent.children.push(textNode)
break
case 'tagEnd':
// 遇到结束标签,将当前栈顶的节点弹出
elementStack.pop()
break
}
// 将处理过的 token 弹出去
tokens.shift();
}
}
```
最终,经过上面的处理,就得到了模板的抽象语法树:
```
{
"type": "Root",
"children": [
{
"type": "Element",
"tag": "div",
"children": [
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "Vue"
}
]
},
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "React"
}
]
}
]
}
]
}
```

View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// HTML模板字符串
const template = `<div><p>Vue</p><p>React</p></div>`;
// 定义不同的解析状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始的状态,例如遇到 '<'
tagName: 3, // 解析标签名称的状态
text: 4, // 文本节点的状态
tagEnd: 5, // 结束标签的初始状态,例如遇到 '</'
tagEndName: 6, // 解析结束标签名称的状态
};
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
function tokenize(str) {
let currentState = State.initial; // 初始状态
const chars = []; // 用于存储字符
const tokens = []; // 存储生成的令牌
// 循环处理字符串中的每个字符
while (str) {
const char = str[0]; // 获取当前字符
switch (currentState) {
case State.initial:
// 初始状态:检查当前字符
if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示标签开始
str = str.slice(1); // 移除已处理的字符
} else if (isAlpha(char)) {
currentState = State.text; // 如果是字母,表示文本开始
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagOpen:
// 标签开启状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagName; // 如果是字母,表示进入标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "/") {
currentState = State.tagEnd; // 如果字符是 '/',表示结束标签开始
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagName:
// 解析标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',标签名称结束,返回初始状态
tokens.push({ type: "tag", name: chars.join("") }); // 创建标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.text:
// 解析文本节点状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示遇到新的标签,返回标签开启状态
tokens.push({ type: "text", content: chars.join("") }); // 创建文本类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEnd:
// 结束标签的开始状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagEndName; // 如果是字母,表示进入结束标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEndName:
// 解析结束标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',结束标签名称结束,返回初始状态
tokens.push({ type: "tagEnd", name: chars.join("") }); // 创建结束标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
}
}
// 返回生成的令牌列表
return tokens;
}
// 解析函数,将 HTML 字符串转换为 AST
function parse(str) {
// 使用tokenize函数将字符串转换为令牌
const tokens = tokenize(str);
// 创建一个根节点对象用于存储解析后的HTML结构
const root = {
type: "Root",
children: [],
};
// 使用一个栈来跟踪当前处理的元素节点
const elementStack = [root];
// 当仍有令牌时,继续处理
while (tokens.length) {
// 获取当前的父元素(栈顶元素)
const parent = elementStack[elementStack.length - 1];
// 获取当前要处理的令牌
const t = tokens[0];
// 根据令牌类型处理不同情况
switch (t.type) {
case "tag":
// 如果是开始标签,创建一个新的元素节点
const elementNode = {
type: "Element",
tag: t.name,
children: [],
};
// 将新节点添加到父元素的子节点中
parent.children.push(elementNode);
// 将新节点压入栈中,成为下一个父节点
elementStack.push(elementNode);
break;
case "text":
// 如果是文本,创建一个文本节点
const textNode = {
type: "Text",
content: t.content,
};
// 将文本节点添加到当前父元素的子节点中
parent.children.push(textNode);
break;
case "tagEnd":
// 如果是结束标签,从栈中弹出当前处理的元素
elementStack.pop();
break;
}
// 移除已处理的令牌
tokens.shift();
}
// 返回解析后的根节点它包含了整个HTML结构
return root;
}
console.log(parse(template));
</script>
</body>
</html>

View File

@ -0,0 +1,591 @@
# 模板编译器
>面试题:说一下 Vue 中 Compiler 的实现原理是什么?
**Vue中的编译器**
Vue 里面的编译器,主要负责将开发者所书写的模板转换为渲染函数。例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
编译后的结果为:
```js
function render(){
return h('div', [
h('h1', {id: someId}, 'Hello')
])
}
```
这里整个过程并非一触而就的,而是经历一个又一个步骤一点一点转换而来的。
整体来讲,整个编译过程如下图所示:
![image-20231113095532166](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-015532.png)
可以看到,在编译器的内部,实际上又分为了:
- 解析器:负责将模板解析为所对应的 AST
- 转换器:负责将模板 AST 转换为 JavaScript AST
- 生成器:根据 JavaScript 的 AST 生成最终的渲染函数
**解析器**
解析器的核心作用是负责将模板解析为所对应的模板 AST。
首先用户所书写的模板,例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
对于解析器来讲仍然就是一段字符串而已,类似于:
```js
'<template><div><h1 :id="someId">Hello</h1></div></template>'
```
那么解析器是如何进行解析的呢?这里涉及到一个 <u>有限状态机</u> 的概念。
### FSM
FSM英语全称为 Finite State Machine翻译成中文就是有限状态机它首先定义了**一组状态**,然后还定义了状态之间的转移以及触发这些转移的事件。然后就会去解析字符串里面的每一个字符,根据字符做状态的转换。
举一个例子,假设我们要解析的模板内容为:
```js
'<p>Vue</p>'
```
那么整个状态的迁移过程如下:
1. 状态机一开始处于 **初始状态**
2. 在 **初始状态** 下,读取字符串的第一个字符 < ,然后状态机的状态会更新为 **标签开始状态**
3. 接下来继续读取下一个字符 p由于 p 是字母,所以状态机的状态会更新为 **标签名称开始状态**
4. 接下来读取的下一个字符为 >,状态机的状态会回到 **初始状态**,并且会记录在标签状态下产生的标签名称 p。
5. 读取下一个字符 V此时状态机会进入到 **文本状态**
6. 读取下一个字符 u状态机仍然是 **文本状态**
7. 读取下一个字符 e状态机仍然是 **文本状态**
8. 读取下一个字符 <,此时状态机会进入到 **标签开始状态**
9. 读取下一个字符 / ,状态机会进入到 **标签结束状态**
10. 读取下一个字符 p状态机进入 **标签名称结束状态**
11. 读取下一个字符 >,状态机进重新回到 **初始状态**
具体如下图所示:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-060437.png" alt="image-20231113140436969" style="zoom:60%;" />
```js
let x = 10 + 5;
```
```
token:
let(关键字) x(标识符) =(运算符) 10(数字) +(运算符) 5(数字) ;(分号)
```
对应代码:
```js
const template = '<p>Vue</p>';
// 首先定义一些状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始状态
tagName: 3, // 标签名称开始状态
text: 4, // 文本状态
tagEnd: 5, // 标签结束状态
tagEndName: 6 // 标签名称结束状态
}
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
// 将字符串解析为 token
function tokenize(str){
// 初始化当前状态
let currentState = State.initial;
// 用于缓存字符
const chars = [];
// 存储解析出来的 token
const tokens = [];
while(str){
const char = str[0]; // 获取字符串里面的第一个字符
switch(currentState){
case State.initial:{
if(char === '<'){
currentState = State.tagOpen;
// 消费一个字符
str = str.slice(1);
} else if(isAlpha(char)){
// 判断是否为字母
currentState = State.text;
chars.push(char);
// 消费一个字符
str = str.slice(1);
}
break;
}
case State.tagOpen: {
// 相应的状态处理
}
case State.tagName: {
// 相应的状态处理
}
}
}
return tokens;
}
tokenize(template);
```
最终解析出来的 token:
```js
[
{type: 'tag', name: 'p'}, // 开始标签
{type: 'text', content: 'Vue'}, // 文本节点
{type: 'tagEnd', name: 'p'}, // 结束标签
]
```
**构造模板AST**
根据 token 列表创建模板 AST 的过程,其实就是对 token 列表进行扫描的过程。从列表的第一个 token 开始,按照顺序进行扫描,直到列表中所有的 token 处理完毕。
在这个过程中,我们需**要维护一个栈**,这个栈将用于维护元素间的父子关系。每遇到一个开始标签节点,就构造一个 Element 类型的 AST 节点,并将其压入栈中。
类似的,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。
举个例子,假设我们有如下的模板内容:
```vue
'<div><p>Vue</p><p>React</p></div>'
```
经过上面的 tokenize 后能够得到如下的数组:
```js
[
{"type": "tag","name": "div"},
{"type": "tag","name": "p"},
{"type": "text","content": "Vue"},
{"type": "tagEnd","name": "p"},
{"type": "tag","name": "p"},
{"type": "text","content": "React"},
{"type": "tagEnd","name": "p"},
{"type": "tagEnd","name": "div"}
]
```
那么接下来会遍历这个数组(也就是扫描 tokens 列表)
1. 一开始有一个 elementStack 栈,刚开始有一个 Root 节点,[ Root ]
2. 首先是一个 **div tag**,创建一个 Element 类型的 AST 节点,并将其压栈到 elementStack当前的栈为 `[ Root, div ]`div 会作为 Root 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070249.png" alt="image-20231113150248725" style="zoom:50%;" />
3. 接下来是 **p tag**,创建一个 Element 类型的 AST 节点,同样会压栈到 elementStack当前的栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070335.png" alt="image-20231113150335866" style="zoom:50%;" />
4. 接下来是 **Vue text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070356.png" alt="image-20231113150356416" style="zoom:50%;" />
5. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
6. 接下来是 **p tag**,同样创建一个 Element 类型的 AST 节点,压栈后栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070442.png" alt="image-20231113150442450" style="zoom:50%;" />
7. 接下来是 **React text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070537.png" alt="image-20231113150537351" style="zoom:50%;" />
8. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
9. 最后是 **div tagEnd**,发现是一个结束标签,将其弹出,栈区重新为 `[ Root ]`,至此整个 AST 构建完毕
落地到具体的代码,大致就是这样的:
```js
// 解析器
function parse(str){
const tokens = tokenize(str);
// 创建Root根AST节点
const root = {
type: 'Root',
children: []
}
// 创建一个栈
const elementStack = [root]
while(tokens.length){
// 获取当前栈顶点作为父节点,也就是栈数组最后一项
const parent = elementStack[elementStack.length - 1];
// 从 tokens 列表中依次取出第一个 token
const t = tokens[0];
switch(t.type){
// 根据不同的type做不同的处理
case 'tag':{
// 创建一个Element类型的AST节点
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
// 将其添加为父节点的子节点
parent.children.push(elementNode)
// 将当前节点压入栈里面
elementStack.push(elementNode)
break;
}
case 'text':
// 创建文本类型的 AST 节点
const textNode = {
type: 'Text',
content: t.content
}
// 将其添加到父级节点的 children 中
parent.children.push(textNode)
break
case 'tagEnd':
// 遇到结束标签,将当前栈顶的节点弹出
elementStack.pop()
break
}
// 将处理过的 token 弹出去
tokens.shift();
}
}
```
最终,经过上面的处理,就得到了模板的抽象语法树:
```
{
"type": "Root",
"children": [
{
"type": "Element",
"tag": "div",
"children": [
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "Vue"
}
]
},
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "React"
}
]
}
]
}
]
}
```
**转换器**
目前为止,我们已经得到了模板的 AST回顾一下 Vue 中整个模板的编译过程,大致如下:
```js
// 编译器
function compile(template){
// 1. 解析器对模板进行解析得到模板的AST
const ast = parse(template)
// 2. 转换器将模板AST转换为JS AST
transform(ast)
// 3. 生成器:在 JS AST 的基础上生成 JS 代码
const code = genrate(ast)
return code;
}
```
转换器的核心作用就是负责将模板 AST 转换为 JavaScript AST。
整体来讲,转换器的编写分为两大部分:
- 模板 AST 的遍历与转换
- 生成 JavaScript AST
**模板AST的遍历与转换**
步骤一:先书写一个简单的工具方法,方便查看一个模板 AST 中的节点信息。
```js
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
```
步骤二:接下来下一步就是遍历整棵模板 AST 树,并且能够做一些改动
```js
function tranverseNode(ast){
// 获取到当前的节点
const currentNode = ast;
// 将p修改为h1
if(currentNode.type === 'Element' && currentNode.tag === 'p'){
currentNode.tag = 'h1';
}
// 新增需求:将文本节点全部改为大写
if(currentNode.type === 'Text'){
currentNode.content = currentNode.content.toUpperCase();
}
// 获取当前节点的子节点
const children = currentNode.children;
if(children){
for(let i = 0;i< children.length; i++){
tranverseNode(children[i])
}
}
}
function transform(ast){
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast);
console.log(dump(ast));
}
```
目前tranverseNode虽然能够正常工作但是内部有两个职责遍历、转换接下来需要将这两个职责进行解耦。
步骤三:在 transform 里面维护一个上下文对象(环境:包含执行代码时用到的一些信息)
```js
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node){
if(node.type === 'Element' && node.tag === 'p'){
node.tag = 'h1';
}
}
function transformText(node){
if(node.type === 'Text'){
node.content = node.content.toUpperCase();
}
}
// 该方法只负责遍历,转换的工作交给转换函数
// 转换函数是存放于上下文对象里面的
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode);
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
}
function transform(ast){
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
}
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
}
```
步骤四:完善 context 上下文对象这里主要是添加2个方法
1. 替换节点方法
2. 删除节点方法
```js
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node){
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode(){
if(context.parent){
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
}
```
注意因为存在删除节点的操作所以在tranverseNode方法里面执行转换函数之后需要进行非空的判断
```js
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode, context);
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if(!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
}
```
步骤五:解决节点处理的次数问题
目前来讲,遍历的顺序是深度遍历,从父节点到子节点。但是我们的需求是:子节点处理完之后,重新回到父节点,对父节点进行处理。
首先需要对转换函数进行改造:返回一个函数
```js
function transformText(node, context) {
// 省略第一次处理....
return ()=>{
// 对节点再次进行处理
}
}
```
tranverseNode需要拿一个数组存储转换函数返回的函数
```js
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = []
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if(onExit){
exitFns.push(onExit)
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if(!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while(i--){
exitFns[i]()
}
}
```

View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// HTML模板字符串
const template = `<div><p>Vue</p><p>React</p></div>`;
// 定义不同的解析状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始的状态,例如遇到 '<'
tagName: 3, // 解析标签名称的状态
text: 4, // 文本节点的状态
tagEnd: 5, // 结束标签的初始状态,例如遇到 '</'
tagEndName: 6, // 解析结束标签名称的状态
};
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
function tokenize(str) {
let currentState = State.initial; // 初始状态
const chars = []; // 用于存储字符
const tokens = []; // 存储生成的令牌
// 循环处理字符串中的每个字符
while (str) {
const char = str[0]; // 获取当前字符
switch (currentState) {
case State.initial:
// 初始状态:检查当前字符
if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示标签开始
str = str.slice(1); // 移除已处理的字符
} else if (isAlpha(char)) {
currentState = State.text; // 如果是字母,表示文本开始
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagOpen:
// 标签开启状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagName; // 如果是字母,表示进入标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "/") {
currentState = State.tagEnd; // 如果字符是 '/',表示结束标签开始
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagName:
// 解析标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',标签名称结束,返回初始状态
tokens.push({ type: "tag", name: chars.join("") }); // 创建标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.text:
// 解析文本节点状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示遇到新的标签,返回标签开启状态
tokens.push({ type: "text", content: chars.join("") }); // 创建文本类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEnd:
// 结束标签的开始状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagEndName; // 如果是字母,表示进入结束标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEndName:
// 解析结束标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',结束标签名称结束,返回初始状态
tokens.push({ type: "tagEnd", name: chars.join("") }); // 创建结束标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
}
}
// 返回生成的令牌列表
return tokens;
}
// 解析函数,将 HTML 字符串转换为 AST
function parse(str) {
// 使用tokenize函数将字符串转换为令牌
const tokens = tokenize(str);
// 创建一个根节点对象用于存储解析后的HTML结构
const root = {
type: "Root",
children: [],
};
// 使用一个栈来跟踪当前处理的元素节点
const elementStack = [root];
// 当仍有令牌时,继续处理
while (tokens.length) {
// 获取当前的父元素(栈顶元素)
const parent = elementStack[elementStack.length - 1];
// 获取当前要处理的令牌
const t = tokens[0];
// 根据令牌类型处理不同情况
switch (t.type) {
case "tag":
// 如果是开始标签,创建一个新的元素节点
const elementNode = {
type: "Element",
tag: t.name,
children: [],
};
// 将新节点添加到父元素的子节点中
parent.children.push(elementNode);
// 将新节点压入栈中,成为下一个父节点
elementStack.push(elementNode);
break;
case "text":
// 如果是文本,创建一个文本节点
const textNode = {
type: "Text",
content: t.content,
};
// 将文本节点添加到当前父元素的子节点中
parent.children.push(textNode);
break;
case "tagEnd":
// 如果是结束标签,从栈中弹出当前处理的元素
elementStack.pop();
break;
}
// 移除已处理的令牌
tokens.shift();
}
// 返回解析后的根节点它包含了整个HTML结构
return root;
}
// 辅助方法用于按照层次结构打印AST的节点信息
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
const templateAST = parse(template);
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node, context) {
// if (node.type === "Element" && node.tag === "p") {
// node.tag = "h1";
// }
}
function transformText(node, context) {
// if (node.type === "Text") {
// node.content = node.content.toUpperCase();
// }
// 测试替换将文本节点替换为span元素
// if (node.type === "Text") {
// context.replaceNode({
// type: "Element",
// tag: "span",
// });
// }
// 测试删除:删除 React 文本节点
// if (node.type === "Text" && node.content === "React") {
// context.removeNode();
// }
return () => {
// 对节点再次进行处理
console.log("可以再次处理节点", node.type, node.tag || node.content);
};
}
function tranverseNode(ast, context) {
// 刚进去时进行处理
console.log("处理节点:", ast.type, ast.tag || ast.content);
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = [];
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if (onExit) {
exitFns.push(onExit);
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if (!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function transform(ast) {
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node) {
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
};
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
console.log(dump(ast));
}
transform(templateAST);
</script>
</body>
</html>

View File

@ -0,0 +1,851 @@
# 模板编译器
>面试题:说一下 Vue 中 Compiler 的实现原理是什么?
**Vue中的编译器**
Vue 里面的编译器,主要负责将开发者所书写的模板转换为渲染函数。例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
编译后的结果为:
```js
function render(){
return h('div', [
h('h1', {id: someId}, 'Hello')
])
}
```
这里整个过程并非一触而就的,而是经历一个又一个步骤一点一点转换而来的。
整体来讲,整个编译过程如下图所示:
![image-20231113095532166](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-015532.png)
可以看到,在编译器的内部,实际上又分为了:
- 解析器:负责将模板解析为所对应的 AST
- 转换器:负责将模板 AST 转换为 JavaScript AST
- 生成器:根据 JavaScript 的 AST 生成最终的渲染函数
**解析器**
解析器的核心作用是负责将模板解析为所对应的模板 AST。
首先用户所书写的模板,例如:
```vue
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
```
对于解析器来讲仍然就是一段字符串而已,类似于:
```js
'<template><div><h1 :id="someId">Hello</h1></div></template>'
```
那么解析器是如何进行解析的呢?这里涉及到一个 <u>有限状态机</u> 的概念。
### FSM
FSM英语全称为 Finite State Machine翻译成中文就是有限状态机它首先定义了**一组状态**,然后还定义了状态之间的转移以及触发这些转移的事件。然后就会去解析字符串里面的每一个字符,根据字符做状态的转换。
举一个例子,假设我们要解析的模板内容为:
```js
'<p>Vue</p>'
```
那么整个状态的迁移过程如下:
1. 状态机一开始处于 **初始状态**
2. 在 **初始状态** 下,读取字符串的第一个字符 < ,然后状态机的状态会更新为 **标签开始状态**
3. 接下来继续读取下一个字符 p由于 p 是字母,所以状态机的状态会更新为 **标签名称开始状态**
4. 接下来读取的下一个字符为 >,状态机的状态会回到 **初始状态**,并且会记录在标签状态下产生的标签名称 p。
5. 读取下一个字符 V此时状态机会进入到 **文本状态**
6. 读取下一个字符 u状态机仍然是 **文本状态**
7. 读取下一个字符 e状态机仍然是 **文本状态**
8. 读取下一个字符 <,此时状态机会进入到 **标签开始状态**
9. 读取下一个字符 / ,状态机会进入到 **标签结束状态**
10. 读取下一个字符 p状态机进入 **标签名称结束状态**
11. 读取下一个字符 >,状态机进重新回到 **初始状态**
具体如下图所示:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-060437.png" alt="image-20231113140436969" style="zoom:60%;" />
```js
let x = 10 + 5;
```
```
token:
let(关键字) x(标识符) =(运算符) 10(数字) +(运算符) 5(数字) ;(分号)
```
对应代码:
```js
const template = '<p>Vue</p>';
// 首先定义一些状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始状态
tagName: 3, // 标签名称开始状态
text: 4, // 文本状态
tagEnd: 5, // 标签结束状态
tagEndName: 6 // 标签名称结束状态
}
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
// 将字符串解析为 token
function tokenize(str){
// 初始化当前状态
let currentState = State.initial;
// 用于缓存字符
const chars = [];
// 存储解析出来的 token
const tokens = [];
while(str){
const char = str[0]; // 获取字符串里面的第一个字符
switch(currentState){
case State.initial:{
if(char === '<'){
currentState = State.tagOpen;
// 消费一个字符
str = str.slice(1);
} else if(isAlpha(char)){
// 判断是否为字母
currentState = State.text;
chars.push(char);
// 消费一个字符
str = str.slice(1);
}
break;
}
case State.tagOpen: {
// 相应的状态处理
}
case State.tagName: {
// 相应的状态处理
}
}
}
return tokens;
}
tokenize(template);
```
最终解析出来的 token:
```js
[
{type: 'tag', name: 'p'}, // 开始标签
{type: 'text', content: 'Vue'}, // 文本节点
{type: 'tagEnd', name: 'p'}, // 结束标签
]
```
**构造模板AST**
根据 token 列表创建模板 AST 的过程,其实就是对 token 列表进行扫描的过程。从列表的第一个 token 开始,按照顺序进行扫描,直到列表中所有的 token 处理完毕。
在这个过程中,我们需**要维护一个栈**,这个栈将用于维护元素间的父子关系。每遇到一个开始标签节点,就构造一个 Element 类型的 AST 节点,并将其压入栈中。
类似的,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。
举个例子,假设我们有如下的模板内容:
```vue
'<div><p>Vue</p><p>React</p></div>'
```
经过上面的 tokenize 后能够得到如下的数组:
```js
[
{"type": "tag","name": "div"},
{"type": "tag","name": "p"},
{"type": "text","content": "Vue"},
{"type": "tagEnd","name": "p"},
{"type": "tag","name": "p"},
{"type": "text","content": "React"},
{"type": "tagEnd","name": "p"},
{"type": "tagEnd","name": "div"}
]
```
那么接下来会遍历这个数组(也就是扫描 tokens 列表)
1. 一开始有一个 elementStack 栈,刚开始有一个 Root 节点,[ Root ]
2. 首先是一个 **div tag**,创建一个 Element 类型的 AST 节点,并将其压栈到 elementStack当前的栈为 `[ Root, div ]`div 会作为 Root 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070249.png" alt="image-20231113150248725" style="zoom:50%;" />
3. 接下来是 **p tag**,创建一个 Element 类型的 AST 节点,同样会压栈到 elementStack当前的栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070335.png" alt="image-20231113150335866" style="zoom:50%;" />
4. 接下来是 **Vue text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070356.png" alt="image-20231113150356416" style="zoom:50%;" />
5. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
6. 接下来是 **p tag**,同样创建一个 Element 类型的 AST 节点,压栈后栈为 `[ Root, div, p ]`p 会作为 div 的子节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070442.png" alt="image-20231113150442450" style="zoom:50%;" />
7. 接下来是 **React text**,此时会创建一个 Text 类型的 AST 节点,作为 p 的子节点。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-13-070537.png" alt="image-20231113150537351" style="zoom:50%;" />
8. 接下来是 **p tagEnd**,发现是一个结束标签,所以会将 p 这个 AST 节点弹出栈,当前的栈为 `[ Root, div ]`
9. 最后是 **div tagEnd**,发现是一个结束标签,将其弹出,栈区重新为 `[ Root ]`,至此整个 AST 构建完毕
落地到具体的代码,大致就是这样的:
```js
// 解析器
function parse(str){
const tokens = tokenize(str);
// 创建Root根AST节点
const root = {
type: 'Root',
children: []
}
// 创建一个栈
const elementStack = [root]
while(tokens.length){
// 获取当前栈顶点作为父节点,也就是栈数组最后一项
const parent = elementStack[elementStack.length - 1];
// 从 tokens 列表中依次取出第一个 token
const t = tokens[0];
switch(t.type){
// 根据不同的type做不同的处理
case 'tag':{
// 创建一个Element类型的AST节点
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
// 将其添加为父节点的子节点
parent.children.push(elementNode)
// 将当前节点压入栈里面
elementStack.push(elementNode)
break;
}
case 'text':
// 创建文本类型的 AST 节点
const textNode = {
type: 'Text',
content: t.content
}
// 将其添加到父级节点的 children 中
parent.children.push(textNode)
break
case 'tagEnd':
// 遇到结束标签,将当前栈顶的节点弹出
elementStack.pop()
break
}
// 将处理过的 token 弹出去
tokens.shift();
}
}
```
最终,经过上面的处理,就得到了模板的抽象语法树:
```
{
"type": "Root",
"children": [
{
"type": "Element",
"tag": "div",
"children": [
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "Vue"
}
]
},
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "React"
}
]
}
]
}
]
}
```
**转换器**
目前为止,我们已经得到了模板的 AST回顾一下 Vue 中整个模板的编译过程,大致如下:
```js
// 编译器
function compile(template){
// 1. 解析器对模板进行解析得到模板的AST
const ast = parse(template)
// 2. 转换器将模板AST转换为JS AST
transform(ast)
// 3. 生成器:在 JS AST 的基础上生成 JS 代码
const code = genrate(ast)
return code;
}
```
转换器的核心作用就是负责将模板 AST 转换为 JavaScript AST。
整体来讲,转换器的编写分为两大部分:
- 模板 AST 的遍历与转换
- 生成 JavaScript AST
**模板AST的遍历与转换**
步骤一:先书写一个简单的工具方法,方便查看一个模板 AST 中的节点信息。
```js
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
```
步骤二:接下来下一步就是遍历整棵模板 AST 树,并且能够做一些改动
```js
function tranverseNode(ast){
// 获取到当前的节点
const currentNode = ast;
// 将p修改为h1
if(currentNode.type === 'Element' && currentNode.tag === 'p'){
currentNode.tag = 'h1';
}
// 新增需求:将文本节点全部改为大写
if(currentNode.type === 'Text'){
currentNode.content = currentNode.content.toUpperCase();
}
// 获取当前节点的子节点
const children = currentNode.children;
if(children){
for(let i = 0;i< children.length; i++){
tranverseNode(children[i])
}
}
}
function transform(ast){
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast);
console.log(dump(ast));
}
```
目前tranverseNode虽然能够正常工作但是内部有两个职责遍历、转换接下来需要将这两个职责进行解耦。
步骤三:在 transform 里面维护一个上下文对象(环境:包含执行代码时用到的一些信息)
```js
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node){
if(node.type === 'Element' && node.tag === 'p'){
node.tag = 'h1';
}
}
function transformText(node){
if(node.type === 'Text'){
node.content = node.content.toUpperCase();
}
}
// 该方法只负责遍历,转换的工作交给转换函数
// 转换函数是存放于上下文对象里面的
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode);
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
}
function transform(ast){
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
}
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
}
```
步骤四:完善 context 上下文对象这里主要是添加2个方法
1. 替换节点方法
2. 删除节点方法
```js
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node){
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode(){
if(context.parent){
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
}
```
注意因为存在删除节点的操作所以在tranverseNode方法里面执行转换函数之后需要进行非空的判断
```js
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode, context);
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if(!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
}
```
步骤五:解决节点处理的次数问题
目前来讲,遍历的顺序是深度遍历,从父节点到子节点。但是我们的需求是:子节点处理完之后,重新回到父节点,对父节点进行处理。
首先需要对转换函数进行改造:返回一个函数
```js
function transformText(node, context) {
// 省略第一次处理....
return ()=>{
// 对节点再次进行处理
}
}
```
tranverseNode需要拿一个数组存储转换函数返回的函数
```js
function tranverseNode(ast, context) {
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = []
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if(onExit){
exitFns.push(onExit)
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if(!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while(i--){
exitFns[i]()
}
}
```
**生成JS AST**
要生成 JavaScript 的 AST我们首先需要知道 JavaScript 的 AST 是如何描述代码的。
假设有这么一段代码:
```js
function render(){
return null
}
```
那么所对应的 JS AST 为:
![image-20231120143716229](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-11-20-063716.png)
这里有几个比较关键的部分:
- id对应函数的名称类型为 Identifier
- params对应的是函数的参数是一个数组
- body对应的是函数体由于函数体可以有多条语句因此是一个数组
要查看一段 JS 代码所对应的 AST 结构,可以在 [这里](https://astexplorer.net/) 进行查看。
于是,我们可以仿造上面的样子,**自己设计一个基本的数据结构**来描述函数声明语句,例如:
```js
const FunctionDeclNode = {
type: 'FunctionDecl', // 代表该节点是一个函数声明
id: {
type: 'Identifier'
name: 'render' // name 用来存储函数名称
},
params: [], // 函数参数
body: [
{
type: 'ReturnStatement',
return: null
}
]
}
```
> 对比真实的 AST这里去除了箭头函数、生成器函数、async 函数等情况。
接下来回到我们上面的模板,假设模板内容仍然为:
```html
<div><p>Vue</p><p>React</p></div>
```
那么转换出来的渲染函数应该是:
```js
function render(){
return h('div', [
h('p', 'Vue'),
h('p', 'React'),
])
}
```
这里出现了 h 函数的调用以及数组表达式还有字符串表达式,仍然可以去参阅这段代码真实的 AST。
这里 h 函数对应的应该是:
```js
// 我们自己设计一个节点表示 h 函数的调用
const callExp = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'h'
}
}
```
字符串对应的是:
```js
// 我们自己设计字符串对应的节点
const Str = {
type: 'StringLiteral',
value: 'div'
}
```
> 这里以最外层的 div 字符串为例
数组对应的是:
```js
const Arr = {
type: 'ArrayExpression',
// 数组中的元素
elements: []
}
```
因此按照我们所设计的 AST 数据结构,上面的模板最终转换出来的 JavaScript AST 应该是这样的:
```js
{
"type": "FunctionDecl",
"id": {
"type": "Identifier",
"name": "render"
},
"params": [],
"body": [
{
"type": "ReturnStatement",
"return": {
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{"type": "StringLiteral", "value": "div"},
{"type": "ArrayExpression","elements": [
{
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{"type": "StringLiteral", "value": "p"},
{"type": "StringLiteral", "value": "Vue"}
]
},
{
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{"type": "StringLiteral", "value": "p"},
{"type": "StringLiteral", "value": "React"}
]
}
]
}
]
}
}
]
}
```
我们需要一些辅助函数,这些辅助函数都很简单,一并给出如下:
```js
function createStringLiteral(value) {
return {
type: 'StringLiteral',
value
}
}
function createIdentifier(name) {
return {
type: 'Identifier',
name
}
}
function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements
}
}
function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments
}
}
```
有了这些辅助函数后,接下来我们来修改转换函数。
首先是文本转换
```js
function transformText(node, context){
if(node.type !== 'Text'){
return
}
// 创建文本所对应的 JS AST 节点
// 将创建好的 AST 节点挂到节点的 jsNode 属性上面
node.jsNode = createStringLiteral(node.content);
}
```
Element元素转换
```js
function transformElement(node, context){
// 这里应该是所有的子节点处理完毕后,再进行处理
return ()=>{
if(node.type !== 'Element'){
return;
}
// 创建函数调用的AST节点
const callExp = createCallExpression('h', [
createStringLiteral(node.tag),
])
// 处理函数调用的参数
node.children.length === 1
? // 如果长度为1说明只有一个子节点直接将子节点的 jsNode 作为参数
callExp.arguments.push(node.children[0].jsNode)
: // 说明有多个子节点
callExp.arguments.push(
createArrayExpression(node.children.map(c=>c.jsNode))
)
node.jsNode = callExp
}
}
```
transformRoot转换
```js
function transformRoot(node, context){
// 在退出的回调函数中书写处理逻辑
// 因为要保证所有的子节点已经处理完毕
return ()=>{
if(node.type !== 'Root'){
return;
}
const vnodeJSAST = node.children[0].jsNode;
node.jsNode = {
type: 'FunctionDecl',
id: {type: 'Identifier', name: 'render'},
params: [],
body: [{
type: 'ReturnStatement',
return: vnodeJSAST
}]
}
}
}
```
最后修改 nodeTransforms将这几个转换函数放进去
```js
nodeTransforms: [
transformRoot,
transformElement,
transformText
]
```
至此,我们就完成模板 AST 转换为 JS AST 的工作。
通过 ast.jsNode 能够拿到转换出来的结果。

View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -0,0 +1,355 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// HTML模板字符串
const template = `<div><p>Vue</p><p>React</p></div>`;
// 定义不同的解析状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始的状态,例如遇到 '<'
tagName: 3, // 解析标签名称的状态
text: 4, // 文本节点的状态
tagEnd: 5, // 结束标签的初始状态,例如遇到 '</'
tagEndName: 6, // 解析结束标签名称的状态
};
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
function tokenize(str) {
let currentState = State.initial; // 初始状态
const chars = []; // 用于存储字符
const tokens = []; // 存储生成的令牌
// 循环处理字符串中的每个字符
while (str) {
const char = str[0]; // 获取当前字符
switch (currentState) {
case State.initial:
// 初始状态:检查当前字符
if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示标签开始
str = str.slice(1); // 移除已处理的字符
} else if (isAlpha(char)) {
currentState = State.text; // 如果是字母,表示文本开始
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagOpen:
// 标签开启状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagName; // 如果是字母,表示进入标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "/") {
currentState = State.tagEnd; // 如果字符是 '/',表示结束标签开始
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagName:
// 解析标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',标签名称结束,返回初始状态
tokens.push({ type: "tag", name: chars.join("") }); // 创建标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.text:
// 解析文本节点状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示遇到新的标签,返回标签开启状态
tokens.push({ type: "text", content: chars.join("") }); // 创建文本类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEnd:
// 结束标签的开始状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagEndName; // 如果是字母,表示进入结束标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEndName:
// 解析结束标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',结束标签名称结束,返回初始状态
tokens.push({ type: "tagEnd", name: chars.join("") }); // 创建结束标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
}
}
// 返回生成的令牌列表
return tokens;
}
// 解析函数,将 HTML 字符串转换为 AST
function parse(str) {
// 使用tokenize函数将字符串转换为令牌
const tokens = tokenize(str);
// 创建一个根节点对象用于存储解析后的HTML结构
const root = {
type: "Root",
children: [],
};
// 使用一个栈来跟踪当前处理的元素节点
const elementStack = [root];
// 当仍有令牌时,继续处理
while (tokens.length) {
// 获取当前的父元素(栈顶元素)
const parent = elementStack[elementStack.length - 1];
// 获取当前要处理的令牌
const t = tokens[0];
// 根据令牌类型处理不同情况
switch (t.type) {
case "tag":
// 如果是开始标签,创建一个新的元素节点
const elementNode = {
type: "Element",
tag: t.name,
children: [],
};
// 将新节点添加到父元素的子节点中
parent.children.push(elementNode);
// 将新节点压入栈中,成为下一个父节点
elementStack.push(elementNode);
break;
case "text":
// 如果是文本,创建一个文本节点
const textNode = {
type: "Text",
content: t.content,
};
// 将文本节点添加到当前父元素的子节点中
parent.children.push(textNode);
break;
case "tagEnd":
// 如果是结束标签,从栈中弹出当前处理的元素
elementStack.pop();
break;
}
// 移除已处理的令牌
tokens.shift();
}
// 返回解析后的根节点它包含了整个HTML结构
return root;
}
// 辅助方法用于按照层次结构打印AST的节点信息
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
const ast = parse(template);
// 这里一些辅助方法,用于生产 JS AST 节点的
// 根据不同的操作,会生成不同类型的 JS AST 节点
function createStringLiteral(value) {
return {
type: "StringLiteral",
value,
};
}
function createIdentifier(name) {
return {
type: "Identifier",
name,
};
}
function createArrayExpression(elements) {
return {
type: "ArrayExpression",
elements,
};
}
function createCallExpression(callee, arguments) {
return {
type: "CallExpression",
callee: createIdentifier(callee),
arguments,
};
}
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node, context) {
// 这里应该是所有的子节点处理完毕后,再进行处理
return () => {
if (node.type !== "Element") {
return;
}
// 创建函数调用的AST节点
const callExp = createCallExpression("h", [
createStringLiteral(node.tag),
]);
// 处理函数调用的参数
node.children.length === 1
? // 如果长度为1说明只有一个子节点直接将子节点的 jsNode 作为参数
callExp.arguments.push(node.children[0].jsNode)
: // 说明有多个子节点
callExp.arguments.push(
createArrayExpression(node.children.map((c) => c.jsNode))
);
node.jsNode = callExp;
};
}
function transformText(node, context) {
if (node.type !== "Text") {
return;
}
// 创建文本所对应的 JS AST 节点
// 将创建好的 AST 节点挂到节点的 jsNode 属性上面
node.jsNode = createStringLiteral(node.content);
}
function transformRoot(node, context) {
// 在退出的回调函数中书写处理逻辑
// 因为要保证所有的子节点已经处理完毕
return () => {
if (node.type !== "Root") {
return;
}
const vnodeJSAST = node.children[0].jsNode;
node.jsNode = {
type: "FunctionDecl",
id: { type: "Identifier", name: "render" },
params: [],
body: [
{
type: "ReturnStatement",
return: vnodeJSAST,
},
],
};
};
}
function tranverseNode(ast, context) {
// 刚进去时进行处理
console.log("处理节点:", ast.type, ast.tag || ast.content);
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = [];
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if (onExit) {
exitFns.push(onExit);
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if (!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function transform(ast) {
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node) {
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformRoot, transformElement, transformText], // 存储具体的转换方法
};
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
// console.log(dump(ast));
}
transform(ast);
console.log(ast.jsNode);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -0,0 +1,484 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// HTML模板字符串
const template = `<div><p>Vue</p><p>React</p></div>`;
// 定义不同的解析状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始的状态,例如遇到 '<'
tagName: 3, // 解析标签名称的状态
text: 4, // 文本节点的状态
tagEnd: 5, // 结束标签的初始状态,例如遇到 '</'
tagEndName: 6, // 解析结束标签名称的状态
};
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
function tokenize(str) {
let currentState = State.initial; // 初始状态
const chars = []; // 用于存储字符
const tokens = []; // 存储生成的令牌
// 循环处理字符串中的每个字符
while (str) {
const char = str[0]; // 获取当前字符
switch (currentState) {
case State.initial:
// 初始状态:检查当前字符
if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示标签开始
str = str.slice(1); // 移除已处理的字符
} else if (isAlpha(char)) {
currentState = State.text; // 如果是字母,表示文本开始
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagOpen:
// 标签开启状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagName; // 如果是字母,表示进入标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "/") {
currentState = State.tagEnd; // 如果字符是 '/',表示结束标签开始
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagName:
// 解析标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',标签名称结束,返回初始状态
tokens.push({ type: "tag", name: chars.join("") }); // 创建标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.text:
// 解析文本节点状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示遇到新的标签,返回标签开启状态
tokens.push({ type: "text", content: chars.join("") }); // 创建文本类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEnd:
// 结束标签的开始状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagEndName; // 如果是字母,表示进入结束标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEndName:
// 解析结束标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',结束标签名称结束,返回初始状态
tokens.push({ type: "tagEnd", name: chars.join("") }); // 创建结束标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
}
}
// 返回生成的令牌列表
return tokens;
}
// 解析函数,将 HTML 字符串转换为 AST
function parse(str) {
// 使用tokenize函数将字符串转换为令牌
const tokens = tokenize(str);
// 创建一个根节点对象用于存储解析后的HTML结构
const root = {
type: "Root",
children: [],
};
// 使用一个栈来跟踪当前处理的元素节点
const elementStack = [root];
// 当仍有令牌时,继续处理
while (tokens.length) {
// 获取当前的父元素(栈顶元素)
const parent = elementStack[elementStack.length - 1];
// 获取当前要处理的令牌
const t = tokens[0];
// 根据令牌类型处理不同情况
switch (t.type) {
case "tag":
// 如果是开始标签,创建一个新的元素节点
const elementNode = {
type: "Element",
tag: t.name,
children: [],
};
// 将新节点添加到父元素的子节点中
parent.children.push(elementNode);
// 将新节点压入栈中,成为下一个父节点
elementStack.push(elementNode);
break;
case "text":
// 如果是文本,创建一个文本节点
const textNode = {
type: "Text",
content: t.content,
};
// 将文本节点添加到当前父元素的子节点中
parent.children.push(textNode);
break;
case "tagEnd":
// 如果是结束标签,从栈中弹出当前处理的元素
elementStack.pop();
break;
}
// 移除已处理的令牌
tokens.shift();
}
// 返回解析后的根节点它包含了整个HTML结构
return root;
}
// 辅助方法用于按照层次结构打印AST的节点信息
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
const ast = parse(template);
// 这里一些辅助方法,用于生产 JS AST 节点的
// 根据不同的操作,会生成不同类型的 JS AST 节点
function createStringLiteral(value) {
return {
type: "StringLiteral",
value,
};
}
function createIdentifier(name) {
return {
type: "Identifier",
name,
};
}
function createArrayExpression(elements) {
return {
type: "ArrayExpression",
elements,
};
}
function createCallExpression(callee, arguments) {
return {
type: "CallExpression",
callee: createIdentifier(callee),
arguments,
};
}
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node, context) {
// 这里应该是所有的子节点处理完毕后,再进行处理
return () => {
if (node.type !== "Element") {
return;
}
// 创建函数调用的AST节点
const callExp = createCallExpression("h", [
createStringLiteral(node.tag),
]);
// 处理函数调用的参数
node.children.length === 1
? // 如果长度为1说明只有一个子节点直接将子节点的 jsNode 作为参数
callExp.arguments.push(node.children[0].jsNode)
: // 说明有多个子节点
callExp.arguments.push(
createArrayExpression(node.children.map((c) => c.jsNode))
);
node.jsNode = callExp;
};
}
function transformText(node, context) {
if (node.type !== "Text") {
return;
}
// 创建文本所对应的 JS AST 节点
// 将创建好的 AST 节点挂到节点的 jsNode 属性上面
node.jsNode = createStringLiteral(node.content);
}
function transformRoot(node, context) {
// 在退出的回调函数中书写处理逻辑
// 因为要保证所有的子节点已经处理完毕
return () => {
if (node.type !== "Root") {
return;
}
const vnodeJSAST = node.children[0].jsNode;
node.jsNode = {
type: "FunctionDecl",
id: { type: "Identifier", name: "render" },
params: [],
body: [
{
type: "ReturnStatement",
return: vnodeJSAST,
},
],
};
};
}
function tranverseNode(ast, context) {
// 刚进去时进行处理
// console.log("处理节点:", ast.type, ast.tag || ast.content);
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = [];
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if (onExit) {
exitFns.push(onExit);
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if (!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function transform(ast) {
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node) {
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformRoot, transformElement, transformText], // 存储具体的转换方法
};
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
// console.log(dump(ast));
}
transform(ast);
// console.log(ast.jsNode);
// 首先是各种生成方法:根据不同的节点类型,做不同的字符串拼接
// 生成字符串字面量
function genStringLiteral(node, context) {
const { push } = context;
push(`'${node.value}'`);
}
// 生成返回语句
function genReturnStatement(node, context) {
const { push } = context;
push(`return `);
genNode(node.return, context);
}
// 生成函数声明
function genFunctionDecl(node, context) {
// 从上下文中获取一些实用函数
const { push, indent, deIndent } = context;
// 向输出中添加 "function 函数名"
push(`function ${node.id.name} `);
// 添加左括号开始参数列表
push(`(`);
// 生成参数列表
genNodeList(node.params, context);
// 添加右括号结束参数列表
push(`) `);
// 添加左花括号开始函数体
push(`{`);
// 缩进,为函数体的代码生成做准备
indent();
// 遍历函数体中的每个节点,生成相应的代码
node.body.forEach((n) => genNode(n, context));
// 减少缩进
deIndent();
// 添加右花括号结束函数体
push(`}`);
}
// 生成节点列表
function genNodeList(nodes, context) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 生成当前节点的代码
genNode(node, context);
// 如果当前节点不是最后一个节点,添加逗号分隔
if (i < nodes.length - 1) {
push(", ");
}
}
}
// 生成函数调用表达式
function genCallExpression(node, context) {
const { push } = context;
const { callee, arguments: args } = node;
// 添加 "函数名("
push(`${callee.name}(`);
// 生成参数列表
genNodeList(args, context);
// 添加 ")"
push(`)`);
}
// 生成数组表达式
function genArrayExpression(node, context) {
const { push } = context;
// 添加 "["
push("[");
// 生成数组元素
genNodeList(node.elements, context);
// 添加 "]"
push("]");
}
function genNode(node, context) {
switch (node.type) {
case "FunctionDecl":
genFunctionDecl(node, context);
break;
case "ReturnStatement":
genReturnStatement(node, context);
break;
case "CallExpression":
genCallExpression(node, context);
break;
case "StringLiteral":
genStringLiteral(node, context);
break;
case "ArrayExpression":
genArrayExpression(node, context);
break;
}
}
function generate(ast) {
const context = {
code: "", // 存储最终生成的代码
// 生成代码本质上就是字符串的拼接
push(code) {
context.code += code;
},
// 当前缩进的级别初始值为0没有缩进
currentIndent: 0,
// 用于换行的,并且会根据缩进的级别添加对应的缩进
newLine() {
context.code += "\n" + ` `.repeat(context.currentIndent);
},
// 增加缩进级别
indent() {
context.currentIndent++;
context.newLine();
},
// 降低缩进级别
deIndent() {
context.currentIndent--;
context.newLine();
},
};
genNode(ast, context);
return context.code;
}
const code = generate(ast.jsNode);
console.log(code);
</script>
</body>
</html>

View File

@ -0,0 +1,350 @@
# 模板编译提升
>面试题:说一下 Vue3 在进行模板编译时做了哪些优化?
1. 静态提升
2. 预字符串化
3. 缓存事件处理函数
4. Block Tree
5. PatchFlag
## 静态提升
静态提升 Static Hoisting在模板编译阶段识别并提升不变的静态节点到渲染函数外部从而减少每次渲染时的计算量。被提升的节点无需重复创建。
**哪些节点会被提升**
1. 元素节点
2. 没有绑定动态内容的节点
**一个提升的示例**
```vue
<template>
<div>
<p>这是一个静态的段落。</p>
<p>{{ dynamicMessage }}</p>
</div>
</template>
```
在 Vue2 时期不管是静态节点还是动态节点,都会编译为 **创建虚拟节点函数** 的调用。
```js
with(this) {
return createElement('div', [
createElement('p', [createTextVNode("这是一个静态的段落。")]),
createElement('p', [createTextVNode(toString(dynamicMessage))])
])
}
```
Vue3 中,编译器会对**静态内容的编译结果进行提升**
```js
const _hoisted_1 = /*#__PURE__*/createStaticVNode("<p>这是一个静态的段落。</p>", 1);
export function render(_ctx, _cache) {
return (openBlock(), createElementBlock("div", null, [
_hoisted_1,
createElementVNode("p", null, toDisplayString(_ctx.dynamicMessage), 1 /* TEXT */)
]))
}
```
除了静态节点,静态属性也是能够提升的,例如:
```vue
<template>
<button class="btn btn-primary">{{ buttonText }}</button>
</template>
```
在这个模板中,虽然 button 是一个动态节点,但是属性是固定的,因此这里也有优化的空间:
```js
// 静态属性提升
const _hoisted_1 = { class: "btn btn-primary" };
export function render(_ctx, _cache) {
return (openBlock(), createElementBlock("button", _hoisted_1, toDisplayString(_ctx.buttonText), 1 /* TEXT */))
}
```
## 预字符串化
当编译器遇到**大量连续的静态内容**时,会直接将其**编译为一个普通的字符串节点**。例如:
```vue
<template>
<div class="menu-bar-container">
<div class="logo">
<h1>logo</h1>
</div>
<ul class="nav">
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
</ul>
<div class="user">
<span>{{ user.name }}</span>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const user = ref({
name: "zhangsan",
});
</script>
```
编译结果中和静态提升相关的部分:
```js
const _hoisted_1 = { class: "menu-bar-container" }
const _hoisted_2 = /*#__PURE__*/_createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>", 2)
const _hoisted_4 = { class: "user" }
```
其中的 _hoisted_2 就是将连续的静态节点编译为了字符串。
思考🤔:这样有什么好处呢?
答案:当大量的连续的静态节点被编译为字符串节点后,整体的虚拟 DOM 节点数量就少了,自然而然 diff 的速度就更快了。
Vue2:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-27-034042.png" alt="vue2" style="zoom:50%;" />
Vue3:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-27-034043.png" alt="vue3" style="zoom:50%;" />
第二个好处就是在 SSR 的时候,无需重复计算和转换,减少了服务器端的计算量和处理时间。
思考🤔:大量连续的静态内容时,会启用预字符串化处理,大量连续的边界在哪里?
答案:在 Vue3 编译器内部有一个阀值,目前是 10 个节点左右会启动预字符串化。
```vue
<template>
<div class="menu-bar-container">
<div class="logo">
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
<h1>logo</h1>
</div>
<div class="user">
<span>{{ user.name }}</span>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const user = ref({
name: "zhangsan",
});
</script>
```
## 缓存内联事件处理函数
模板在进行编译的时候,会针对**内联的事件处理函数**做缓存。例如:
```vue
<button @click="count++">plus</button>
```
在 Vue2 中,每次渲染都会为这个内联事件创建一个新的函数,这会产生不必要的内存开销和性能损耗。
```js
render(ctx){
return createVNode("button", {
// 每次渲染的时候,都会创建一个新的函数
onClick: function($event){
ctx.count++;
}
})
}
```
在 Vue3 中,为了优化这种情况,编译器会自动为内联事件处理函数生成缓存代码。
```js
render(ctx, _cache){
return createVNode("button", {
// 如果缓存里面有,直接从缓存里面取
// 如果缓存里面没有,创建一个新的事件处理函数,然后将其放入到缓存里面
onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))
})
}
```
思考🤔:为什么仅针对内联事件处理函数进行缓存?
答案:非内联事件处理函数不需要缓存,因为非内联事件处理函数在组件实例化的时候就存在了,不会在每次渲染时重新创建。缓存机制主要是为了解决内联事件处理函数在每次渲染的时候重复创建的问题。
## block tree
Vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上,例如下面的代码:
```vue
<form>
<div>
<label>账号:</label>
<input v-model="user.loginId" />
</div>
<div>
<label>密码:</label>
<input v-model="user.loginPwd" />
</div>
</form>
```
![20200929172002](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-27-041058.png)
每次状态更新时Vue2 需要遍历整个虚拟 DOM 树来寻找差异。这种方法虽然通用,但在大型组件或复杂页面中,性能损耗会比较明显,因为它浪费了大量时间在静态节点的比较上。
思考🤔:前面不是说静态节点会提升么?
答案静态提升解决的是不再重复生成静态节点所对应的虚拟DOM节点。现在要解决的问题是虚拟DOM树中静态节点比较能否跳过的问题。
**什么是Block**
一个 Block 本质上也是一个虚拟 DOM 节点,不过该**虚拟 DOM 节点上面会多出来一个 dynamicChildren 属性**,该属性对应的值为数组,**数组里面存储的是动态子节点**。以上面的代码为例form 对应的虚拟 DOM 节点就会存在 dynamicChildren 属性:
![20200929172555](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-27-041226.png)
有了 block 之后,就不需要再像 Vue2 那样一层一层,每个节点进行对比了,对比的粒度变成了直接找 dynamicChildren 数组,然后对比该数组里面的动态节点,这样就很好的实现了跳过静态节点比较。
**哪些节点会成为 block 节点?**
1. 模板中的根节点都会是一个 block 节点。
```vue
<template>
<!-- 这是一个block节点 -->
<div>
<p>{{ bar }}</p>
</div>
<!-- 这是一个block节点 -->
<h1>
<span :id="test"></span>
</h1>
</template>
```
2. 任何带有 v-if、v-else-if、v-else、v-for 指令的节点,也需要作为 block 节点。
答案因为这些指令会让虚拟DOM树的结构不稳定。
```vue
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
```
按照之前的设计div是一个 block 节点,收集到的动态节点只有 p. 这意味着无论 foo 是 true 还是 false最终更新只会去看 p 是否发生变化,从而产生 bug.
解决方案也很简单,让带有这些指令的节点成为一个 block 节点即可
```
block(div)
- block(section)
- block(div)
```
此时这种设计父级block除了收集动态子节点以外还会收集子block节点。
多个 block 节点自然就形成了树的结构,这就是 block tree.
## 补丁标记
补丁标记 PatchFlags这是 Vue 在做节点对比时的近一步优化。
即便是动态的节点,一般也不会是节点所有信息(类型、属性、文本内容)都发生了更改,而仅仅只是一部分信息发生更改。
之前在 Vue2 时期对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对,例如:
```vue
<div :class="user" data-id="1" title="user name">
{{user.name}}
</div>
```
在 Vue2 中:
- 全面对比会逐个去检查节点的每个属性class、data-id、title以及子节点的内容
- 性能瓶颈:这种方式自然就存在一定的性能优化空间
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-08-27-062917.png" alt="20200929172805" style="zoom:60%;" />
在 Vue3 中PatchFlag 通过为每个节点生成标记,显著优化了对比过程。编译器在编译模板时,能够识别哪些属性或内容是动态的,并为这些动态部分生成特定的标记。
Vue3 的 PatchFlag 包括多种类型,每种类型标记不同的更新需求:
- TEXT表示节点的文本内容可能会发生变化。
- CLASS表示节点的 class 属性是动态的,可能会发生变化。
- STYLE表示节点的 style 属性是动态的,可能会发生变化。
- PROPS表示节点的一个或多个属性是动态的可能会发生变化。
- FULL_PROPS表示节点有多个动态属性且这些属性不是简单的静态值。
- HYDRATE_EVENTS表示节点的事件监听器是动态的需要在客户端进行水合处理。
- STABLE_FRAGMENT表示节点的子节点顺序稳定允许按顺序进行更新。
- KEYED_FRAGMENT表示节点的子节点带有 key可以通过 key 进行高效的更新。
- UNKEYED_FRAGMENT表示节点的子节点无 key但可以通过简单的比较进行更新。
例如上面的代码,编译出来的函数:
```js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
class: _normalizeClass($setup.user),
"data-id": "1",
title: "user name"
}, _toDisplayString($setup.user.name), 3 /* TEXT, CLASS */))
}
```
通过这些标记Vue3 在更新时不再需要对每个属性都进行全面的对比,而是只检查和更新那些被标记为动态的部分,从而显著减少了不必要的计算开销。
>面试题:说一下 Vue3 在进行模板编译时做了哪些优化?
>
>参考答案:
>
>Vue3 的编译器在进行模板编译的时候,主要做了这么一些优化:
>
>1. 静态提升:解决的是静态内容不要重复生成新的虚拟 DOM 节点的问题
>2. 预字符串化:解决的是大量的静态内容,干脆虚拟 DOM 节点都不要了,直接生成字符串,虚拟 DOM 节点少了diff 的时间花费也就更少。
>3. 缓存内联事件处理函数:每次运行渲染函数时,内联的事件处理函数没有必要重新生成,这样会产生不必要的内存开销和性能损耗。所以可以将内联事件处理函数缓存起来,在下一次执行渲染函数的时候,直接从缓存中获取。
>4. Block Tree解决的是跳过静态节点比较的问题。
>5. 补丁标记:能够做到即便动态节点进行比较,也只比较有变化的部分的效果。
---
-EOF-

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View File

@ -0,0 +1,29 @@
# demo
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
{
"name": "demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"vite": "^5.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,53 @@
<script setup>
import HelloWorld from "./components/HelloWorld.vue";
import TheWelcome from "./components/TheWelcome.vue";
</script>
<template>
<header>
<img
alt="Vue logo"
class="logo"
src="./assets/logo.svg"
width="125"
height="125"
/>
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<TheWelcome />
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,88 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

View File

@ -0,0 +1,66 @@
# 组件name作用
>面试题:组件 name 有什么用?可不可以不写 name
**如何定义组件name**
- Vue2 OptionsAPI添加 name 配置项即可
```js
export default {
name: 'xxxx', // 组件的name
}
```
- Vue3 CompositionAPI
- 多书写一个script标签仍然导出对象在对象中配置name
```vue
<script setup>
// ...
</script>
<script>
export default {
name: 'xxx'
}
</script>
```
- 通过一个 defineOptions 的宏来配置name
```vue
<script setup>
defineOptions({
name: 'xxx'
})
</script>
```
**组件name的作用**
1. 通过名字找到对应的组件
- 递归组件
- 跨级组件通信
2. 通过 name 属性指定要缓存的组件
3. 使用 vue-devtools 进行调试时,组件名称也是由 name 决定的
>面试题:组件 name 有什么用?可不可以不写 name
>
>参考答案:
>
>在 Vue 中,组件的 name 选项有多个作用,虽然它不是必须的,但在某些场景下它非常有用。
>
>1. 通过名字找到对应的组件
>
> - 递归组件
> - 跨级组件通信
>2. 通过 name 属性指定要缓存的组件
>3. 使用 vue-devtools 进行调试时,组件名称也是由 name 决定的
>
>即使在没有上述特殊需求的情况下,添加 name 也有助于提高代码的可读性,尤其是在调试和分析性能时。为组件命名可以使开发者更清楚地了解每个组件的用途和角色。
---
-EOF-

View File

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
// 添加自定义规则
'vue/multi-word-component-names': 'off'
}
}

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@ -0,0 +1,35 @@
# demo
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"prettier": "^3.2.5",
"vite": "^5.3.1",
"vite-plugin-vue-devtools": "^7.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,76 @@
<template>
<div>
<h1>文件列表</h1>
<FolderTree :items="fileItems" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import FolderTree from './components/FolderTree.vue'
const fileItems = ref([
{
id: 1,
name: '文档',
type: 'folder',
children: [
{
id: 2,
name: '自我介绍.docx',
type: 'file'
},
{
id: 3,
name: '推荐信.docx',
type: 'file'
}
]
},
{
id: 4,
name: '照片',
type: 'folder',
children: [
{
id: 5,
name: '假期度假',
type: 'folder',
children: [
{
id: 6,
name: '沙滩.png',
type: 'file'
},
{
id: 7,
name: '深山.png',
type: 'file'
}
]
},
{
id: 6,
name: '工作学习',
type: 'folder',
children: [
{
id: 6,
name: '制作手剪报.png',
type: 'file'
},
{
id: 7,
name: '朗读课文.png',
type: 'file'
}
]
}
]
},
{
id: 8,
name: '待办事项.txt',
type: 'file'
}
])
</script>

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,42 @@
<template>
<div>
<ul>
<li v-for="item in items" :key="item.id">
<div>
<!-- 先显示目录名称 -->
<span v-if="item.type === 'folder'">目录{{ item.name }}</span>
<!-- 如果是目录就递归调用自己 -->
<component :is="name" v-if="item.type === 'folder'" :items="item.children" />
<!-- 如果是文件就显示文件的名称 -->
<span v-else>{{ item.name }}</span>
</div>
</li>
</ul>
</div>
</template>
<script setup>
defineProps({
items: {
type: Array,
required: true
}
})
defineOptions({
name: 'FolderTree'
})
const name = 'FolderTree'
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 20px;
}
li {
margin: 5px 0;
}
</style>

View File

@ -0,0 +1,8 @@
// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

Some files were not shown because too many files have changed in this diff Show More