緣起
在這個 find a job 地獄難度的時間,整理一份 React 核心指南,共勉之
目錄結構
文章目錄
壹、Context
Context
提供了一個無需爲每層組件手動添加props
,就能在組件樹間進行數據傳遞的方法。
for example
組件A —— time 數據 需要向下傳遞,我們可以通過 props 來傳遞,但是那樣過於耦合
組件B
組件C
組件D
...
典型解決方案就是Context
- 創建context :
const ThemeContext = React.createContext('C_data');
hook 寫法
const value = useContext(MyContext);
- API
- React.createContext
const MyContext = React.createContext(defaultValue);
- Context.Provider
<MyContext.Provider value={/* 某個值 */}>
- React.createContext
// 創建context
const DataContext = React.createContext('123');
class App extends React.Component {
render() {
return (
// 使用 Provider 將當前值傳遞下去
<DataContext.Provider value="abc">
<A />
</DataContext.Provider>
);
}
}
// 中間的組件再也不必指明往下傳遞 theme 了。
function A() {
return (
<div>
<B />
</div>
);
}
class B extends React.Component {
// 當前值爲 abc
// React 會往上找到最近的 theme Provider,然後使用它的值
static contextType = DataContext;
render() {
return <p/>this.context</p>;
}
貳、Refs & DOM
Refs 提供了一種方式,允許我們訪問 DOM 節點或在 render 方法中創建的 React 元素
- 創建 Refs
React.createRef()
// hook 寫法
const refContainer = useRef(initialValue);
for example
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
// 訪問 ref
const node = this.myRef.current;
叄、Refs轉發 forwardRef
如果要在函數組件中使用 ref,你可以使用 forwardRef(可與 useImperativeHandle 結合使用),或者可以將該組件轉化爲 class 組件。
有了上面 Refs 操作Dom 的認識,下面跑拋出一個問題,現在我要在父組件 <F_component />
中 獲取子組件的ref
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
// 我可以在這裏訪問 被轉發的 `button` 的`ref`
render() {
return <F_component ref={this.ref} />;
}
}
const F_component = React.forwardRef((props, ref) => (
<button ref={ref} className="F_component">
{props.children}
</button>
));
// 你可以直接獲取 DOM button 的 ref:
const ref = React.createRef();
<F_component ref={ref}>Click me!</F_component>;
- 我們通過調用 React.createRef 創建了一個 React ref 並將其賦值給 ref 變量。
- 我們通過指定 ref 爲 JSX 屬性,將其向下傳遞給 。
- React 傳遞 ref 給 forwardRef 內函數 (props, ref) =>…,作爲其第二個參數。
- 我們向下轉發該 ref 參數到
<button ref={ref}>
,將其指定爲 JSX 屬性。 - 當 ref 掛載完成,ref.current 將指向
<button>
DOM 節點。
肆、 Fragments
Fragments 允許你將子列表分組,而無需向 DOM 添加額外節點。
Fragments = jsx: <> … </>
or
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
- 短語法
<></> 不支持 key
伍、高階組件(HOC)
高階組件(HOC)是 React 中用於複用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基於 React 的組合特性而形成的設計模式。
上面👆是官方短解釋 以下我通俗一點解釋吧:
想必大家都知道JavaScript的高階函數吧。其實高階組件就相當於一個高階函數。即:高階函數,就是函數中可以傳入另一個函數作爲參數的函數。
- 在React中 => JSX 函數即組件 = 高階組件是將組件轉換爲另一個組並返回
for example
假設有一個組件MyComponent,需要從LocalStorage中獲取數據,然後渲染數據到界面。我們可以這樣寫組件代碼:
import React, { Component } from 'react'
class MyComponent extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
return <div>{this.state.data}</div>
}
}
- 現在我們要封裝一個高階組件
withPersistentData
來達到上述代碼邏輯的複用
import React, { Component } from 'react'
function withPersistentData(WrappedComponent, key) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({data});
}
render() {
// 通過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
class MyComponent2 extends Component {
render() {
return <div>{this.props.data}</div>
}
//省略其他邏輯...
}
class MyComponent3 extends Component {
render() {
return <div>{this.props.data}</div>
}
//省略其他邏輯...
}
const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data');
const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');
請注意,HOC 不會修改傳入的組件,也不會使用繼承來複制其行爲。相反,HOC 通過將組件包裝在容器組件中來組成新組件。HOC 是純函數,沒有副作用。
- HOC 高階組件 => 高階函數
在我們的使用過程中,我們可以在這個過程中對傳入的組件進行更多的 React 模式的處理,例如我們想在
componentWillMount
中來獲取數據.
也是 高階組件最常見的函數簽名形式
HOC([param])([WrappedComponent])
import React, { Component } from 'react'
const withPersistentData = (key) => (WrappedComponent) => {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({data});
}
render() {
// 通過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
class MyComponent2 extends Component {
render() {
return <div>{this.props.data}</div>
}
//省略其他邏輯...
}
class MyComponent3 extends Component {
render() {
return <div>{this.props.data}</div>
}
//省略其他邏輯...
}
const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2);
const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);
-
這種形式的高階組件因其特有的便利性——結構清晰(普通參數和被包裹組件分離)、易於組合,大量出現在第三方庫中
react-redux
中的connect
就是一個典型 -
注意事項
-
不要在 render 方法中使用 HOC
React
的diff 算法
(稱爲協調)使用組件標識來確定它是應該更新現有子樹還是將其丟棄並掛載新子樹。 如果從 render 返回的組件與前一個渲染中的組件相同(===),則React
通過將子樹與新子樹進行區分來遞歸更新子樹。 如果它們不相等,則完全卸載前一個子樹。
- 錯誤示例
render() {
// 每次調用 render 函數都會創建一個新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 這將導致子樹每次渲染都會進行卸載,和重新掛載的操作!
return <EnhancedComponent />;
}
- 務必複製靜態方法
如果需要使用被包裝組件的靜態方法,那麼必須手動拷貝這些靜態方法。因爲高階組件返回的新組件,是不包含被包裝組件的靜態方法。
ps: 我們可以使用 hoist-non-react-statics 這個庫來解決這個問題
當然你也可以手動拷貝
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必須準確知道應該拷貝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
// 使用這種方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...單獨導出該方法...
export { someFunction };
// ...並在要使用的組件中,import 它們
import MyComponent, { someFunction } from './MyComponent.js';
陸、Portals (插槽)
Portal 提供了一種將子節點渲染到存在於父組件以外的 DOM 節點的優秀的方案。
ReactDOM.createPortal(child, container)
第一個參數(child)是任何可渲染的 React 子元素,例如一個元素,字符串或 fragment。第二個參數(container)是一個 DOM 元素。
ps: 一個 portal 的典型用例是當父組件有 overflow: hidden 或 z-index 樣式時,但你需要子組件能夠在視覺上“跳出”其容器。例如,對話框、懸浮卡以及提示框
HTML 和 CSS 屬性我就不展示了 參照 如上⬆️鏈接🔗,把 JS 拿出來講一下
// 根節點
const appRoot = document.getElementById('app-root');
// 被插入Dom 節點元素
const modalRoot = document.getElementById('modal-root');
// 創建模態框組件
class Modal extends React.Component {
constructor(props) {
super(props);
// 創建一個Dom元素容器
this.el = document.createElement('div');
}
componentDidMount() {
// 將被插入節點放到dom容器中作爲它的子元素
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
// 組件卸載的時候刪除這個容器
modalRoot.removeChild(this.el);
}
render() {
// 使用 Portal 來把需要展示的元素放到其他節點
return ReactDOM.createPortal(
// 可以是任何有效的React子代:JSX,字符串,數組等。
this.props.children,
// 一個DOM 元素
this.el,
);
}
}
.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {showModal: false};
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow() {
this.setState({showModal: true});
}
handleHide() {
this.setState({showModal: false});
}
render() {
// 處理 邏輯
const modal = this.state.showModal ? (
<Modal>
<div className="modal">
<div>
With a portal, we can render content into a different
part of the DOM, as if it were any other React child.
</div>
This is being rendered inside the #modal-container div.
<button onClick={this.handleHide}>Hide modal</button>
</div>
</Modal>
) : null;
return (
<div className="app">
This div has overflow: hidden.
<button onClick={this.handleShow}>Show modal</button>
{modal}
</div>
);
}
}
ReactDOM.render(<App />, appRoot);
柒、Profiler & Profiler API 性能監控
Profiler 分爲2個方面 一個是 瀏覽器插件 Profiler 一個是 React 提供給我們的 Profiler API
他們都是用來做 性能分析的
簡單介紹下:
Profiler 能添加在 React 樹中的任何地方來測量樹中這部分渲染所帶來的開銷。 它需要兩個 prop :一個是 id(string),一個是當組件樹中的組件“提交”更新的時候被React調用的回調函數 onRender(function)。
for example
例如,爲了分析 Navigation 組件和它的子代:
render(
<App>
<Profiler id="Navigation" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
- onRender 回調
function onRenderCallback(
id, // 發生提交的 Profiler 樹的 “id”
phase, // "mount" (如果組件樹剛加載) 或者 "update" (如果它重渲染了)之一
actualDuration, // 本次更新 committed 花費的渲染時間
baseDuration, // 估計不使用 memoization 的情況下渲染整顆子樹需要的時間
startTime, // 本次更新中 React 開始渲染的時間
commitTime, // 本次更新中 React committed 的時間
interactions // 屬於本次更新的 interactions 的集合
) {
// 合計或記錄渲染時間。。。
}
Profiler 插件
捌、Diffing 算法
該算法的複雜程度爲 O(n 3 ),其中 n 是樹中元素的數量。
如果在 React 中使用了該算法,那麼展示 1000 個元素所需要執行的計算量將在十億的量級範圍。這個開銷實在是太過高昂。於是 React 在以下兩個假設的基礎之上提出了一套 O(n) 的啓發式算法:
- 兩個不同類型的元素會產生出不同的樹;
- 開發者可以通過 key prop 來暗示哪些子元素在不同的渲染下能保持穩定;
這也就是我們在React中遍歷key
對於性能的重要性了,瞭解過 tree 算法我們都知道在樹的子節點中 我們只要能確定 child tree
的 key
(相當於權值 我就可以進行大幅優化)
-
元素對比
分爲: 1. 比對不同類型的元素 2. 比對同一類型的元素
- 在根節點以下的組件也會被卸載,它們的狀態會被銷燬。比如,當比對以下更變時:
<div> <Counter /> </div> <span> <Counter /> </span>
React 會銷燬 Counter 組件並且重新裝載一個新的組件。
- 當比對兩個相同類型的 React 元素時,React 會保留 DOM 節點,僅比對及更新有改變的屬性。比如:
<div className="before" title="stuff" /> <div className="after" title="stuff" />
通過比對這兩個元素,React 知道只需要修改 DOM 元素上的 className 屬性。
-
React 繼續對子節點進行遞歸。
Keys [敲黑板 面試問爛了的題目 下面我來解析下原理]
在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表;當產生差異時,生成一個 mutation。
<ul>
<li>first</li>
<li>second</li>
</ul>
########################################## 添加 => `<li>third</li>`
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
這樣子是 在列表頭部添加元素 這樣子開銷較小
但是如果是在頭部增加的話
<ul>
<li>first</li>
<li>second</li>
</ul>
########################################## 添加 => `<li>third</li>`
<ul>
<li>top third</li>
<li>first</li>
<li>second</li>
</ul>
如果簡單實現的話,那麼在列表頭部插入會很影響性能,那麼更變開銷會比較大.
爲了解決以上問題,React 支持 key 屬性。當子元素擁有 key 時,React 使用 key 來匹配原有樹上的子元素以及最新樹上的子元素。以下例子在新增 key 之後使得之前的低效轉換變得高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
現在 React 知道只有帶着 ‘2014’ key 的元素是新元素,帶着 ‘2015’ 以及 ‘2016’ key 的元素僅僅移動了。
玖、Render Props
術語 “render prop” 是指一種在 React 組件之間使用一個值爲函數的 prop 共享代碼的簡單技術
ps: 我在這個地方吃過虧,一下子沒反應過來. 比較簡單:其實 Render Props
就是 Render
這個 API Render Props
是一種模式
劃重點:任何被用於告知組件需要渲染什麼內容的函數 prop 在技術上都可以被稱爲 “render prop”.
for example
具有 render prop 的組件接受一個函數,該函數返回一個 React 元素並調用它而不是實現自己的渲染邏輯。
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
使用 render prop
的庫有 React Router
、Downshift
以及 Formik
。
我們來看這樣一個示例:👇
// 創建一個需要被傳入的props
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
//封裝接受 Render props 的方法
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
// Mouse組件的靜態展示
// 使用`render` 動態確定要渲染的內容
{this.props.render(this.state)}
</div>
);
}
}
// 在組件中靈活的 複用Mouse的數據
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移動鼠標!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
拾、 深入理解 JSX(口水文記一下 冷不丁 被問到)
ps:這個篇幅比較容易理解只是例舉不做詳解
- JSX 僅僅只是 React.createElement(component, props, …children) 函數的語法糖
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
會編譯爲:
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
如果沒有子節點,你還可以使用自閉合的標籤形式,如:
<div className="sidebar" />
// 會編譯爲:
React.createElement(
'div',
{className: 'sidebar'}
)
- React 必須在作用域內
// 例如,在如下代碼中,雖然 React 和 CustomButton 並沒有被直接使用,但還是需要導入:
import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
- 在 JSX 類型中使用點語法
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
- 用戶定義的組件必須以大寫字母開頭
不必多說六吧 基本常識
- JavaScript 表達式作爲 Props
<MyComponent foo={1 + 2 + 3 + 4} />
- if 語句以及 for 循環不是 JavaScript 表達式,所以不能在 JSX 中直接使用
所以我們要在 jsx 外來判斷條件 和遍歷數據
function NumberDescriber(props) {
let description;
if (props.number % 2 == 0) {
description = <strong>even</strong>;
} else {
description = <i>odd</i>;
}
return <div>{props.number} is an {description} number</div>;
}
- 字符串字面量
// 你可以將字符串字面量賦值給 prop. 如下兩個 JSX 表達式是等價的:
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
- Props 默認值爲 “True”
// 如果你沒給 prop 賦值,它的默認值是 true。以下兩個 JSX 表達式是等價的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
- 屬性展開
// 如果你已經有了一個 props 對象,你可以使用展開運算符 ... 來在 JSX 中傳遞整個 props 對象。以下兩個組件是等價的:
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
// 你還可以選擇只保留當前組件需要接收的 props,並使用展開運算符將其他 props 傳遞下去。
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
const App = () => {
return (
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
- 布爾類型、Null 以及 Undefined 將會忽略