2024-09-18 15:08:22 +08:00

485 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 渲染器核心功能
>面试题:说一说渲染器的核心功能是什么?
渲染器的核心功能,是根据拿到的 vnode进行节点的**挂载**与**更新**。
**挂载属性**
vnode
```js
const vnode = {
type: 'div',
// props 对应的就是节点的属性
props: {
id: 'foo'
},
children: [
type: 'p',
children: 'hello'
]
}
```
渲染器内部有一个 mountElement 方法:
```js
function mountElement(vnode, container){
// 根据节点类型创建对应的DOM节点
const el = document.createElement(vnode.type);
// 省略children的处理
// 对属性的处理
if(vnode.props){
for(const key in vnode.props){
el.setAttribute(key, vnode.props[key])
}
}
insert(el, container);
}
```
除了使用setAttribute方法来设置属性以外也可以使用DOM对象的方式
```js
if(vnode.props){
for(const key in vnode.props){
// el.setAttribute(key, vnode.props[key])
el[key] = vnode.props[key];
}
}
```
思考🤔:哪种设置方法好?两种设置方法有区别吗?应该使用哪种来设置?
**HTML Attributes**
Attributes 是元素的**初始**属性值,在 HTML 标签中定义,用于**描述元素的初始状态**。
- 在元素被解析的时候,只会初始化一次
- 只能是字符串值,而且这个值仅代表初始的状态,无法反应运行时的变化
```vue
<input type="text" id="username" value="John">
```
**DOM Properties**
Properties 是 JavaScript 对象上的属性,代表了 DOM 元素在 **内存中** 的实际状态。
- 反应的是 DOM 元素的当前状态
- 属性类型可以是字符串、数字、布尔值、对象之类的
很多 HTML attributes 在 DOM 对象上有与之相同的 DOM Properties例如
| HTML attributes | DOM properties |
| --------------- | -------------- |
| id="username" | el.id |
| type="text" | el.type |
| value="John" | el.value |
但是,两者并不总是相等的,例如:
| HTML attributes | DOM properties |
| --------------- | -------------- |
| class="foo" | el.className |
还有很多其他的情况:
- HTML attributes 有但是 DOM properties 没有的属性:例如 aria-* 之类的HTML Attributes
- DOM properties 有但是 HTML attributes 没有的属性:例如 el.textContent
- 一个 HTML attributes 关联多个 DOM properties 的情况:例如 value="xxx" 和 el.value 以及 el.defaultValue 都有关联
另外,在设置的时候,不是单纯的用某一种方式,而是两种方式结合使用。因为需要考虑很多特殊情况:
1. disabled
2. 只读属性
**1. disabled**
模板:我们想要渲染的按钮是非禁用状态
```vue
<button :disabled="false">Button</button>
```
vnode:
```js
const vnode = {
type: 'button',
props: {
disable: false
}
}
```
通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态
```js
el.setAttribute('disabled', 'false')
```
解决方案:优先设置 DOM Properties
遇到新的问题:本意是要禁用按钮
```vue
<button disabled>Button</button>
```
```js
const vnode = {
type: 'button',
props: {
disable: ''
}
}
```
```js
el.disabled = ''
```
在对 DOM 的 disabled 属性设置值的时候,任何非布尔类型的值都会被转为布尔类型:
```js
el.disabled = false
```
最终渲染出来的按钮是非禁用状态。
**渲染器内部的实现,不是单独用 HTML Attribute 或者 DOM Properties而是两者结合起来使用并且还会考虑很多的细节以及特殊情况针对特殊情况做特殊处理**
```js
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 用 in 操作符判断 key 是否存在对应的 DOM Properties
if (key in el) {
// 获取该 DOM Properties 的类型
const type = typeof el[key];
const value = vnode.props[key];
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
// 如果要设置的属性没有对应的 DOM Properties则使用 setAttribute 函数设置属性
el.setAttribute(key, vnode.props[key]);
}
}
}
insert(el, container);
}
```
**2. 只读属性**
```vue
<input form="form1"/>
```
例如 el.form但是这个属性是只读的所以这种情况又只能使用 setAttribute 方法来设置
```js
function shouldSetAsProps(el, key, value) {
// 特殊处理
// 遇到其他特殊情况再进行重构
if (key === "form" && el.tagName === "INPUT") return false;
// 兜底
return key in el;
}
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (shouldSetAsProps(el, key, value)) {
const type = typeof el[key];
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
el.setAttribute(key, value);
}
}
}
insert(el, container);
}
```
shouldSetAsProps 这个方法返回一个布尔值,由布尔值来决定是否使用 DOM Properties 来设置。
还可以进一步优化,将属性的设置提取出来:
```js
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === "form" && el.tagName === "INPUT") return false;
// 兜底
return key in el;
}
/**
*
* @param {*} el 元素
* @param {*} key 属性
* @param {*} prevValue 旧值
* @param {*} nextValue 新值
*/
function patchProps(el, key, prevValue, nextValue) {
if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 函数即可
patchProps(el, key, null, vnode.props[key]);
}
}
insert(el, container);
}
```
**class处理**
class 本质上也是属性的一种,但是在 Vue 中针对 class 做了增强,因此 Vue 模板中的 class 的值可能会有这么一些情况:
情况一:字符串值
```vue
<template>
<p class="foo bar"></p>
</template>
```
```js
const vnode = {
type: "p",
props: {
class: "foo bar",
},
};
```
情况二:对象值
```vue
<template>
<p :class="cls"></p>
</template>
<script setup>
import { ref } from 'vue'
const cls = ref({
foo: true,
bar: false
})
</script>
```
```js
const vnode = {
type: "p",
props: {
class: { foo: true, bar: false },
},
};
```
情况三:数组值
```vue
<template>
<p :class="arr"></p>
</template>
<script setup>
import { ref } from 'vue'
const arr = ref([
'foo bar',
{
baz: true
}
])
</script>
```
```js
const vnode = {
type: "p",
props: {
class: ["foo bar", { baz: true }],
},
};
```
这里首先第一步就是需要做参数归一化统一成字符串类型。Vue内部有一个方法 normalizeClass 就是做 class 的参数归一化的。
```js
function isString(value) {
return typeof value === "string";
}
function isArray(value) {
return Array.isArray(value);
}
function isObject(value) {
return value !== null && typeof value === "object";
}
function normalizeClass(value) {
let res = "";
if (isString(value)) {
res = value;
} else if (isArray(value)) {
// 如果是数组,递归调用 normalizeClass
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i]);
if (normalized) {
res += (res ? " " : "") + normalized;
}
}
} else if (isObject(value)) {
// 如果是对象,则检查每个 key 是否为真值
for (const name in value) {
if (value[name]) {
res += (res ? " " : "") + name;
}
}
}
return res;
}
console.log(normalizeClass("foo")); // 'foo'
console.log(normalizeClass(["foo", "bar"])); // 'foo bar'
console.log(normalizeClass({ foo: true, bar: false })); // 'foo'
console.log(normalizeClass(["foo", { bar: true }])); // 'foo bar'
console.log(normalizeClass(["foo", ["bar", "baz"]])); // 'foo bar baz'
```
```js
const vnode = {
type: "p",
props: {
class: normalizeClass(["foo bar", { baz: true }]),
},
};
```
```js
const vnode = {
type: "p",
props: {
class: 'foo bar baz',
},
};
```
设置class的时候设置方法也有多种
1. setAttribute
2. el.className这种方式效率是最高的
3. el.classList
```js
function patchProps(el, key, prevValue, nextValue) {
// 对 class 进行特殊处理
if (key === "class") {
el.className = nextValue || "";
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}
```
**子节点的挂载**
除了对自身节点的处理,还需要对子节点进行处理,不过处理子节点时涉及到 diff 计算。
```js
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 针对子节点进行处理
if (typeof vnode.children === "string") {
// 如果 children 是字符串,则直接将字符串插入到元素中
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它们
vnode.children.forEach((child) => {
patch(null, child, el);
});
}
insert(el, container);
}
```
>面试题:说一说渲染器的核心功能是什么?
>
>参考答案:
>
>渲染器最最核心的功能是处理从虚拟 DOM 到真实 DOM 的渲染过程,这个过程包含几个阶段:
>
>1. 挂载:初次渲染时,渲染器会将虚拟 DOM 转化为真实 DOM 并插入页面。它会根据虚拟节点树递归创建 DOM 元素并设置相关属性。
>2. 更新:当组件的状态或属性变化时,渲染器会计算新旧虚拟 DOM 的差异,并通过 Patch 过程最小化更新真实 DOM。
>3. 卸载:当组件被销毁时,渲染器需要将其从 DOM 中移除,并进行必要的清理工作。
>
>每一个步骤都有大量需要考虑的细节,就拿挂载来讲,光是处理元素属性如何挂载就有很多需要考虑的问题,比如:
>
>1. 最终设置属性的时候是用 setAttribute 方法来设置,还是用给 DOM 对象属性赋值的方式来设置
>2. 遇到像 disabled 这样的特殊属性该如何处理
>3. class、style 这样的多值类型,该如何做参数的归一化,归一为哪种形式
>4. 像 class 这样的属性,设置的方式有哪种,哪一种效率高
>
>另外,渲染器和响应式系统是紧密结合在一次的,当组件首次渲染的时候,组件里面的响应式数据会和渲染函数建立依赖关系,当响应式数据发生变化后,渲染函数会重新执行,生成新的虚拟 DOM 树,渲染器随即进入更新阶段,根据新旧两颗虚拟 DOM 树对比来最小化更新真实 DOM这涉及到了 Vue 中的 diff 算法。diff 算法这一块儿Vue2 采用的是双端 diffVue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么?
---
-EOF-