1、浏览器渲染过程
在浏览器接收到,网页的 HTML、CSS 和 JavaScript 数据后,在经历一系列步骤来渲染网页。
以下是浏览器渲染过程的主要步骤:
- 解析 HTML 和构建 DOM 树:
- 浏览器开始解析 HTML 数据,并构建文档对象模型(DOM)树,表示网页的结构和内容。DOM 树是一个树状结构,其中每个 HTML 元素都被表示为一个节点,包括文本、标签、属性等。
- 解析 CSS 和构建 CSSOM 树:
- 同时,浏览器也开始解析 CSS 数据,并构建 CSS 对象模型(CSSOM)树,表示网页的样式和布局信息。CSSOM 树与 DOM 树一一对应,每个元素都有对应的样式信息。
- 合并 DOM 和 CSSOM,构建渲染树:
- 浏览器将 DOM 树和 CSSOM 树合并,构建渲染树(Render Tree)。渲染树包含了需要渲染的页面内容,但不包括那些被 CSS 隐藏的元素。
- 布局计算:
- 浏览器开始计算每个元素在页面中的精确位置和尺寸,这个过程称为布局计算。浏览器确定元素如何放置和相互布局。
- 绘制页面:
- 浏览器使用计算出的位置和尺寸信息来绘制页面。这包括将页面内容绘制到屏幕上的像素点上,以及处理字体渲染、图像显示等。
- 处理 JavaScript:
- 如果页面包含 JavaScript,浏览器将执行 JavaScript 代码。JavaScript 可能会修改 DOM 树和样式,从而触发重新布局和绘制。
- 反复执行布局和绘制:
- 如果 JavaScript 或用户交互导致页面内容发生变化,浏览器会根据需要执行布局和绘制的步骤。这个过程可能会多次发生。
- 渲染完毕:
- 一旦浏览器完成所有的布局和绘制,页面就会呈现给用户,用户可以看到并与页面进行交互。
这个渲染过程是高度优化的,浏览器会尽力减少布局和绘制的次数,以提供更快的性能。
2、重绘和回流
重绘(Repaint)和回流(Reflow)是与浏览器渲染引擎相关的,最大的问题就是,影响网页的性能。
- 重绘 (Repaint) :
重绘是指更新页面元素的可见样式,而不影响其布局。这包括改变颜色、背景、字体等属性,使元素的外观发生变化。
- 重绘不会影响元素的位置或大小,因此开销较小。
- 触发重绘的情况:改变元素的样式,但不影响布局,如修改背景颜色、文本颜色等。
- 回流 (Reflow) :
回流是指更新页面元素的布局,通常涉及大小、位置或结构的变化。当页面布局发生变化时,浏览器需要重新计算元素的位置和大小。
- 回流是开销较大的操作,因为它会触发整个页面的重新布局计算,可能涉及多个元素。
- 触发回流的情况:改变页面布局的操作,如修改元素的宽度、高度、边距、添加/删除元素等。
3、判断对象是否为空对象
Object.getOwnPropertyNames()
静态方法返回一个数组,其包含给定对象中所有,自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
Object.getOwnPropertySymbols()
静态方法返回一个包含给定对象所有,自有 Symbol 属性的数组。
使用上面的两个 Object 方法,就可以得到包含给定对象的数组,通过属性的length属性,判断是否为空数组。
js
const a = { [Symbol()]: 'a' }
const b = { a: 'a' }
const c = {}
console.log(Object.getOwnPropertyNames(a).length === 0 && Object.getOwnPropertySymbols(a).length === 0) // false
console.log(Object.getOwnPropertyNames(b).length === 0 && Object.getOwnPropertySymbols(b).length === 0) // false
console.log(Object.getOwnPropertyNames(c).length === 0 && Object.getOwnPropertySymbols(c).length === 0) // true
4、 什么是 MVVM?比之 MVC 有什么区别?
- MVC(Model-View-Controller)
- Model(模型): 负责应用程序的数据和业务逻辑。它表示应用程序的状态和行为,与数据库通信以检索或更新数据。
- View(视图): 负责显示数据和用户界面元素。它接收来自控制器的数据,并将其渲染成用户界面。
- Controller(控制器): 负责处理用户输入、更新模型和调整视图。它充当用户界面和应用程序逻辑之间的协调者。
在传统的 MVC 模式中,View 和 Controller 之间的关系是相对紧密的,Controller 负责管理用户输入并更新 Model,同时直接影响 View。
- MVVM(Model-View-ViewModel)
- Model(模型): 负责应用程序的数据和业务逻辑,与 MVC 中的 Model 类似。
- View(视图): 负责显示数据和用户界面元素,与 MVC 中的 View 类似。
- ViewModel(视图模型): 介于 View 和 Model 之间,负责处理用户输入、与 Model 交互,并为 View 提供渲染所需的数据。ViewModel 是一个用于封装视图状态和行为的抽象层。
在 MVVM 中,View 和 ViewModel 是通过数据绑定关联的,ViewModel 通常会包含一个或多个属性,这些属性与 View 中的元素绑定。当 ViewModel 的属性变化时,View 会自动更新,而用户的操作也会直接影响到 ViewModel 中的数据。
区别:
- 数据绑定: MVVM 强调数据绑定,通过双向绑定实现视图和数据的同步。MVC 中,视图和控制器之间的关系通常是单向的。
- 视图模型: MVVM 引入了 ViewModel,它是一个专门用于管理视图状态和行为的组件。MVC 中,控制器负责处理用户输入和更新模型,直接影响视图。
- 松散耦合: MVVM 中的各个组件之间相对较为松散耦合,每个组件都有明确定义的职责。MVC 中,视图和控制器之间的联系相对较紧密。
总体而言,MVVM 强调数据驱动视图,通过数据绑定实现自动更新,而 MVC 强调用户输入驱动视图,通过控制器更新视图。 MVVM 的数据绑定使得前端开发更加简洁和高效,特别适用于大规模的前端应用。
5、Vue2,3 的区别
Vue 2 和 Vue 3 之间有一些显著的区别。以下是它们的一些主要区别:
- 性能优化: Vue 3 在性能方面有显著的改进。它引入了响应式系统的重写,使用 Proxy 替代 Object.defineProperty,这使得 Vue 3 的性能比 Vue 2 更好。
- Composition API: Vue 3 引入了 Composition API,这是一个新的组织组件逻辑的方式。相比于 Vue 2 的选项 API,Composition API 更灵活,更容易组织和重用代码。
- Teleport: Vue 3 引入了 Teleport 特性,允许你在 DOM 中的任何位置渲染组件的内容。这对于在应用程序中移动组件的位置或在不同的层次结构中渲染组件很有用。
- 新的特性和改进: Vue 3 引入了一些新的特性,如 Fragments(片段)、Custom Directives(自定义指令)等,并对一些现有特性进行了改进。这使得开发者在构建复杂应用时更加方便。
- Tree-shaking(瑶树) 支持: Vue 3 更好地支持 tree-shaking,这意味着在构建时可以更有效地剔除未使用的代码,从而减小应用程序的体积。
- TypeScript 集成: Vue 3 对 TypeScript 的支持更加紧密和友好,包括完全的 TypeScript 类型定义。
6、 Vue3 性能更好在哪里
Vue 3 相对于 Vue 2 在性能方面有一些改进,主要体现在以下几个方面:
- Proxy 替代 Object.defineProperty: Vue 3 中的响应式系统采用 Proxy 替代了 Vue 2 中使用的 Object.defineProperty。Proxy 允许更细粒度的拦截操作,这使得 Vue 3 能够更高效地追踪属性的变化,提高了响应式系统的性能。
- 编译器优化: Vue 3 的编译器经过优化,生成的代码更加精简和高效。这有助于减小应用程序的体积并提高运行时的性能。
- 静态树提升(Static Tree Hoisting): Vue 3 引入了静态树提升的概念,可以将一些静态节点在渲染时提升为常量,减少不必要的重复渲染,从而提高性能。
- 优化的事件处理: Vue 3 对事件处理进行了优化,使其更加高效。特别是对于频繁触发的事件,Vue 3 的性能相对更好。
- Teleport 特性的引入: Vue 3 的 Teleport 特性允许在 DOM 中的任何位置渲染组件的内容,这在性能优化方面提供了更多的灵活性,能够更高效地处理组件的渲染和移动。
- Tree-shaking(瑶树) 支持: Vue 3 更好地支持 tree-shaking,使得在构建时可以更有效地剔除未使用的代码,减小应用程序的体积,从而提高加载和运行时的性能。
7、 Vue2,3 diff 算法原理
Vue 2 和 Vue 3 在数据响应方面采用了不同的 diff 算法,用于比较虚拟 DOM 树并更新视图。
Vue 2 的 Diff 算法:
Vue 2 使用的是经典的Virtual DOM和基于深度优先的双端比较的算法。它的主要步骤如下:
- 创建虚拟 DOM 树: 将组件的状态映射到虚拟 DOM 树,这是一个 JavaScript 对象表示的抽象层次结构。
- 渲染为真实 DOM: 将虚拟 DOM 树渲染为真实 DOM。
- 数据变更时的 Diff: 当组件状态变化时,生成新的虚拟 DOM 树,然后和之前的虚拟 DOM 树进行比较。
- 差异计算: 通过深度优先的算法,逐层比较新旧虚拟 DOM 树,找到两者之间的差异。
- 更新: 根据差异,只对需要更新的部分进行实际 DOM 操作,最小化了对真实 DOM 的修改,提高了性能。
Vue 2 使用的是经典的 Virtual DOM 和基于深度优先的双端比较的 diff 算法。下面是 Vue 2 中 diff 算法的一般步骤:
- 创建虚拟 DOM 树(Virtual DOM Tree): 当组件的状态发生变化时,Vue 2 会首先根据新的状态生成一个新的虚拟 DOM 树。
- 比较新旧虚拟 DOM 树: Vue 2 通过逐层比较新旧虚拟 DOM 树的节点,找出两者之间的差异。这一步是通过深度优先的算法实现的。
- 生成差异(Diff): 在比较过程中,当发现节点有差异时,Vue 2 会生成一个差异对象,该对象描述了如何更新真实 DOM 以保持与虚拟 DOM 的一致性。
- 应用差异(Patch): Vue 2 将生成的差异对象应用到真实 DOM 上,从而更新视图。这一步是通过对真实 DOM 进行相应的操作,比如修改节点的属性、插入新节点、删除不需要的节点等来实现的。
- 更新组件状态: 最后,Vue 2 会更新组件的状态,确保虚拟 DOM 和组件的状态保持同步。
在这个过程中,Vue 2 采用了一些优化措施,例如通过限制比较的深度,只对同一层级的节点进行比较,以减小比较的范围。同时,对于列表渲染,Vue 2 采用了一种叫做"key"的机制,通过给列表中的元素添加唯一的标识符,可以帮助 Vue 更准确地追踪列表中元素的变化。
Vue 3 的 Diff 算法:
Vue 3 的 diff 算法相对于 Vue 2 有一些改进,采用了一种称为"优化的渲染器"(Optimized Renderer)的策略。以下是 Vue 3 中 diff 算法的一般步骤:
- 生成虚拟 DOM(Virtual DOM): 当组件的状态发生变化时,Vue 3 会生成一个新的虚拟 DOM 树,该树表示组件的当前状态。
- 计算新旧虚拟 DOM 之间的差异: Vue 3 使用 Patch Flag 机制,通过在虚拟 DOM 节点上标记一些信息,例如节点的静态性、是否有子节点等,来帮助 diff 算法更快速地找到差异。这使得 Vue 3 在比较新旧虚拟 DOM 时能够更高效地确定需要更新的节点。
- 生成差异(Diff): 在比较过程中,Vue 3 生成一个差异对象,描述了需要对真实 DOM 进行的操作,如添加、移动、删除节点等。Vue 3 放弃了双端比较,采用的是更为高效的单端比较。
- 应用差异(Patch): Vue 3 将生成的差异对象应用到真实 DOM 上,通过对真实 DOM 进行相应的操作,更新视图。与 Vue 2 不同,Vue 3 的渲染器对 DOM 操作进行了优化,采用了更高效的算法,如可选的静态树提升等。
- 更新组件状态: 最后,Vue 3 更新组件的状态,确保虚拟 DOM 和组件的状态保持同步。
Vue 3 引入了一些新的概念和优化,如 Patch Flag、静态树提升等,以提高 diff 算法的性能。总体而言,Vue 3 的 diff 算法在性能上有一些提升,尤其是在处理大型组件和复杂视图时。
8、 Vue 生命周期
Vue.js 的生命周期钩子函数是在组件的不同阶段执行的一组函数,允许你在组件的生命周期中执行特定的操作。以下是 Vue 2.x 中常用的生命周期钩子函数:
- beforeCreate(创建前): 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。在这个阶段,组件的数据、事件等都还没有初始化。
- created(创建后): 实例已经创建完成之后被调用。在这个阶段,可以访问数据、计算属性等,但 DOM 元素还未生成。
- beforeMount(挂载前): 在挂载开始之前被调用:相关的 render 函数首次被调用。
- mounted(挂载后): el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。在这一步,组件已经渲染完成,DOM 元素也已经生成。
- beforeUpdate(更新前): 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步修改数据,但避免直接操作 DOM。
- updated(更新后): 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。在这一阶段,组件的 DOM 已经更新。
- beforeDestroy(销毁前): 实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed(销毁后): Vue 实例销毁后调用。在这一步,组件的所有指令都已经解绑,所有事件监听器已经移除,所有子实例也已经销毁。
以上是 Vue 2.x 的生命周期钩子函数。在 Vue 3 中,由于引入了 Composition API,生命周期钩子的使用可能会有所不同。
Vue 3 中的生命周期钩子函数与 Vue 2.x 有些许不同,点击这里更加详细了解Vue3中的生命周期。
Vue 3 引入了 Composition API,使组件的逻辑更加灵活。以下是 Vue 3 中常用的生命周期钩子函数:
setup
: 在组件实例初始化之后,返回render
函数之前执行。这是 Composition API 的一部分,用于设置组件的状态、引入响应式数据等。beforeCreate
(创建前): 与 Vue 2.x 相同,在实例初始化之后,数据观测 (data observer) 和事件/watcher 事件配置之前调用。created
(创建后): 与 Vue 2.x 相同,在实例已经创建完成之后调用。beforeMount
(挂载前): 与 Vue 2.x 相同,在挂载开始之前调用。onBeforeMount
: Vue 3 新增的生命周期钩子,在beforeMount
之前调用。mounted
(挂载后): 与 Vue 2.x 相同,在挂载完成之后调用。onMounted
: Vue 3 新增的生命周期钩子,在mounted
之后调用。beforeUpdate
(更新前): 与 Vue 2.x 相同,在数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。onBeforeUpdate
: Vue 3 新增的生命周期钩子,在beforeUpdate
之前调用。updated
(更新后): 与 Vue 2.x 相同,在数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。onUpdated
: Vue 3 新增的生命周期钩子,在updated
之后调用。beforeUnmount
(卸载前): 在实例卸载之前调用。在这一步,实例仍然可用。onBeforeUnmount
: Vue 3 新增的生命周期钩子,在beforeUnmount
之前调用。unmounted
(卸载后): 在实例销毁后调用。在这一步,组件的所有指令都已经解绑,所有事件监听器已经移除,所有子实例也已经销毁。
这些生命周期钩子函数的命名和调用时机与 Vue 2.x 大致相似,但在使用 Composition API 的情况下,可以更灵活地使用 setup
函数来组织组件的逻辑。
9、v-for 的 key 值是什么,没有唯一性的 key 值怎么办
key
是用于识别 v-for
循环中的每个节点的特殊属性。它的作用是帮助 Vue 识别每个节点的身份,从而在列表发生变化时更高效地更新 DOM。key
应该具有唯一性,即在同一列表中,key
的值不能重复。
js
// `item.id` 被用作 `key`,假设 `item.id` 具有唯一性,这有助于 Vue 在更新列表时更准确地识别每个节点。
<div v-for="(item, index) in items" :key="item.id">
{{ item.name }}
</div>
如果数据中没有唯一标识符可供用作 key
,可以使用索引作为 key
。
js
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
注意:
最好在有唯一标识符的情况下使用唯一标识符,因为使用索引作为
key
有时可能引发一些问题,特别是在列表项的顺序发生变化时。
10、 实现一个 Promise.all
js
function customPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
reject(new TypeError('Promises must be an array'));
}
// 存储结果
const results = [];
// 存储结果数量
let completedCount = 0;
// 结果统一处理函数
const handleCompletion = (index, result) => {
results[index] = result;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
};
// 处理传入的数据
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then((result) => {
handleCompletion(i, result);
})
.catch((error) => {
reject(error);
});
}
if (promises.length === 0) {
resolve(results);
}
});
}
// 示例用法
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
customPromiseAll([promise1, promise2, promise3])
.then((results) => {
console.log('All promises resolved:', results);
})
.catch((error) => {
console.error('One or more promises rejected:', error);
});
会返回一个新的 Promise,该 Promise 在所有传入的 Promise 都成功时将解析为一个数组,包含每个 Promise 的结果。如果其中任何一个 Promise 被拒绝,它会立即将新的 Promise 拒绝,并传递拒绝的原因。