前言
大家好,今天新开了个vue3设计与实现 专栏
,希望能够把自己在这本书中阅读到的知识结合自己的见解,一点点分享给大家,希望自己在进步的同时,也能够和大家共同进步!不积跬步无以至千里,不积小流无以成江海
。本文旨在了解渲染器的工作原理
,以及如何将真实dom
或者组件
用虚拟dom
的形式进行描述并渲染。
关键词: 渲染器、虚拟dom、真实dom
虚拟dom如何渲染为真实dom
虚拟dom
转换为真实dom
其实就是编写一个渲染函数
,将虚拟dom逐个创建为真实的dom元素并将对应事件添加到当前创建的元素上,再挂载
到页面的指定元素下。
认识虚拟dom
虚拟dom
其实就是用来描述真实dom
的javascript对象
。
来看下面例子,把一个真实dom用虚拟dom来进行描述:
真实dom
html
<div onclick="onAlert()"><span>点击我</span><span></span></div>
<script>
function onAlert() {
alert('点击事件回调函数')
}
</script>
转换为虚拟dom
js
const vnode = {
tag: 'div',
props: {
onClick: () => alert('点击事件回调函数')
},
children: [
{
tag: 'span',
children: '点击我'
},
{
tag: 'span',
}
]
}
渲染器实现
js
/**
* @param {object} vnode 虚拟dom对象
* @param {HTMLElement} container 挂载虚拟dom的真实dom容器
*/
function renderer(vnode, container) {
const { tag, props, children } = vnode
const el = document.createElement(tag)
for(const key in props) {
if(/^on/.test(key)) {
// 转换为合法的监听事件名称
const eventNmae = key.substring(2).toLowerCase()
// 在当前创建的el元素上挂载监听事件
el.addEventListener(eventNmae, props[key])
}
}
if(typeof children === 'string') {
// 创建一个文本节点添加到el元素下
el.appendChild(document.createTextNode(children))
} else if(Array.isArray(children)) {
// 子节点为数组,递归调用renderer函数
children.forEach(vnode => renderer(vnode, el))
}
console.log(container)
// 将元素挂载到容器上
container.appendChild(el)
}
现在将上面转换的虚拟dom传入函数执行看下效果
js
// 把虚拟dom渲染到id为app的元素下
renderer(vnode, document.getElementById('app'))
下图可看到虚拟dom已经成功渲染为真实dom,并且点击事件也成功触发了!
虚拟dom描述组件
以上讲了如何使用虚拟dom(vnode
)描述真实dom,但还不够!如果我们封装了一个组件
,又该如何使用虚拟dom进行描述呢?
总的来说,组件就是一组dom元素的封装 ,这组dom元素就是组件要渲染的内容,比如前面例子的vnode
对象就可以认为是一个组件。
方法组件
js
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent点击事件回调函数')
},
children: [
{
tag: 'span',
children: 'MyComponent'
},
{
tag: 'span',
}
]
}
}
对象组件
js
const MyComponent2 = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent2点击事件回调函数')
},
children: [
{
tag: 'span',
children: 'MyComponent2'
},
{
tag: 'span',
}
]
}
}
}
修改渲染器支持组件渲染
js
/**
* @param {object} vnode 虚拟dom对象
* @param {HTMLElement} container 挂载虚拟dom的真实dom容器
*/
function renderer(vnode, container) {
const { tag } = vnode
if(typeof tag === 'string') {
mountElement(vnode, container)
} else if(typeof tag === 'function') {
mountComponent(tag(), container)
} else if(typeof tag === 'object') {
mountComponent(tag.render(), container)
}
}
function mountElement(vnode, container) {
const { tag, props, children } = vnode
const el = document.createElement(tag)
for(const key in props) {
if(/^on/.test(key)) {
// 转换为合法的监听事件名称
const eventNmae = key.substring(2).toLowerCase()
// 在当前创建的el元素上挂载监听事件
el.addEventListener(eventNmae, props[key])
}
}
if(typeof children === 'string') {
// 创建一个文本节点添加到el元素下
el.appendChild(document.createTextNode(children))
} else if(Array.isArray(children)) {
// 子节点为数组,递归调用renderer函数
children.forEach(vnode => renderer(vnode, el))
}
console.log(container)
// 将元素挂载到容器上
container.appendChild(el)
}
function mountComponent(vnode, container) {
// 递归调用renderer
renderer(vnode, container)
}
渲染组件
js
const vnode = {
tag: 'div',
children: [
{
tag: 'span',
props: {
onClick: () => alert('span点击事件回调函数')
},
children: '我是span标签'
},
// 组件
{
tag: MyComponent,
},
// 组件
{
tag: MyComponent2,
}
]
}
// 把虚拟dom渲染到id为app的元素下
renderer(vnode, document.getElementById('app'))
下图可看到,对应的组件及事件都已经挂载成功!
总结
最后,是不是觉得渲染器其实也没有想象中那么难!其实这只是一个创建节点的渲染器,但其精髓
在于更新节点。假设我们对虚拟dom做了一些小修改
,渲染器需要精确找到vnode对象的变更点且只更新变更的内容
,而不是重新走一遍创建节点的流程。更新虚拟dom部分后面有时间再进行单独写一篇文章进行讲解,如果对你有帮助可以先加一波关注哦~
。