虛擬DOM
虛擬DOM(下面簡化稱為Vnode)簡而言之 ,就是用js去描述一個dom節點樹,而DOM變化的對比,都放在js層來做。
- 傳統的dom節點,是這樣的
<div>
<p className='text'>寫個啥內容啊</p>
</div>
- Vnode是長這樣的
{
nodeName:'div', //節點名字
attributes:{}, //屬性鍵值對
children:[], //子節點
key:undefined, //節點的唯一值
...
}
為什么需要Vnode?
這里,我們來引入一個傳統的操作dom栗子。
var arr = [1,2,3,4]
function render(data){
function createElement(tag){
var dom = document.createElement(tag)
return dom
}
var ul= createElement('ui')
data.forEach((elem)=>{
var liDom = createElement('li')
liDom.innerHTML = elem
ul.append(liDom)
})
return ul
}
render(arr)
輸出打印的結果是:
但是這樣操作dom的結果,當項目越大,頁面交互越復雜,頻繁的去操作dom,會導致頁面卡頓,性能差,如何去減少dom操作是性能優化的一個關鍵點。
千呼萬喚的,Vnode可以解決這樣的問題!!!
Vnode是vue和react的核心。將DOM對比操作放在js層,提高效率。
如何使用Vnode?
首先vdom的兩個核心api
- h函數:用于生成vnode
- path函數:
h是指hyperscript,一種可以通過js來創建html的庫。
<div>
<p className='text'>寫個啥內容啊</p>
</div>
//經過babel編譯,然后將它們傳遞給h函數調用
h(
'div',
null,
h('p',{className:'text'},'寫個啥內容啊')
)
//react的React.createElement函數的作用就跟這里的h函數一樣,結果是為了獲得一個vnode,虛擬節點
h函數輸出的元素是一個dom節點的js對象,類似這樣
{
'nodeName':'div',
'attributes':{},
'children':[...],
'key':undefined,
...
}
h函數結束后,會調用render函數啦!!!
Render函數
前面我們提到了jsx是如何轉換為虛擬dom的js對象,那么虛擬dom又是如何轉為真實的DOM?
這里需要思考兩個問題:
- render是什么?
- 什么時候觸發render?
- render 的過程發生了什么?
render是什么?
寫過React的人都知道,我們每個組件中有且只有一個render方法
//class方式創建的組件
class Home extends React.Component{
//省略
render(){
return (
<div>
<p>一個節點</p>
</div>
)
}
}
// 函數申明創建的組件
function Page(){
return (
<div>
<p>另一個節點</p>
</div>
)
}
以上的代碼栗子容易看出,無論是class方式還是函數申明方式創建出來的組件,返回的有且只有一個頂點節點。調用render方法,可以將react元素渲染到真實的dom中。
什么時候觸發render?
在組件實例化和存在期時會執行render。
從下圖中可以看出:
- 實例化過程中,當執行componentWillMount之后會執行render,開始將節點掛載在頁面上。
-
存在期的過程中,setState會導致組件的重新渲染。
componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate
React的重渲染機制,當狀態更新后,我們只想讓狀態相關的組件重新渲染,并不喜歡其他不相關的組件被重渲染,對此也有相關的優化操作。shouldComponentUpdate(nextProps,nextState)方法中是render函數調用前執行的函數,開發者可以通過nextProps,nextState參數來判斷當前場景是否需要重新渲染,當shouldComponentUpdate方法return true則重新渲染,return false則阻止組件渲染。
同樣,在PureComponent中,只接受props和state參數,如果props和state沒有改變,PureComponent不會重渲染,可以一定程度上減少了render帶來的消耗。
render 的過程發生了什么?
前面提到,React的核心虛擬DOM可以講真實的dom節點以obj對象的形式來表示,通過對比新舊的obj對象的差異,更改頁面相對應的變化節點。而React.render實際上就相當于是vdom里面的path函數,path函數接收兩個參數。
- 當首次渲染的時候,調用的是path(container,vnode)
- 更新渲染的時候,調用的是 path(vnode,newVnode)
以下例子,創建一個節點的實現思路(簡易的)
var vnode
function render(data){
var newVnode = h(....)//前面章節提到h函數,執行后返回一個虛擬的js對象,用來描繪dom節點的
/*
{
tag:’div’,
attrs: {id:’’},
children:[…]
}
*/
if(vnode){
//如果節點已經存在,則重復渲染,將新舊節點傳入path函數中,新舊對比
path(vnode,newVnode)
}else{
//如果節點不存在,則首次渲染,將節點掛在在根節點container上
path(container,newVnode)
}
// 將舊節點儲存起來,便于下次新節點的新舊對比
vnode = newVnode
}
第一次渲染是如何進行?
- path(container,newVnode)
// 創建一個真實節點
function createElement(vnode){
var tag = vnode.tag // 獲取虛擬節點的tag類型
var attrs = vnode.attrs|| [ ] // 儲存虛擬節點的屬性
var children = vnode.children || [] // 儲存虛擬節點的子節點
if(!tag){
return null
}
var elem = document.createElement(tag) // 創建一個真實的dom節點
for(attrName in attrs){ //遍歷所有屬性,給真實節點添加屬性
if(atrs.hasOwnProperty(attrName)){
elem.setAttribute(attrName,attrs[attrName])
}
}
children.forEach(function(childVode){ //遞歸虛擬節點的子節點,創建節點追加到父節點中
elem.appendChild(createElement(childVnode))
})
return elem
}
再次渲染是如何進行?
- path(vnode,newVnode)
//更新渲染,通過對比新舊vnode,更新節點樹
function updateChildren (vnode,newVnode){
var children = vnode.children || [ ]
var newChildren = newVnode.children || [ ]
//遍歷所有的children
children.forEach(function (child,index){
var newChild = newChildren[index]
if(newChild==null){
return
}
if(child.tag === newChild.tag){
updateChildren(child,newChild)
}else{
replaceNode(child,newChild)
}
})
}
path(container,vnode)和path(vnode,newVnode)的實現也是diff算法的一個實現過程,通過調用createElement和updateChildren方法讓頁面上的節點創建和更新。
當然,真正的diff算法是非常復雜的,以上的方法只是一個解題思路,增刪節點,重新排序,屬性樣式事件等變化,都是非常復雜的,在下能力有限,這里就不過多研究啦~有興趣的同學自行找diff算法研究下咯
寫在最后
這一節的主要講的render函數在react中的一個工作過程,減少和控制不必要的重復渲染可以有效的提高頁面性能,如果對virtual-dom 和diff算法感興趣的話,后面會增加相關的學習內容,一起探討啊~