大家好,我是微微笑的蝸牛,??。
上一篇文章介紹了 virtual dom,以及簡單的 diff 操作。
這篇文章將介紹 Component、State、組件生命周期的實(shí)現(xiàn)。
React.Component
在 React 中,Component 是所有組件的基類。
它內(nèi)部定義了 render 方法用于返回節(jié)點(diǎn)信息,同時(shí)包含 state、props 屬性用于存儲(chǔ)數(shù)據(jù)。在更新 state 后,組件會(huì)自動(dòng)根據(jù)前后數(shù)據(jù)差異來更新頁面。
比如我們可自定義 MyComponent,繼承自 React.Component。在 render 方法中返回節(jié)點(diǎn)描述。
class MyComponent extends React.Component {
render() {
const { name } = this.state;
return <span>{name}</span>;
}
}
在使用自定義組件時(shí),像標(biāo)簽一樣使用即可。
const element = (
<MyComponent id="container">
<a href="/bar">bar</a>
</MyComponent>
);
上述也是 jsx 的寫法,那么它在經(jīng)過 babel 轉(zhuǎn)義后,結(jié)構(gòu)會(huì)與 div 這類內(nèi)置標(biāo)簽的有所不同嗎?
通過 babel 提供的工具,以上代碼轉(zhuǎn)義后的結(jié)構(gòu)如下:

我們可以發(fā)現(xiàn),組件和普通標(biāo)簽轉(zhuǎn)換后還是有些差異的。
標(biāo)簽傳入的節(jié)點(diǎn)類型是字符串,而組件卻是類 MyComponent。所以,這可作為區(qū)分它們的標(biāo)志。
Component 實(shí)現(xiàn)
我們可以仿造 React 定義自己的 Component 基類,包括 props 和 state。
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
}
由于組件和標(biāo)簽經(jīng) babel 轉(zhuǎn)換后的類型不同,各自的處理也不一樣。這樣一來,構(gòu)建 virutal dom 的實(shí)現(xiàn)需要做出修改,根據(jù)類型做不同處理。
// virtual dom,保存真實(shí)的 dom,element,childInstance
function instantitate(element) {
const { type, props } = element;
// 如果 type 是字符串,則表明非組件
const isDomElement = typeof type === "string";
if (isDomElement) {
// 省略
} else {
// 處理組件
}
}
組件 vdom 結(jié)構(gòu)
由于組件的 render 方法返回的是節(jié)點(diǎn)信息,那么我們必然會(huì)主動(dòng)調(diào)用到 render 方法來獲取節(jié)點(diǎn)描述信息。因此,在 vdom 中關(guān)聯(lián)組件的實(shí)例是很有必要的,這樣才能方便調(diào)用到組件的 render 方法。
所以呢,在原有 vdom 的結(jié)構(gòu)中就需多出一個(gè)字段來記錄其關(guān)聯(lián)的組件實(shí)例,我們稱之為 publicInstance。
同時(shí),由于組件的 render 方法只能返回一個(gè)節(jié)點(diǎn),所以子節(jié)點(diǎn)的 virtual dom 無需用數(shù)組來保存,只需一個(gè)普通對象即可。
組件 vdom 結(jié)構(gòu)修改如下:
- 新增 publicInstance,保存關(guān)聯(lián)的組件實(shí)例。
- 子節(jié)點(diǎn) vdom 由
childInstances → childInstance,不再是數(shù)組。
組件 vdom 各字段定義如下:
{
dom,
element,
childInstance,
publicInstance,
}
組件 vdom 構(gòu)建
組件 vdom 構(gòu)建方式也有所改變,大體過程如下:
- 創(chuàng)建組件實(shí)例
- 調(diào)用組件 render 方法,返回節(jié)點(diǎn)描述信息
- 創(chuàng)建子節(jié)點(diǎn) dom & vdom
- 返回 vdom 結(jié)構(gòu)
const instance = {};
// 創(chuàng)建組件實(shí)例
const publicInstance = createPublicInstance(element, instance);
// 調(diào)用組件的 render 方法,返回節(jié)點(diǎn)
const childElement = publicInstance.render();
// 調(diào)用 instantiate 創(chuàng)建 dom & virual dom,因?yàn)?render 方法只能返回一個(gè)節(jié)點(diǎn)
const childInstance = instantitate(childElement);
const dom = childInstance.dom;
Object.assign(instance, {
dom,
element,
childInstance,
publicInstance,
});
return instance;
State
我們知道,Component 是以數(shù)據(jù)來驅(qū)動(dòng)界面刷新的。當(dāng)要刷新 UI 時(shí),需主動(dòng)調(diào)用 setState 來更新數(shù)據(jù),直接更新 state 是無效的。
??用頭發(fā)絲想想,在 setState 方法中,一定會(huì)涉及到 dom diff 的處理。
這樣一來,在 Component 內(nèi)部,也就需要知道對應(yīng)的 vdom 實(shí)例,以此來觸發(fā) diff 操作。
下面,我們來看看這兩個(gè)關(guān)鍵點(diǎn)的實(shí)現(xiàn)。
1. 在創(chuàng)建組件實(shí)例時(shí),關(guān)聯(lián) vdom 節(jié)點(diǎn)。
// 創(chuàng)建組件對象,內(nèi)部關(guān)聯(lián) vdom 實(shí)例
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
// 創(chuàng)建組件實(shí)例
const publicInstance = new type(props);
// 關(guān)聯(lián) vdom 節(jié)點(diǎn)
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
可注意看,組件實(shí)例的創(chuàng)建是使用 new type(props); 的方式,同時(shí)傳入了 props。
從上圖 babel 轉(zhuǎn)義后的 type 數(shù)據(jù),應(yīng)該就能明白為什么可以這樣寫。
2. 調(diào)用 setState 時(shí),觸發(fā) diff 操作。
先更新 state 數(shù)據(jù),然后做 diff 操作更新組件。
setState(state) {
this.state = Object.assign({}, this.state, state);
// __interalInstace 為 virutal dom
updateInstance(this.__internalInstance);
}
function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
注意,由于普通標(biāo)簽和組件的子節(jié)點(diǎn) vdom 結(jié)構(gòu)是不一樣的。一個(gè)是數(shù)組,一個(gè)是單獨(dú)的對象,所以在進(jìn)行 diff 時(shí)也需區(qū)分處理。
普通標(biāo)簽處理
普通標(biāo)簽的 reconcile 處理:
- 更新節(jié)點(diǎn)屬性
- 處理子節(jié)點(diǎn) diff
- 更新 vdom 屬性
代碼處理如下:
if (typeof element.type === "string") {
console.log("reuse dom");
// 重用節(jié)點(diǎn),更新屬性
updateDomProperties(instance.dom, instance.element.props, element.props);
// 處理子節(jié)點(diǎn) diff
instance.childInstances = reconcileChildren(instance, element);
// 更新 element
instance.element = element;
return instance;
}
組件處理
組件的 reconcile 處理:
- 組件屬性的更新
- 調(diào)用組件 render 方法
- 進(jìn)行 diff 操作
- 更新 vdom 各個(gè)屬性
代碼處理如下:
{
console.log("update component");
// component
// 更新 props
instance.publicInstance.props = element.props;
// 重新構(gòu)建節(jié)點(diǎn)信息
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
// 更新 dom 節(jié)點(diǎn)
const childInstance = reconcile(parentDom, oldChildInstance, childElement);
// 更新 vdom 各個(gè)屬性
instance.childInstance = childInstance;
instance.dom = childInstance.dom;
instance.element = element;
return instance;
}
組件生命周期
React 中的組件有一系列的生命周期。在此,我們打算實(shí)現(xiàn)如下生命周期:
// 組件即將掛載
componentWillMount();
// 組件掛載完畢
componentDidMount();
// 組件即將移除
componentWillUnmount();
// 將收到新的 props
componentWillReceiveProps(nextProps);
// 組件即將更新
componentWillUpdate(nextProps, nextState);
// 組件更新完畢
componentDidUpdate(prevProps, prevState);
componentWillMount
- 含義:組件即將掛載。
- 調(diào)用時(shí)機(jī):在組件即將添加到 dom 樹上調(diào)用。
function reconcile(parentDom, instance, element) {
if (instance == null) {
const isComponent = typeof newInstance.element.type !== "string";
const newInstance = instantitate(element);
// 組件生命周期 componentWillMount
if (isComponent) {
newInstance.publicInstance.componentWillMount();
}
parentDom.appendChild(newInstance.dom);
// 組件生命周期 componentDidMount
if (isComponent) {
newInstance.publicInstance.componentDidMount();
}
return newInstance;
}
// ...
}
componentDidMount
- 含義:組件掛載完畢。
- 調(diào)用時(shí)機(jī):在組件添加到 dom 樹后調(diào)用。代碼可參照 componentWillMount 一節(jié)。
componentWillUnmount
- 含義:組件即將移除。
- 調(diào)用時(shí)機(jī):準(zhǔn)備從 dom 樹移除時(shí)調(diào)用。
function reconcile(parentDom, instance, element) {
if (element == null) {
console.log("remove dom");
// remove,若新子節(jié)點(diǎn)數(shù) < 原節(jié)點(diǎn)數(shù),需移除
parentDom.removeChild(instance.dom);
// 組件生命周期 componentWillUnmount
if (typeof instance.element.type !== "string") {
instance.publicInstance.componentWillUnmount();
}
return null;
}
}
componentWillReceiveProps
- 含義:組件即將更新屬性。
- 調(diào)用時(shí)機(jī):在 reconcile 的組件 diff 中處理。
function reconcile(parentDom, instance, element) {
// ....
else {
// component
// 更新 props
// 組件生命周期 componentWillReceiveProps
instance.publicInstance.componentWillReceiveProps(element.props);
instance.publicInstance.props = element.props;
// ...
return instance;
}
}
componentWillUpdate
- 含義:組件即將更新。
- 調(diào)用時(shí)機(jī):在更新 dom 樹前,需調(diào)用 shouldComponentUpdate 判斷是否需要真正的更新。
shouldComponentUpdate 的默認(rèn)實(shí)現(xiàn)是:判斷當(dāng)前屬性和傳入屬性是否不等,或者當(dāng)前 state 和傳入 state 是否不等。任一不等,即認(rèn)為組件應(yīng)該進(jìn)行更新。
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state;
}
componentWillUpdate 調(diào)用時(shí)機(jī),在更新組件前。
setState(nextState) {
if (this.__internalInstance && this.shouldComponentUpdate(this.props, nextState)) {
// 組件生命周期 componentWillUpdate
this.componentWillUpdate(this.props, nextState);
this.state = Object.assign({}, this.state, nextState);
updateInstance(this.__internalInstance);
// 組件生命周期 componentDidUpdate
this.componentDidUpdate(this.props, nextState);
} else {
this.state = Object.assign({}, this.state, nextState);
}
}
componentDidUpdate
- 含義:組件更新完畢。
- 調(diào)用時(shí)機(jī):在調(diào)用 updateInstance 后,即可觸發(fā)。代碼可參照 componentWillUpdate 一節(jié)。
Demo
這樣,我們就可以繼承于自己實(shí)現(xiàn)的 SLReact.Component,來自定義組件了。
class Story extends SLReact.Component {
constructor(props) {
super(props);
this.state = {
likes: ramdomLikes(),
};
}
componentDidMount() {
super.componentDidMount();
console.log("Story componentDidMount");
}
handleClick() {
this.setState({
likes: this.state.likes + 1,
});
}
render() {
const { name, url } = this.props;
const { likes } = this.state;
return (
<li>
<button onClick={(e) => this.handleClick()}>{likes}??</button>
<a href={url}>{name}</a>
</li>
);
}
}
跟 React 中的寫法一毛一樣。
以上只是組件的定義。有興趣的童鞋可下載完整 Demo 代碼,自行運(yùn)行試試。
Demo 地址:https://github.com/silan-liu/slreact/tree/master/part4。
總結(jié)
這篇文章主要介紹了 Component 和 State 的實(shí)現(xiàn),組件實(shí)例和 virutal dom 實(shí)例相互關(guān)聯(lián)。
至此,「聽說你想寫個(gè) React」已全部完結(jié),整體內(nèi)容比較簡單,感謝閱讀~
該系列全部文章如下: