mini-vue渲染的簡(jiǎn)易實(shí)現(xiàn)
前言
目前的主流框架Vue、React 都是通過(guò) Virtual Dom(虛擬Dom)來(lái)實(shí)現(xiàn)的,通過(guò)Virtual Dom技術(shù)提高頁(yè)面的渲染效率。Vue中我們通過(guò)在 template 模板中編寫html代碼,React中我們通過(guò)在內(nèi)部的一個(gè) render 函數(shù)里編寫html代碼,這個(gè)函數(shù)通過(guò) jsx 編譯后,實(shí)際會(huì)輸出一個(gè)h函數(shù),也就是我們的 Virtual Dom(虛擬Dom),下面簡(jiǎn)單來(lái)實(shí)現(xiàn)一個(gè)虛擬dom渲染真實(shí)dom,以及更新的方法。
目標(biāo)
主要實(shí)現(xiàn)以下三個(gè)功能:
- 通過(guò)h函數(shù)返回Vnodes;
- 通過(guò) mount 函數(shù)將 虛擬dom 掛載到真實(shí)節(jié)點(diǎn)上;
- 通過(guò) patch 函數(shù)通過(guò) newVnodes 與 oldVnodes比較來(lái)實(shí)現(xiàn)dom的更新;
第一步:
在body標(biāo)簽內(nèi)創(chuàng)建一個(gè)id為app的節(jié)點(diǎn),后面會(huì)將虛擬節(jié)點(diǎn)掛載到這個(gè)節(jié)點(diǎn)上,而renderer.js用來(lái)實(shí)現(xiàn)上面三個(gè)功能。
<body> <div id="app"></div> <script src="./renderer.js"></script> </body>
第二步:
編寫h函數(shù),用來(lái)返回tag(標(biāo)簽元素)、props(屬性對(duì)象)、children(子節(jié)點(diǎn)),簡(jiǎn)單來(lái)說(shuō)虛擬Dom就是一個(gè)普通javaScript對(duì)象。
// renderer.js
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
那么通過(guò)這個(gè)h函數(shù),我們簡(jiǎn)單來(lái)看看下面一段代碼會(huì)輸出什么?
const vdom = h("div", {class: "header"}, [
h("h2", null, "Hello World"),
h("h2", {id: 0}, h("span", null, "啦啦啦啦")) // 當(dāng)props沒(méi)有值,必須傳一個(gè)null,不能不傳
]);
console.log(vdom);
由下圖可以看出,通過(guò)h函數(shù),給我們返回了一個(gè)javaScript對(duì)象,這個(gè)便是 Virtual Dom(虛擬Dom),形成樹狀節(jié)點(diǎn)。

那么我們拿到這個(gè)vnodes,如何掛載到真實(shí)節(jié)點(diǎn)上呢?下面我們來(lái)看看第三步
第三步:
我們先創(chuàng)建一個(gè) mount 函數(shù),需要傳入兩個(gè)參數(shù),第一個(gè)是我們剛剛通過(guò)h函數(shù)返回的vnodes,第二個(gè)參數(shù)是我們需要將這些vnode掛載到哪個(gè)節(jié)點(diǎn)上,接下來(lái)看代碼:
const mount = (vnodes, app) => {
// 通過(guò)vnodes里面的tag值,比如("div", "h2"),創(chuàng)建一個(gè)節(jié)點(diǎn)
// 同樣在vnodes對(duì)象里保存一份真實(shí)dom,方便以后進(jìn)行更新,新增等操作
const el = vnodes.el = document.createElement(vnodes.tag);
// 拿到這個(gè)節(jié)點(diǎn)后,我們通過(guò)判斷props值,進(jìn)行添加屬性
if (vnodes.props) {
for (let key in vnodes.props) {
// 這兒通過(guò)拿到props中key值后,在做判斷
let value = vnodes.props[key];
if (key.startsWith('on')) {
// 比如用戶寫了一個(gè)onClick="changeData",處理為監(jiān)聽函數(shù)的事件
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
// 這下面還有些判斷,比如指令啊v-if等等,進(jìn)行邊界化處理
}
}
// 處理完props后,最后是Children節(jié)點(diǎn)
if (vnodes.children) {
if (typeof vnodes.children === 'string') {
// 如果這兒是個(gè)字符串類型,那么就可以直接添加到節(jié)點(diǎn)中去
el.textContent = vnodes.children
} else {
// 這種情況為數(shù)組類型,含有子節(jié)點(diǎn),通過(guò)遍歷,再生成子節(jié)點(diǎn)
vnodes.children.forEach(vnode => {
// 通過(guò)遞歸,再次將子節(jié)點(diǎn)掛載到當(dāng)前節(jié)點(diǎn)上去
mount(vnode, el)
})
}
}
// 最終將這個(gè)真實(shí)節(jié)點(diǎn)掛載到我們傳入的app節(jié)點(diǎn)中去
app.appendChild(el);
}
const app = document.querySelector("#app")
mount(vdom, app)
我們來(lái)看看通過(guò)mount函數(shù)掛載的實(shí)際效果:

那么到這里,我們就已經(jīng)實(shí)現(xiàn)了通過(guò)虛擬dom來(lái)創(chuàng)建真實(shí)dom了,那在vue當(dāng)中是如何對(duì)這些dom進(jìn)行更新操作呢,接下來(lái)我們?cè)賱?chuàng)建一個(gè) patch函數(shù)(更新):
第四步:
通過(guò)patch函數(shù),我們需要傳入兩個(gè)參數(shù)(vnodes1、vnodes2),分別是新的虛擬dom和舊的虛擬dom,通過(guò)比較新舊虛擬dom,來(lái)指定更新哪些節(jié)點(diǎn)。(這兒不考慮key值,需要參考key值可以查看鏈接:http://www.dbjr.com.cn/article/219078.htm)
// patch函數(shù) n1: 舊節(jié)點(diǎn)、 n2:新節(jié)點(diǎn)
// 在vue源碼中,舊vnode,新vnode分別用n1, n2表示
const patch = (n1, n2) => {
// 在上面我們通過(guò)mount函數(shù)給n2添加了節(jié)點(diǎn)屬性el,綁定到n2上
const el = n2.el = n1.el
// 首先,還是從兩個(gè)中的tag入手
if (n1.tag == n2.tag) {
// n1、n2的tag相同,再對(duì)比props
const n1Props = n1.props || {};
const n2Props = n2.props || {};
// 分別取到n1,n2中的props,進(jìn)行比較
for (let key in n2Props) {
// 取出n2中所有key,判斷n2的key值和n1key值是否相同
const n1Value = n1Props[key] || '';
const n2Value = n2Props[key] || '';
if (n1Value !== n2Value) {
if (key.startsWith('on')) {
// 比如用戶寫了一個(gè)onClick="changeData",處理為監(jiān)聽函數(shù)的事件
el.addEventListener(key.slice(2).toLowerCase(), n2Value)
} else {
el.setAttribute(key, n2Value)
}
}
// 相同則不作處理
}
for (let key in n1Props) {
const oldValue = n1Props[key];
if (!(key in n2Props)) {
if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
} else {
el.removeAttribute(key)
}
}
}
} else {
// tag不同,拿到n1的父節(jié)點(diǎn)
const n1Parent = n1.el.parentElement;
// 通過(guò)removeChild將舊節(jié)點(diǎn)從父節(jié)點(diǎn)中移除,然后將n2掛載到父節(jié)點(diǎn)中
n1Parent.removeChild(n1.el); //n1.el是通過(guò)mount函數(shù)往對(duì)象里添加的真實(shí)dom節(jié)點(diǎn)
mount(n2, n1Parent)
}
// 最后處理children,相對(duì)于來(lái)說(shuō)復(fù)雜些
// children可以為字符串也可以為數(shù)組,那么先看字符串時(shí)怎么處理
const n1Children = n1.children || [];
const n2Children = n2.children || [];
if (typeof n2Children === "string") {
// 如果新節(jié)點(diǎn)內(nèi)容為字符串,直接使用innerhtml進(jìn)行替換
el.innerHtml = n2Children;
} else {
// 下面情況是n2.children為數(shù)組情況時(shí)
if (typeof n1.children === "string") {
// n1.children為字符串,n2.children為數(shù)組
el.innerHtml = ''; // 先將節(jié)點(diǎn)內(nèi)容情況,再講新的內(nèi)容添加進(jìn)去
mount(n2.children, el)
} else {
// 兩種都為數(shù)組類型時(shí),這兒不考慮key值
const minLength = Math.min(n1Children.length, n2Children.length);
for (let i = 0 ; i < minLength ; i++) {
patch(n1Children[i], n2Children[i]);
}
if(n2Children.length > n1Children.length) {
n2Children.slice(minLength).forEach(item => {
mount(item, el)
})
}
if(n2Children.length < n1Children.length) {
n1Children.slice(minLength).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
上面簡(jiǎn)單的實(shí)現(xiàn)了patch的作用,其實(shí)就是我們說(shuō)的diff算法(當(dāng)然這兒沒(méi)有考慮key值的情況,只能兩個(gè)依次比較),同一層級(jí)進(jìn)行比較?,F(xiàn)在模擬演示一下,看看是否能更新成功:
const vdom = h("div", {class: "header"}, [
h("h2", null, "Hello World"),
h("h2", {id: 0}, [h("span", null, "啦啦啦啦")]) // 當(dāng)props沒(méi)有值,必須傳一個(gè)null,不能不傳
]);
const app = document.querySelector("#app")
mount(vdom, app)
setTimeout(()=> { // 3秒后向patch傳入新舊Vnodes
const vdom1 = h("div", {class: "header"}, [
h("h3", null, "Hello World"),
h("span", null, "哈哈哈")
])
patch(vdom, vdom1)
},3000)
通過(guò)下圖,我們可以看到已經(jīng)簡(jiǎn)單的實(shí)現(xiàn)了虛擬dom更新節(jié)點(diǎn)。

總結(jié)
簡(jiǎn)單的實(shí)現(xiàn)了下虛擬Dom生成真實(shí)節(jié)點(diǎn),然后通過(guò)patch進(jìn)行更新。再去看看源碼,就能更好的理解vue的渲染器是如何實(shí)現(xiàn)的了。
到此這篇關(guān)于mini-vue渲染的簡(jiǎn)易實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)mini-vue渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue頁(yè)面跳轉(zhuǎn)動(dòng)畫效果的實(shí)現(xiàn)方法
百度了好久都沒(méi)辦法實(shí)現(xiàn)vue中一個(gè)頁(yè)面跳到另一個(gè)頁(yè)面,甚至到google上搜索也是沒(méi)辦法的,最終還是找了高人親自指導(dǎo),所以下面這篇文章主要給大家介紹了關(guān)于Vue頁(yè)面跳轉(zhuǎn)動(dòng)畫效果的實(shí)現(xiàn)方法,需要的朋友可以參考下2018-09-09
vue-router之實(shí)現(xiàn)導(dǎo)航切換過(guò)渡動(dòng)畫效果
今天小編就為大家分享一篇vue-router之實(shí)現(xiàn)導(dǎo)航切換過(guò)渡動(dòng)畫效果,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10
Vue3使用hook封裝媒體查詢和事件監(jiān)聽的代碼示例
這篇文章主要給大家詳細(xì)介紹Vue3如何使用hook封裝媒體查詢和事件監(jiān)聽,使得Vue的開發(fā)更加絲滑,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),感興趣的同學(xué)跟著小編一起來(lái)學(xué)習(xí)吧2023-07-07
vue3 elementPlus 表格實(shí)現(xiàn)行列拖拽及列檢索功能(完整代碼)
本文通過(guò)實(shí)例代碼給大家介紹vue3 elementPlus 表格實(shí)現(xiàn)行列拖拽及列檢索功能,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-10-10
關(guān)于el-table表格組件中插槽scope.row的使用方式
這篇文章主要介紹了關(guān)于el-table表格組件中插槽scope.row的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
vue的異步數(shù)據(jù)更新機(jī)制與$nextTick用法解讀
這篇文章主要介紹了vue的異步數(shù)據(jù)更新機(jī)制與$nextTick用法解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03

