詳解Vue如何手寫(xiě)虛擬dom并進(jìn)行渲染
虛擬dom如何渲染為真實(shí)dom

虛擬dom轉(zhuǎn)換為真實(shí)dom其實(shí)就是編寫(xiě)一個(gè)渲染函數(shù),將虛擬dom逐個(gè)創(chuàng)建為真實(shí)的dom元素并將對(duì)應(yīng)事件添加到當(dāng)前創(chuàng)建的元素上,再掛載到頁(yè)面的指定元素下。
認(rèn)識(shí)虛擬dom
虛擬dom其實(shí)就是用來(lái)描述真實(shí)dom的javascript對(duì)象。
來(lái)看下面例子,把一個(gè)真實(shí)dom用虛擬dom來(lái)進(jìn)行描述:
真實(shí)dom
<div onclick="onAlert()"><span>點(diǎn)擊我</span><span></span></div>
<script>
function onAlert() {
alert('點(diǎn)擊事件回調(diào)函數(shù)')
}
</script>
轉(zhuǎn)換為虛擬dom
const vnode = {
tag: 'div',
props: {
onClick: () => alert('點(diǎn)擊事件回調(diào)函數(shù)')
},
children: [
{
tag: 'span',
children: '點(diǎn)擊我'
},
{
tag: 'span',
}
]
}
渲染器實(shí)現(xiàn)
/**
* @param {object} vnode 虛擬dom對(duì)象
* @param {HTMLElement} container 掛載虛擬dom的真實(shí)dom容器
*/
function renderer(vnode, container) {
const { tag, props, children } = vnode
const el = document.createElement(tag)
for(const key in props) {
if(/^on/.test(key)) {
// 轉(zhuǎn)換為合法的監(jiān)聽(tīng)事件名稱
const eventNmae = key.substring(2).toLowerCase()
// 在當(dāng)前創(chuàng)建的el元素上掛載監(jiān)聽(tīng)事件
el.addEventListener(eventNmae, props[key])
}
}
if(typeof children === 'string') {
// 創(chuàng)建一個(gè)文本節(jié)點(diǎn)添加到el元素下
el.appendChild(document.createTextNode(children))
} else if(Array.isArray(children)) {
// 子節(jié)點(diǎn)為數(shù)組,遞歸調(diào)用renderer函數(shù)
children.forEach(vnode => renderer(vnode, el))
}
console.log(container)
// 將元素掛載到容器上
container.appendChild(el)
}
現(xiàn)在將上面轉(zhuǎn)換的虛擬dom傳入函數(shù)執(zhí)行看下效果
// 把虛擬dom渲染到id為app的元素下
renderer(vnode, document.getElementById('app'))
下圖可看到虛擬dom已經(jīng)成功渲染為真實(shí)dom,并且點(diǎn)擊事件也成功觸發(fā)了!

虛擬dom描述組件
以上講了如何使用虛擬dom(vnode)描述真實(shí)dom,但還不夠!如果我們封裝了一個(gè)組件,又該如何使用虛擬dom進(jìn)行描述呢?

總的來(lái)說(shuō),組件就是一組dom元素的封裝,這組dom元素就是組件要渲染的內(nèi)容,比如前面例子的vnode對(duì)象就可以認(rèn)為是一個(gè)組件。
方法組件
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent點(diǎn)擊事件回調(diào)函數(shù)')
},
children: [
{
tag: 'span',
children: 'MyComponent'
},
{
tag: 'span',
}
]
}
}
對(duì)象組件
const MyComponent2 = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent2點(diǎn)擊事件回調(diào)函數(shù)')
},
children: [
{
tag: 'span',
children: 'MyComponent2'
},
{
tag: 'span',
}
]
}
}
}
修改渲染器支持組件渲染
/**
* @param {object} vnode 虛擬dom對(duì)象
* @param {HTMLElement} container 掛載虛擬dom的真實(shí)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)) {
// 轉(zhuǎn)換為合法的監(jiān)聽(tīng)事件名稱
const eventNmae = key.substring(2).toLowerCase()
// 在當(dāng)前創(chuàng)建的el元素上掛載監(jiān)聽(tīng)事件
el.addEventListener(eventNmae, props[key])
}
}
if(typeof children === 'string') {
// 創(chuàng)建一個(gè)文本節(jié)點(diǎn)添加到el元素下
el.appendChild(document.createTextNode(children))
} else if(Array.isArray(children)) {
// 子節(jié)點(diǎn)為數(shù)組,遞歸調(diào)用renderer函數(shù)
children.forEach(vnode => renderer(vnode, el))
}
console.log(container)
// 將元素掛載到容器上
container.appendChild(el)
}
function mountComponent(vnode, container) {
// 遞歸調(diào)用renderer
renderer(vnode, container)
}
渲染組件
const vnode = {
tag: 'div',
children: [
{
tag: 'span',
props: {
onClick: () => alert('span點(diǎn)擊事件回調(diào)函數(shù)')
},
children: '我是span標(biāo)簽'
},
// 組件
{
tag: MyComponent,
},
// 組件
{
tag: MyComponent2,
}
]
}
// 把虛擬dom渲染到id為app的元素下
renderer(vnode, document.getElementById('app'))
下圖可看到,對(duì)應(yīng)的組件及事件都已經(jīng)掛載成功!

總結(jié)
最后,是不是覺(jué)得渲染器其實(shí)也沒(méi)有想象中那么難!其實(shí)這只是一個(gè)創(chuàng)建節(jié)點(diǎn)的渲染器,但其精髓在于更新節(jié)點(diǎn)。假設(shè)我們對(duì)虛擬dom做了一些小修改,渲染器需要精確找到vnode對(duì)象的變更點(diǎn)且只更新變更的內(nèi)容,而不是重新走一遍創(chuàng)建節(jié)點(diǎn)的流程。
以上就是詳解Vue如何手寫(xiě)虛擬dom并進(jìn)行渲染的詳細(xì)內(nèi)容,更多關(guān)于Vue虛擬dom的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue自定義v-has指令,做按鈕權(quán)限判斷的步驟
這篇文章主要介紹了Vue自定義v-has指令,做按鈕權(quán)限判斷的步驟,幫助大家更好的理解和學(xué)習(xí)使用vue框架,感興趣的朋友可以了解下2021-04-04
詳解vue-video-player使用心得(兼容m3u8)
這篇文章主要介紹了詳解vue-video-player使用心得(兼容m3u8),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
vue-cli開(kāi)發(fā)時(shí),關(guān)于ajax跨域的解決方法(推薦)
下面小編就為大家分享一篇vue-cli開(kāi)發(fā)時(shí),關(guān)于ajax跨域的解決方法(推薦),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
vue在取對(duì)象長(zhǎng)度length時(shí)候出現(xiàn)undefined的解決
這篇文章主要介紹了vue在取對(duì)象長(zhǎng)度length時(shí)候出現(xiàn)undefined的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
解決vue項(xiàng)目刷新后,導(dǎo)航菜單高亮顯示的位置不對(duì)問(wèn)題
今天小編就為大家分享一篇解決vue項(xiàng)目刷新后,導(dǎo)航菜單高亮顯示的位置不對(duì)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11

