在上一篇文章中我們實現了節點建立和渲染,但是忽略元件的情況,這一篇,我們來說說元件如何渲染,並實現一個setState,來初步完成我們自己的React
React元件
在react中元件大體分為兩種,一種是一個純函式,沒有生命週期的。另一個通過繼承自React.Component
的類來實現。
我們先來寫一個Component
類。
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateComponent(this);
}
}
複製程式碼
我們完成了一個Component
類,同時該類的例項有一個setState
函式,用來更新該元件。updateComponent
我們下面會實現它。
createNode函式
我們前面提到過虛擬節點的概念,但是我們但是直接使用Element
下面這種形式來作為我們的虛擬節點的。
{
type: 'div'
props: {}
}
複製程式碼
但是但我們要更新我們的元件是,我們需要記錄Element
和DOM節點之間的關係,為了不汙染Element
,我們引入我們新的虛擬節點的概念,我這裡稱之為Node
(實際情況並不是如此,這裡只是為了方便命名)。之後相關的虛擬幾點命名也會採用xxxNode
的命名方式。下面我們來改造我們以前的render
函式,將其重新命名為createNode
並且只接受一個型別為Element
的引數,其返回值為一個Node
型別的節點。
function createNode(element) {
const { type, props } = element;
// 是文字節點則建立文字節點,這裡建立一個空的文字節點,後面利用nodeValue直接給該節點賦值
const isTextNode = type === 'TEXT ELEMENT';
const isComponent = typeof type === 'function';
// 元件情況
if (isComponent) {
const instance = new type(props);
let childElement = null;
if (instance.render) {
// 類情況
childElement = instance.render();
} else {
// 函式情況,直接執行
childElement = type(props);
}
// 建立Node節點
const childNode = createNode(childElement);
const dom = childNode.dom;
const node = { dom, element, childNodes: childNode.childNodes || [] };
// 在例項中記錄舊的node節點,以便之後進行更新
instance._internalNode = node;
return node;
}
// dom情況
const childElements = props.children || [];
const childDom = isTextNode
? document.createTextNode('')
: document.createElement(type);
const isEvent = name => name.startsWith('on');
const isAttribute = name => !isEvent(name) && name !== 'children';
// 繫結事件
Object.keys(props).filter(isEvent).forEach(name => {
const eventName = name.toLowerCase().substring(2);
childDom.addEventListener(eventName, props[name]);
});
// 新增屬性
Object.keys(props).filter(isAttribute).forEach(name => {
childDom[name] = props[name];
});
// 遞迴建立
const childNodes = childElements.map(createNode);
// 掛載到父節點
return { dom: childDom, element, childNodes }
}
複製程式碼
從上面可以看到,我們的虛擬節點Node
記錄我們需要的資訊,如element、dom、childrenNodes等,它是一個物件,結構是:
{ dom, element, childNodes }
複製程式碼
render函式
有了createNode
函式,現在再寫我們的render函式:
function render(element, containerDom) {
// 獲取虛擬節點
const node = createNode(element);
// 獲取對應的dom元素
const childDom = node.dom;
// 獲取子虛擬節點
const childNodes = node.childNodes || [];
// 渲染子虛擬節點
childNodes.forEach(childNode => render(childNode.element, childDom));
// 掛載至容器dom節點
containerDom.appendChild(childDom);
}
複製程式碼
在render
函式中,我們所需要做的就是獲取虛擬節點並直接渲染它,並且需要同時渲染其孩子節點,最後掛載到根元素就完成了我們的渲染過程。
到這裡我們已經可以渲染元件了,我們還有一個setState
需要實現,實現setState
就需要我們上面提到的updateComponent
函式了。
updateComponent函式
updateComponent
接收一個元件例項,它需要做哪些事情那?我們想一下,其實很簡單,它只需要拿到舊的dom節點,然後渲染新的dom節點,最後將舊的替換為新的就能夠實現重新整理的效果了。在這裡我們上面在例項中儲存的_internalNode
就能發揮作用了。它記錄了舊節點的所有資訊。下面來實現吧:
function updateComponent(instance) {
// 執行render函式,得到要渲染的element
const childElement = instance.render();
// 舊的虛擬節點
const internalNode = instance._internalNode;
// 獲取要掛載的父親節點
const parentDom = internalNode.dom.parentNode;
// 獲取新的虛擬節點
const newNode = createNode(childElement);
// 更新虛擬幾點
instance._internalNode = newNode;
// 渲染孩子節點
const newDom = newNode.dom;
(newNode.childNodes || []).forEach(childNode => render(childNode.element, newDom));
// 將舊dom節點替換為新的dom節點
parentDom.replaceChild(newDom, internalNode.dom);
}
複製程式碼
實現完成,現在我們已經可以更新我們的元件了。這是codepen中的例項。
在元件的實現過程中為了簡化,我們去掉了元件的生命週期,它需要作為一個個鉤子掛載在不同的位置。如果你使用了例項,或者自己跑過之後會發現,我們每次更新都要重新渲染整個dom樹,這樣代價很大,在React中使用了一種diff
演算法來重用不需要更新的節點和屬性,下一節我們就來實現React中的diff
演算法reconciliation
。
這是github原文地址。接下來我會持續更新,歡迎star,歡迎watch。
實現React系列列表: