1. 程式人生 > >react 虛擬dom 淺析

react 虛擬dom 淺析

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 操作時讀寫分離的重要性 以及一些 效能優化的注意點