前言
在现在主流框架中,大多都有 visual dom 的实现,这里通过常见的笔试题来实现个最简单的 render 函数。
开始实现简版
// dom 结构 ul>li*2
const root = {
tagName: 'ul',
props: {
className: 'list',
},
children: [
{
tagName: 'li',
children: ['A'],
},
{
tagName: 'li',
children: ['B'],
}
]
};
这个编程题一是考察一下树结构的遍历,其实同时也会考察怎么更高效点渲染一个 dom tree。如果是频繁的进行 dom 的变更,性能肯定是最差的,所以这里实现 2 种:
- 基于字符串拼接,其实字符串拼接经过浏览器的优化,是很高效的结构,就是阅读性会差一些
- 基于 Fragment 的结构来实现,类似以前 jQuery 中缓存 dom 结构最后统一 appendChild 的形式
// 缓存下 document 的访问链,这里默认浏览器环境
const doc = document;
// 实现 1,通过字符串去拼接
function renderString(el, root) {
let html = '';
iter(root);
function iter(node) {
if (node === null) {
html += `<span></span>`;
return;
}
const { props = {}, children = [], tagName } = node;
html += `<${tagName}`;
for (const prop in props) {
const attr = prop === 'className' ? 'class' : prop;
html += ` ${attr}="${props[prop]}"`;
}
html += '>';
for (const child of children) {
if (typeof child === 'string') {
html += child;
} else {
iter(child);
}
}
html += `</${tagName}>`;
}
const tmp = document.createElement('div');
tmp.innerHTML = html;
el.appendChild(tmp);
}
// 实现 2,通过 Fragment 去拼接
function renderFragment(el, root) {
const fragment = doc.createDocumentFragment();
iter(root);
el.appendChild(fragment);
function iter(node) {
// 如果 node 为空,也认为是一个可识别的节点
if (node === null) {
// 空节点,默认可以是空串,也可以是 span
const emptyNode = doc.createElement('span');
fragment.appendChild(emptyNode);
return emptyNode;
}
const element = doc.createElement(node.tagName);
fragment.appendChild(element);
// props
const props = node.props ?? {};
for (let prop in props) {
// 这里暂时不考虑更多 attr, prop 的区别
const attr = prop === 'className' ? 'class' : prop;
element.setAttribute(attr, props[prop]);
}
// children
const children = node.children || [];
for (let child of children) {
if (typeof child === 'string') {
const textElement = doc.createTextNode(child);
element.appendChild(textElement);
} else {
element.appendChild(iter(child));
}
}
return element;
}
}
function render(el, root, options = {}) {
// platform 可以预留处理不同平台的渲染模式,不过耦合到 render 这个时候不是特别好,可以参考 JVM 的思路,直接从平台做大的实现切分会好一些
// mode 默认 string 类型,此类型兼容性是最好的
const { platform = 'web', mode = 'string' } = options;
if (mode === 'string') {
return renderString(el, root);
}
if (mode === 'fragment') {
return renderFragment(el, root);
}
}
render(document.body, root, { mode: 'string' });
render(document.body, root, { mode: 'fragment' });
这里其实可以看到,string 模式新增了一层 div 结构来保证代码的便利性,其实这也能间接反想一下,为什么以前框架(react/vue)第一版本在写组件的时候,需要我们必须包裹一个元素。
还可以思考,如果是一个较完整的框架实现,是否增加 Root 节点和子节点,会进行分开初始化呢?
React 的 Fiber 版本和非 Fiber 版本的 render 流程有什么不同吗?