目錄
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,渲染到頁面
總結:
- 初始化階段根據默認的屬性以及初始化狀態,調用 createElement 生成 ReactElement ,也就是我們常說的虛擬 Dom。其實就是一顆抽象語法樹。
- render 函數會根據上一步生成的 ReactElement 的 type 字段來判斷它的類型,生成相應的 ReactComponent 實例。類型有文本、原生dom、自定義組件三種。
- 每一種 ReactComponent 都有 mountComponent 函數,按照自己的規則來生成 html 結構
- 子節點 children 會掛在 props 屬性上,循環處理時會根據子節點的類型從第 2 步開始走,如此遞歸下去直到沒有子節點爲止。
- 將遞歸處理出來的 html 結構拼裝起來,innerHTML 到 container 中去
- 到此完成初始化渲染,觸發 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);
}
}
原生節點的更新
原生節點的更新是比較複雜的,主要包括兩個部分:
- 屬性的更新,包括對特殊屬性比如事件的處理
- 子節點的更新,拿新節點和老節點對比,找出差異,稱之爲 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);
}
首先來看屬性的更新
- 先遍歷老集合,不在新集合裏的屬性,需要刪除。具體就是清除監聽事件以及dom上的屬性
- 在遍歷新集合,對於事件屬性需要先清除再綁定。對於普通屬性需要掛載到當前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])
}
}
子節點的更新是最複雜的
- 首先,用一個 updateDepth 來記錄當前節點的深度,用 diffQueue 更新隊列來保存需要更新的內容
- 內部主要通過一個_diff 函數來找出差異,放入更新隊列
- _diff 中會先調用 flattenChildren 把當前 children 數組轉化成一個 map,如果子節點上設置了 key 就用 key,沒設置就用當前位置 index 當做 key
- 老的 children map 轉好了,再在 generateComponentChildren 中去轉新的 children map。過程就是遍歷新的 children,通過 key 去找老的 child,通過全局方法 _shouldUpdateReactComponent 判斷是需要更新還是重新生成新的 component 實例。如果需要更新,就遞歸調用子節點的 receiveComponent。
- 現在新的 children map 也有了,先遍歷新的 map,通過 key 去找到老的 child
- 如果老節點===新節點,代表同一個 component 實例,移動位置就可以了,push 到更新隊列中
- 如果老節點存在但是!==新節點,代表 element 變了,老節點需要刪除,push 到更新隊列
- 將全新的節點 push 到更新隊列中
- 再遍歷老的 map,把老 map 中存在但是新 map 中不存在的節點刪掉,push 到更新隊列中
- 等到整個虛擬 Dom 數遞歸 receiveComponent 後,執行 patch 執行更新操作
- 在 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;
}
}
}
自定義組件的更新
- 首先,receiveComponent 內部會將最新的 state 與老的 state 進行合併。
- 觸發 shouldComponentUpdate,判斷組件是否需要更新,需要的話繼續往下走。
- 觸發 componentWillUpdate,表示組件即將更新
- 然後拿這個最新的 state 和 props 生成一個虛擬 Dom,與原來的的虛擬 Dom 進行結構比較。
- 如果判斷不需要更新,如 key 變了,或者類型都變了,直用最新的 state 和 props mount 出新的 html,替換掉老的節點
- 如果判斷需要更新,繼續遞歸調用子節點的 receiveComponent
- 所有的子節點都處理完了,觸發 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