# 渲染器核心功能 >面试题:说一说渲染器的核心功能是什么? 渲染器的核心功能,是根据拿到的 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 ``` **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 ``` vnode: ```js const vnode = { type: 'button', props: { disable: false } } ``` 通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态 ```js el.setAttribute('disabled', 'false') ``` 解决方案:优先设置 DOM Properties 遇到新的问题:本意是要禁用按钮 ```vue ``` ```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 ``` 例如 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 ``` ```js const vnode = { type: "p", props: { class: "foo bar", }, }; ``` 情况二:对象值 ```vue ``` ```js const vnode = { type: "p", props: { class: { foo: true, bar: false }, }, }; ``` 情况三:数组值 ```vue ``` ```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-