上一篇描述了什麼是虛擬DOM。
在React和Vue中,虛擬DOM的創建都是由模板或者JSX來完成的。但是由模板變成render或者JSX完成虛擬DOM的創建都是由webpack的loader來完成。
我們現在就用原生的方法去完成虛擬DOM是如何去新建和渲染的。
如何新建
假設我們要生成下面這樣一個虛擬DOM
<div id="test">
<p>節點1</p>
</div>
1.我們新建一個"vdom.js"文件,新建createElement函數,這個函數就是用來創建虛擬DOM。
思路:
1、DOM一般由三部分組成:1.標籤 2.標籤屬性 3.子節點
2、我們創建一個函數,傳入三個參數:tag,data,children
3.我們判斷tag是什麼類型的,並記錄,可分爲HTML,COMPONENT,TEXT等。我們這次只說HTML和TEXT
4.我們判斷children是什麼類型的,並記錄,可分爲EMPTY(無),SINGLE(單個),MULTIPE(多個)
5.如果children爲文本,我們創建children爲文本標籤
6.以對象的形式返回這些數據
代碼:
//虛擬DOM的類型
const vnodeType = {
HTML: 'HTML',
TEXT: 'TEXT',
COMPONENT: 'COMPONENT'
}
//子節點的類型
const childType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTIPLE: 'MULTIPLE'
}
// 創建虛擬DOM
// 三個參數 tag(標籤名),data:屬性值,children: 子節點,默認是null
function createElement(tag, data, children=null) {
// 記錄vnode的類型
let flag
// 如果是string,如'div',我們就認爲是普通節點HTML
if (typeof tag === 'string') {
flag = vnodeType.HTML
} else if (typeof tag === 'function') {
// 如果是function,我們就認爲是組件
flag = vnodeType.COMPONENT
} else {
// 其他的默認是文本類型
flag = vnodeType.TEXT
}
// 記錄字節點類型
let childrenFlag
// 如果爲空,說明沒有字節點
if (children === null) {
childrenFlag = childType.EMPTY
} else if (Array.isArray(children)) {
// 如果它是一個數組,根據長度判斷,如果爲0,認爲沒有子節點,否則認爲有多個節點
const lenght = children.length
if (lenght === 0) {
childrenFlag = childType.EMPTY
} else {
childrenFlag = childType.MULTIPLE
}
} else {
// 其他情況,都默認是文本
childrenFlag = childType.SINGLE
children = createTextVnode(children + '')
}
// 返回虛擬DOM
return {
flag, //vnode的類型
tag, // 標籤,div ,文本沒有tag,組件就是一個函數
data, // 屬性
children, // 子節的
childrenFlag,// 子節點類型
el: null
}
}
//新建文本類型的vnode
function createTextVnode (text) {
//文本節點的tag 爲null,且它沒有子節點
return {
flag: vnodeType.TEXT,
tag: null,
data: null,
children: text,
childrenFlag: childType.EMPTY
}
}
2.我們在"index.html"中調用以上函數
代碼:
<body>
<!-- <div id="test">
<p>節點1</p>
</div> -->
<script src="./vdom.js"></script>
<script>
let div = createElement('div', {id: 'test'}, [
createElement('p', {}, '節點1')
])
console.log(JSON.stringify(div, null, 2))
</script>
</body>
3、我們控制檯打印出來
結果以對象的形式展示出來了,最終完成了虛擬DOM的新建。
如何渲染
1.渲染分爲首次渲染和非首次渲染
我們將上述虛擬節點變得複雜一些。我們新增了多個子節點,每個子節點含有key屬性及其他屬性。
let vnode = createElement('div', {id: 'test'}, [
createElement('p', {key: 'a', style: {color: 'blue'}}, '節點1'),
createElement('p', {key: 'b', '@click': () => {alert('節點2')}}, '節點2'),
createElement('p', {key: 'c', 'class':'item-header' }, '節點3'),
createElement('p', {key: 'd'}, '節點4'),
])
// 執行渲染函數
render(vnode, document.getElementById('app'))
現在我們需要將它渲染到頁面上。
思路:
1.我們在“vdom.js”中新建一個render函數,參數爲要渲染的虛擬節點和要渲染到哪個節點中的元素(盒子)
2.判斷它是首次渲染,還是非首次渲染。(我們現在只寫首次渲染)
3.調用首次渲染函數mount函數,將要渲染的虛擬節點和盒子傳入
4.新建mount函數,判斷要渲染的vnode的類型,如果是節點類型,調用mountElement函數,如果是文本類型,調用mountText函數。
5.新建mountElement函數,根據vnode的tag新建一個虛擬dom,並將dom賦值給el
6.遍歷data,渲染屬性
7.判斷vnode的子節點類型,如果是單個節點,直接調用mount函數遞歸,參數爲子節點和dom,如果是多節點,遍歷子節點遞歸
8.將dom添加盒子中
9.新建mountText函數,創建一個文本節點dom,並記錄el
10.將dom添加盒子中
這樣我們就完成了首次渲染
代碼:
function render(vnode, container) {
// 首次渲染
mount(vnode, container)
}
// 首次渲染
function mount(vnode, container) {
// 得到vnode的類型
const { flag } = vnode
// 如果是節點類型,執行mountElement
if (flag === vnodeType.HTML) {
mountElement(vnode, container)
} else if (flag === vnodeType.TEXT) {
// 如果是文本類型,執行mountText
mountText(vnode, container)
}
}
function mountElement(vnode, container) {
// 根據tag新建dom元素
const dom = document.createElement(vnode.tag)
// 賦值給el
vnode.el = dom
// 結構出 data, children, childrenFlag
const { data, children, childrenFlag } = vnode
// 掛載屬性
if (data) {
for (let key in data) {
// 傳入4個參數,當前節點,屬性名, 上個屬性值,這此屬性值,因爲是首次渲染,所以perv爲null
patchData(dom, key, null, data[key])
}
}
// 掛載子節點
if (childrenFlag !== childType.EMPTY) {
if (childrenFlag === childType.SINGLE) {
// 遞歸
mount(children, dom)
} else if (childrenFlag === childType.MULTIPLE) {
for(let i = 0; i < children.length; i++) {
// 遞歸
mount(children[i], dom)
}
}
}
// 添加到 container 中
container.appendChild(dom)
}
function patchData(el, key, perv, next) {
switch(key) {
case 'style':
for (let i in next) {
el.style[i] = next[i]
}
break
case 'class':
el.className = next
break
default:
if (key[0] === '@') {
el.addEventListener(key.slice(1), next)
} else {
el.setAttribute(key, next)
}
}
}
function mountText(vnode, container) {
// 創建文本節點
const dom = document.createTextNode(vnode.children)
// 記錄el
vnode.el = dom
// 添加到 container 中
container.appendChild(dom)
}
效果: