react 虛擬dom 淺析
阿新 • • 發佈:2018-11-25
react 虛擬dom 淺析
虛擬dom 的概念 隨著 react vue 等框架的普及 在前端圈一度成為一個熱議的話題
爭論點在於 虛擬dom 真的可以提高 操作dom的效能麼
與傳統的jq 相比 效能到底有多大提升
於是帶著這兩個問題 我研究了下 這塊的知識( 以下純屬個人見解如有誤 請圈內各大佬指正)
首先我們來看下虛擬dom的 構建過程
- jsx語法的轉換 我們程式碼中的jsx 主要有babel 負責語法解析轉換 這塊主要是用到了babel-preset-react 這個預設 babel的預設其實就相當於是一些babel 外掛的集合 pabael-preset-react 所含蓋的外掛包括(preset-flow,syntax-jsx,transform-react-jsx,transform-react-display-name) 他負責將我們的jsx語法轉 換成js可識別 dom描述物件 原理是生成一棵抽象語法樹 然後進行相應的語法轉換 - React.createElement 方法接受轉譯後的dom 描述物件 建立虛擬dom樹 在didmount的時候將這棵虛擬dom樹轉換成真正的dom 掛載到頁面上
//根據傳進來的值 產生虛擬dom 物件 class Element{ constructor(type,props,children){ this.type = type; this.props = props; this.children = children; } } //生成虛擬dom function createElement(type,props,children){ return new Element(type,props,children) } let vertualDom = createElement('ul',{class:"list"},[ createElement('li',{class:"item"},['a']), createElement('li',{class:"item"},['a']), createElement('li',{class:"item"},['a']) ]) //負責將虛擬dom處理成真實的dom function render(eleObj){ let el = document.createElement(eleObj.type); for(let key in eleObj.props){ //設定屬性的方法 setAttr(el,key,eleObj.props[key]); } eleObj.children.forEach((child)=>{ child = (child instanceof Element)?render(child):document.createTextNode(child); el.appendChild(child) }) return el; } //設定屬性 function setAttr(node,key,value){ switch(key){ case"value": if(node.tagName.toUpperCase()==="INPUT"|| node.tagName.toUpperCase()==="TEXTAREA"){ node.value = value }else{ node.setAttribute(key,value); } break; case"style": node.style.cssText = value; break; default: node.setAttribute(key,value); break; } } //把dom掛在到頁面中 function renderDOm(el,target){ target.appendChild(el); }
虛擬dom 建立 到此結束 (簡陋版實現)
接下來就是我們的dom diff
在每次呼叫setState 的時候 會生成 一顆新的虛擬dom 樹 與原先老的樹進行對比 得到一個補丁包 然後 拿著這個補丁包去dom 中進行相應的操作 ,dom diff的演算法 也是整個虛擬dom 中最核心的部分
個人的理解: 原先的的老樹相當於是一個資源池 我們的新樹是我們最終想要渲染的結果 通過兩個樹對比 我們能以最小的代價 來更新的我們的檢視
const ATTRS = "ATTRS"; const TEXT = "TEXT"; const REMOVE = "REMOVE"; const REPLACE = 'REPLACE'; let Index = 0; // 對比前後兩棵樹 之前的差別 返回一個補丁物件 function diff(oldTree,newTree){ let patches = {}; let index = 0; walk(oldTree,newTree,index,patches); return patches; } // 具體的對比方法 function walk(oldNode,newNode,index,patches){ let currentPatches = []; if(!newNode){ //沒有新的節點。刪除 currentPatches.push({type:REMOVE,index}); }else if(isString(oldNode)&&isString(newNode)){ //判斷文字是否變換 if(oldNode!==newNode){ currentPatches.push({type:TEXT,text:newNode}); } }else if(oldNode.type === newNode.type){ //type 相同 判斷屬性 let attrs = diffAttr(oldNode.props,newNode.props); if(Object.keys(attrs).length>0){ currentPatches.push({type:ATTRS,attrs}) } diffChildren(oldNode.children,newNode.children,index,patches) }else{ //節點被替換了 currentPatches.push({type:REPLACE,newNode}) } if(currentPatches.length>0){ //產生補丁包 patches[index] = currentPatches; } } //對比屬性的不同 function diffAttr(oldAttrs,newAttrs){ let patch = {} //更新 for(let attr in oldAttrs){ if(oldAttrs[attr]!== newAttrs[attr]){ patch[attr] = newAttrs[attr] } } //新增 for(let attr in newAttrs){ if(!oldAttrs.hasOwnProperty(attr)){ patch[attr] = newAttrs[attr] } } return patch; } //遞迴遍歷子節點的不同 function diffChildren(oldNode,newNode,index,patches){ oldNode.forEach((child,idx)=>{ walk(child ,newNode[idx],++Index,patches) }) } function isString(node){ return Object.prototype.toString.call(node)=="[object String]"; }
有幾個值得注意的 地方
- 虛擬dom的建立過程 先序深度優先
- diff 對比的過程只會對同級進行相應的比較
- 打補丁的過程是 從下到上 倒著來的
我們這個地方沒有引入key 其實key 在整個dom diff 中扮演了重要的 角色主要優化了這個過程的效能 key 主要意義是為了以最小的代價來更新dom 就是最小化效能對資源池(老樹)的操作
虛擬dom的意義是 我們把對dom 的操作 都交由他來處理 避免了一些不必要的dom 迴流和重繪 相關知識可以參考 阮老師的這篇文章: 網頁效能管理詳解 也就是我們在對dom 操作時讀寫分離的重要性 以及一些 效能優化的注意點