渲染器简单实现
参考vue实现一个简易的渲染器, 这边只考虑基本html元素不考虑自定义组件, 通过render的h函数到mount函数在最后到patch函数实现一个生成虚拟dom渲染到真实dom的效果
应用示例
//1. 通过h函数实现一个vnode虚拟节点
const vnode = h(
'div',
{
class: 'cqc'
},
[
h('h2', null, '这是一个h2'),
h('button', {
onClick: function() {
console.log('这是一个点击方法')
}
}, 'btn')
]
);
// 2. 通过mount函数挂载虚拟节点至 div#app
mount(vnode, document.querySelector('#app'))
方法实现
h、mount 函数实现
// 转成虚拟dom
const h = function(tag, props, children) {
return {
tag,
props,
children
}
}
const mount = function(vnode, container) {
const {
children,
tag,
props
} = vnode;
const el = vnode.el = document.createElement(tag);
// 处理porps
if(props) {
for(const key in props) {
const value = props[key];
if(key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else {
el.setAttribute(key, value);
}
}
}
// 处理children
if(children) {
if(typeof children === 'string') {
el.textContent = children;
} else {
children.forEach(item => mount(item, el));
}
}
container.appendChild(el);
}
效果图
patch 函数实现
// 对比新旧虚拟节点
const patch = (n1, n2) => {
const el = n1.el;
// 标签不同直接移除原先节点插入新节点
if(n1.tag !== n2.tag) {
const n1Parent = el.parentElement;
n1Parent.removeChild(el);
mount(n2, n1Parent);
} else {
// 保存el至新虚拟节点
n2.el = el;
// 处理props
const oldProps = n1.props || {};
const newProps = n2.props || {};
for(const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
// 添加新属性至el
if(oldValue !== newValue) {
if(key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
// 删除旧props
for(const key in oldProps) {
const oldValue = oldProps[key];
if(!(key in newProps)) {
if(key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), oldValue);
} else {
el.removeAttribute(key);
}
}
}
// 处理children
const newChildren = n2.children || [];
const oldChildren = n1.children || [];
if(typeof newChildren === 'string'){
if(oldChildren !== newChildren) {
el.innerHTML = newChildren;
}
} else {
// 旧节点是字符串新节点是虚拟节点数组
if(typeof oldChildren === 'string') {
el.innerHTML = '';
newChildren.forEach(o => mount(o, el));
} else {
// 两者都是虚拟节点数组
/**
* oldChildren: [v1, v2, v3]
* newChildren: [v1, v4, v5]
*/
const oldLen = oldChildren.length;
const newLen = newChildren.length;
const commonLen = Math.min(oldLen, newLen);
// 相同节点patch
for(let i = 0; i < commonLen; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 新节点比旧节点多 挂载新节点
if(newLen > oldLen) {
newChildren.slice(oldLen).forEach(o => mount(o, el));
}
// 旧节点比新节点多 卸载
if(newLen < oldLen) {
oldChildren.slice(newLen).forEach(o => el.removeChild(o.el));
}
}
}
}
}
}
<script>
// 1. 通过h创建一个 VNode
const vnode = h(
'div',
{
class: 'cqc'
},
[
h('h2', null, '这是一个h2'),
h('button', {
onClick: function() {
console.log('这是一个点击方法')
}
}, 'btn')
]
);
// 2. 通过mount函数, 将vnode挂载到 div#app 上 (虚拟dom -> 真实dom)
mount(vnode, document.querySelector('#app'));
const vnode1 = h('div', { class: 'qcqcqc'}, [h(
'div',
{
class: 'cqc'
},
[
h('h2', null, 'hhh'),
h('button', {
onClick: function() {
console.log('cqqc');
}
}, '+1')
]
)]);
patch(vnode, vnode1);
</script>