作为计算机工程师,框架是实际开发中都会用到的。理解框架原理,对更好地使用它和定位问题是很有帮助的事情。本文实现了一个简单的vdom渲染过程,来帮助理解vdom原理。
关键词:虚拟dom、虚拟dom渲染、render、template、jsx
vdom对象
vdom也叫虚拟dom,是用来描述页面的js对象。
在原生开发中,我们用HTML语言来描述web页面,它是由元素构成的。元素可拥有属性和文本内容。例如,下面的html代码:
<ul>
<li class="item" style="color:red;" onclick="alert(0)">a</li>
<li>b</li>
</ul>
它是一个ul元素,有两个li子元素。其中,li元素它有三个属性: class 、样式style和事件click,同时还有文本内容a、b。
这些元素信息,我们用一种数据结构来表示,如下:
{
tag: 'ul',
children: [{
tag: 'li',
props: {
class:['item'],
style: {
color:"red"
},
on:{
click: function() {
alert(0);
}
}
},
children: [{
text:'a'
}]
},
{
tag: 'li',
children: [{
text:'b'
}]
}]
}
vue.js和react框架就是用这样的vdom对象来描述页面的。根据vdom描述的元素信息,创建真实dom的过程,就是vdom的渲染。
vdom渲染
在浏览器内部,HTML页面是一个DOM树的结构。我们可以用Documentapi 创建新的元素,并挂载到DOM树。
首先,根据不同的节点类型,创建元素节点或文本节点,并设置元素节点的属性。
function createElement(vdom) {
if (vdom?.tag) {
// 创建元素节点
const dom = document.createElement(vdom.tag);
// 设置属性
for(let prop in vdom.props){
setAttribute(dom, prop, vdom.props[prop]);
}
return dom;
}else if(vdom?.text){
// 创建文本节点
return document.createTextNode(vdom.text);
}
};
function setAttribute(dom, prop, value) {
switch(prop){
case 'class':
if(value instanceof Array){
dom.setAttribute('class', [].concat(value).join(' '));
}
break;
case 'style':
if(typeof value == 'object'){
Object.assign(dom.style, value);
}
break;
case 'on':
if(typeof value == 'object'){
for(let event in value){
dom.addEventListener(event.toLowerCase(), value[event]);
}
}
break;
default:
dom.setAttribute(prop, value);
}
}
然后,处理节点的层级关系:如果元素节点有子节点则递归处理,并把创建好的节点挂载到父节点,构成完整的DOM树。
function createElement(vdom, parent){
if (vdom?.tag) {
const dom = document.createElement(vdom.tag);
for (const prop in vdom.props) {
setAttribute(dom, prop, vdom.props[prop]);
}
for (const child of vdom.children) {
render(child,dom);
}
return mount(parent,dom);
}else if(vdom?.text){
return mount(parent,document.createTextNode(vdom));
}
};
function mount(parent, elm){
return parent ? parent.appendChild(elm) : elm;
}
到这里,我们就完成了一个vdom渲染的过程。
测试代码
<body>
<div id="root"></div>
</body>
<script>
document.addEventListener("DOMContentLoaded", (event) => {
render(ulVdom, document.getElementById("root"));
}
</script>
小结
vdom是一个用来描述页面的js对象。根据vdom描述的元素信息来创建真实dom的过程,就是vdom的渲染。首先,根据不同的节点类型,创建元素节点或文本节点,并设置元素节点的属性。然后,处理节点的层级关系:递归创建子节点,并把创建好的节点挂载到父节点,构成完整的DOM树。
渲染函数
上面我们已经实现了vdom的渲染,只需要写vdom js对象就可以开发页面。但是,此时渲染出来的是静态页面,因为vdom是在写代码时(编译时)就确定的。为了能够实现条件渲染、循环渲染和从上下文取值等动态渲染,把vdom的创建过程封装成函数,这就是渲染函数。
当render function被调用时,可以执行一些动态逻辑,包括条件判断、循环处理和从上下文取值等,动态创建vdom,然后再渲染vdom,从而实现动态渲染。
例如下面这段代码,对父组件传入的列表list进行循环渲染,并根据disable标识进行条件渲染,最后返回h函数创建的vdom对象,渲染vdom。
注: 下文render()入参h就是createElement()。
Vue.component("smart-list", {
props: {
list: Array,
},
render: function (h) {
let li = [];
for (let item of this.list) {
!item.disable && li.push(h("li", item.value));
}
return h("ul", li);
},
});
template
虽然render function已经实现了vdom动态渲染,但在render function中需要手动调用createElement函数来创建vdom,代码不够简洁。所以框架支持更友好的DSL来声明式描述vdom渲染,包括template和jsx。
上面的smart-list用template实现如下:
Vue.component("smart-list-template", {
props: {
list: Array,
},
template:
"<ul>" +
'<li v-for="(item,index) in list" v-if="!item.disable" :key=index>' +
"{{item.value}}" +
"</li>" +
"</ul>",
});
jsx
template语法是基于html的,代码更加简洁,同时支持v-for、v-if等指令来实现动态渲染。开发中常用的是template语法,如果想兼具js的灵活性和html的直观性,可以选择jsx。jsx是js的语法扩展,支持在js中使用xml语法。
上面的smart-list用jsx实现如下:
Vue.component("smart-list-jsx", {
props: {
list: Array,
},
render: function (h) {
let li = [];
for (let item of this.list) {
!item.disable && li.push(<li>{item.value}</li>);
}
let jsx = <ul>{li}</ul>;
return jsx;
},
});
总结
根据vdom描述的元素信息,调用document api创建dom,然后递归处理子节点并挂载到父节点,构成完整的dom树,这就是vdom的渲染。
在此基础上,把vdom的创建过程封装成渲染函数,实现了动态渲染:例如条件渲染、循环渲染和从上下文取值等等。
为了提高开发效率,框架提供了DSL支持,包括template和jsx。在vue中常用的是template语法,代码简洁直观。js则兼具了template的直观和js的灵活性。template和jsx最终都是被编译成render function。