React 全家桶之 react

目錄

 

react 生命週期

零、編譯階段

一、初始化階段

二、運行中的狀態

文本節點的更新

原生節點的更新

自定義組件的更新

三、銷燬階段


react 生命週期

react的生命週期大致分爲三個時期:初始化、運行中、銷燬。

零、編譯階段

通常我們在寫 react 時會採用 jsx 法語,經過 babel 編譯之後實際上調用的是 createElement。

ReactDOM.render(
    <h1 style={{"color":"blue"}}>hello world</h1>,
    document.getElementById('root')
);

編譯後會是這個樣子 

ReactDOM.render(
    React.createElement(
        'h1',
        { style: { "color": "blue" } },
        'hello world'
    ),
    document.getElementById('root')
);

 

一、初始化階段

getDefaultProps:獲取實例的默認屬性

getInitialState:獲取每個實例的初始化狀態

componentWillMound:組件即將被掛載、渲染到頁面上

render:組件在這裏生產虛擬Dom節點

componentDidMound:生成真實Dom,渲染到頁面

總結:

  1. 初始化階段根據默認的屬性以及初始化狀態,調用 createElement 生成 ReactElement ,也就是我們常說的虛擬 Dom。其實就是一顆抽象語法樹。
  2. render 函數會根據上一步生成的 ReactElement 的 type 字段來判斷它的類型,生成相應的 ReactComponent 實例。類型有文本、原生dom、自定義組件三種。
  3. 每一種 ReactComponent 都有 mountComponent 函數,按照自己的規則來生成 html 結構
  4. 子節點 children 會掛在 props 屬性上,循環處理時會根據子節點的類型從第 2 步開始走,如此遞歸下去直到沒有子節點爲止。
  5. 將遞歸處理出來的 html 結構拼裝起來,innerHTML 到 container 中去
  6. 到此完成初始化渲染,觸發 componentDidMound
這個 ReactElement 的結構 
    //節點的類型,string代表原生節點,如果是一個class代表自定義組件
    type: type, 
    //節點的唯一標識,更新的時候會用的到
    key: key,
    //節點的引用,通常爲父組件所用,如this.refs.child
    ref: ref,
    //節點的屬性
    props: props,

    // 注意這個owner是創建ReactElement時,根據這個元素的類型所創建的ReactComponent
    // 而這個ReactComponent是ReactElement的控制類,控制節點的掛載、更新、卸載等操作,它兩也是一一對應的
    _owner: owner

 

二、運行階段

 

componentWillReceiveProps:組件將要接收到屬性的時候

shouldComponentUpdate:組件接收到新的狀態或者屬性的時候(如果返回false,後續的render流程將不再執行)

componentWillUpdate:組件即將更新,不能在改函數中修改屬性和狀態

render:組件重新構建虛擬Dom

componentDidUpdate:組件完成更新

總結:

在初始化階段所有類型的 Component 都實現了 mountComponent 來處理第一次渲染。同理,所有的 Component 都實現了 receiveComponent 來處理更新。

文本節點的更新

  • 判斷新的文本內容與老的是否一樣,不一樣就直接替換
ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
    var nextStringText = '' + nextText;
    //跟以前保存的字符串比較
    if (nextStringText !== this._currentElement) {
        this._currentElement = nextStringText;
        //替換整個節點
        $('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);

    }
}

原生節點的更新

原生節點的更新是比較複雜的,主要包括兩個部分:

  1. 屬性的更新,包括對特殊屬性比如事件的處理
  2. 子節點的更新,拿新節點和老節點對比,找出差異,稱之爲 diff。找出差異後,再一次性更新,稱之爲 patch(批處理)
ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
    var lastProps = this._currentElement.props;
    var nextProps = nextElement.props;

    this._currentElement = nextElement;
    //需要單獨的更新屬性
    this._updateDOMProperties(lastProps, nextProps);
    //再更新子節點
    this._updateDOMChildren(nextElement.props.children);
}

首先來看屬性的更新

  1. 先遍歷老集合,不在新集合裏的屬性,需要刪除。具體就是清除監聽事件以及dom上的屬性
  2. 在遍歷新集合,對於事件屬性需要先清除再綁定。對於普通屬性需要掛載到當前dom上,對於children屬性不處理
ReactDOMComponent.prototype._updateDOMProperties = function(lastProps, nextProps) {
    var propKey;
    //遍歷,當一個老的屬性不在新的屬性集合裏時,需要刪除掉。

    for (propKey in lastProps) {
        //新的屬性裏有,或者propKey是在原型上的直接跳過。這樣剩下的都是不在新屬性集合裏的。需要刪除
        if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
            continue;
        }
        //對於那種特殊的,比如這裏的事件監聽的屬性我們需要去掉監聽
        if (/^on[A-Za-z]/.test(propKey)) {
            var eventType = propKey.replace('on', '');
            //針對當前的節點取消事件代理
            $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]', eventType, lastProps[propKey]);
            continue;
        }

        //從dom上刪除不需要的屬性
        $('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey)
    }

    //對於新的屬性,需要寫到dom節點上
    for (propKey in nextProps) {
        //對於事件監聽的屬性我們需要特殊處理
        if (/^on[A-Za-z]/.test(propKey)) {
            var eventType = propKey.replace('on', '');
            //以前如果已經有,說明有了監聽,需要先去掉
            lastProps[propKey] && $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]', eventType, lastProps[propKey]);
            //針對當前的節點添加事件代理,以_rootNodeID爲命名空間
            $(document).delegate('[data-reactid="' + this._rootNodeID + '"]', eventType + '.' + this._rootNodeID, nextProps[propKey]);
            continue;
        }

        if (propKey == 'children') continue;

        //添加新的屬性,或者是更新老的同名屬性
        $('[data-reactid="' + this._rootNodeID + '"]').prop(propKey, nextProps[propKey])
    }

}

子節點的更新是最複雜的

  1. 首先,用一個 updateDepth 來記錄當前節點的深度,用 diffQueue 更新隊列來保存需要更新的內容
  2. 內部主要通過一個_diff 函數來找出差異,放入更新隊列
  3. _diff 中會先調用 flattenChildren 把當前 children 數組轉化成一個 map,如果子節點上設置了 key 就用 key,沒設置就用當前位置 index 當做 key
  4. 老的 children map 轉好了,再在 generateComponentChildren 中去轉新的 children map。過程就是遍歷新的 children,通過 key 去找老的 child,通過全局方法 _shouldUpdateReactComponent 判斷是需要更新還是重新生成新的 component 實例。如果需要更新,就遞歸調用子節點的 receiveComponent。
  5. 現在新的 children map 也有了,先遍歷新的 map,通過 key 去找到老的 child
    1. 如果老節點===新節點,代表同一個 component 實例,移動位置就可以了,push 到更新隊列中
    2. 如果老節點存在但是!==新節點,代表 element 變了,老節點需要刪除,push 到更新隊列
    3. 將全新的節點 push 到更新隊列中
  6.  再遍歷老的 map,把老 map 中存在但是新 map 中不存在的節點刪掉,push 到更新隊列中
  7. 等到整個虛擬 Dom 數遞歸 receiveComponent 後,執行 patch 執行更新操作
  8. 在 patch 中會遍歷更新隊列,處理不同差異類型的更新,包括移動、刪除、插入三種

 

//全局的更新深度標識
var updateDepth = 0;
//全局的更新隊列,所有的差異都存在這裏
var diffQueue = [];

ReactDOMComponent.prototype._updateDOMChildren = function(nextChildrenElements){
    updateDepth++
    //_diff用來遞歸找出差別,組裝差異對象,添加到更新隊列diffQueue。
    this._diff(diffQueue,nextChildrenElements);
    updateDepth--
    if(updateDepth == 0){
        //在需要的時候調用patch,執行具體的dom操作
        this._patch(diffQueue);
        diffQueue = [];
    }
}

就像我們之前說的一樣,更新子節點包含兩個部分,一個是遞歸的分析差異,把差異添加到隊列中。然後在合適的時機調用_patch把差異應用到dom上。

那麼什麼是合適的時機,updateDepth又是幹嘛的?

這裏需要注意的是,_diff內部也會遞歸調用子節點的receiveComponent於是當某個子節點也是瀏覽器普通節點,就也會走_updateDOMChildren這一步。所以這裏使用了updateDepth來記錄遞歸的過程,只有等遞歸回來updateDepth爲0時,代表整個差異已經分析完畢,可以開始使用patch來處理差異隊列了。

所以我們關鍵是實現_diff_patch兩個方法。

我們先看_diff的實現:

//差異更新的幾種類型
var UPATE_TYPES = {
    MOVE_EXISTING: 1,
    REMOVE_NODE: 2,
    INSERT_MARKUP: 3
}


//普通的children是一個數組,此方法把它轉換成一個map,key就是element的key,如果是text節點或者element創建時並沒有傳入key,就直接用在數組裏的index標識
function flattenChildren(componentChildren) {
    var child;
    var name;
    var childrenMap = {};
    for (var i = 0; i < componentChildren.length; i++) {
        child = componentChildren[i];
        name = child && child._currentelement && child._currentelement.key ? child._currentelement.key : i.toString(36);
        childrenMap[name] = child;
    }
    return childrenMap;
}


//主要用來生成子節點elements的component集合
//這邊注意,有個判斷邏輯,如果發現是更新,就會繼續使用以前的componentInstance,調用對應的receiveComponent。
//如果是新的節點,就會重新生成一個新的componentInstance,
function generateComponentChildren(prevChildren, nextChildrenElements) {
    var nextChildren = {};
    nextChildrenElements = nextChildrenElements || [];
    $.each(nextChildrenElements, function(index, element) {
        var name = element.key ? element.key : index;
        var prevChild = prevChildren && prevChildren[name];
        var prevElement = prevChild && prevChild._currentElement;
        var nextElement = element;

        //調用_shouldUpdateReactComponent判斷是否是更新
        if (_shouldUpdateReactComponent(prevElement, nextElement)) {
            //更新的話直接遞歸調用子節點的receiveComponent就好了
            prevChild.receiveComponent(nextElement);
            //然後繼續使用老的component
            nextChildren[name] = prevChild;
        } else {
            //對於沒有老的,那就重新新增一個,重新生成一個component
            var nextChildInstance = instantiateReactComponent(nextElement, null);
            //使用新的component
            nextChildren[name] = nextChildInstance;
        }
    })

    return nextChildren;
}



//_diff用來遞歸找出差別,組裝差異對象,添加到更新隊列diffQueue。
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements) {
  var self = this;
  //拿到之前的子節點的 component類型對象的集合,這個是在剛開始渲染時賦值的,記不得的可以翻上面
  //_renderedChildren 本來是數組,我們搞成map
  var prevChildren = flattenChildren(self._renderedChildren);
  //生成新的子節點的component對象集合,這裏注意,會複用老的component對象
  var nextChildren = generateComponentChildren(prevChildren, nextChildrenElements);
  //重新賦值_renderedChildren,使用最新的。
  self._renderedChildren = []
  $.each(nextChildren, function(key, instance) {
    self._renderedChildren.push(instance);
  })


  var lastIndex = 0;//代表訪問的最後一次的老的集合的位置
  var nextIndex = 0;//代表到達的新的節點的index
    //通過對比兩個集合的差異,組裝差異節點添加到隊列中
    for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        //相同的話,說明是使用的同一個component,所以我們需要做移動的操作
        if (prevChild === nextChild) {
          //添加差異對象,類型:MOVE_EXISTING
          。。。。
          /**注意新增代碼**/
          prevChild._mountIndex < lastIndex && diffQueue.push({
                parentId:this._rootNodeID,
                parentNode:$('[data-reactid='+this._rootNodeID+']'),
                type: UPATE_TYPES.REMOVE_NODE,
                fromIndex: prevChild._mountIndex,
                toIndex:null
          })
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        } else {
          //如果不相同,說明是新增加的節點,
          if (prevChild) {
            //但是如果老的還存在,就是element不同,但是component一樣。我們需要把它對應的老的element刪除。
            //添加差異對象,類型:REMOVE_NODE
            。。。。。
            /**注意新增代碼**/
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          }
          。。。
        }
        //更新mount的inddex
        nextChild._mountIndex = nextIndex;
        nextIndex++;
      }



  //對於老的節點裏有,新的節點裏沒有的那些,也全都刪除掉
  for (name in prevChildren) {
    if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
      //添加差異對象,類型:REMOVE_NODE
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $('[data-reactid=' + self._rootNodeID + ']'),
        type: UPATE_TYPES.REMOVE_NODE,
        fromIndex: prevChild._mountIndex,
        toIndex: null
      })
      //如果以前已經渲染過了,記得先去掉以前所有的事件監聽
      if (prevChildren[name]._rootNodeID) {
        $(document).undelegate('.' + prevChildren[name]._rootNodeID);
      }
    }
  }
}

好了,整個的diff就完成了,這個時候當遞歸完成,我們就需要開始做patch的動作了,把這些差異對象實打實的反映到具體的dom節點上。

//用於將childNode插入到指定位置
function insertChildAt(parentNode, childNode, index) {
    var beforeChild = parentNode.children().get(index);
    beforeChild ? childNode.insertBefore(beforeChild) : childNode.appendTo(parentNode);
}

ReactDOMComponent.prototype._patch = function(updates) {
    var update;
    var initialChildren = {};
    var deleteChildren = [];
    for (var i = 0; i < updates.length; i++) {
        update = updates[i];
        if (update.type === UPATE_TYPES.MOVE_EXISTING || update.type === UPATE_TYPES.REMOVE_NODE) {
            var updatedIndex = update.fromIndex;
            var updatedChild = $(update.parentNode.children().get(updatedIndex));
            var parentID = update.parentID;

            //所有需要更新的節點都保存下來,方便後面使用
            initialChildren[parentID] = initialChildren[parentID] || [];
            //使用parentID作爲簡易命名空間
            initialChildren[parentID][updatedIndex] = updatedChild;


            //所有需要修改的節點先刪除,對於move的,後面再重新插入到正確的位置即可
            deleteChildren.push(updatedChild)
        }

    }

    //刪除所有需要先刪除的
    $.each(deleteChildren, function(index, child) {
        $(child).remove();
    })


    //再遍歷一次,這次處理新增的節點,還有修改的節點這裏也要重新插入
    for (var k = 0; k < updates.length; k++) {
        update = updates[k];
        switch (update.type) {
            case UPATE_TYPES.INSERT_MARKUP:
                insertChildAt(update.parentNode, $(update.markup), update.toIndex);
                break;
            case UPATE_TYPES.MOVE_EXISTING:
                insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
                break;
            case UPATE_TYPES.REMOVE_NODE:
                // 什麼都不需要做,因爲上面已經幫忙刪除掉了
                break;
        }
    }
}

 

自定義組件的更新

  1. 首先,receiveComponent 內部會將最新的 state 與老的 state 進行合併。
  2. 觸發 shouldComponentUpdate,判斷組件是否需要更新,需要的話繼續往下走。
  3. 觸發 componentWillUpdate,表示組件即將更新
  4. 然後拿這個最新的 state 和 props 生成一個虛擬 Dom,與原來的的虛擬 Dom 進行結構比較。
  5. 如果判斷不需要更新,如 key 變了,或者類型都變了,直用最新的 state 和 props mount 出新的 html,替換掉老的節點
  6. 如果判斷需要更新,繼續遞歸調用子節點的 receiveComponent 
  7. 所有的子節點都處理完了,觸發 componentDidUpdate,表示更新完成
ReactCompositeComponent.prototype.receiveComponent = function(nextElement, newState) {

    //如果接受了新的,就使用最新的element
    this._currentElement = nextElement || this._currentElement

    var inst = this._instance;
    //合併state
    var nextState = $.extend(inst.state, newState);
    var nextProps = this._currentElement.props;


    //改寫state
    inst.state = nextState;


    //如果inst有shouldComponentUpdate並且返回false。說明組件本身判斷不要更新,就直接返回。
    if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return;

    //生命週期管理,如果有componentWillUpdate,就調用,表示開始要更新了。
    if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps, nextState);


    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    //重新執行render拿到對應的新element;
    var nextRenderedElement = this._instance.render();


    //判斷是需要更新還是直接就重新渲染
    //注意這裏的_shouldUpdateReactComponent跟上面的不同哦 這個是全局的方法
    if (_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
        //如果需要更新,就繼續調用子節點的receiveComponent的方法,傳入新的element更新子節點。
        prevComponentInstance.receiveComponent(nextRenderedElement);
        //調用componentDidUpdate表示更新完成了
        inst.componentDidUpdate && inst.componentDidUpdate();

    } else {
        //如果發現完全是不同的兩種element,那就乾脆重新渲染了
        var thisID = this._rootNodeID;
        //重新new一個對應的component,
        this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
        //重新生成對應的元素內容
        var nextMarkup = _renderedComponent.mountComponent(thisID);
        //替換整個節點
        $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);

    }

}

//用來判定兩個element需不需要更新
//這裏的key是我們createElement的時候可以選擇性的傳入的。用來標識這個element,當發現key不同時,我們就可以直接重新渲染,不需要去更新了。
var _shouldUpdateReactComponent = function(prevElement, nextElement){
    if (prevElement != null && nextElement != null) {
        var prevType = typeof prevElement;
        var nextType = typeof nextElement;
        if (prevType === 'string' || prevType === 'number') {
          // 
          return nextType === 'string' || nextType === 'number';
        } else {
          return nextType === 'object' && prevElement.type === nextElement.type &&         
          prevElement.key === nextElement.key;
        }
    }
  return false;
}

三、銷燬階段

componentWillUnmound:組件即將銷燬

 

下圖是react中方法調用的整個鏈路

上下文 Context

Context API 可以說是 React中最有趣的一個特性了。一方面很多流行的框架(例如 react-redux、 mobx-react、 react-router等)都在使用它;另一方面官方文檔中卻不推薦我們使用它。

一、場景

在 react 中,我們在傳遞消息時,通常的做法是父節點通過 props 屬性,一層一層的傳遞到目標子節點,當子節點層級較深時,代碼寫起來就顯得相當繁瑣。這時 context 就派上用場了,只要在最外層組件上將數據塞進 context,然後在任意層級的子節點都可以獲取到,省去了所有不必要的的中間傳遞過程。

二、用法

老版本的用法

class Parent extends React.Component {
    getChildContext () {
        return { name: '張三' }
    }
}
Parent.childContextTypes = {
    name: ProtoTypes.string
}

然後在任意層級的子組件的即可使用

class Child extends React.Component {

    render () {
        return (
            <div>{ this.context.name }</div>
        )
    }
}
Child.contextTypes = {
    name: ProtoTypes.string
}

缺點:當中間節點在 shouldComponentUpdate 中 return false 的話,後續的子節點將不再更新,也就接收不到 context 中傳遞的消息了

新版本的用法

React 在版本 16.3-alpha 裏引入了新的 Context API,主要由以下幾部分組成:

  • React.createContext 方法用於創建一個 Context 對象,改對象包含 Provider 和 Consumer 兩個屬性,均爲 React 組件
  • Provider 組件用在組件樹的最外層,它接收一個屬性 value,值可以是任意 js 類型
  • Consumer 用在 Provider 內部任意層級,它接收一個屬性 children,值是一個函數,該函數的入參是第一步創建的 Context 對象,返回值是一個 React 元素
// createContext.js
export default React.createContext({
    name: '張三'
})

// parent.js
import mycontext from './createContext';
class Parent extends React.Component {
    render () {
        return (
            <mycontext.Provider value={{ name: '李四' }}>
                <child></child>
            </mycontext.Provider>
        )
    }
}
// child.js
import mycontext from './createContext';
class Child extends React.Component {
    render () {
        return (
            <mycontext.Consumer>
            {
                context => {
                    return <div>{ context.name }</div>
                }
            }
            </mycontext.Consumer>
        )
    }    
}

這裏需要注意幾點:

Provider 和 Consumer 必須來自同一次 createContext 調用

createContext 會接收一個默認的值做爲參數,當 Consumer 外層沒有 Provider 時就會使用該默認值

當 Provider 中的 value 變化時,Consumer 組件會接收到新值並觸發 rerender,此過程不受 shouldComponentUpate 影響

Provider 組件利用 Object.js 檢測 value 值是否有更新,注意 Object.js 和 === 並不完全相同

參考:

https://www.cnblogs.com/enoy/articles/react.html

https://segmentfault.com/a/1190000021178528?utm_source=tag-newest 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章