详解Vue如何手写虚拟dom并进行渲染
作者:不要止步于此
这篇文章主要为大家详细介绍了渲染器的工作原理,以及如何将真实dom或者组件用虚拟dom的形式进行描述并渲染,感兴趣的小伙伴可以跟随小编一起学习一下
虚拟dom如何渲染为真实dom
虚拟dom转换为真实dom其实就是编写一个渲染函数,将虚拟dom逐个创建为真实的dom元素并将对应事件添加到当前创建的元素上,再挂载到页面的指定元素下。
认识虚拟dom
虚拟dom其实就是用来描述真实dom的javascript对象。
来看下面例子,把一个真实dom用虚拟dom来进行描述:
真实dom
<div onclick="onAlert()"><span>点击我</span><span></span></div> <script> function onAlert() { alert('点击事件回调函数') } </script>
转换为虚拟dom
const vnode = { tag: 'div', props: { onClick: () => alert('点击事件回调函数') }, children: [ { tag: 'span', children: '点击我' }, { tag: 'span', } ] }
渲染器实现
/** * @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传入函数执行看下效果
// 把虚拟dom渲染到id为app的元素下 renderer(vnode, document.getElementById('app'))
下图可看到虚拟dom已经成功渲染为真实dom,并且点击事件也成功触发了!
虚拟dom描述组件
以上讲了如何使用虚拟dom(vnode
)描述真实dom,但还不够!如果我们封装了一个组件
,又该如何使用虚拟dom进行描述呢?
总的来说,组件就是一组dom元素的封装,这组dom元素就是组件要渲染的内容,比如前面例子的vnode
对象就可以认为是一个组件。
方法组件
const MyComponent = function () { return { tag: 'div', props: { onClick: () => alert('MyComponent点击事件回调函数') }, children: [ { tag: 'span', children: 'MyComponent' }, { tag: 'span', } ] } }
对象组件
const MyComponent2 = { render() { return { tag: 'div', props: { onClick: () => alert('MyComponent2点击事件回调函数') }, children: [ { tag: 'span', children: 'MyComponent2' }, { tag: 'span', } ] } } }
修改渲染器支持组件渲染
/** * @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) }
渲染组件
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对象的变更点且只更新变更的内容,而不是重新走一遍创建节点的流程。
以上就是详解Vue如何手写虚拟dom并进行渲染的详细内容,更多关于Vue虚拟dom的资料请关注脚本之家其它相关文章!