聽說你想寫個(gè) React - Component

大家好,我是微微笑的蝸牛,??。

上一篇文章介紹了 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)如下:

image

我們可以發(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)容比較簡單,感謝閱讀~

該系列全部文章如下:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容