這一篇主要來實現React的節點渲染部分。該篇需要用到介紹和準備中的內容。
Element概念
首先引出我們的第一個概念element
,它是如下形式:
{
type: `div`,
props: {
className: `container`,
children: [`hello world`]
}
}
複製程式碼
暫時可以先把這個當做React中虛擬節點的概念,後面會有一些變化,這裡先看做是虛擬節點。上節也提到過babel-plugin-transform-react-jsx 會幫助我幫將JSX轉換成以下形式:
// 轉換前的jsx
<div className="container">hello world</div>
// 轉換後
React.createElement("div", { className: "container" }, "hello world");
複製程式碼
函式createElement
的第一個引數字串標籤名(此處先忽略元件的情況),第二個引數是它的所接受到的屬性,從第三個引數開始,後面都是它的子節點。
createElement
理解了這些下面我們來實現我們自己的createElement
,函式引數與上面一致,返回值是一個“虛擬節點”。
function createElement(type, initProps, ...args) {
const props = Object.assign({}, initProps);
const hasChildren = args.length > 0;
// 這裡不直接使用args,而使用`[].concat(...args)`是為了扁平化陣列
// 將[1, [2], [3]]轉為[1, 2, 3]
const rawChildren = hasChildren ? [].concat(...args) : [];
// 將children屬性賦值給props
props.children = children;
return { type, props };
}
複製程式碼
在這個函式中,還有一種子節點是文字節點的情況,像我們上面寫的那樣。它跟其他節點不同,其他節點都可以通過React.createElement
函式執行後,返回一個element
。文字節點是一個字串,為了後面能夠減少if
判斷,這裡我們需要把字串的形式統一為虛擬節點的形式,下面來寫一個createTextElement
函式:
function createTextElement(text) {
return {
type: `TEXT ELEMENT`,
props: { nodeValue: text }
}
}
複製程式碼
將其型別規定為TEXT ELEMENT
的形式,同時將值放在nodeValue
裡面,方便後面直接給文字節點賦值。有了createTextElement
函式,我們需要改造下我們的createElement
函式:
function createElement(type, initProps, ...args) {
const props = Object.assign({}, initProps);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? args : [];
// 過濾null、undefined和false,不做任何渲染,同時將文字節點進行轉換
const children = rawChildren
.filter(child => child != null && child !== false)
.map(child => child instanceof Object ? child : createTextElement(child));
props.children = children;
return { type, props };
}
複製程式碼
createElement
函式到這裡完成了。
render
下一步我們要開始渲染我們的節點,就像我們使用ReactDOM.render
那樣,我們的渲染函式render
接收兩個引數,第一個是我們要渲染的element
,第二個是我們要掛載的節點。注意後面所有的命名element
都會使用xxxElement
的形式,dom
節點都會使用xxxDom
的形式。
function render(element, parentDom) {
const { type, props } = element;
// 是文字節點則建立文字節點,這裡建立一個空的文字節點,後面利用nodeValue直接給該節點賦值
const isTextNode = type === `TEXT ELEMENT`;
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];
});
// 遞迴渲染
childElements.forEach(childElement => {
render(childElement, childDom);
});
// 掛載到父節點
parentDom.appendChild(childDom);
}
複製程式碼
我們首先建立了要渲染的節點。當為文字節點時,我們使用建立了一個空節點,隨後在新增屬性的步驟,通過childDom[`nodeValue`] = name
的形式給文字節點賦值。所有onXxx
的屬性都繫結為事件,其他有效屬性都直接賦值給childDom
節點。然後對子節點進行遞迴渲染,最後掛載到父節點。整個渲染過程到這裡就完成了。
我們可以使用我們實現的函式來渲染一個介面了,這是Codepen上的演示地址。我們成功渲染了介面,但是我們忽略了元件的情況,下一篇我們來實現元件和setState。
這是github原文地址。接下來我會持續更新,歡迎star,歡迎watch。
實現React系列列表: