一步一步帶你實現virtualdom(一)
一步一步帶你實現virtual dom(一)
一步一步帶你實現virtual dom(二)–Props和事件
要寫你自己的虛擬DOM,有兩件事你必須知道。你甚至都不用翻看React的原始碼,或者其他的基於虛擬DOM的程式碼。他們程式碼量都太大,太複雜。然而要實現一個虛擬DOM的主要部分只需要大約50行的程式碼。50行程式碼!!
下面就是那兩個你要知道的事情:
- 虛擬DOM和真實DOM的有某種對應關係
- 我們在虛擬DOM樹的更改會生成另外一個虛擬DOM樹。我們會用一種演算法來比較兩個樹有哪些不同,然後對真實的DOM做最小的更改。
下面我們就來看看這兩條是如何實現的。
生成虛擬DOM樹
首先我們需要在記憶體裡儲存我們的DOM樹。只要使用js就可以達到這個目的。假設我們有這樣的一個樹:
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ur>
看起來非常簡單對吧。我們怎麼用js的物件來對應到這個樹呢?
{ type: `ul`, props: {`class`: `list}, children: [
{type: `li`, props: {}, children: [`item 1`]},
{type: `li`, props: {}, children: [`item 2`]}
]}
這裡我們會注意到兩件事:
- 我們使用這樣的物件來對應到真實的DOM上:
{type: `...`, props: {...}, children: [...]}
。 -
DOM的文字節點會對應到js的字串上。
但是如果用這個方法來對應到巨大的DOM樹的話那將是非常困難的。所以我們來寫一個helper方法,這樣結構上也就容易理解一些:function h(type, props, ...children) { return {type, props, children}; }
現在我們可以這樣生成一個虛擬DOM樹:
h(`ul`, {`class`: `list`}, h(`li`, {}, `item 1`), h(`li`, {}, `item 2`), )
這樣看起來就清晰了很多。但是我們還可以做的更好。你應該聽說過JSX對吧。是的,我們也要用那種方式。但是,這個應該如何下手呢?
如果你讀過Babel的JSX文件的話,你就會知道這些都是Babel的功勞。Babel會把下面的程式碼轉碼:
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
轉碼為:
React.createElement(`ul`, {className: `list`}),
React.createElement(`li`, {}, `item 1`),
React.createElement(`li`, {}, `item 2`)
);
你注意到多相似了嗎?如果把React.createElement(...)
體換成我們自己的h
方法的話,那我們也已使用類似於JSX的語法。我們只需要在我們的檔案最頂端加這麼一句話:
/** @jsx h */
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
這一行/** @jsx h */
就是在告訴Babel“大兄弟,按照jsx的方式轉碼,但是不要用React.createElement
, 使用h
。你可以使用任意的東西來代替h。
那麼把上面我們說的總結一下,我們會這樣寫我們的虛擬DOM:
/** @jsx h */
const a = {
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
};
然後Babel就會轉碼成這樣:
const a = {
h(`ul`, {className: `list`},
h(`li`, {}, `item 1`),
h(`li`, {}, `item 2`),
)
};
當方法h
執行的時候,它就會返回js的物件–我們的虛擬DOM樹。
const a = (
{ type: ‘ul’, props: { className: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
);
應用我們的DOM展示
現在我們的DOM樹用純的JS物件來代表了。很酷了。但是我們需要根據這些建立實際的DOM。因為我們不能只是把虛擬節點轉換後直接載入DOM裡。
首先我們來定義一些假設和一些術語:
- 實際的DOM都會使用
$
開頭的變數來表示。所以$parent
是一個實際的DOM。 - 虛擬DOM使用node變數表示
- 和React一樣,你只可以有一個根節點。其他的節點都在某個根節點裡。
我們來寫一個方法:createElement()
,這個方法可以接收一個虛擬節點之後返回一個真實的DOM節點。先不考慮props
和children
,這個之後會有介紹。
function createElement(node) {
if(typeof node === `string`) {
return document.createTextNode(node);
}
return document.createElement(node.type);
}
因為我們不僅需要處理文字節點(js的字串),還要處理各種元素(element)。這些元素都是想js的物件一樣的:
{ type: `-`, props: {...}, children: [...]}
我們可以用這個結構來處理文字節點和各種element了。
那麼子節點如何處理呢,他們也基本是文字節點或者各種元素。這些子節點也可以用createElement()
方法來處理。父節點和子節點都使用這個方法,看到了麼?其實這就是遞迴處理了。我們可以呼叫createElement
方法來建立子節點,然後用appendChild
方法來把他們新增到根節點上。
function createElement(node) {
if(typeof node === `string`) {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
看起來還不錯,我們先不考慮節點的props
。要理解虛擬節點的概念並不需要這些東西卻會增加很多的複雜度。
處理修改
我們可以把虛擬節點轉化為真實的DOM了。現在該考慮比較我們的虛擬樹了。基本上我們需要寫一點演算法了。虛擬樹的比較需要用到這個演算法,比較之後只做必要的修改。
如何比較樹的不同?
- 如果新節點的子節點增加了,那麼我們就需要呼叫
appendChild
方法來新增。
//new
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
//old
<ul>
<li>item 1</li>
</ul>
- 新節點比舊節點的子節點少,那麼就需要呼叫
removeChild
方法來刪除掉多餘的子節點。
//new
<ul>
<li>item 1</li>
</ul>
//old
<ul>
<li>item 1</li>
<li>item 2</li> // 這個要被刪掉
</ul>
- 新舊節點的某個子節點不同,也就是某個節點上發生了修改。那麼,我們就呼叫
replaceChild
方法。
//new
<div>
<p>hi there!</p>
<p>hello</p>
</div>
//old
<div>
<p>hi there!</p>
<button>click it</button> //發生了修改,變成了new裡的<p />節點
</div>
- 各節點都一樣。那麼我們就需要做進一步的比較
//new
<ul>
<li>item 1</li>
<li> //*
<span>hello</span>
<span>hi!</span>
</li>
</ul>
//old
<ul>
<li>item 1</li>
<li> //*
<span>hello</span>
<div>hi!</div>
</li>
</ul>
加醒的兩個節點可以看到都是<li>
,是相等的。但是它的子節點裡面卻有不同的節點。
我們來寫一個方法updateElement
,它接收三個引數:$parent
、newNode
和oldNode
。$parent
是真的DOM元素。它是我們虛擬節點的父節點。現在我們來看看如何處理上面提到的全部問題。
沒有舊節點
這個問題很簡單:
function updateElement($parent, newNode, oldNode) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}
沒有新節點
如果當前沒有新的虛擬節點,我們就應該把它從真的DOM裡刪除掉。但是,如何做到呢?我們知道父節點(作為引數傳入了方法),那麼我們就可以呼叫$parent.removeChild
方法,並傳入真DOM的引用。但是我們無法得到它,如果我們知道的節點在父節點的位置,就可以用$parent.childNodes[index]
來獲取它的引用。index
就是節點的位置。
假設index
也作為引數傳入了我們的方法,我們的方法就可以這麼寫:
function updateElement($parent, newNode, oldNode, index = 0) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode);
);
} else if(!newNode) {
$parent.removeChild(
$parent.childNodes[index];
);
}
}
節點改變
首先寫一個方法來比較兩個節點(新的和舊的)來區分節點是否發生了改變。要記住,節點可以是文字節點,也可以是元素(element):
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === `string` && node1 !== node2 ||
node1.type !== node2.type;
}
現在有了當前節點的index
了,index就是當前節點在父節點的位置。這樣可以很容易用新建立的節點來代替當前節點了。
function updateElement($parent, newNode, oldNode, index = 0) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode);
);
} else if(!newNode) {
$parent.removeChild(
$parent.childNOdes[index];
);
} else if(chianged(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}
對比子節點的不同
最後,需要遍歷新舊節點的子節點,並比較他們。可以在每個節點上都使用updateElement
方法。是的,遞迴。
但是在開始程式碼之前需要考慮一些問題:
- 只有在節點是一個元素(element)的時候再去比較子節點(文字節點不可能有子節點)。
- 當前節點作為父節點傳入方法中。
- 我們要一個一個的比較子節點,即使會遇到
undefined
的情況。沒有關係,我們的方法可以處理。 -
index
,當前節點在直接父節點中的位置。
function updateElement($parent, newNode, oldNode, index = 0) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode);
);
} else if(!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if(changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent,childNodes[index]
);
} else if(newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
在JSFiddle裡看看程式碼把!
結語
祝賀你!我們搞定了。我們寫出了虛擬節點的實現。從上面的例子中你已經可以理解虛擬節點的概念了,也大體可以知道React是如何運作的了。
當時還有很多需要講述的內容,其中包括:
- 設定節點的屬性(props)和比較、更新他們
- 處理事件,在元素上新增事件監聽器
- 讓我們的節點像React的Component那樣運作
- 獲取實際DOM的引用
- 虛擬節點和其他的庫一起使用來修改真實的DOM,這些庫有jQuery等其他的類似的庫。
- 更多。。
原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
本文轉自張昺華-sky部落格園部落格,原文連結:http://www.cnblogs.com/sunshine-anycall/p/7897140.html,如需轉載請自行聯絡原作者
相關文章
- 一步一步帶你實現一個canvas抽獎轉盤Canvas
- 一步一步帶你掌握webpack(一)——入門Web
- 一步一步帶你掌握webpack(二)——資產管理Web
- 一步一步帶你掌握webpack(四)——開發Web
- 一步步帶你實現簡版 ButterKnife
- 一步一步帶你掌握webpack(三)——輸出管理Web
- 一步一步實現一個PromisePromise
- 帶你一步一步瞭解Python中的ClassPython
- 一步一步帶你瞭解EventBus3.1.1 原始碼S3原始碼
- 一步一步實現手寫PromisePromise
- 一步一步,實現自己的ButterKnife(一)
- 一步一步帶你封裝基於react的modal元件封裝React元件
- Linux驅動實踐:帶你一步一步編譯核心驅動程式Linux編譯
- 一步一步,實現自己的ButterKnife(二)
- 一步一步實現單身狗雨
- promise原理—一步一步實現一個promisePromise
- 帶你一步一步手寫一個簡單的Spring MVCSpringMVC
- 一步一步帶你認識MVP+Retrofit+Rxjava並封裝(一)MVPRxJava封裝
- 一步一步實現Vue資料繫結Vue
- 一步一步實現 .NET 8 部署到 DockerDocker
- 一步一步實現現代前端單元測試前端
- 一步一步帶你認識MVP+Retrofit+Rxjava並封裝(二)MVPRxJava封裝
- 一步一步帶你做WebApi遷移ASP.NET Core2.0WebAPIASP.NET
- 【前端Talkking】CSS系列——一步一步帶你認識animation動畫效果前端CSS動畫
- 細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現記憶體
- 一步步帶你實現Android網路狀態監聽Android
- 一步一步用Delphi6實現Web Service (轉)Web
- 帶你一步一步手撕Spring MVC原始碼加手繪流程圖SpringMVC原始碼流程圖
- 手挽手帶你學React:四檔(下篇)一步一步學會react-reduxReactRedux
- 一步一步教你實現iOS音訊頻譜動畫(一)iOS音訊動畫
- 跟我一步一步實現 Flutter 視訊播放外掛 (一)Flutter
- 一步一步實現一個Promise A+規範的 PromisePromise
- 帶你一步一步探索Flutter(一)-- Flutter初體驗以及認識常用的WidgetFlutter
- 帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分MyBatis原始碼流程圖
- 一步一步教你實現iOS音訊頻譜動畫(二)iOS音訊動畫
- TensorFlow 一步一步實現卷積神經網路卷積神經網路
- 一步一步實現一個符合Promises/A+規範的PromisePromise
- 從零開始帶你一步一步使用YOLOv3測試自己的資料YOLO