react核心思想
簡單來説,就是virtual dom & react diff。
我們都知道在前端開發中,js運行很快,dom操作很慢,而react充分利用了這個前提。在react中render的執行結果是樹形結構的javascript對象,當數據(state || props)發生變化時,會生成一個新的樹形結構的javascript對象,這兩個javascript對象我們可以稱之為virtual dom。然後對比兩個virtual dom,找出最小的有變化的點,這個對比的過程我們稱之為react diff,將這個變化的部分(patch)加入到一個隊列中,最終批量更新這些patch到dom中。
react執行render和setState進行渲染時主要有兩個階段
- 調度階段(Reconciler):React 會自頂向下通過遞歸, 用新數據生成一顆新樹,遍歷虛擬dom,diff新老virtual dom樹,蒐集具體的UI差異,找到需要更新的元素(Patch),放到更新隊列中。
- 渲染階段(Renderer):遍歷更新隊列,通過調用宿主環境的API(比如 DOM、Native、WebGL)實際更新渲染對應元素。
引入虛擬dom的好處是什麼?
- js運行很快,dom操作很慢。配合react diff算法,通過對比virtual Dom,可以快速找出真實dom的最小變化,這樣前端其實是不需要去關注那個變化的點,把這個變化交給react來做就好,同時你也不必自己去完成屬性操作、事件處理、DOM更新,React會替你完成這一切,這讓我們更關注我們的業務邏輯而非DOM操作,基於以上兩點可大大提升我們的開發效率。
- 跨瀏覽器、跨平台兼容。react基於virtual dom自己實現了一套自己的事件機制,自己模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,抹平了各個瀏覽器的事件兼容性問題。跨平台virtual dom為React帶來了跨平台渲染的能力。以React Native為例子。React根據virtual dom畫出相應平台的ui層,只不過不同平台畫的姿勢不同而已。
react對性能的提升
關於提升性能,很多人説virtual dom可以提升性能,這一説法實際上是很片面的。因為我們知道,直接操作dom是非常耗費性能的,但是即使我們用了react,最終依然要去操作真實的dom。而react幫我們做的事情就是儘量用最佳的方式有操作dom。如果是首次渲染,virtual dom不具有任何優勢,甚至它要進行更多的計算,消耗更多的內存。
react本身的優勢在於react diff算法和批處理策略。react在頁面更新之前,提前計算好了如何進行更新和渲染DOM,實際上,這個計算過程我們在直接操作DOM時,也是可以自己判斷和實現的,但是一定會耗費非常多的精力和時間,而且往往我們自己做的是不如React好的。所以,在這個過程中React幫助我們"提升了性能"。
所以,我更傾向於説,virtual dom幫助我們提高了開發效率,在重複渲染時它幫助我們計算如何更高效的更新,而不是它比DOM操作更快。
什麼是jsx?
我們在實現一個React組件時可以選擇兩種編碼方式,第一種是使用JSX編寫,第二種是直接使用React.createElement編寫。實際上,上面兩種寫法是等價的,jsx只是為React.createElemen方法的語法糖,最終所有的jsx都會被babel轉換成React.createElement。
但是請注意,babel在編譯時會判斷jsx中組件的首字母,當首字母為小寫時,其被認定為原生dom標籤,createElement的第一個變量被編譯為字符串。當首字母為大寫時,其被認定為自定義組件,createElement的第一個變量被編譯為對象。
react的生命週期是怎樣的?
在react16中,廢棄了三個will屬性componentWillMount,componentWillReceiveProps,comonentWillUpdate,但是目前還未刪除,react17計劃會刪除,同時通過UNSAFF_前綴向前兼容。
在 React 中,我們可以將其生命週期分為三個階段。
掛載階段
- constructor()
組件在掛載前,會調用它的構造函數,在構造函數內部必須執行一次super(props),否則不能在constructor內部使用this,constructor通常用於給this.state初始化內部狀態,為事件處理函數綁定this。 - static getDerivedStateFromProps(newProps,prevState)
是一個靜態方法,父組件傳入的newProps和當前組件的prevState進行比較,判斷時需要更新state,返回值用作更新state,如果不需要則返回null。在render()方法之前調用,並且在初始掛載和後續更新時調用。 - render()
render()是組件中唯一必須實現的方法。需要返回以下類型,React元素、數組、fragments、Portals、字符串或者、值類型、布爾類型或null。同時render函數應該是純函數。不能夠調用setState。 - componentDidMount()
更新階段
- static getDerivedStateFromProps(props,state)
- shouldComponentUpate()
當props或者state發生變化時,會在渲染前調用。根據父組件的props和當前的state進行對比,返回true/false。決定是否觸發後續的 UNSAFE_componentWillUpdate(),render()和componentDidUpdate()。。 - render()
- getSnapshotBeforeUpdate(prevProps,prevSteate)
在render()之後componentDidUpdate()之前調用。此方法的返回值(snaphot)可作為componentDidUpdate()的第三個參數使用。如不需要返回值則直接返回null。 - componentDidUpdate(prevProps, prevState, snapshot)
該方法會在更新完成後立即調用。首次渲染不會執行此方法,當組件更新後,可以在此處對dom進行操作。可以在此階段使用setState,觸發render()但必須包裹在一個條件語句裏,以避免死循環。
卸載階段
- componentWillUnmount()
會在組件卸載和銷燬之前直接調用。此方法主要用來執行一些清理工作,例如:定時器,清除事件綁定,取消網絡請求。此階段不能調用setState,因為組件永遠不會重新渲染。
react diff解決什麼問題?是怎樣的實現思路?
react diff會幫助我們計算出virtual dom中真正變化的部分,並只針對該部分進行實際dom操作,而非重新渲染整個頁面,從而保證了每次操作更新後頁面的高效渲染。傳統diff算法通過循環遞歸對節點進行依次對比,效率低下,算法複雜度達到 O(n^3)。react diff基於一下三個策略實現了O(n)的算法複雜度。
- Web UI中dom節點跨層級的移動操作特別少,可以忽略不計。
- 擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。
- 對於同一層級的一組子節點,它們可以通過唯一id進行區分。
基於以上三個前提策略,React分別對tree diff、component diff以及element diff 進行算法優化,事實也證明這三個前提策略是合理且準確的,它保證了整體界面構建的性能。
react中key的作用,能不能用index作為Key。
首先説一下element diff的過程。比如有老的集合(A,B,C,D)和新的集合(B,A,D,C),我們考慮在不增加空間複雜度的情況下如何以O(n)的時間複雜度找出老集合中需要移動的元素。
在react裏的思路是這樣的,遍歷新集合,初始化lastIndex=0(代表訪問過的老集合中最右側的位置),表達式為max(prev.mountIndex, lastIndex),如果當前節點在老集合中的位置即(prev.mountIndex)比lastIndex大説明當前訪問節點在老集合中就比上一個節點位置靠後則該節點不會影響其他節點的位置,因此不用添加到差異隊列中,即不執行移動操作,只有當訪問的節點比 lastIndex 小時,才需要進行移動操作。
部分源碼為
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
var prevChild = prevChildren && prevChildren[name]; // 老節點
var nextChild = nextChildren[name]; // 新節點
if (prevChild === nextChild) { // 如果新節點存在老節點集合裏
// 移動節點
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) { // 如果不存在在
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 刪除節點
this._unmountChild(prevChild);
}
// 初始化並創建節點
this._mountChildAtIndex(
nextChild, nextIndex, transaction, context
);
}
nextIndex++;
}
// 移動節點
moveChild: function(child, toIndex, lastIndex) {
if (child._mountIndex < lastIndex) {
this.prepareToManageChildren();
enqueueMove(this, child._mountIndex, toIndex);
}
}
React 16有哪些新特性?
- render支持返回數組和字符串
- Error Boundaries
- createPortal
- rollup減小文件體積
- fiber
- Fragment
- createRef
- Strict Mode
React Fiber是什麼?解決什麼問題?
React Fiber是React對核心算法的一次重新實現。
在協調階段階段,以前由於是採用的遞歸的遍歷方式,這種也被稱為Stack Reconciler,主要是為了區別Fiber Reconciler取的一個名字。這種方式有一個特點: 一旦任務開始進行,就無法中斷,那麼js將一直佔用主線程,一直要等到整棵virtual dom樹計算完成之後,才能把執行權交給渲染引擎,那麼這就會導致一些用户交互、動畫等任務無法立即得到處理,就會有卡頓,非常的影響用户體驗。
頁面是一幀一幀繪製出來的,當每秒繪製的幀數(FPS)達到60時,頁面是流暢的,小於這個值時,用户會感覺到卡頓。1秒60幀,所以每一幀分到的時間是1000/60 ≈ 16ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。如果任意一個步驟所佔用的時間過長,超過16ms了之後,用户就能看到卡頓。
Fiber如何實現
簡單來説就是時間分片 + 鏈表結構。而fiber就是維護每一個分片的數據結構。
Fiber利用分片的思想,把一個耗時長的任務分成很多小片,每一個小片的運行時間很短,在每個小片執行完之後,就把控制權交還給React負責任務協調的模塊,如果有緊急任務就去優先處理,如果沒有就繼續更新,這樣就給其他任務一個執行的機會,唯一的線程就不會一直被獨佔。
因此,在組件更新時有可能一個更新任務還沒有完成,就被另一個更高優先級的更新過程打斷,優先級高的更新任務會優先處理完,而低優先級更新任務所做的工作則會完全作廢,然後等待機會重頭再來。所以 React Fiber把一個更新過程分為兩個階段:
- 第一個階段 Reconciliation Phase,Fiber會找出需要更新的DOM,這個階段是可以被打斷的。
- 第二個階段 Commit Phase,是無法別打斷,完成dom的更新並展示。
什麼是高階組件
高階組件(HOC)是React中用於複用組件邏輯的一種高級技巧。HOC自身不是React API的一部分,它是一種基於 React 的組合特性而形成的設計模式。具體而言,高階組件是參數為組件,返回值為新組件的函數。
請注意,HOC 不會修改傳入的組件,也不會使用繼承來複制其行為。相反,HOC 通過將組件包裝在容器組件中來組成新組件。HOC 是純函數,沒有副作用。
我理解的高階組件是,將組件以參數的方式傳遞給另外一個函數,在該函數中,對組件進行包裝,封裝了一些公用的組件邏輯,實現組件的邏輯複用,該函數被稱為高階組件。但是請注意,高階組件不應修改傳入的組件行為。
屬性代理
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
反向繼承
function hoc(ComponentClass) {
return class HOC extends ComponentClass {
render() {
if (this.state.success) {
return super.render()
}
return <div>Loading...</div>
}
}
}
export default class ComponentClass extends Component {
state = {
success: false,
data: null
};
async componentDidMount() {
const result = await fetch(...請求);
this.setState({
success: true,
data: result.data
});
}
render() {
return <div>主要內容</div>
}
}
什麼是渲染屬性
術語 “render prop” 是指一種技術,用於使用一個值為函數的 prop 在 React 組件之間的代碼共享。
帶有渲染屬性(Render Props)的組件需要一個返回 React 元素並調用它的函數,而不是實現自己的渲染邏輯。
我理解的渲染屬性是,提供渲染頁面的props給子組件,共享可以共享子組件的狀態,複用子組件的狀態,並告訴子組件如何進行渲染。
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 與 HOC 不同,我們可以使用具有 render prop 的普通組件來共享代碼
class Mouse extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = React.createClass({
render() {
return (
<div style={{ height: '100%' }}>
<Mouse render={({ x, y }) => (
// render prop 給了我們所需要的 state 來渲染我們想要的
<h1>The mouse position is ({x}, {y})</h1>
)}/>
</div>
)
}
})
ReactDOM.render(<App/>, document.getElementById('app'))
什麼是React Hooks,它是為了解決什麼問題?説一下它的實現原理!
React Hooks 是 React 16.7.0-alpha 版本推出的新特性,它可以讓你在不編寫class的情況下使用state以及其他的 React特性。React Hooks要解決的問題是狀態共享,是繼render-props和hoc之後的第三種狀態共享方案,不會產生JSX嵌套地獄問題。這個狀態指的是狀態邏輯,所以稱為狀態邏輯複用會更恰當,因為只共享數據處理邏輯,不會共享數據本身。
簡單實現
let memoizedState = []; // hooks 存放在這個數組
let cursor = 0; // 當前 memoizedState 下標
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回當前 state,並把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
React為什麼要在構造函數中調用super(props),為什麼要bind(this)?
super代表父類的構造函數,javascript規定如果子類不調用super是不允許在子類中使用this的,這不是React的限制,而是javaScript的限制,同時你也必須給super傳入props,否則React.Component就沒法初始化this.props
在 React 的類組件中,當我們把事件處理函數引用作為回調傳遞過去,事件處理程序方法會丟失其隱式綁定的上下文。當事件被觸發並且處理程序被調用時,this的值會回退到默認綁定,即值為 undefined,這是因為類聲明和原型方法是以嚴格模式運行。
説一下react事件機制?
react為什麼要用自己的事件機制
- 減少內存消耗,提升性能,不需要註冊那麼多的事件了,一種事件類型只在document上註冊一次。
- 統一規範,解決 ie 事件兼容問題,簡化事件邏輯。
- 對開發者友好。
react的合成事件
SyntheticEvent是react合成事件的基類,定義了合成事件的基礎公共屬性和方法。react會根據當前的事件類型來使用不同的合成事件對象,比如鼠標單機事件 - SyntheticMouseEvent,焦點事件-SyntheticFocusEvent等,但是都是繼承自SyntheticEvent。在合成事件中主要做了以下三件事情。
- 對原生事件的封裝
- 對某些原生事件的升級和改造
- 不同瀏覽器事件兼容的處理
事件註冊
組件掛載階段,根據組件內的聲明的事件類型-onclick,onchange等,給document上添加事件addEventListener,並指定統一的事件處理程序dispatchEvent。
通過virtual dom的props屬性拿到要註冊的事件名,回調函數,通過listenTo方法使用原生的addEventListener進行事件綁定。
事件存儲
事件存儲,就是把react組件內的所有事件統一的存放到一個二級map對象裏,緩存起來,為了在觸發事件的時候可以查找到對應的方法去執行。先查找事件名,然後找對對應的組件id相對應的事件。如下圖:
setState是異步的?為什麼要這麼做?setState執行機制?
由執行機制看,setState本身並不是異步的,而是在調用setState時,如果react正處於更新過程,當前更新會被暫存,等上一次更新執行後再執行,這個過程給人一種異步的假象。
ReactComponent.prototype.setState = function(partialState, callback) {
// 將setState事務放進隊列中
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
enqueueSetState: function (publicInstance, partialState) {
// 獲取當前組件的instance
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 將要更新的state放入一個數組裏
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// 將要更新的component instance也放在一個隊列裏
enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
// 如果沒有處於批量創建/更新組件的階段,則處理update state事務
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果正處於批量創建/更新組件的過程,將當前的組件放在dirtyComponents數組中
dirtyComponents.push(component);
}
這裏的partialState可以傳object,也可以傳function,它會產生新的state以一種Object.assgine()的方式跟舊的state進行合併。
由這段代碼可以看到,當前如果正處於創建/更新組件的過程,就不會立刻去更新組件,而是先把當前的組件放在dirtyComponent裏,所以不是每一次的setState都會更新組件。這段代碼就解釋了我們常聽説的:setState是一個異步的過程,它會集齊一批需要更新的組件然後一起更新。而batchingStrategy 又是個什麼東西呢?
ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy = {
// 用於標記當前是否出於批量更新
isBatchingUpdates: false,
// 當調用這個方法時,正式開始批量更新
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// 如果當前事務正在更新過程在中,則調用callback,既enqueueUpdate
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 否則執行更新事務
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
react-router原理
前端路由的原理思路大致上都是相同的,即實現在無刷新頁面的條件下切換顯示不同的頁面。而前端路由的本質就是頁面的URL發生改變時,頁面的顯示結果可以根據URL的變化而變化,但是頁面不會刷新。目前實現前端路由有兩種方式:
通過Hash實現前端路由
路徑中hash值改變,並不會引起頁面刷新,同時我們可以通過hashchange事件,監聽hash的變化,從而實現我們根據不同的hash值展示和隱藏不同UI顯示的功能,進而實現前端路由。
通過H5的history實現前端路由
HTML5的History接口,History對象是一個底層接口,不繼承於任何的接口。History接口允許我們操作瀏覽器會話歷史記錄。
而history的pushState和repalce方法可以實現改變當前頁面顯示的url,但都不會刷新頁面。
未完待續~
參考文檔:
react生命週期詳解
React diff
react 16新特性
react fiber1
react fiber2
react hooks
react 事件機制
setState機制1
setState機制2
react-router原理
集合