渲染器核心功能
# 渲染器核心功能
面试题:说一说渲染器的核心功能是什么?
渲染器的核心功能,是根据拿到的 vnode,进行节点的挂载与更新。
挂载属性
vnode:
const vnode = {
type: 'div',
// props 对应的就是节点的属性
props: {
id: 'foo'
},
children: [
type: 'p',
children: 'hello'
]
}
渲染器内部有一个 mountElement 方法:
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对象的方式:
if(vnode.props){
for(const key in vnode.props){
// el.setAttribute(key, vnode.props[key])
el[key] = vnode.props[key];
}
}
思考🤔:哪种设置方法好?两种设置方法有区别吗?应该使用哪种来设置?
HTML Attributes
Attributes 是元素的初始属性值,在 HTML 标签中定义,用于描述元素的初始状态。
- 在元素被解析的时候,只会初始化一次
- 只能是字符串值,而且这个值仅代表初始的状态,无法反应运行时的变化
<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 都有关联
另外,在设置的时候,不是单纯的用某一种方式,而是两种方式结合使用。因为需要考虑很多特殊情况:
- disabled
- 只读属性
1. disabled
模板:我们想要渲染的按钮是非禁用状态
<button :disabled="false">Button</button>
vnode:
const vnode = {
type: 'button',
props: {
disable: false
}
}
通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态
el.setAttribute('disabled', 'false')
解决方案:优先设置 DOM Properties
遇到新的问题:本意是要禁用按钮
<button disabled>Button</button>
const vnode = {
type: 'button',
props: {
disable: ''
}
}
el.disabled = ''
在对 DOM 的 disabled 属性设置值的时候,任何非布尔类型的值都会被转为布尔类型:
el.disabled = false
最终渲染出来的按钮是非禁用状态。
渲染器内部的实现,不是单独用 HTML Attribute 或者 DOM Properties,而是两者结合起来使用,并且还会考虑很多的细节以及特殊情况,针对特殊情况做特殊处理。
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. 只读属性
<input form="form1"/>
例如 el.form,但是这个属性是只读的,所以这种情况,又只能使用 setAttribute 方法来设置
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 来设置。
还可以进一步优化,将属性的设置提取出来:
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 的值可能会有这么一些情况:
情况一:字符串值
<template>
<p class="foo bar"></p>
</template>
const vnode = {
type: "p",
props: {
class: "foo bar",
},
};
情况二:对象值
<template>
<p :class="cls"></p>
</template>
<script setup>
import { ref } from 'vue'
const cls = ref({
foo: true,
bar: false
})
</script>
const vnode = {
type: "p",
props: {
class: { foo: true, bar: false },
},
};
情况三:数组值
<template>
<p :class="arr"></p>
</template>
<script setup>
import { ref } from 'vue'
const arr = ref([
'foo bar',
{
baz: true
}
])
</script>
const vnode = {
type: "p",
props: {
class: ["foo bar", { baz: true }],
},
};
这里首先第一步就是需要做参数归一化,统一成字符串类型。Vue内部有一个方法 normalizeClass 就是做 class 的参数归一化的。
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'
const vnode = {
type: "p",
props: {
class: normalizeClass(["foo bar", { baz: true }]),
},
};
const vnode = {
type: "p",
props: {
class: 'foo bar baz',
},
};
设置class的时候,设置方法也有多种:
- setAttribute
- el.className:这种方式效率是最高的
- el.classList
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 计算。
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 的渲染过程,这个过程包含几个阶段:
- 挂载:初次渲染时,渲染器会将虚拟 DOM 转化为真实 DOM 并插入页面。它会根据虚拟节点树递归创建 DOM 元素并设置相关属性。
- 更新:当组件的状态或属性变化时,渲染器会计算新旧虚拟 DOM 的差异,并通过 Patch 过程最小化更新真实 DOM。
- 卸载:当组件被销毁时,渲染器需要将其从 DOM 中移除,并进行必要的清理工作。
每一个步骤都有大量需要考虑的细节,就拿挂载来讲,光是处理元素属性如何挂载就有很多需要考虑的问题,比如:
- 最终设置属性的时候是用 setAttribute 方法来设置,还是用给 DOM 对象属性赋值的方式来设置
- 遇到像 disabled 这样的特殊属性该如何处理
- class、style 这样的多值类型,该如何做参数的归一化,归一为哪种形式
- 像 class 这样的属性,设置的方式有哪种,哪一种效率高
另外,渲染器和响应式系统是紧密结合在一次的,当组件首次渲染的时候,组件里面的响应式数据会和渲染函数建立依赖关系,当响应式数据发生变化后,渲染函数会重新执行,生成新的虚拟 DOM 树,渲染器随即进入更新阶段,根据新旧两颗虚拟 DOM 树对比来最小化更新真实 DOM,这涉及到了 Vue 中的 diff 算法。diff 算法这一块儿,Vue2 采用的是双端 diff,Vue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么?
-EOF-