React系列文章:無狀態組件生成真實DOM結點
在上一篇文章中,我們總結並模擬了JSX生成真實DOM結點的過程,今天接著來介紹一下無狀態組件的生成過程。
先以下面一段簡單的代碼舉例:
const Greeting = function ({name}) {
return <div>{`hello ${name}`}</div>;
};
const App = <Greeting name="scott"/>;
console.log(App);
ReactDOM.render(App, document.getElementById(‘root‘));
可以看出,Greeting
是一個無狀態組件,我們來看看編譯過後的可執行代碼:
var Greeting = function Greeting(_ref) {
var name = _ref.name;
return React.createElement(
"div",
null,
"hello " + name
);
};
var App = React.createElement(Greeting, { name: "scott" });
console.log(App);
ReactDOM.render(App, document.getElementById(‘root‘));
我們看到,調用Greeting
組件時傳入的name
React.createElement()
方法的第二個參數中,這和前面介紹的JSX是一致的,不同的是,React.createElement()
方法的第一個參數不再是一個標簽名,而是一個函數引用,指向了我們聲明的Greeting
組件,而name
屬性也作為參數的成員出現在組件內部,這個參數名為_ref
,實則是我們熟知的props
。
下圖是我們運行上面代碼之後,打印出的App
數據結構,即虛擬DOM結構:
我們再來看一個稍微復雜些的例子:
const Greeting = function ({name}) { return ( <div>{`hello ${name}`}</div> ); }; const Container = function ({children}) { return ( <div className="container"> {children} </div> ); }; const App = ( <Container> <Greeting name="scott"/> <Greeting name="jack"/> <Greeting name="john"/> </Container> ); console.log(App); ReactDOM.render(App, document.getElementById(‘root‘));
在上面代碼中,我們定義了兩個無狀態組件,其中Container
用來作為外層的容器,Greeting
則用來顯示實際的業務視圖。
現在再來看看編譯後的代碼結構:
var Greeting = function Greeting(_ref) {
var name = _ref.name;
return React.createElement(
"div",
null,
"hello " + name
);
};
var Container = function Container(_ref2) {
var children = _ref2.children;
return React.createElement(
"div",
{ className: "container" },
children
);
};
var App = React.createElement(
Container,
null,
React.createElement(Greeting, { name: "scott" }),
React.createElement(Greeting, { name: "jack" }),
React.createElement(Greeting, { name: "john" })
);
console.log(App);
ReactDOM.render(App, document.getElementById(‘root‘));
這次我們主要觀察Container
的結構,它實際上是將React.createElement()
方法的第三個參數作為props.children
傳遞到了組件內部,而這個children
是一個Greeting
,最終是將Greeting
渲染在Container
組件內部。
接下來,我們要改進一下之前實現的React.createElement()
和ReactDOM.render()
方法,使它們支持組件的形式,模擬生成虛擬DOM和真實DOM。
先來看看React.createElement()
方法:
const React = {
// 創建DOM描述對象 即虛擬DOM
createElement(type, props, ...children) {
let propsChildren = children;
// 組件參數的props.children本身是數組
// 所以調用組件函數時這裏需要特殊處理
if (Array.isArray(children[0])) {
propsChildren = children[0];
}
// 結點
let vnode = {
type,
props: {
...props,
children: propsChildren,
}
};
// 掛載組件函數體的虛擬DOM
if (typeof type === ‘function‘) {
vnode.body = type({
...props,
children,
});
}
return vnode;
}
};
上面的代碼主要對組件做了特殊處理。如果當前處理對象是組件,則對應的type
就是函數的引用,我們會調用這個組件函數,然後將函數體的結果作為body
屬性掛載到該結點上。需要註意的是,我們在上面方法參數中使用了可變參數的形式,如果直接引用這個children
,它本身就是一個變參數組,如果組件體內使用了props.children
,那麽在調用React.createElement()
時,變參數組的形式將會是[[...]],所以我們需要特殊處理一下。
現在,我們運行程序,看看上面代碼生成的虛擬DOM結構:
最後,再來看看ReactDOM.render()
方法:
const ReactDOM = {
// 渲染真實DOM
render(vnode, container) {
let realDOM = this.generateDOM(vnode);
container.appendChild(realDOM);
},
// 獲取真實DOM
generateDOM(vnode) {
if (typeof vnode.type === ‘function‘) {
// 將組件函數體的虛擬DOM生成真實DOM
return this.generateDOM(vnode.body);
}
let elem = document.createElement(vnode.type);
// 特殊key值映射
let specialKeyMap = {
className: ‘class‘,
fontSize: ‘font-size‘,
};
let {props} = vnode;
// 設置DOM屬性
props && Object.keys(props).forEach(key => {
if (key === ‘children‘) {
// 處理子節點
props.children.forEach(child => {
if (typeof child === ‘string‘) {
// 純內容節點
elem.appendChild(document.createTextNode(child));
} else {
// DOM節點
elem.appendChild(this.generateDOM(child));
}
});
} else if (key === ‘style‘) {
// 設置樣式屬性
let styleObj = props.style;
let styleItems = [];
Object.keys(styleObj).forEach(styleKey => {
styleItems.push(`${specialKeyMap[styleKey] || styleKey}:${styleObj[styleKey]}`);
});
elem.setAttribute(‘style‘, styleItems.join(‘;‘));
} else {
// 設置其他屬性
elem.setAttribute(specialKeyMap[key] || key, props[key]);
}
});
return elem;
}
};
上面代碼中改動較小,我們只添加了幾行針對組件的處理邏輯,如果是組件函數,則將函數體的虛擬DOM生成真實DOM。
最後,我們來看看最終生成的DOM結構:
React系列文章:無狀態組件生成真實DOM結點