React 源碼漂流(二)之 Component

1、組件

1. 純組件

React.PureComponent ,和 React.Component 相似,都是定義一個組件類。不一樣是 React.Component 沒有實現 shouldComponentUpdate(),而 React.PureComponent 經過 propsstate淺比較實現了。html

// React.PureComponent 純組件
class Counter extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 0};
  }
  render() {
    return (
      <button onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button>
    );
  }
}
複製代碼

在下一節中將會詳細介紹。前端

2. 函數組件

定義React組件的最簡單方式就是定義一個函數組件,它接受單一的 props 並返回一個React元素。react

// 函數組件
function Counter(props) {
    return <div>Counter: {props.count}</div>
}
// 類組件
class Counter extends React.Component {
  render() {
    return <div>Counter: {this.props.count}</div>
  }
}
複製代碼
  • 在 函數組件 中,它的輸入輸出所有由 props 決定,且不會產生任何反作用,這說明 函數組件 也是 無狀態組件
  • 在函數組件中,沒法修改 props,沒法使用 state 及組件的生命週期,說明 函數組件 也是 展現組件
  • 函數組件 的功能只是接收 props,渲染頁面,它不執行與 UI 無關的邏輯處理,它只是一個純函數
  • 函數組件,相對於類組件來講,更加簡潔。不管是複用性仍是性能,都優於類組件

3. 受控組件與非受控組件

受控和非受控主要是取決於組件是否受父級傳入的 props 控制git

用 props 傳入數據的話,組件能夠被認爲是受控(由於組件被父級傳入的 props 控制)。github

數據只保存在組件內部的 state 的話,是非受控組件(由於外部沒辦法直接控制 state)。api

export default class AnForm extends React.Component {
  state = {
    name: ""
  }
  handleSubmitClick = () => {
    console.log("非受控組件: ", this._name.value);
    console.log("受控組件: ", this.state.name);
  }
  handleChange = (e) => {
    this.setState({
      name: e.target.value
    })
  }

  render() {
    return (
      <form onSubmit={this.handleSubmitClick}>
      <label>
        非受控組件:
        <input 
        	type="text" 
        	defaultValue="default" 
        	ref={input => this._name = input} 
        />
      </label>
      <label>
        受控組件:
        <input 
        	type="text" 
        	value={this.state.name} 
        	onChange={this.handleChange}
        />
      </label>
      <input type="submit" value="Submit" />
    </form>
    );
  }
}
複製代碼
受控組件

與 html 不一樣的是,在 React 中,<input><select><textarea>等這類組件,不會主動維持自身狀態,並根據用戶輸入進行更新。它們都要綁定一個onChange事件;每當狀態發生變化時,都要寫入組件的 state 中,在 React 中被稱爲受控組件數組

export default class AnForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ""};
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    this.setState({value: event.target.value});
  }
  render() {
    return <input type="text" value={this.state.value} onChange={this.handleChange} />; } } 複製代碼
  • onChange & value 模式(單選按鈕和複選按鈕對應的是 checked props)瀏覽器

  • react經過這種方式消除了組件的局部狀態,使得應用的整個狀態可控性能優化

  • 注意 <input type="file" />,它是一個非受控組件app

  • 可使用計算屬性名將多個類似的操做組合成一個。

    this.setState({
      [name]: value
    });
    複製代碼
非受控組件

非受控組件再也不將數據保存在 state,而使用 refs,將真實數據保存在 DOM 中。

export default class AnForm extends Component {
  handleSubmitClick = () => {
    const name = this._name.value;
  }

  render() {
    return (
      <div> <input type="text" ref={input => this._name = input} /> <button onClick={this.handleSubmitClick}>Sign up</button> </div> ); } } 複製代碼
  • 非受控組件是最簡單快速的實現方式,項目中出現極簡的表單時,使用它,但受控組件纔是是最權威的

  • 一般指定一個 defaultValue/defaultChecked 默認值來控制初始狀態,不使用 value。

  • 非受控組件相比於受控組件,更容易同時集成 React 和非 React 代碼。

  • 使用場景

    特徵 非受控組件 受控組件
    one-time value retrieval (e.g. on submit)
    validating on submit
    instant field validation
    conditionally disabling submit button
    enforcing input format
    several inputs for one piece of data
    dynamic inputs

4. 有狀態組件與無狀態組件

有狀態組件

經過 state 管理狀態

export default class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = { clicks: 0 }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    this.setState(state => ({ clicks: state.clicks + 1 }))
  }
  render() {
    return (
      <Button onClick={this.handleClick} text={`You've clicked me ${this.state.clicks} times!`} /> ) } } 複製代碼
無狀態組件

輸入輸出數據徹底由props決定,並且不會產生任何反作用。

const Button = props =>
  <button onClick={props.onClick}>
    {props.text}
  </button>
複製代碼
  • 無狀態組件通常會搭配高階組件(簡稱:HOC)一塊兒使用,高階組件用來託管state,Redux 框架就是經過 store 管理數據源和全部狀態,其中全部負責展現的組件都使用無狀態函數式的寫法。
  • 一個簡單的 無狀態(stateless) 按鈕組件,僅依賴於 props(屬性) ,這也稱爲函數式組件

5. 展現組件與容器組件

展現組件

展現組件指不關心數據是怎麼加載和變更的,只關注於頁面展現效果的組件。

class TodoList extends React.Component{
    constructor(props){
        super(props);
    }
    render(){
        const {todos} = this.props;
        return (<div> <ul> {todos.map((item,index)=>{ return <li key={item.id}>{item.name}</li> })} </ul> </div>)
    }
}
複製代碼
  • 只能經過 props 的方式接收數據和進行回調(callback)操做。
  • 不多擁有本身的狀態,即便有也是用於展現UI狀態的。
  • 一般容許經過 this.props.children 方式來包含其餘組件。
  • 內部能夠包含展現組件和容器組件,一般會包含一些本身的DOM標記和樣式(style)
  • 對應用程序的其餘部分沒有依賴關係,例如Flux操做或store。
  • 會被寫成函數式組件除非該組件須要本身的狀態,生命週期或者作一些性能優化。
容器組件

容器組件只關心數據是怎麼加載和變更的,而不關注於頁面展現效果。

//容器組件
class TodoListContainer extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            todos:[]
        }
        this.fetchData = this.fetchData.bind(this);
    }
    componentDidMount(){
        this.fetchData();
    }
    fetchData(){
        fetch('/api/todos').then(data =>{
            this.setState({
                todos:data
            })
        })
    }
    render(){
        return (<div> <TodoList todos={this.state.todos} /> </div>) } } 複製代碼
  • 內部能夠包含容器組件和展現組件,但一般沒有任何本身的DOM標記,除了一些包裝divs,而且從不具備任何樣式。
  • 提供數據和行爲給其餘的展現組件或容器組件。
  • 能夠調用 Flux 操做並將它們做爲回調函數(callback)提供給展現組件。
  • 每每是有狀態的,由於它們傾向於做爲數據源
  • 一般使用高階組件生成,例如React Redux的connect()

6. 高階組件

高階函數的定義:接收函數做爲輸入,或者輸出另外一個函數的一類函數,被稱做高階函數。

對於高階組件,它描述的即是接受 React 組件做爲輸入,輸出一個新的 React 組件的組件。

更通俗的描述爲,高階組件經過包裹(wrapped)被傳入的 React 組件,通過一系列處理,最終返回一個相對加強(enhanced)的 React 組件,供其餘組件調用。使咱們的代碼更具備複用性、邏輯性和抽象特性,它能夠對 render 方法作劫持,也能夠控制 props 、state

實現高階組件的方法有如下兩種:

  • 屬性代理(props proxy),高階組件經過被包裹的 React 組件來操做 props。
  • 反向繼承(inheritance inversion),高階組件繼承於被包裹的 React 組件。
// 屬性代理
export default function withHeader(WrappedComponent) {
  return class HOC extends React.Component { // 繼承與 React.component
    render() {
      const newProps = {
        test:'hoc'
      }
      // 透傳props,而且傳遞新的newProps
      return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } // 反向繼承 export default function (WrappedComponent) { return class Inheritance extends WrappedComponent { // 繼承於被包裹的 React 組件 componentDidMount() { // 能夠方便地獲得state,作一些更深刻的修改。 console.log(this.state); } render() { return super.render(); } } } 複製代碼
  • 注意:不要在 HOC 內修改一個組件的原型(或以其它方式修改組件)
  • 貫穿傳遞不相關props屬性給被包裹的組件,幫助確保高階組件最大程度的靈活性和可重用性
  • 應該使用最大化的組合性
  • 爲了便於調試,能夠選擇一個顯示名字,傳達它是一個高階組件的結果,WrappedComponent.displayName || WrappedComponent.name || 'Component';
  • 不要在 render() 方法中建立 HOC,不然,每一次渲染,都會從新建立渲染 HOC
  • 必須將原始組件的靜態方法在 HOC 中作拷貝,不然 HOC 將沒有原始組件的任何靜態方法
  • Refs 屬性不能貫穿傳遞,咱們可使用 React.forwardRef 解決

7. Hook 組件

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

但與 class 生命週期不一樣的是,Hook 更接近於實現狀態同步,而不是響應生命週期事件。

import React, { useState, useEffect } from 'react';

function Example() {
  // 聲明一個叫 "count" 的 state 變量
  const [count, setCount] = useState(0);
    
  useEffect(()=>{
    // 須要在 componentDidMount 執行的內容
    return function cleanup() {
      // 須要在 componentWillUnmount 執行的內容 
  	}
  }, [])

  useEffect(() => { 
    // 在 componentDidMount,以及 count 更改時 componentDidUpdate 執行的內容
    document.title = 'You clicked ' + count + ' times'; 
    return () => {
      // 須要在 count 更改時 componentDidUpdate(先於 document.title = ... 執行,遵照先清理後更新)
      // 以及 componentWillUnmount 執行的內容 
    } // 當函數中 Cleanup 函數會按照在代碼中定義的順序前後執行,與函數自己的特性無關
  }, [count]); // 僅在 count 更改時更新

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼
  • Hooks 組件更接近於實現狀態同步,而不是響應生命週期事件
  • 只能在函數最外層調用 Hook。只能在 React 的函數組件中調用 Hook。
  • useLayoutEffect 與 componentDidMountcomponentDidUpdate 的調用階段是同樣的。可是,咱們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect
  • 與 componentDidMount 或 componentDidUpdate 不一樣的是,Hook 在瀏覽器完成佈局與繪製以後,傳給 useEffect 的函數會延遲調用,但會保證在任何新的渲染前執行
  • effect 的清除(cleanup)並不會讀取「最新」的 props 。它只能讀取到定義它的那次渲染中的 props 值
  • effect 中能夠讀取到最新的 count 狀態值,並非 count 的值在「不變」的effect中發生了改變,而是effect 函數自己在每一次渲染中都不相同
  • 在 class 組件生命週期的思惟模型中,反作用的行爲和渲染輸出是不一樣的。UI渲染是被 props 和 state 驅動的,而且能確保步調一致,但反作用並非這樣。這是一類常見問題的來源。
  • 而在 useEffect 的思惟模型中,默認都是同步的。反作用變成了 React 數據流的一部分。對於每個 useEffect 調用,一旦你處理正確,你的組件可以更好地處理邊緣狀況。

2、Component 源碼解讀

首先看一下 React.Component 結構

// ReactBaseClasses.js 文件
/** * Base class helpers for the updating state of a component. */
function Component(props, context, updater) {
  this.props = props; // 屬性 props
  this.context = context; // 上下文 context
  // If a component has string refs, we will assign a different object later.
  // 初始化 refs,爲 {},主要在 stringRef 中使用,將 stringRef 節點的實例掛載在 this.refs 上
  this.refs = emptyObject; 
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue; // updater
}

Component.prototype.isReactComponent = {};

/** * 設置 state 的子集,使用該方法更新 state,避免 state 的值爲可突變的狀態 * `shouldComponentUpdate`只是淺比較更新, * 可突變的類型可能致使 `shouldComponentUpdate` 返回 false,沒法從新渲染 * Immutable.js 能夠解決這個問題。它經過結構共享提供不可突變的,持久的集合: * 不可突變: 一旦建立,集合就不能在另外一個時間點改變。 * 持久性: 可使用原始集合和一個突變來建立新的集合。原始集合在新集合建立後仍然可用。 * 結構共享: 新集合儘量多的使用原始集合的結構來建立,以便將複製操做降至最少從而提高性能。 * * 並不能保證 `this.state` 經過 `setState` 後不可突變的更新,它可能還返回原來的數值 * 不能保證 `setrState` 會同步更新 `this.state` * `setState` 是經過隊列形式來更新 state ,當 執行 `setState` 時, * 會把 state 淺合併後放入狀態隊列,而後批量執行,即它不是當即更新的。 * 不過,你能夠在 callback 回調函數中獲取最新的值 * * 注意:對於異步渲染,咱們應在 `getSnapshotBeforeUpdate` 中讀取 `state`、`props`, * 而不是 `componentWillUpdate` * * @param {object|function} partialState Next partial state or function to * produce next partial state to be merged with current state. * @param {?function} callback Called after state is updated. * @final * @protected */
Component.prototype.setState = function(partialState, callback) {
  // 當 partialState 狀態爲 object 或 function類型 或 null 時,
  // 執行 this.updater.enqueueSetState 方法,不然報錯
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  // 將 `setState` 事務放入隊列中
  this.updater.enqueueSetState(this, partialState, callback, 'setState'); 
};

/** * 強制更新,當且僅當當前不處於 DOM 事物(transaction)中才會被喚起 * This should only be invoked when it is known with * certainty that we are **not** in a DOM transaction. * * 默認狀況下,當組件的state或props改變時,組件將從新渲染。 * 若是你的`render()`方法依賴於一些其餘的數據, * 你能夠告訴React組件須要經過調用`forceUpdate()`從新渲染。 * 調用`forceUpdate()`會致使組件跳過 `shouldComponentUpdate()`, * 直接調用 `render()`。但會調用 `componentWillUpdate` 和 `componentDidUpdate`。 * 這將觸發組件的正常生命週期方法,包括每一個子組件的 shouldComponentUpdate() 方法。 * forceUpdate 就是從新 render 。 * 有些變量不在 state 上,當時你又想達到這個變量更新的時候,刷新 render ; * 或者 state 裏的某個變量層次太深,更新的時候沒有自動觸發 render 。 * 這些時候均可以手動調用 forceUpdate 自動觸發 render * * @param {?function} callback 更新完成後的回調函數. * @final * @protected */
Component.prototype.forceUpdate = function(callback) {
  // updater 強制更新
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); 
};
複製代碼

其中 this.refsemptyObject 爲:

// 設置 refs 初始值爲 {}
const emptyObject = {};
if (__DEV__) {
  Object.freeze(emptyObject); // __DEV__ 模式下, 凍結 emptyObject
}
// Object.freeze() 凍結一個對象,被凍結的對象不能被修改(添加,刪除,
// 修改已有屬性的可枚舉性、可配置性、可寫性與屬性值,原型);返回和傳入的參數相同的對象。

複製代碼

ReactNoopUpdateQueue 爲:

// ReactNoopUpdateQueue.js 文件
/** * 這是一個關於 更新隊列(update queue) 的抽象 API */
const ReactNoopUpdateQueue = {
  /** * 檢查複合組件是否裝載完成(被插入樹中) * @param {ReactClass} publicInstance 測試實例單元 * @return {boolean} 裝載完成爲 true,不然爲 false * @protected * @final */
  isMounted: function(publicInstance) {
    return false;
  },

  /** * 強制更新隊列,當且僅當當前不處於 DOM 事物(transaction)中才會被喚起 * * 當 state 裏的某個變量層次太深,更新的時候沒有自動觸發 render 。 * 這些時候就能夠調用該方法強制更新隊列 * * 該方法將跳過 `shouldComponentUpdate()`, 直接調用 `render()`, 但它會喚起 * `componentWillUpdate` 和 `componentDidUpdate`. * * @param {ReactClass} publicInstance 將被從新渲染的實例 * @param {?function} callback 組件更新後的回調函數. * @param {?string} callerName 在公共 API 調用該方法的函數名稱. * @internal */
  enqueueForceUpdate: function(publicInstance, callback, callerName) {
    warnNoop(publicInstance, 'forceUpdate');
  },

  /** * 徹底替換state,與 `setState` 不一樣的是,`setState` 是以修改和新增的方式改變 `state `的, * 不會改變沒有涉及到的 `state`。 * 而 `enqueueReplaceState` 則用新的 `state` 徹底替換掉老 `state` * 使用它或 `setState` 來改變 state,而且應該把 this.state 設置爲不可突變類型對象, * 而且this.state不會當即更改 * 咱們應該在回調函數 callback 中獲取最新的 state * * @param {ReactClass} publicInstance The instance that should rerender. * @param {object} completeState Next state. * @param {?function} callback Called after component is updated. * @param {?string} callerName name of the calling function in the public API. * @internal */
  enqueueReplaceState: function( publicInstance, completeState, callback, callerName, ) {
    warnNoop(publicInstance, 'replaceState');
  },

  /** * 設置 state 的子集 * 它存在的惟一理由是 _pendingState 是內部方法。 * `enqueueSetState` 實現淺合併更新 `state` * * @param {ReactClass} publicInstance The instance that should rerender. * @param {object} partialState Next partial state to be merged with state. * @param {?function} callback Called after component is updated. * @param {?string} Name of the calling function in the public API. * @internal */
  enqueueSetState: function( publicInstance, partialState, callback, callerName, ) {
    warnNoop(publicInstance, 'setState');
  },
};

export default ReactNoopUpdateQueue;
複製代碼

注意,React API 只是簡單的功能介紹,具體的實現是在 react-dom 中,這是由於不一樣的平臺,React API 是一致的,但不一樣的平臺,渲染的流程是不一樣的,具體的 Component 渲染流程不一致,會根據具體的平臺去定製。

組件生命週期請參考 Hooks 與 React 生命週期的關係

系列文章

想看更過系列文章,點擊前往 github 博客主頁

走在最後,歡迎關注:前端瓶子君,每日更新

前端瓶子君
相關文章
相關標籤/搜索