React淺析(五):虛擬DOM和Render函數

虛擬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)

輸出打印的結果是:


打印結果.png

但是這樣操作dom的結果,當項目越大,頁面交互越復雜,頻繁的去操作dom,會導致頁面卡頓,性能差,如何去減少dom操作是性能優化的一個關鍵點。

千呼萬喚的,Vnode可以解決這樣的問題!!!


想不到吧.png

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官方聲明周期圖.png

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算法感興趣的話,后面會增加相關的學習內容,一起探討啊~


搬磚使我快樂!.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 2,719評論 0 3
  • React Native 是最近非常火的一個話題,介紹如何利用 React Native 進行開發的文章和書籍多如...
    零度_不結冰閱讀 691評論 0 1
  • 3. JSX JSX是對JavaScript語言的一個擴展語法, 用于生產React“元素”,建議在描述UI的時候...
    pixels閱讀 2,873評論 0 24
  • 老大不在公司的第十八天。我和迪哥又說起了他,作為員工,替他擔心,作為朋友,替他難過。心里最近的想法全都表露出來了,...
    花開蘭亭敘閱讀 196評論 0 1
  • 之一在冬天 在冬天 一個孩童 看見四條魚在桶里游 他毫不猶豫地端起一壺開水 向桶里倒去 他想 這樣魚就不會被凍死了...
    閑不語閱讀 132評論 4 4