- 1.(Didact)一個DIY教程:創(chuàng)建你自己的react
- 2.渲染dom元素
- 3.JSX和創(chuàng)建元素
- 4.虛擬DOM和調(diào)和過程
- 5.組件和狀態(tài)(state)
1.(Didact)一個DIY教程:創(chuàng)建你自己的react
[更新]這個系列從老的react架構(gòu)寫起,你可以跳過前面,直接看使用新的fiber架構(gòu)重寫的:文章
[更新2]聽Dan的沒錯,我是認真的?
這篇深入fiber架構(gòu)的文章真的很棒。
—?@dan_abramov
1.1 引言
很久以前,當學數(shù)據(jù)結(jié)構(gòu)和算法時,我有個作業(yè)就是實現(xiàn)自己的數(shù)組,鏈表,隊列,和棧(用Modula-2語言)。那之后,我再也沒有過要自己來實現(xiàn)鏈表的需求。總會有庫讓我不需要自己重造輪子。
所以,那個作業(yè)還有意義嗎?當然,我從中學到了很多,知道如何合理使用各種數(shù)據(jù)結(jié)構(gòu),并知道根據(jù)場景合理選用它們。
這個系列文章以及對應的(倉庫)的目的也是一樣,不過要實現(xiàn)的是一個,我們比鏈表使用更多的東西:React
我好奇如果不考慮性能和設備兼容性,POSIX(可移植操作系統(tǒng)接口)核心可以實現(xiàn)得多么小而簡單。
—?@ID_AA_Carmack
??????我對react也這么好奇
幸運的是,如果不考慮性能,調(diào)試,平臺兼容性等等,react的主要3,4個特性重寫并不難。事實上,它們很簡單,甚至只要不足200行代碼
這就是我們接下來要做的事,用不到200行代碼寫一個有一樣的API,能跑的React。因為這個庫的說教性(didactic)特點,我們打算就稱之為Didact
用Didact寫的應用如下:
const stories = [
{ name: "Didact introduction", url: "http://bit.ly/2pX7HNn" },
{ name: "Rendering DOM elements ", url: "http://bit.ly/2qCOejH" },
{ name: "Element creation and JSX", url: "http://bit.ly/2qGbw8S" },
{ name: "Instances and reconciliation", url: "http://bit.ly/2q4A746" },
{ name: "Components and state", url: "http://bit.ly/2rE16nh" }
];
class App extends Didact.Component {
render() {
return (
<div>
<h1>Didact Stories</h1>
<ul>
{this.props.stories.map(story => {
return <Story name={story.name} url={story.url} />;
})}
</ul>
</div>
);
}
}
class Story extends Didact.Component {
constructor(props) {
super(props);
this.state = { likes: Math.ceil(Math.random() * 100) };
}
like() {
this.setState({
likes: this.state.likes + 1
});
}
render() {
const { name, url } = this.props;
const { likes } = this.state;
const likesElement = <span />;
return (
<li>
<button onClick={e => this.like()}>{likes}<b>??</b></button>
<a href={url}>{name}</a>
</li>
);
}
}
Didact.render(<App stories={stories} />, document.getElementById("root"));
這就是我們在這個系列文章里要使用的例子。效果如下

我們將會從下面幾點來一步步添加Didact的功能:
這個系列暫時不講的地方:
- Functional components
- Context(上下文)
- 生命周期方法
- ref屬性
- 通過key的調(diào)和過程(這里只講根據(jù)子節(jié)點原順序的調(diào)和)
- 其他渲染引擎 (只支持DOM)
- 舊瀏覽器支持
你可以從react實現(xiàn)筆記,Paul O’Shannessy的這個youtube演講視頻,或者react倉庫地址,找到更多關于如何實現(xiàn)react的細節(jié).
2.渲染dom元素
2.1 什么是DOM
開始之前,讓我們回想一下,我們經(jīng)常使用的DOM API
// Get an element by id
const domRoot = document.getElementById("root");
// Create a new element given a tag name
const domInput = document.createElement("input");
// Set properties
domInput["type"] = "text";
domInput["value"] = "Hi world";
domInput["className"] = "my-class";
// Listen to events
domInput.addEventListener("change", e => alert(e.target.value));
// Create a text node
const domText = document.createTextNode("");
// Set text node content
domText["nodeValue"] = "Foo";
// Append an element
domRoot.appendChild(domInput);
// Append a text node (same as previous)
domRoot.appendChild(domText);
注意到我們設置元素的屬性而不是特性屬性和特性的區(qū)別,只有合法的屬性才可以設置。
2.2 Didact元素
我們用js對象來描述渲染過程,這些js對象我們稱之為Didact元素.這些元素有2個屬性,type和props。type可以是一個字符串或者方法。在后面講到組件之前,我們先用字符串。props是一個可以為空的對象(不過不能為null)。props可能有children屬性,這個children屬性是一個Didact元素的數(shù)組。
我們將多次使用Didact元素,目前我們先稱之為元素。不要和html元素混淆,在變量命名的時候,我們稱它們?yōu)镈OM元素或者dom(preact就是這么做的)
一個元素就像下面這樣:
const element = {
type: "div",
props: {
id: "container",
children: [
{ type: "input", props: { value: "foo", type: "text" } },
{ type: "a", props: { href: "/bar" } },
{ type: "span", props: {} }
]
}
};
對應描述下面的dom:
<div id="container">
<input value="foo" type="text">
<a href="/bar"></a>
<span></span>
</div>
Didact元素和react元素很像,但是不像react那樣,你可能使用JSX或者createElement,創(chuàng)建元素就和創(chuàng)建js對象一樣.Didatc我們也這么做,不過在后面章節(jié)我們再加上create元素的代碼
2.3 渲染dom元素
下一步是渲染一個元素以及它的children到dom里。我們將寫一個render方法(對應于react的ReactDOM.render),它接受一個元素和一個dom 容器。然后根據(jù)元素的定義生成dom樹,附加到容器里。
function render(element, parentDom) {
const { type, props } = element;
const dom = document.createElement(type);
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
parentDom.appendChild(dom);
}
我們?nèi)匀粵]有對其添加屬性和事件監(jiān)聽。現(xiàn)在讓我們使用object.keys來遍歷props屬性,設置對應的值:
function render(element, parentDom) {
const { type, props } = element;
const dom = document.createElement(type);
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
parentDom.appendChild(dom);
}
2.4 渲染DOM文本節(jié)點
現(xiàn)在render函數(shù)不支持的就是文本節(jié)點,首先我們定義文本元素什么樣子,比如,在react中描述<span>Foo</span>:
const reactElement = {
type: "span",
props: {
children: ["Foo"]
}
};
注意到子節(jié)點,只是一個字符串,并不是其他元素對象。這就讓我們的Didact元素定義不合適了:children元素應該是一個數(shù)組,數(shù)組里的元素都有type和props屬性。如果我們遵守這個規(guī)則,后面將減少不必要的if判斷.所以,Didact文本元素應該有一個“TEXT ELEMENT”的類型,并且有在對應的節(jié)點有文本的值。比如:
const textElement = {
type: "span",
props: {
children: [
{
type: "TEXT ELEMENT",
props: { nodeValue: "Foo" }
}
]
}
};
現(xiàn)在我們來定義文本元素應該如何渲染。不同的是,文本元素不使用createElement方法,而用createTextNode代替。節(jié)點值就和其他屬性一樣被設置上去。
function render(element, parentDom) {
const { type, props } = element;
// Create DOM element
const isTextElement = type === "TEXT ELEMENT";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
// Add event listeners
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});
// Set properties
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});
// Render children
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
// Append to parent
parentDom.appendChild(dom);
}
2.5 總結(jié)
我們現(xiàn)在創(chuàng)建了一個可以渲染元素以及子元素的render方法。后面我們需要實現(xiàn)如何創(chuàng)建元素。我們將在下節(jié)講到如何使JSX和Didact很好地融合。
3.JSX和創(chuàng)建元素
3.1 JSX
我們之前講到了Didact元素,講到如何渲染到DOM,用一種很繁瑣的方式.這一節(jié)我們來看看如何使用JSX簡化創(chuàng)建元素的過程。
JSX提供了一些創(chuàng)建元素的語法糖,不用使用下面的代碼:
const element = {
type: "div",
props: {
id: "container",
children: [
{ type: "input", props: { value: "foo", type: "text" } },
{
type: "a",
props: {
href: "/bar",
children: [{ type: "TEXT ELEMENT", props: { nodeValue: "bar" } }]
}
},
{
type: "span",
props: {
onClick: e => alert("Hi"),
children: [{ type: "TEXT ELEMENT", props: { nodeValue: "click me" } }]
}
}
]
}
};
我們現(xiàn)在可以這么寫:
const element = (
<div id="container">
<input value="foo" type="text" />
<a href="/bar">bar</a>
<span onClick={e => alert("Hi")}>click me</span>
</div>
);
如果你不熟悉JSX的話,你可能懷疑下面的代碼是否是合法的js--它確實不是。要讓瀏覽器理解它,上面的代碼必須使用預處理工具處理。比如babel.babel會把上面的代碼轉(zhuǎn)成下面這樣:
const element = createElement(
"div",
{ id: "container" },
createElement("input", { value: "foo", type: "text" }),
createElement(
"a",
{ href: "/bar" },
"bar"
),
createElement(
"span",
{ onClick: e => alert("Hi") },
"click me"
)
);
支持JSX我們只要在Didact里添加一個createElement方法。其他事的交給預處理器去做。這個方法的第一個參數(shù)是元素的類型type,第二個是含有props屬性的對象,剩下的參數(shù)都是子節(jié)點children。createElement方法需要創(chuàng)建一個對象,并把第二個參數(shù)上所有的值賦給它,把第二個參數(shù)后面的所有參數(shù)放到一個數(shù)組,并設置到children屬性上,最后返回一個有type和props的對象。用代碼實現(xiàn)很容易:
function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
props.children = hasChildren ? [].concat(...args) : [];
return { type, props };
}
同樣,這個方法對文本元素不適用。文本的子元素是作為字符串傳給createElement方法的。但是我們的Didact需要文本元素一樣有type和props屬性。所以我們要把不是didact元素的參數(shù)都轉(zhuǎn)成一個'文本元素'
const TEXT_ELEMENT = "TEXT ELEMENT";
function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? [].concat(...args) : [];
props.children = rawChildren
.filter(c => c != null && c !== false)
.map(c => c instanceof Object ? c : createTextElement(c));
return { type, props };
}
function createTextElement(value) {
return createElement(TEXT_ELEMENT, { nodeValue: value });
}
我同樣從children列表里過濾了null,undefined,false參數(shù)。我們不需要把它們加到props.children上因為我們根本不會去渲染它們。
3.2總結(jié)
到這里我們并沒有為Didact加特殊的功能.但是我們有了更好的開發(fā)體驗,因為我們可以使用JSX來定義元素。我已經(jīng)更新了codepen上的代碼。因為codepen用babel轉(zhuǎn)譯JSX,所以以/** @jsx createElement */開頭的注釋都是為了讓babel知道使用哪個函數(shù)。
你同樣可以查看github提交
下面我們將介紹Didact用來更新dom的虛擬dom和所謂的調(diào)和算法.
4.虛擬DOM和調(diào)和過程
到目前為止,我們基于JSX的描述方式實現(xiàn)了dom元素的創(chuàng)建機制。這里開始,我們專注于怎么更新DOM.
在下面介紹setState之前,我們之前更新DOM的方式只有再次調(diào)用render()方法,傳入不同的元素。比如:我們要渲染一個時鐘組件,代碼是這樣的:
const rootDom = document.getElementById("root");
function tick() {
const time = new Date().toLocaleTimeString();
const clockElement = <h1>{time}</h1>;
render(clockElement, rootDom);
}
tick();
setInterval(tick, 1000);
我們現(xiàn)在的render方法還做不到這個。它不會為每個tick更新之前同一個的div,相反它會新添一個新的div.第一種解決辦法是每一次更新,替換掉div.在render方法的最下面,我們檢查父元素是否有子元素,如果有,我們就用新元素生產(chǎn)的dom替換它:
function render(element, parentDom) {
// ...
// Create dom from element
// ...
// Append or replace dom
if (!parentDom.lastChild) {
parentDom.appendChild(dom);
} else {
parentDom.replaceChild(dom, parentDom.lastChild);
}
}
在這個小列子里,這個辦法很有效。但在復雜情況下,這種重復創(chuàng)建所有子節(jié)點的方式并不可取。所以我們需要一種方式,來對比當前和之前的元素樹之間的區(qū)別。最后只更新不同的地方。
4.1 虛擬DOM和調(diào)和過程
React把這種diff過程稱之為調(diào)和過程,我們現(xiàn)在也這么稱呼它。首先我們要保存之前的渲染樹,從而可以和新的樹對比。換句話說,我們將實現(xiàn)自己的DOM,虛擬dom.
這種虛擬dom的‘節(jié)點’應該是什么樣的呢?首先考慮使用我們的Didact元素。它們已經(jīng)有一個props.children屬性,我們可以根據(jù)它來創(chuàng)建樹。但是這依然有兩個問題,一個是為了是調(diào)和過程容易些,我們必須為每個虛擬dom保存一個對真實dom的引用,并且我們更希望元素都不可變(imumutable).第二個問問題是后面我們要支持組件,組件有自己的狀態(tài)(state),我們的元素還不能處理那種。
4.2 實例(instance)
所以我們要介紹一個新的名詞:實例。實例代表的已經(jīng)渲染到DOM中的元素。它其實是一個有著,element,dom,chilInstances屬性的JS普通對象。childInstances是有著該元素所以子元素實例的數(shù)組。
注意我們這里提到的實例和Dan Abramov在react組件,元素和實列這篇文章提到實例不是一個東西。他指的是React調(diào)用繼承于React.component的那些類的構(gòu)造函數(shù)所獲得的‘公共實例’(public instances)。我們會在以后把公共實例加上。
每一個DOM節(jié)點都有一個相應的實例。調(diào)和算法的一個目標就是盡量避免創(chuàng)建和刪除實例。創(chuàng)建刪除實例意味著我們在修改DOM,所以重復利用實例就是越少地修改dom樹。
4.3 重構(gòu)
我們來重寫render方法,保留同樣健壯的調(diào)和算法,但添加一個實例化方法來根據(jù)給定的元素生成一個實例(包括其子元素)
let rootInstance = null;
function render(element, container) {
const prevInstance = rootInstance;
const nextInstance = reconcile(container, prevInstance, element);
rootInstance = nextInstance;
}
function reconcile(parentDom, instance, element) {
if (instance == null) {
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else {
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
}
}
function instantiate(element) {
const { type, props } = element;
// Create DOM element
const isTextElement = type === "TEXT ELEMENT";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
// Add event listeners
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});
// Set properties
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});
// Instantiate and append children
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
}
這段代碼和之前一樣,不過我們對上一次調(diào)用render方法保存了實例,我們也把調(diào)和方法和實例化方法分開了。
為了復用dom節(jié)點而不需要重新創(chuàng)建dom節(jié)點,我們需要一種更新dom屬性(className,style,onClick等等)的方法。所以,我們將把目前用來設置屬性的那部分代碼抽出來,作為一個更新屬性的更通用的方法。
function instantiate(element) {
const { type, props } = element;
// Create DOM element
const isTextElement = type === "TEXT ELEMENT";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
updateDomProperties(dom, [], props);
// Instantiate and append children
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
}
function updateDomProperties(dom, prevProps, nextProps) {
const isEvent = name => name.startsWith("on");
const isAttribute = name => !isEvent(name) && name != "children";
// Remove event listeners
Object.keys(prevProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// Remove attributes
Object.keys(prevProps).filter(isAttribute).forEach(name => {
dom[name] = null;
});
// Set attributes
Object.keys(nextProps).filter(isAttribute).forEach(name => {
dom[name] = nextProps[name];
});
// Add event listeners
Object.keys(nextProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
updateDomProperties 方法刪除所有舊屬性,然后添加上新的屬性。如果屬性沒有變,它還是照做一遍刪除添加屬性。所以這個方法會做很多無謂的更新,為了簡單,目前我們先這樣寫。
4.4 復用dom節(jié)點
我們說過調(diào)和算法會盡量復用dom節(jié)點.現(xiàn)在我們?yōu)檎{(diào)和(reconcile)方法添加一個校驗,檢查是否之前渲染的元素和現(xiàn)在渲染的元素有一樣的類型(type),如果類型一致,我們將重用它(更新舊元素的屬性來匹配新元素)
function reconcile(parentDom, instance, element) {
if (instance == null) {
// Create instance
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (instance.element.type === element.type) {
// Update instance
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.element = element;
return instance;
} else {
// Replace instance
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
}
}
4.5 子元素的調(diào)和
現(xiàn)在調(diào)和算法少了重要的一步,忽略了子元素。子元素調(diào)和是react的關鍵。它需要元素上一個額外的key屬性來匹配之前和現(xiàn)在渲染樹上的子元素.我們將實現(xiàn)一個該算法的簡單版。這個算法只會匹配子元素數(shù)組同一位置的子元素。它的弊端就是當兩次渲染時改變了子元素的排序,我們將不能復用dom節(jié)點。
實現(xiàn)這個簡單版,我們將匹配之前的子實例 instance.childInstances 和元素子元素 element.props.children,并一個個的遞歸調(diào)用調(diào)和方法(reconcile)。我們也保存所有reconcile返回的實例來更新childInstances。
function reconcile(parentDom, instance, element) {
if (instance == null) {
// Create instance
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (instance.element.type === element.type) {
// Update instance
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
// Replace instance
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
}
}
function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
return newChildInstances;
}
4.6 刪除Dom節(jié)點
如果nextChildElements數(shù)組比childInstances數(shù)組長度長,reconcileChildren將為所有子元素調(diào)用reconcile方法,并傳入一個undefined實例。這沒什么問題,因為我們的reconcile方法里if (instance == null)語句已經(jīng)處理了并創(chuàng)建新的實例。但是另一種情況呢?如果childInstances數(shù)組比nextChildElements數(shù)組長呢,因為element是undefined,這將導致element.type報錯。
這是我們并沒有考慮到的,如果我們是從dom中刪除一個元素情況。所以,我們要做兩件事,在reconcile方法中檢查element == null的情況并在reconcileChildren方法里過濾下childInstances
function reconcile(parentDom, instance, element) {
if (instance == null) {
// Create instance
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
// Remove instance
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type === element.type) {
// Update instance
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
// Replace instance
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
}
}
function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
return newChildInstances.filter(instance => instance != null);
}
4.7 總結(jié)
這一章我們增強了Didact使其支持更新dom.我們也通過重用dom節(jié)點避免大范圍dom樹的變更,使didact性能更好。另外也使管理一些dom內(nèi)部的狀態(tài)更方便,比如滾動位置和焦點。
這里我更新了codepen,在每個狀態(tài)改變時調(diào)用render方法,你可以在devtools里查看我們是否重建dom節(jié)點。

因為我們是在根節(jié)點調(diào)用render方法,調(diào)和算法是作用在整個樹上。下面我們將介紹組件,組件將允許我們只把調(diào)和算法作用于其子樹上。
5.組件和狀態(tài)(state)
5.1 回顧
我們上一章的代碼有幾個問題:
- 每一次變更觸發(fā)整個虛擬樹的調(diào)和算法
- 狀態(tài)是全局的
- 當狀態(tài)變更時,我們需要顯示地調(diào)用render方法
組件解決了這些問題,我們可以:
- 為jsx定義我們自己的‘標簽’
- 生命周期的鉤子(我們這章不講這個)
5.2 組件類
首先我們要提供一個供組件繼承的Component的基類。我們還需要提供一個含props參數(shù)的構(gòu)造方法,一個setState方法,setState接收一個partialState參數(shù)來更新組件狀態(tài):
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
}
}
我們的應用里將和其他元素類型(div或者span)一樣繼承這個類再這樣使用:<Mycomponent/>。注意到我們的createElement方法不需要改變?nèi)魏螙|西,createElement會把組件類作為元素的type,并正常的處理props屬性。我們真正需要的是一個根據(jù)所給元素來創(chuàng)建組件實例(我們稱之為公共實例)的方法。
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
const publicInstance = new type(props);
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
除了創(chuàng)建公共實例外,我們保留了對觸發(fā)組件實例化的內(nèi)部實例(從虛擬dom)引用,我們需要當公共實例狀態(tài)發(fā)生變化時,能夠只更新該實例的子樹。
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}
function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
我們也需要更新實例化方法。對組件而言,我們需要創(chuàng)建公共實例,然后調(diào)用組件的render方法來獲取之后要再次傳給實例化方法的子元素:
function instantiate(element) {
const { type, props } = element;
const isDomElement = typeof type === "string";
if (isDomElement) {
// Instantiate DOM element
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
updateDomProperties(dom, [], props);
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
} else {
// Instantiate component element
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;
Object.assign(instance, { dom, element, childInstance, publicInstance });
return instance;
}
}
組件的內(nèi)部實例和dom元素的內(nèi)部實例不同,組件內(nèi)部實例只能有一個子元素(從render函數(shù)返回),所以組件內(nèi)部只有childInstance屬性,而dom元素有childInstances數(shù)組。另外,組件內(nèi)部實例需要有對公共實例的引用,這樣在調(diào)和期間,才可以調(diào)用render方法。
唯一缺失的是處理組件實例調(diào)和,所以我們將為調(diào)和算法添加一些處理。如果組件實例只能有一個子元素,我們就不需要處理子元素的調(diào)和,我們只需要更新公共實例的props屬性,重新渲染子元素并調(diào)和算法它:
function reconcile(parentDom, instance, element) {
if (instance == null) {
// Create instance
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
// Remove instance
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
// Replace instance
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === "string") {
// Update dom instance
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
//Update composite instance
instance.publicInstance.props = element.props;
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(parentDom, oldChildInstance, childElement);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}
這就是全部代碼了,我們現(xiàn)在支持組件,我更新了codepen,我們的應用代碼就像下面這樣:
const stories = [
{ name: "Didact introduction", url: "http://bit.ly/2pX7HNn" },
{ name: "Rendering DOM elements ", url: "http://bit.ly/2qCOejH" },
{ name: "Element creation and JSX", url: "http://bit.ly/2qGbw8S" },
{ name: "Instances and reconciliation", url: "http://bit.ly/2q4A746" },
{ name: "Components and state", url: "http://bit.ly/2rE16nh" }
];
class App extends Didact.Component {
render() {
return (
<div>
<h1>Didact Stories</h1>
<ul>
{this.props.stories.map(story => {
return <Story name={story.name} url={story.url} />;
})}
</ul>
</div>
);
}
}
class Story extends Didact.Component {
constructor(props) {
super(props);
this.state = { likes: Math.ceil(Math.random() * 100) };
}
like() {
this.setState({
likes: this.state.likes + 1
});
}
render() {
const { name, url } = this.props;
const { likes } = this.state;
const likesElement = <span />;
return (
<li>
<button onClick={e => this.like()}>{likes}<b>??</b></button>
<a href={url}>{name}</a>
</li>
);
}
}
Didact.render(<App stories={stories} />, document.getElementById("root"));
使用組件使我們可以創(chuàng)建自己的'JSX標簽',封裝組件狀態(tài),并且只在子樹上進行調(diào)和算法

最后的codepen使用這個系列的所有代碼。