小米
Vue 虚拟DOM原理
Vue 的虚拟 DOM(Virtual DOM)是 Vue 框架在底层用于优化视图更新的技术之一。虚拟 DOM 的核心思想是通过 JavaScript 对象(虚拟节点,简称 vnode)来表示真实 DOM 的结构,并通过 diff 算法比较前后的虚拟 DOM,找到需要更新的部分,再将这些部分更新到真实 DOM 中。这种方式大大减少了不必要的 DOM 操作,提高了性能。
Vue 虚拟 DOM 的工作原理
Vue 的虚拟 DOM 实现遵循以下主要步骤:
1. 创建虚拟 DOM
当 Vue 组件的状态或数据发生变化时,Vue 会重新调用组件的渲染函数(render
),并生成一个虚拟 DOM 树。这个虚拟 DOM 树是用 JavaScript 对象表示的,类似于下面的结构:
1 | const vnode = { |
这个对象结构类似于真实的 DOM 树,但只是简单的 JavaScript 对象,不会进行实际的 DOM 操作。
2. Diff 算法比较新旧虚拟 DOM
当组件的数据发生变化时,Vue 会生成新的虚拟 DOM 树。为了避免直接更新整个真实 DOM,Vue 会通过 Diff 算法对比新旧两个虚拟 DOM 树,找出差异部分。
Diff 算法的核心思想是:
- 同层比较:只会比较同一层级的节点,而不会跨层级比较。这样可以将算法复杂度从 O(n^3) 降低到 O(n)。
- 标签比较:如果两个节点的标签(
tag
)不同,则直接替换该节点及其子节点。 - 属性比较:如果标签相同,则比较该节点的属性和事件绑定。
- 子节点比较:如果有子节点,递归比较子节点。
例如,假设有一个简单的 DOM 结构如下:
1 | <div id="app"> |
假设新的数据发生变化,导致虚拟 DOM 变为:
1 | <div id="app"> |
Vue 的 Diff 算法会发现旧虚拟 DOM 中的 <h1>
内容和新虚拟 DOM 不同,只更新这一部分,而不是重新渲染整个 DOM 树。
更新真实 DOM
根据 Diff 算法比较得出的结果,Vue 会通过最小化的 DOM 操作来更新真实 DOM。比如,在上面的例子中,Vue 只会更新 <h1>
元素的内容,而不会动其他部分。这种局部更新的方式大大提高了性能。
Vue 虚拟 DOM 关键点
1. VNode 结构
Vue 中每个虚拟节点(VNode)都是一个 JavaScript 对象,表示一个 DOM 节点。其结构通常包括以下字段:
- tag:当前节点的标签名,例如
div
、p
等。 - data:节点的属性或事件绑定等信息。
- children:子节点的数组,递归地表示节点树。
- text:节点的文本内容,如果是纯文本节点则包含文本。
- elm:该虚拟节点对应的真实 DOM 元素(当其挂载到 DOM 时生成)。
2. Patch 过程
patch
是 Vue 中实际将虚拟 DOM 转换为真实 DOM 的过程。在首次渲染时,Vue 会将虚拟 DOM 转换为真实 DOM 并插入到页面中。在数据更新时,Vue 会根据新的虚拟 DOM 和旧的虚拟 DOM 进行对比,并在 patch
过程中执行最小的更新操作。
3. Diff 算法的优化
Vue 的 Diff 算法做了很多针对性能的优化,主要包括:
- 同层比较:只比较同层级的节点,不跨层级。
- Key 优化:通过给列表中的每个子节点设置唯一
key
,Vue 可以更加精确地判断节点是否发生变化,从而避免不必要的重新渲染和移动操作。 - 静态节点的优化:Vue 会在编译阶段标记哪些节点是静态的,即不会随数据变化而更新的节点。对于这些节点,Vue 在后续的更新中会跳过它们,从而提高性能。
4. 模板与虚拟 DOM
Vue 的模板语法(template
)最终会被编译为渲染函数,渲染函数生成虚拟 DOM。这个过程可以简单理解为:
- 开发者编写的模板会通过 Vue 的编译器转换成渲染函数。
- 渲染函数执行时生成虚拟 DOM 树。
- Vue 在每次数据更新时重新执行渲染函数生成新的虚拟 DOM 树,并通过 Diff 算法更新真实 DOM。
Vue 虚拟 DOM 的优点
- 性能优化:虚拟 DOM 减少了直接操作真实 DOM 的次数,避免了频繁的重绘和重排,提升了性能。
- 跨平台渲染:虚拟 DOM 是抽象的,不依赖具体平台。Vue 可以将虚拟 DOM 渲染到浏览器、服务端(SSR)、甚至是移动端(如使用 Weex)等不同平台上。
- 便于调试:通过虚拟 DOM 进行更新,Vue 提供了更清晰的渲染流程,并且可以通过开发工具轻松调试和跟踪变化。
总结
Vue 的虚拟 DOM 是通过使用 JavaScript 对象来表示 DOM 结构,避免频繁的真实 DOM 操作,从而提高性能。在虚拟 DOM 中,Vue 通过 Diff 算法找到新旧节点的差异,并以最小的成本更新真实 DOM。这一过程使得 Vue 能够高效地处理复杂的 UI 更新,并为跨平台渲染提供了基础。
如果你想进一步深入了解虚拟 DOM 或 Vue 的内部机制,可以研究其渲染函数的生成过程和 patch
阶段的实现细节。
React Class AP和Hook APl的区别
SPA有什么缺点
TS的装饰器
TypeScript 中的装饰器(Decorators)是一个实验性的特性,它允许你通过一种声明方式来修改类、方法、属性或参数的行为。装饰器本质上是一个函数,可以用于在类或类的成员上添加额外的功能,比如日志记录、权限控制、数据验证等。
装饰器的类型
在 TypeScript 中,装饰器主要分为以下几种:
- 类装饰器(Class Decorators)
- 方法装饰器(Method Decorators)
- 访问器装饰器(Accessor Decorators)
- 属性装饰器(Property Decorators)
- 参数装饰器(Parameter Decorators)
启用装饰器
要在 TypeScript 中使用装饰器,需要在 tsconfig.json
中开启 experimentalDecorators
选项:
1 | { |
1. 类装饰器
类装饰器应用于类的定义上,可以用于修改类的构造函数或添加额外的功能。它的装饰器函数接收一个参数:目标类的构造函数。
示例:
1 | // 类装饰器函数 |
在这个例子中,@Logger
装饰器在类 Person
被定义时调用,并输出类的名称。
类装饰器的返回值
类装饰器可以返回一个新的构造函数来替换原始类,实现类的扩展或修改行为。
1 | function WithAge(constructor: Function) { |
2. 方法装饰器
方法装饰器用于修饰类的实例方法,它可以用来修改或扩展方法的行为。它接收三个参数:
target
:类的原型对象。propertyKey
:方法的名称。descriptor
:方法的属性描述符,可以用于修改方法的行为。
示例:
1 | function LogMethod( |
在这个例子中,@LogMethod
装饰器在方法调用前后添加了日志记录。
3. 访问器装饰器
访问器装饰器可以用于类的 getter 或 setter,它接收三个参数:
target
:类的原型对象。propertyKey
:属性名称。descriptor
:属性的描述符。
示例:
1 | function LogAccessor( |
在这个例子中,@LogAccessor
装饰器会在 area
访问器被访问时记录日志。
4. 属性装饰器
属性装饰器用于修饰类的属性,接收两个参数:
target
:类的原型对象。propertyKey
:属性的名称。
属性装饰器没有属性描述符,因此无法直接修改属性的行为。它通常用于添加元数据或用于依赖注入。
示例:
1 | function LogProperty(target: any, propertyKey: string) { |
5. 参数装饰器
参数装饰器用于修饰方法的参数,接收三个参数:
target
:类的原型对象。propertyKey
:方法的名称。parameterIndex
:参数在方法中的索引。
示例:
1 | function LogParameter( |
装饰器的执行顺序
当多个装饰器同时应用到一个类上时,装饰器的执行顺序是从下到上,而对于方法和属性装饰器,执行顺序是从外到内。
1 | function ClassDecorator1() { /*...*/ } |
执行顺序为:
MethodDecorator2
MethodDecorator1
ClassDecorator2
ClassDecorator1
总结
- 类装饰器:修饰类,可以修改类的行为。
- 方法装饰器:修饰方法,常用于日志记录或修改方法的行为。
- 访问器装饰器:修饰 getter 或 setter,通常用于数据监控。
- 属性装饰器:修饰属性,常用于元数据注入。
- 参数装饰器:修饰方法的参数,常用于依赖注入或参数验证。
TypeScript 装饰器为开发者提供了强大的功能,可以方便地在类和类成员上添加额外的逻辑。不过,装饰器是一个实验性特性,需要开启相关配置,并且在使用时要小心处理其带来的副作用。
用过哪些git命令,如何理解rebase
git rebase
是 Git 中一个强大的命令,用于将一个分支的更改重新应用到另一个分支的基础上。简单来说,rebase
的作用是让你的代码历史更加整洁,并且避免了不必要的合并提交(merge commit
),这在保持线性提交历史时非常有用。
git rebase
的概念与原理
在理解 rebase
时,你可以想象它是把一系列的提交移动到另一个基础上。rebase
的核心是将一个分支上的提交“摘取”下来,然后将它们“重新播放”在另一个分支的基础之上,产生一个新的提交历史。
举个例子
假设你有如下分支结构:
1 | A---B---C (main) |
main
分支有提交A -> B -> C
feature
分支从B
分叉出来,包含提交D -> E
如果你在 feature
分支上运行 git rebase main
,Git 会做以下操作:
- 将
D
和E
从当前分支中摘下来,暂时存储起来。 - 将
feature
分支移到main
分支的最新提交(即C
)。 - 将
D
和E
重新应用到C
的基础上。
最终的历史看起来像这样:
1 | A---B---C---D'---E' (feature) |
你可以看到,D
和 E
提交被重新应用在了 C
之后,并且产生了新的提交 D'
和 E'
,它们是 D
和 E
的新的“副本”,但它们的基础是最新的 C
提交。
为什么使用 git rebase
保持提交历史的线性:当你使用
git rebase
时,提交历史不会像git merge
那样出现分叉和合并的merge commit
,它会保持干净的线性提交历史。这在审查代码时特别有帮助,代码历史更容易追踪和理解。对比:
git merge
可能会生成一个新的合并提交,并导致提交历史出现分叉。git rebase
会将所有提交“重新排列”,保持提交历史线性。
避免合并提交:在某些情况下,合并提交可能会使得提交历史变得冗杂,而
rebase
通过直接移动提交,避免了创建额外的合并提交。代码整合:当你正在一个特性分支(
feature
)上开发,而主分支(main
或master
)不断更新时,你可以使用rebase
来将主分支的最新更改整合到你的分支中,而不会产生额外的合并提交。
git rebase
常用场景
1. 将一个分支变基到另一个分支上
1 | git checkout feature |
这将 feature
分支重新基于 main
分支,类似前面的例子。D
和 E
会被重新应用到 main
分支的基础上。
2. 交互式 rebase(git rebase -i
)
交互式 rebase
允许你对提交历史进行更精细的控制,可以进行如编辑提交信息、合并提交(squash
)等操作。
1 | git rebase -i HEAD~3 |
这将对最近的 3 次提交进行交互式操作,命令会打开一个编辑器窗口,你可以选择对每个提交进行修改、删除、合并等操作。例如:
pick
:保留提交。squash
:将多个提交合并为一个。edit
:修改某个提交的内容。
交互式 rebase
是用来清理提交历史的好工具。
3. 拆分提交历史
如果你想将历史上的某次大的提交分成多个小的提交,也可以通过交互式 rebase
来实现。例如,选择 edit
后,你可以在那个提交的基础上进一步修改和拆分它。
git rebase
与 git merge
的区别
特性 | git merge |
git rebase |
---|---|---|
提交历史 | 保留分支合并历史,可能会生成 merge commit |
生成线性的提交历史,不会生成 merge commit |
合并冲突 | 可能发生冲突,冲突后通过合并提交记录合并过程 | 可能发生冲突,冲突后可以继续 rebase ,不会生成新提交 |
适用场景 | 需要保留合并历史、并且合并过程较为复杂时 | 提交历史干净、追求线性历史时 |
代码整合效率 | 如果冲突频繁,可能增加 merge commit 数量,影响历史整洁 |
将分支“重新基于”其他分支,避免多余合并历史 |
提交历史可读性 | 合并历史可能比较复杂 | 保持简洁线性,历史清晰,适合代码审查 |
解决冲突
无论是 merge
还是 rebase
,在合并过程中都可能发生冲突。当冲突发生时,Git 会暂停 rebase
操作,提示你解决冲突。你需要手动解决冲突后继续 rebase
。
1 | git rebase --continue |
如果你想中止当前的 rebase
操作,可以使用:
1 | git rebase --abort |
注意事项
**不要对已经共享的分支使用
rebase
**:rebase
会重写提交历史,因此如果你对已经推送到远程的分支进行rebase
,可能会导致协作中的其他人遇到冲突或提交丢失。rebase
通常在本地分支上操作,避免对远程分支使用。历史重写:
rebase
会重写提交历史,因此你要非常谨慎。如果误操作,可以使用git reflog
查看之前的提交历史并恢复。
总结
git rebase
是一个强大的工具,它通过“重新应用”提交来保持历史的整洁和线性,特别适合用于团队协作中的代码整合和提交历史的清理。但由于它会修改历史记录,因此要谨慎使用,特别是在处理已经共享的分支时。
修改对象说输出
手撕深拷贝(需要处理Date、function等复杂对象)
要实现一个能够处理 Date
、Function
等复杂对象的深拷贝,我们需要特别注意原始值类型和引用类型的区别,以及不同的对象结构和类型。简单的 JSON 序列化无法处理 Date
、Function
、RegExp
等类型,因此我们需要自己实现一个递归拷贝函数,来处理这些复杂对象。
以下是一个手撕深拷贝的实现,可以处理常见的复杂对象类型,包括 Date
、Function
、RegExp
等:
1 | function deepClone(obj, hash = new WeakMap()) { |
代码讲解:
基本类型的处理:
- 如果
obj
是基本类型(如null
、undefined
、string
、number
、boolean
等),直接返回该值,因为基本类型是按值传递的。
- 如果
处理循环引用:
- 使用
WeakMap
来记录已经拷贝过的对象。如果再次遇到同一个对象,可以直接从WeakMap
中取出已拷贝的对象,防止递归陷入死循环。
- 使用
处理特殊对象类型:
- **
Date
**:直接创建新的Date
对象并复制原始值。 - **
RegExp
**:创建新的正则表达式对象,并保持原始的正则表达式内容和标志。 - **
Function
**:函数直接返回,函数不应该被深拷贝(如果确实有需求,也可以通过解析函数源码来克隆,但这种做法一般不推荐)。
- **
处理数组和对象:
- 如果
obj
是数组,就创建一个空数组并递归拷贝数组的每一项。 - 如果是对象,则创建一个空对象,并递归拷贝其属性。
- 如果
处理 Symbol 属性:
- 使用
Object.getOwnPropertySymbols()
获取对象的Symbol
属性并进行拷贝。
- 使用
测试示例:
1 | const original = { |
复杂对象的处理说明:
Date
会拷贝成一个新的日期对象,两个日期对象互不影响。Function
会直接返回同一个函数引用(函数一般不会被深拷贝,函数的行为不变)。RegExp
会被深拷贝为一个新的正则表达式对象。Symbol
属性也会被拷贝到新对象中。- 循环引用 的对象在这个实现中也能被正确处理,不会导致无限递归。
这个实现可以应对绝大多数复杂对象的深拷贝需求,也可以扩展以支持更多自定义的对象类型(如 Map
、Set
等)。
手写并发请求控制
ES6的字符串插值怎么做i18n?
在ES6中,字符串插值通常使用模板字面量(template literals)来进行字符串拼接,而在国际化(i18n)中,字符串的多语言支持是关键任务。你可以通过结合i18n库(如i18next
、react-i18next
等)来处理带有字符串插值的翻译内容。
实现i18n字符串插值的几种方法:
1. 使用i18next库
这是一个非常流行的i18n库,支持模板字面量的插值。
步骤:
安装
i18next
和i18next-browser-languagedetector
:1
npm install i18next i18next-browser-languagedetector
初始化 i18next 并配置语言资源:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
resources: {
en: {
translation: {
greeting: "Hello {{name}}, welcome to our platform!"
}
},
zh: {
translation: {
greeting: "你好 {{name}},欢迎来到我们的平台!"
}
}
},
lng: 'en', // 设置默认语言
interpolation: {
escapeValue: false // 插值时不进行HTML转义
}
});在代码中使用:
1
2
3
4
5
6
7
8import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
const name = 'John';
return <p>{t('greeting', { name })}</p>;
};
在这个例子中,t('greeting', { name })
会自动根据语言环境插入变量name
并生成对应语言的字符串。
2. 使用自定义模板函数
如果你不想使用额外的库,可以通过手动实现一个简单的国际化逻辑和插值功能。
步骤:
定义语言文件:
1
2
3
4
5
6
7
8const translations = {
en: {
greeting: "Hello ${name}, welcome to our platform!"
},
zh: {
greeting: "你好 ${name},欢迎来到我们的平台!"
}
};创建一个简单的i18n函数:
1
2
3
4const i18n = (key, lang, variables) => {
const template = translations[lang][key];
return new Function('return `' + template + '`;').call(variables);
};使用该函数进行字符串插值:
1
2
3
4
5
6const name = 'John';
const greetingEn = i18n('greeting', 'en', { name });
const greetingZh = i18n('greeting', 'zh', { name });
console.log(greetingEn); // 输出: Hello John, welcome to our platform!
console.log(greetingZh); // 输出: 你好 John,欢迎来到我们的平台!
总结
- 如果你想要更全面的国际化支持,可以使用像
i18next
这样成熟的库,它不仅支持字符串插值,还支持复杂的语言规则、日期/时间格式化等。 - 如果你的项目较小,并不需要复杂的功能,也可以通过自定义模板函数实现简单的i18n逻辑。
如果你对哪种方式有更具体的问题,欢迎随时提问!
微前端实现,shadow DOM和iframe各有什么优缺点,还有没有别的实现方法?
微前端(Micro-Frontend)是一种将前端应用拆分成多个独立的子应用的架构模式,使得每个子应用可以独立开发、部署和维护。实现微前端的方式有多种,常见的包括使用 Shadow DOM 和 iframe,每种方法都有各自的优缺点。除此之外,还有其他实现微前端的技术和策略。以下是这些方法的优缺点分析以及其他可能的实现方法:
1. Shadow DOM
Shadow DOM 是 Web Components 技术的一部分,用来封装组件的 DOM 和样式,避免与外部产生冲突。
优点:
- 样式隔离:Shadow DOM 提供了天然的样式隔离功能,防止不同子应用的样式相互影响。
- 事件隔离:Shadow DOM 还可以隔离事件,不会因为全局事件处理器而造成干扰。
- 共享 DOM 环境:与主应用共享同一个 DOM 环境,不需要跨上下文(如 iframe)传递数据,子应用可以直接与主应用或其他子应用通信。
缺点:
- 兼容性问题:某些浏览器对 Shadow DOM 的支持较弱,特别是在旧的浏览器中,虽然有 polyfill,但有时性能和兼容性可能会有问题。
- 样式覆盖难度:由于样式隔离,想要跨组件进行样式覆盖比较复杂,这在某些情况下可能带来不便。
- 开发复杂度:需要开发者熟悉 Web Components 以及 Shadow DOM 的特性,增加了开发复杂度。
2. iframe
iframe 是 HTML 提供的标准内嵌框架,通常用于嵌入外部网站或应用。
优点:
- 完全隔离:iframe 提供了完全的隔离,子应用和父应用之间是完全独立的,互不干扰。包括样式、脚本、全局变量、事件等。
- 安全性:由于隔离性,iframe 可以防止跨站脚本攻击(XSS)和其他安全风险。
- 兼容性:iframe 是标准的 HTML 标签,所有现代浏览器都支持,兼容性非常好。
缺点:
- 性能问题:每个 iframe 都是一个独立的浏览器上下文,加载新资源时开销大,特别是当页面中有多个 iframe 时,性能影响显著。
- 通信困难:iframe 和父页面之间的通信较为麻烦,需要使用
postMessage
等 API,增加了复杂性。 - SEO 不友好:iframe 内容不会被搜索引擎抓取,影响 SEO。
- 用户体验差异:iframe 的尺寸、滚动、显示行为等容易和主应用不一致,可能导致用户体验不佳。
3. 其他实现方法
3.1 Web Components
除了 Shadow DOM,使用完整的 Web Components 也可以实现微前端。Web Components 包括了三大技术:自定义元素(Custom Elements)、HTML 模板(HTML Template)和 Shadow DOM。
优点:
- 组件化的开发方式,易于管理和复用。
- 自定义元素可以作为子应用的载体,和 Shadow DOM 配合使用实现隔离。
- 不依赖第三方框架,浏览器原生支持。
缺点:
- 开发复杂度高。
- 一些高级功能和状态管理需要自行实现,难以与现代框架(如 React、Vue)直接集成。
3.2 Module Federation(Webpack 5)
Module Federation 是 Webpack 5 提供的功能,它允许多个独立的构建应用在运行时共享代码和模块。这种方法特别适合微前端场景。
优点:
- 动态加载模块:每个子应用可以作为一个独立的模块,在需要时动态加载和使用,避免重复加载代码。
- 共享依赖:可以在子应用之间共享依赖库,减少加载体积。
- 框架无关:适用于不同的 JavaScript 框架(如 React、Vue、Angular)。
缺点:
- 复杂性高:配置复杂,尤其是跨团队和应用的依赖共享可能会产生很多细节问题。
- 紧耦合风险:如果依赖共享处理不当,子应用之间可能出现耦合。
3.3 Single SPA
Single SPA 是一个用于实现微前端架构的 JavaScript 框架。它允许多个框架(如 React、Vue、Angular)在一个页面中共存。
优点:
- 支持多框架应用,可以在同一个页面中运行不同的框架应用。
- 提供路由、应用管理等功能,帮助你管理和协调各个子应用。
- 有丰富的社区支持和插件。
缺点:
- 上手门槛较高,学习曲线陡峭。
- 配置复杂,可能需要大量自定义配置来满足特定需求。
3.4 Custom Events + Global State
可以使用浏览器的原生 Custom Events(自定义事件)和 Global State(全局状态管理)来实现子应用间的通信与隔离。
优点:
- 使用简单,可以轻量化实现,不需要引入复杂的框架。
- 子应用可以通过自定义事件与主应用或其他子应用通信。
缺点:
- 可能引入全局事件污染,增加调试复杂度。
- 没有内置的样式和事件隔离,需要开发者自己管理。
总结
- Shadow DOM:适合需要较高样式和事件隔离的场景,但开发复杂度稍高。
- iframe:提供完全隔离,适合安全性高、与主应用解耦的场景,但在性能和通信上有局限。
- Module Federation 和 Single SPA 等现代工具:适合复杂项目、需要模块共享和动态加载的场景,但配置复杂。
- Web Components:适合原生支持和跨框架的需求,但复杂度较高。
- Custom Events + Global State:适合轻量的场景,适合不需要太多复杂功能的项目。
选择具体实现方法取决于项目需求、团队技术栈以及性能、安全性的考虑。