485 lines
12 KiB
Markdown
485 lines
12 KiB
Markdown
# 渲染器核心功能
|
||
|
||
>面试题:说一说渲染器的核心功能是什么?
|
||
|
||
渲染器的核心功能,是根据拿到的 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 采用的是双端 diff,Vue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么?
|
||
|
||
---
|
||
|
||
-EOF- |