前言
说到虚拟DOM大家都不陌生,其实就是用一个对象来描述dom节点。例如Vue中的h方法,第一个参数是标签,第二个参数是属性,以后所有标签都是子节点,如下:
h(
'div',
{id: 'testDom', data: '测试属性'},
h('span', {style: {color: 'red'}}, '我是span内容'),
'测试内容'
)
实际上渲染出来就是:
<div id="testDom" data="测试属性">
<span style="color: red"> 我是span内容 </span>
测试内容
</div>
而用虚拟DOM表示出来就是:
{
type: 'div',
props: {id: 'testDom', data: '测试属性'},
children: [
{
type: 'span',
props: {style : {color: 'red'}},
children: [
text: '我是span内容'
]
},
{
type: '',
props: '',
children: [],
text: '测试内容'
}
]
}
接下来我们来具体看下实现过程,可能如你所想就是那么简单~~
createElement() 方法实现
/**
* 创建标签
* @param {*} type 类型
* @param {*} props 节点属性
* @param {...any} children 所有孩子
*/
function createElement(type, props={}, ...children){
let key;
if(props.key){
key = props.key;
delete props.key;
}
return{
type,
props,
key,
children
text: undefined
}
}
由上可知:key可以通过props属性传入,且方法最终返回一个对象。但为了操作方便,我们可以把返回对象的操作单独提出一个方法叫vnode来单独处理, 如下:
/**
* 创建标签
* @param {*} type 类型
* @param {*} props 节点属性
* @param {...any} children 所有孩子
*/
function createElement(type, props={}, ...children){
let key;
if(props.key){
key = props.key;
delete props.key;
}
return vnode(type, props, key, children);
}
function vnode(type,props,key,children,text){
return {
type,
props,
key,
children,
text
}
}
测试下我们写的代码,如下:
let vd = createElement(
'div',
{id: 'testDom', data: '测试属性', key: 'testKey'},
createElement('span', {style: {color: 'red'}}, '我是span内容'),
'测试内容'
)
console.log(vd);
输出结果如下:
可以看到部分节点已经被转换成虚拟节点了,但children里有点问题,纯文本节点没有被处理成虚拟节点,这里需要将createElement优化一下,如下:
function createElement(type, props, ...children){
let key;
if(props.key){
key = props.key;
delete props.key;
}
// 将不是虚拟节点的处理成虚拟节点
children = children.map(child => {
if(typeof child === 'string'){
return vnode(undefined, undefined, undefined, undefined, child);
}else{
return child;
}
})
return vnode(type, props, key, children);
}
优化后所有节点已经被处理成虚拟节点了,输出结果如下:
render() 方法
createElement()方法只是创建虚拟节点,我们要把创建好的虚拟节点渲染到页面上,这里就需要使用render()方法了,如下:
/**
* 渲染虚拟节点
* @param {*} vnode 用户写的虚拟节点
* @param {*} container 要渲染到哪个容器中
*/
function render(vnode, container){
// 将虚拟节点转换为真实节点
let ele = createDomElementFromVnode(vnode);
// 简洁点渲染到容器中
container.appendChild(ele);
}
// 通过虚拟的对象,创建一个真实的dom元素
function createDomElementFromVnode(vnode){
let {type, key, props, children, text} = vnode;
// 如果有类型,则是一个标签
if(type){
// 建立虚拟节点和真实元素的一个关系,后边可用来更新真实的dom
vnode.domElement = document.createElement(type);
}
// 否则就是一个文本
else{
vnode.domElement = document.createElement(text);
}
return vnode.domElement;
}
写到这里我们测试下我们写的代码,如下
html ------
<template>
<div id="test_container"> </div>
</template>
js ------
let vd = createElement(
'div',
{id: 'testDom', data: '测试属性', key: 'testKey'},
createElement('span', {style: {color: 'red'}}, '我是span内容'),
'测试内容'
)
window.onload = function(){
let div = document.getElementById('test_container');
render(vd, div)
}
渲染结果如下:
如上:在容器test_containern内创建了一个空的div
updateProperties() 方法
我们已经可以在指定容器中创建一个标签,但标签的属性及子标签并没有创建,所以接下来我们需要把属性和子标签更新上,如下:
// 通过虚拟的对象,创建一个真实的dom元素
function createDomElementFromVnode(vnode){
let {type, key, props, children, text} = vnode;
// 如果有类型,则是一个标签
if(type){
// 建立虚拟节点和真实元素的一个关系,后边可用来更新真实的dom
vnode.domElement = document.createElement(type);
//根据当前的虚拟节点的属性更新真实的dom元素
updateProperties(vnode);
}
// 否则就是一个文本
else{
vnode.domElement = document.createElement(text);
}
return vnode.domElement;
}
/**
* 更新属性:会根据老的属性和新的属性重新更新节点
* @param {*} newVnode 新节点
* @param {*} oldVnode 旧节点
*/
function updateProperties(newVnode, oldVnode = {}){
let domElement = newVnode.domElement; // 当前真实dom元素
let newProps = newVnode.props; // 当前虚拟节点中的属性
// 如果老的里面有,新的里面没有,则移除这个属性
for(let oldPropName in oldProps){
if(!newProps[oldPropName]){
delete domElement[oldPropName]
}
}
let newStyleObj = newProps.style || {};
let oldStyleObj = oldProps.style || {};
// 比对新旧连个style, 新的里面没有则删除此属性
for(let propName in oldStyleObj){
if(!newStyleObj[propName]){
domElement.style[propName] = '';
}
}
// 如果如果老的里面没有,新的里面有,则添加这个属性
for(let newPropsName in newProps){
// 这里只单独处理下 style, 其他的 @click 之类的暂不处理
if(newPropsName === 'style'){
let styleObj = newProps.style;
for(let s in styleObj){
domElement.style[s] = styleObj[s];
}
}else{
// 用新节点的属性直接覆盖掉点老节点的属性即可
domElement[newPropsName] = newPropsName;
}
}
}
写到此:第一层的div的属性应该加上了,如下图:
我们还是只创建了第一层的标签,即子标签并没有创建,该怎么创建子标签呢?其实也很简单:遍历一下子节点即可,如下:
// 通过虚拟的对象,创建一个真实的dom元素
function createDomElementFromVnode(vnode){
let {type, key, props, children, text} = vnode;
// 如果有类型,则是一个标签
if(type){
// 建立虚拟节点和真实元素的一个关系,后边可用来更新真实的dom
vnode.domElement = document.createElement(type);
// 根据当前的虚拟节点的属性更新真实的dom元素
updateProperties(vnode);
// 递归遍历子的虚拟节点
children.forEach(child => render(child, vnode.domElement));
}
// 否则就是一个文本
else{
vnode.domElement = document.createTextNode(text);
}
return vnode.domElement;
}
这时渲染的节点如下图:
到这里简单的虚拟Dom咱们就说完了,希望对你有帮助~~ \
下篇文章 聊聊 Vue 的 Diff 算法也许没你想的那么难 欢迎阅读
欢迎评论留言~~~