提到前端框架,那就不得不提到两个重要的概念:渲染器。本文将Vue为例,简单介绍下渲染器的作用。
在了解这两个是什么之前,我们要先理解一个概念,那就是......
虚拟DOM
所谓虚拟DOM就是用JS对象来描述真实DOM的一种数据结构。举个例子
js
const title = {
// 标签名字
tag: "h1",
// 标签属性
props: {
onClick: handler
},
// 子列表
children: [
{ tag: "span" }
]
}
对应到模板中就是
js
<h1 @click="handler"><span></span></h1>
如果还有子节点,那么需要编写的js对象就更长了。那么,虚拟DOM是如何变成真实DOM并渲染到页面中的呢?
渲染器
渲染器的作用,就是将这种JS对象转换成真实DOM。我们平时开发Vue组件都是依赖渲染器工作的。
假设我们有如下虚拟DOM
js
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello'),
style: 'width:100px;height:100px;font-size:24px;'
},
children: 'click'
}
为了将vnode
正确转换成真实DOM,我们需要编写一个renderer函数
js
function renderer(vnode, container) {
// 使用tag创建DOM元素
const el = document.createElement(vnode.tag)
// 遍历props,将属性和时间挂载到DOM元素上
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果是以on开头,代表是事件,这里要进行截取,将onCLick转成click
el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
} else {
// 其余的认定为属性
el.setAttribute(key, vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
// 如果children是字符串,说明它是文本内容
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归调用renderder方法,循环渲染子节点,挂载到生成的DOM元素上
vnode.children.forEach(child => renderer(child, el))
}
// 将渲染出来的节点挂载到指定节点
container.appendChild(el)
}
这里的renderer有两个函数
- vnode:虚拟DOM对象
- container:一个真实DOM节点,作为挂载对象传入,渲染器会把渲染好的节点挂载到这里
接下来只要调用renderer函数就可以了
js
renderer(vnode, document.body)
就可以在浏览器中看到真实节点了
回过头分析一下渲染器的流程,分三个步骤
- 根据tag创建DOM元素
- 将props上的方法和属性挂载到DOM元素上
- 循环递归处理children,将处理好的child挂载到DOM元素上
当然,这只是简易的示意代码,在Vue中则更为复杂,比如目前我们每次执行renderer都是全量渲染,但如果我们只将children的值'click'改成'click again'呢?这个时候需要渲染器进行更精确的渲染,也就是我们比较熟悉的diff流程。当然更详细的不在这里多讲,后续会专门开一篇去讲,这里只是给大家讲解一下概念。
现在,我们还有个问题,那就是我们平时开发Vue组件的时候基本是很少见到虚拟DOM这种结构的,大多数时候都是组件的形态,那么组件是什么?组件和渲染器之间的关系又是什么呢?
组件的本质
其实组件就是一组DOM元素的封装。一个组件可以是对象,也可以是对象,最终输出的都是虚拟DOM,举个例子
js
const myComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello'),
style: 'width:100px;height:100px;font-size:24px;'
},
children: 'click'
}
}
可以看到,最终返回的还是虚拟DOM。实际上,对于虚拟DOM来讲,组件也可以是一个tag,比如
js
const vnode = {
tag: myComponent
}
同理,组件引用的方式也可以变换成
js
const vnode = {
tag: 'div',
children: [myComponent]
}
那么为了支持这种组件tag,我们需要对renderer进行一定程度的改造,改造起来也非常简单,让其支持函数就可以了
js
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
// 说明是正常的标签
mountElement(vnoode, container)
} else if (typeof vnode.tag === 'function') {
// 说明是组件
mountComponent(vnode, container)
}
}
正常的标签还是走原来的逻辑就可以了
js
function mountElement(vnode, container) {
// 使用tag创建DOM元素
const el = document.createElement(vnode.tag)
// 遍历props,将属性和时间挂载到DOM元素上
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果是以on开头,代表是事件,这里要进行截取,将onCLick转成click
el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
} else {
// 其余的认定为属性
el.setAttribute(key, vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
// 如果children是字符串,说明它是文本内容
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归调用renderder方法,循环渲染子节点,挂载到生成的DOM元素上
vnode.children.forEach(child => renderer(child, el))
}
// 将渲染出来的节点挂载到指定节点
container.appendChild(el)
}
组件也非常简单
js
function mountComponent(vnode, container) {
// 执行函数,获取虚拟DOM
cosnt subtree = vnode.tag()
// 重新走一遍渲染即可
renderer(suvtree, container)
}
上述代码只演示了函数形态,如果是对象则需要简单变通一下即可
js
const myComponent = {
render: function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello'),
style: 'width:100px;height:100px;font-size:24px;'
},
children: 'click'
}
}
}
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
// 说明是正常的标签
mountElement(vnoode, container)
} else if (typeof vnode.tag === 'object') {
// 说明是组件
mountComponent(vnode, container)
}
}
function mountComponent(vnode, container) {
// 执行函数,获取虚拟DOM
cosnt subtree = vnode.tag.render()
// 重新走一遍渲染即可
renderer(suvtree, container)
}
有这么几处改动
- 将组件形态变更为对象形态,内部包含一个render函数。
- 判断条件由function改为object
- 不再直接执行tag,而是执行tag.render
如此一来,我们就可以正确的渲染一个组件了
总结
本文介绍了何为虚拟DOM,渲染器的基本概念,以及组件是什么。虚拟DOM本质上是描述真实DOM的一种JS对象结构,组件本身也是一种描述真实DOM的结构,解析组件之后最终返回的是虚拟DOM。而渲染器是一种将虚拟DOM转为真实DOM的工具。