Virtual Dom和Diff算法
React.creaeElement()
Babel 會對將 JSX 編譯為 React API(React.creaeElement()),React.creaeElement()會返回一個Virtual Dom,React會將Virtual Dom轉換為真是Dom,顯示到頁面中。
jsx轉換為Virtual Dom結構,type,props,children
<div className="container">
<h3>Hello World</h3>
<p>React Demo </p>
</div>
轉換後
{
type: "div",
props: { className: "container" },
children: [
{
type: "h3",
props: null,
children: [
{
type: "text",
props: {
textContent: "Hello World"
}
}
]
},
{
type: "p",
props: null,
children: [
{
type: "text",
props: {
textContent: "React Demo"
}
}
]
}
]
}
1, 創建 Virtual DOM
在 React 代碼執行前,JSX 會被 Babel 轉換為 React.createElement 方法的調用,在調用 createElement 方法時會傳入元素的類型,元素的屬性,以及元素的子元素,createElement 方法的返回值為構建好的 Virtual DOM 對象。根據返回的virtualDom對象,進行處理成需要的數據結構,在這個過程中,需要處理virtualDom中的布爾值或者null
{
type,
props: Object.assign({ children: childElements }, props),
children: childElements
}
2,渲染 Virtual DOM 對象為 DOM 對象
調用 render 方法可以將 Virtual DOM 對象更新為真實 DOM 對象。
在更新之前需要確定是否存在舊的 Virtual DOM,如果存在需要比對差異,如果不存在可以直接將 Virtual DOM 轉換為 DOM 對象。
先只考慮不存在舊的 Virtual DOM 的情況,先直接將 Virtual DOM 對象更新為真實 DOM 對象。
export default function diff(virtualDOM, container, oldDOM) {
// 判斷 oldDOM 是否存在
if (!oldDOM) {
// 如果不存在 不需要對比 直接將 Virtual DOM 轉換為真實 DOM
mountElement(virtualDOM, container)
}
}
mountElement方法中需要判斷是組件還是元素
普通元素直接掛載
export default function mountNativeElement(virtualDOM, container) {
const newElement = createDOMElement(virtualDOM)
container.appendChild(newElement)
}
createDOMElement方法中需要判斷是普通文本節點還是元素節點,並且判斷是否有子元素,遞歸渲染
export default function createDOMElement(virtualDOM) {
let newElement = null
if (virtualDOM.type === "text") {
// 創建文本節點
newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
// 創建元素節點
newElement = document.createElement(virtualDOM.type)
// 更新元素屬性
updateElementNode(newElement, virtualDOM)
}
// 遞歸渲染子節點
virtualDOM.children.forEach(child => {
// 因為不確定子元素是 NativeElement 還是 Component 所以調用 mountElement 方法進行確定
mountElement(child, newElement)
})
return newElement
}
3,為元素節點添加屬性
元素渲染完成後,需要給元素添加屬性,元素屬性也分為事件屬性,input標籤類的value屬性或者checked屬性,還有className或者其他屬性,在這個過程中,要刨除children,它存在於virtualDom數據結構中,但不是我們需要的節點屬性
export default function updateElementNode(element, virtualDOM) {
// 獲取要解析的 VirtualDOM 對象中的屬性對象
const newProps = virtualDOM.props
// 將屬性對象中的屬性名稱放到一個數組中並循環數組
Object.keys(newProps).forEach(propName => {
const newPropsValue = newProps[propName]
// 考慮屬性名稱是否以 on 開頭 如果是就表示是個事件屬性 onClick -> click
if (propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(2)
element.addEventListener(eventName, newPropsValue)
// 如果屬性名稱是 value 或者 checked 需要通過 [] 的形式添加
} else if (propName === "value" || propName === "checked") {
element[propName] = newPropsValue
// 刨除 children 因為它是子元素 不是屬性
} else if (propName !== "children") {
// className 屬性單獨處理 不直接在元素上添加 class 屬性是因為 class 是 JavaScript 中的關鍵字
if (propName === "className") {
element.setAttribute("class", newPropsValue)
} else {
// 普通屬性
element.setAttribute(propName, newPropsValue)
}
}
})
}
4,渲染組件
普通的html元素處理完之後需要處理渲染組件的情況,組件的virtualDom數據結構
// 組件的 Virtual DOM
{
type: f function() {},
props: {}
children: []
}
組件的virtualDom中type屬性是function
export default function mountComponent(virtualDOM, container) {
// 存放組件調用後返回的 Virtual DOM 的容器
let nextVirtualDOM = null
// 區分函數型組件和類組件
if (isFunctionalComponent(virtualDOM)) {
// 函數組件 調用 buildFunctionalComponent 方法處理函數組件
nextVirtualDOM = buildFunctionalComponent(virtualDOM)
} else {
// 類組件
}
// 判斷得到的 Virtual Dom 是否是組件
if (isFunction(nextVirtualDOM)) {
// 如果是組件 繼續遞歸調用 mountComponent 解剖組件
mountComponent(nextVirtualDOM, container)
} else {
// 如果是 Navtive Element 就去渲染
mountNativeElement(nextVirtualDOM, container)
}
}
// Virtual DOM 是否為函數型組件
// 條件有兩個: 1. Virtual DOM 的 type 屬性值為函數 2. 函數的原型對象中不能有render方法
// 只有類組件的原型對象中有render方法
export function isFunctionalComponent(virtualDOM) {
const type = virtualDOM && virtualDOM.type
return (
type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
)
}
// 函數組件處理
function buildFunctionalComponent(virtualDOM) {
// 通過 Virtual DOM 中的 type 屬性獲取到組件函數並調用
// 調用組件函數時將 Virtual DOM 對象中的 props 屬性傳遞給組件函數 這樣在組件中就可以通過 props 屬性獲取數據了
// 組件返回要渲染的 Virtual DOM
return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}
5,渲染類組件
類組件本身也是 Virtual DOM,可以通過 Virtual DOM 中的 type 屬性值確定當前要渲染的組件是類組件還是函數組件。類組件有render方法
在確定當前要渲染的組件為類組件以後,需要實例化類組件得到類組件實例對象,通過類組件實例對象調用類組件中的 render 方法,獲取組件要渲染的 Virtual DOM。
類組件需要繼承 Component 父類,子類需要通過 super 方法將自身的 props 屬性傳遞給 Component 父類,父類會將 props 屬性掛載為父類屬性,子類繼承了父類,自己本身也就自然擁有props屬性了。當 props 發生更新後,父類可以根據更新後的 props 幫助子類更新視圖。
在 mountComponent中處理類組件時
// 處理類組件
function buildStatefulComponent(virtualDOM) {
// 實例化類組件 得到類組件實例對象 並將 props 屬性傳遞進類組件
const component = new virtualDOM.type(virtualDOM.props)
// 調用類組件中的render方法得到要渲染的 Virtual DOM
const nextVirtualDOM = component.render()
// 返回要渲染的 Virtual DOM
return nextVirtualDOM
}
6. Virtual DOM 比對
比對過程遵循同級比對,深度遍歷優先原則
需要處理節點類型相同的情況,如果是元素節點,就對比元素節點屬性是否發生變化
在diff方法中獲取oldVirtualDom
// diff.js
// 獲取未更新前的 Virtual DOM
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
oldVirtualDOM 是否存在, 如果存在則繼續判斷要對比的 Virtual DOM 類型是否相同,如果類型相同判斷節點類型是否是文本,如果是文本節點對比,就調用 updateTextNode 方法,變化直接替換textContent屬性值,如果是元素節點對比就調用 setAttributeForElement 方法.
setAttributeForElement 方法用於設置/更新元素節點屬性
思路是先分別獲取更新後的和更新前的 Virtual DOM 中的 props 屬性,循環新 Virtual DOM 中的 props 屬性,通過對比看一下新 Virtual DOM 中的屬性值是否發生了變化,如果發生變化 需要將變化的值更新到真實 DOM 對象中
再循環未更新前的 Virtual DOM 對象,通過對比看看新的 Virtual DOM 中是否有被刪除的屬性,如果存在刪除的屬性 需要將 DOM 對象中對應的屬性也刪除掉
export default function updateNodeElement(
newElement,
virtualDOM,
oldVirtualDOM = {}
) {
// 獲取節點對應的屬性對象
const newProps = virtualDOM.props || {}
const oldProps = oldVirtualDOM.props || {}
Object.keys(newProps).forEach(propName => {
// 獲取屬性值
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (newPropsValue !== oldPropsValue) {
// 判斷屬性是否是否事件屬性 onClick -> click
if (propName.slice(0, 2) === "on") {
// 事件名稱
const eventName = propName.toLowerCase().slice(2)
// 為元素添加事件
newElement.addEventListener(eventName, newPropsValue)
// 刪除原有的事件的事件處理函數
if (oldPropsValue) {
newElement.removeEventListener(eventName, oldPropsValue)
}
} else if (propName === "value" || propName === "checked") {
newElement[propName] = newPropsValue
} else if (propName !== "children") {
if (propName === "className") {
newElement.setAttribute("class", newPropsValue)
} else {
newElement.setAttribute(propName, newPropsValue)
}
}
}
})
// 判斷屬性被刪除的情況
Object.keys(oldProps).forEach(propName => {
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (!newPropsValue) {
// 屬性被刪除了
if (propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(2)
newElement.removeEventListener(eventName, oldPropsValue)
} else if (propName !== "children") {
newElement.removeAttribute(propName)
}
}
})
}
對比完成以後還需要遞歸對比子元素
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
// 遞歸對比 Virtual DOM 的子元素
virtualDOM.children.forEach((child, i) => {
diff(child, oldDOM, oldDOM.childNodes[i])
})
}
當Virtual DOM 類型不同時,就不需要繼續對比了,直接使用新的 Virtual DOM 創建 DOM 對象,用新的 DOM 對象直接替換舊的 DOM 對象。當前這種情況要將組件刨除,組件要被單獨處理。
對比完需要考慮刪除節點的情況,發生在節點更新以後並且發生在同一個父節點下的所有子節點身上。舊節點對象的數量多於新 VirtualDOM 節點的數量,就説明有節點需要被刪除。
// 獲取就節點的數量
let oldChildNodes = oldDOM.childNodes
// 如果舊節點的數量多於要渲染的新節點的長度
if (oldChildNodes.length > virtualDOM.children.length) {
for (
let i = oldChildNodes.length - 1;
i > virtualDOM.children.length - 1;
i--
) {
oldDOM.removeChild(oldChildNodes[i])
}
}
類組件的狀態更新,需要使用setState方法,定義在父類 Component 中的,該方法的作用是更改子類的 state,產生一個全新的 state 對象。子類可以調用父類的 setState 方法更改狀態值之後,當組件的 state 對象發生更改時,要調用 render 方法更新組件視圖。
在更新組件之前,要使用更新的 Virtual DOM 對象和未更新的 Virtual DOM 進行對比找出更新的部分,達到 DOM 最小化操作的目的。
在 setState 方法中可以通過調用 this.render 方法獲取更新後的 Virtual DOM,由於 setState 方法被子類調用,this 指向子類,所以此處調用的是子類的 render 方法。
頁面中的 DOM 對象是通過 mountNativeElement 方法掛載到頁面中的,所以我們只需要在這個方法中調用 Component 類中的方法就可以將 DOM 對象保存在 Component 類中了。在子類調用 setState 方法的時候,在 setState 方法中再調用另一個獲取 DOM 對象的方法就可以獲取到之前保存的 DOM 對象了。這裏存在一個問題,如何在mountNativeElement中通過類的實例對象調用setDom方法。mountNativeElement 方法接收最新的 Virtual DOM 對象,如果這個 Virtual DOM 對象是類組件產生的,在產生這個 Virtual DOM 對象時一定會先得到這個類的實例對象,然後再調用實例對象下面的 render 方法進行獲取。我們可以在那個時候將類組件實例對象添加到 Virtual DOM 對象的屬性中,而這個 Virtual DOM 對象最終會傳遞給 mountNativeElement 方法,這樣我們就可以在 mountNativeElement 方法中獲取到組件的實例對象了,既然類組件的實例對象獲取到了,我們就可以調用 setDOM 方法了。
在 buildClassComponent 方法中為 Virtual DOM 對象添加 component 屬性, 值為類組件的實例對象。
// 保存 DOM 對象的方法
setDOM(dom) {
this._dom = dom
}
// 獲取 DOM 對象的方法
getDOM() {
retur
setState(state) {
// setState 方法被子類調用 此處this指向子類
// 所以改變的是子類的 state
this.state = Object.assign({}, this.state, state)
// 通過調用 render 方法獲取最新的 Virtual DOM
let virtualDOM = this.render()
}
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type(virtualDOM.props)
const nextVirtualDOM = component.render()
nextVirtualDOM.component = component
return nextVirtualDOM
}
export default function mountNativeElement(virtualDOM, container) {
// 獲取組件實例對象
const component = virtualDOM.component
// 如果組件實例對象存在
if (component) {
// 保存 DOM 對象
component.setDOM(newElement)
}
}
這裏在setState方法中還需要調用diff方法,進行狀態更新
setState(state) {
this.state = Object.assign({}, this.state, state)
let virtualDOM = this.render()
let oldDOM = this.getDOM()
// 獲取真實 DOM 對象父級容器對象
let container = oldDOM.parentNode
}
組件更新,如果更新的是組件,還需要判斷是否是同一個組件,如果不是同一個組件就不需要做組件更新操作,直接調用 mountElement 方法將組件返回的 Virtual DOM 添加到頁面中。如果是同一個組件,就執行更新組件操作,就是將最新的 props 傳遞到組件中,再調用組件的render方法獲取組件返回的最新的 Virtual DOM 對象,再將 Virtual DOM 對象傳遞給 diff 方法,讓 diff 方法找出差異,從而將差異更新到真實 DOM 對象中。
在更新組件的過程中還要在不同階段調用其不同的組件生命週期函數。
在 diff 方法中判斷要更新的 Virtual DOM 是否是組件,如果是組件又分為多種情況,新增 diffComponent 方法進行處理
else if (typeof virtualDOM.type === "function") {
// 要更新的是組件
// 1) 組件本身的 virtualDOM 對象 通過它可以獲取到組件最新的 props
// 2) 要更新的組件的實例對象 通過它可以調用組件的生命週期函數 可以更新組件的 props 屬性 可以獲取到組件返回的最新的 Virtual DOM
// 3) 要更新的 DOM 象 在更新組件時 需要在已有DOM對象的身上進行修改 實現DOM最小化操作 獲取舊的 Virtual DOM 對象
// 4) 如果要更新的組件和舊組件不是同一個組件 要直接將組件返回的 Virtual DOM 顯示在頁面中 此時需要 container 做為父級容器
diffComponent(virtualDOM, oldComponent, oldDOM, container)
}
在 diffComponent 方法中判斷要更新的組件是未更新前的組件是否是同一個組件
function isSameComponent(virtualDOM, oldComponent) {
return oldComponent && virtualDOM.type === oldComponent.constructor
}
不是同一個組件的話,就不需要執行更新組件的操作,直接將組件內容顯示在頁面中,替換原有內容
// 不是同一個組件 直接將組件內容顯示在頁面中
// 這裏為 mountElement 方法新增了一個參數 oldDOM
// 作用是在將 DOM 對象插入到頁面前 將頁面中已存在的 DOM 對象刪除 否則無論是舊DOM對象還是新DOM對象都會顯示在頁面中
mountElement(virtualDOM, container, oldDOM)
在 mountNativeElement 方法中刪除原有的舊 DOM 對象 unmount(oldDOM),調用node.remove()方法
如果是同一個組件的話,需要執行組件更新操作,需要調用組件生命週期函數
export default class Component {
// 生命週期函數
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state
}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
}
更新時的操作,updateComponent
export default function updateComponent(
virtualDOM,
oldComponent,
oldDOM,
container
) {
// 生命週期函數
oldComponent.componentWillReceiveProps(virtualDOM.props)
if (
// 調用 shouldComponentUpdate 生命週期函數判斷是否要執行更新操作
oldComponent.shouldComponentUpdate(virtualDOM.props)
) {
// 拷貝props
let prevProps = oldComponent.props
// 生命週期函數
oldComponent.componentWillUpdate(virtualDOM.props)
// 更新組件的 props 屬性 updateProps 方法定義在 Component 類型
oldComponent.updateProps(virtualDOM.props)
// 因為組件的 props 已經更新 所以調用 render 方法獲取最新的 Virtual DOM
const nextVirtualDOM = oldComponent.render()
// 將組件實例對象掛載到 Virtual DOM 身上
nextVirtualDOM.component = oldComponent
// 調用diff方法更新視圖
diff(nextVirtualDOM, container, oldDOM)
// 生命週期函數
oldComponent.componentDidUpdate(prevProps)
}
}
export default class Component {
updateProps(props) {
this.props = props
}
}
6,處理ref屬性
ref屬性可以是元素的,也可以是組件的
元素節點時,在創建節點時判斷其 Virtual DOM 對象中是否有 ref 屬性,如果有就調用 ref 屬性中所存儲的方法並且將創建出來的DOM對象作為參數傳遞給 ref 方法,這樣在渲染組件節點的時候就可以拿到元素對象並將元素對象存儲為組件屬性了。
// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
virtualDOM.props.ref(newElement)
}
類組件的元素有ref屬性時,判斷當前處理的是類組件,就通過類組件返回的 Virtual DOM 對象中獲取組件實例對象,判斷組件實例對象中的 props 屬性中是否存在 ref 屬性,如果存在就調用 ref 方法並且將組件實例對象傳遞給 ref 方法。
let component = null
if (isFunctionalComponent(virtualDOM)) {}
else {
// 類組件
nextVirtualDOM = buildStatefulComponent(virtualDOM)
// 獲取組件實例對象
component = nextVirtualDOM.component
}
// 如果組件實例對象存在的話
if (component) {
// 判斷組件實例對象身上是否有 props 屬性 props 屬性中是否有 ref 屬性
if (component.props && component.props.ref) {
// 調用 ref 方法並傳遞組件實例對象
component.props.ref(component)
}
}
與此同時,執行
// 如果組件實例對象存在的話
if (component) {
component.componentDidMount()
}
7,key屬性
節點對比時,在兩個元素進行比對時,如果類型相同,就循環舊的 DOM 對象的子元素,查看其身上是否有key 屬性,如果有就將這個子元素的 DOM 對象存儲在一個 JavaScript 對象中,接着循環要渲染的 Virtual DOM 對象的子元素,在循環過程中獲取到這個子元素的 key 屬性,然後使用這個 key 屬性到 JavaScript 對象中查找 DOM 對象,如果能夠找到就説明這個元素是已經存在的,是不需要重新渲染的。如果通過key屬性找不到這個元素,就説明這個元素是新增的是需要渲染的。
// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
// 將擁有key屬性的元素放入 keyedElements 對象中
let keyedElements = {}
for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
let domElement = oldDOM.childNodes[i]
if (domElement.nodeType === 1) {
let key = domElement.getAttribute("key")
if (key) {
keyedElements[key] = domElement
}
}
}
}
// diff.js
// 看一看是否有找到了擁有 key 屬性的元素
let hasNoKey = Object.keys(keyedElements).length === 0
// 如果沒有找到擁有 key 屬性的元素 就按照索引進行比較
if (hasNoKey) {
// 遞歸對比 Virtual DOM 的子元素
virtualDOM.children.forEach((child, i) => {
diff(child, oldDOM, oldDOM.childNodes[i])
})
} else {
// 使用key屬性進行元素比較
virtualDOM.children.forEach((child, i) => {
// 獲取要進行比對的元素的 key 屬性
let key = child.props.key
// 如果 key 屬性存在
if (key) {
// 到已存在的 DOM 元素對象中查找對應的 DOM 元素
let domElement = keyedElements[key]
// 如果找到元素就説明該元素已經存在 不需要重新渲染
if (domElement) {
// 雖然 DOM 元素不需要重新渲染 但是不能確定元素的位置就一定沒有發生變化
// 所以還要查看一下元素的位置
// 看一下 oldDOM 對應的(i)子元素和 domElement 是否是同一個元素 如果不是就説明元素位置發生了變化
if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
// 元素位置發生了變化
// 將 domElement 插入到當前元素位置的前面 oldDOM.childNodes[i] 就是當前位置
// domElement 就被放入了當前位置
oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
}
} else {
mountElement(child, oldDOM, oldDOM.childNodes[i])
}
}
})
}
// mountNativeElement.js
if (oldDOM) {
container.insertBefore(newElement, oldDOM)
} else {
// 將轉換之後的DOM對象放置在頁面中
container.appendChild(newElement)
}
節點卸載。在比對節點的過程中,如果舊節點的數量多於要渲染的新節點的數量就説明有節點被刪除了,繼續判斷 keyedElements 對象中是否有元素,如果沒有就使用索引方式刪除,如果有就要使用 key 屬性比對的方式進行刪除。
實現思路是循環舊節點,在循環舊節點的過程中獲取舊節點對應的 key 屬性,然後根據 key 屬性在新節點中查找這個舊節點,如果找到就説明這個節點沒有被刪除,如果沒有找到,就説明節點被刪除了,調用卸載節點的方法卸載節點即可。
// 獲取就節點的數量
let oldChildNodes = oldDOM.childNodes
// 如果舊節點的數量多於要渲染的新節點的長度
if (oldChildNodes.length > virtualDOM.children.length) {
if (hasNoKey) {
for (
let i = oldChildNodes.length - 1;
i >= virtualDOM.children.length;
i--
) {
oldDOM.removeChild(oldChildNodes[i])
}
} else {
for (let i = 0; i < oldChildNodes.length; i++) {
let oldChild = oldChildNodes[i]
let oldChildKey = oldChild._virtualDOM.props.key
let found = false
for (let n = 0; n < virtualDOM.children.length; n++) {
if (oldChildKey === virtualDOM.children[n].props.key) {
found = true
break
}
}
if (!found) {
unmount(oldChild)
i--
}
}
}
}
卸載節點並不僅包含將節點直接刪除的情況,還有以下幾種情況
- 如果要刪除的節點是文本節點的話可以直接刪除
- 如果要刪除的節點由組件生成,需要調用組件卸載生命週期函數
- 如果要刪除的節點中包含了其他組件生成的節點,需要調用其他組件的卸載生命週期函數
- 如果要刪除的節點身上有 ref 屬性,還需要刪除通過 ref 屬性傳遞給組件的 DOM 節點對象
- 如果要刪除的節點身上有事件,需要刪除事件對應的事件處理函數
export default function unmount(dom) {
// 獲取節點對應的 virtualDOM 對象
const virtualDOM = dom._virtualDOM
// 如果要刪除的節點時文本
if (virtualDOM.type === "text") {
// 直接刪除節點
dom.remove()
// 阻止程序向下運行
return
}
// 查看節點是否由組件生成
let component = virtualDOM.component
// 如果由組件生成
if (component) {
// 調用組件卸載生命週期函數
component.componentWillUnmount()
}
// 如果節點具有 ref 屬性 通過再次調用 ref 方法 將傳遞給組件的DOM對象刪除
if (virtualDOM.props && virtualDOM.props.ref) {
virtualDOM.props.ref(null)
}
// 事件處理
Object.keys(virtualDOM.props).forEach(propName => {
if (propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(2)
const eventHandler = virtualDOM.props[propName]
dom.removeEventListener(eventName, eventHandler)
}
})
// 遞歸刪除子節點
if (dom.childNodes.length > 0) {
for (let i = 0; i < dom.childNodes.length; i++) {
unmount(dom.childNodes[i])
i--
}
}
dom.remove()
}