React面試題

語言: CN / TW / HK

theme: qklhk-chocolate

React

如何建立一個react的專案(使用腳手架)

  • 安裝cr腳手架:npm install -g create-react-app
  • 進入資料夾:create-react-app 專案名稱
  • 進入專案:cd 專案名稱
  • 執行專案:npm start

如何不使用腳手架建立一個專案

之前面試官問過是否有不使用腳手架建立專案的經歷。

詳細可以檢視這篇文章

我理解這個問題說出來大概就可以,比如: 1. yarn init 初始化package.json檔案 2. 安裝react和react-dom 3. 配置webpack

- 配置babel支援ES6
- 配置@babel/preset-react支援react
- 支援ts:ts-loader @types/react @types/react-dom
- 支援antd
- 支援less:less-loader,css-loader,style-loader
- 配置plugins,常用的有html-webpack-plugin(當使用 webpack 打包時,建立一個 html 檔案,並把 webpack 打包後的靜態檔案自動插入到這個 html 檔案當中。)和 clean-webpack-plugin(是一個清除檔案的外掛。 在每次打包後,磁碟空間會存有打包後的資源,在再次打包的時候,我們需要先把本地已有的打包後的資源清空,來減少它們對磁碟空間的佔用。 外掛clean-webpack-plugin就可以幫我們做這個事情)
  1. 安裝router
  2. 安裝redux

對於React 框架的理解(React的特性有哪些)

React是一個用於構建使用者介面的 JavaScript 庫,只提供了 UI 層面的解決方案。

它有以下特性:

  • 元件化:將介面成了各個獨立的小塊,每一個塊就是元件,這些元件之間可以組合、巢狀,構成整體頁面,提高程式碼的複用率和開發效率。
  • 資料驅動檢視:
    • React通過setState實現資料驅動檢視,通過setState來引發一次元件的更新過程從而實現頁面的重新渲染。
    • 資料驅動檢視是我們只需要關注資料的變化,不用再去操作dom。同時也提升了效能。
  • JSX 語法:用於宣告元件結構,是一個 JavaScript 的語法擴充套件。
  • 單向資料繫結:從高階元件到低階元件的單向資料流,單向響應的資料流會比雙向繫結的更安全,速度更快
  • 虛擬 DOM:使用虛擬 DOM 來有效地操作 DOM
  • 宣告式程式設計:

    如實現一個標記的地圖: 通過命令式建立地圖、建立標記、以及在地圖上新增的標記的步驟如下:

    ```js // 建立地圖 const map = new Map.map(document.getElementById("map"), { zoom: 4, center: { lat, lng }, });

    // 建立標記 const marker = new Map.marker({ position: { lat, lng }, title: "Hello Marker", });

    // 地圖上新增標記 marker.setMap(map); ```

    而用 React 實現上述功能則如下: js <Map zoom={4} center={(lat, lng)}> <Marker position={(lat, lng)} title={"Hello Marker"} /> </Map>
    宣告式程式設計方式使得 React 元件很容易使用,最終的程式碼簡單易於維護

jsx語法是必須的嗎

以下是經過babel轉譯之後的jsx:

```jsx // jsx const element =

Hello, world!

; const container = document.getElementById( 'root' ); ReactDOM.render(element, container);

// babel 處理後 const element = /#PURE/React.createElement("h1", null, "Hello, world!"); const container = document.getElementById('root'); ReactDOM.render(element, container); ```

注:React.createElement(標籤名,屬性物件,子元素)

所以不使用jsx語法也可以使用React:

```jsx import React from "react";

// 本檔案用於測試jsx 語法是否是必須的

// 不使用jsx語法建立的元素 const ReactCreateElement = React.createElement("h1", null, "Hello, createElement!");

// 使用jsx語法建立的元素 const JsxElement =

Hello, JSX!

;

export { JsxElement,ReactCreateElement }

// 使用 import {ReactCreateElement,JsxElement} from './components/JsxNecessary'; // 驗證jsx是否是必須的

function App() { return (

{ReactCreateElement} {JsxElement}
); }

export default App;

```

兩者均可正常顯示,但是兩者的優劣顯而易見,使用createElement方法會使程式碼更加的冗餘,而jsx更加簡潔。

為什麼提出jsx

JSX是JS的語法擴充套件,主要用於宣告元素,可以理解為React.createElement()的語法糖,React並不強制使用JSX,即使使用了JSX最後也會被babel編譯成createElement。

React認為檢視和邏輯內在耦合,比如,在 UI 中需要繫結處理事件、在某些時刻狀態發生變化時需要通知到 UI,以及需要在 UI 中展示準備好的資料。

React並沒有採用將檢視與邏輯進行分離到不同檔案這種人為地分離方式,而是通過將二者共同存放在稱之為“元件”的鬆散耦合單元之中,來實現關注點分離。 為了實現其元件化的目的,而不引入更多的概念(比如Vue引入了模板語法,這就是新的概念,學習成本會比較高),使用人們熟悉的js語法的擴充套件更加適用。

並且相比於createElement,JSX更加的簡潔。

關注點分離是日常生活和生產中廣泛使用的解決複雜問題的一種系統思維方法。大體思路是,先將複雜問題做合理的分解,再分別仔細研究問題的不同側面(關注點),最後綜合各方面的結果,合成整體的解決方案。

Babel 外掛是如何實現 JSX 到 JS 的編譯 ?

需要的依賴:

  • @babel/cli
  • @babel/core
  • @babel/preset-react

babel.rc檔案新增配置:

json { "presets": ["@babel/preset-react"] }

Babel 讀取程式碼並解析,生成 AST,再將 AST 傳入外掛層進行轉換,在轉換時就可以將 JSX 的結構轉換為 React.createElement 的函式。

React.createElement原始碼:

```js export function createElement(type, config, children) { // propName 變數用於儲存後面需要用到的元素屬性 let propName; // props 變數用於儲存元素屬性的鍵值對集合 const props = {}; // key、ref、self、source 均為 React 元素的屬性,此處不必深究 let key = null; let ref = null; let self = null; let source = null;

// config 物件中儲存的是元素的屬性
if (config != null) {
    // 進來之後做的第一件事,是依次對 ref、key、self 和 source 屬性賦值
    if (hasValidRef(config)) {
        ref = config.ref;
    }
    // 此處將 key 值字串化
    if (hasValidKey(config)) {
        key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;

    // 接著就是要把 config 裡面的屬性都一個一個挪到 props 這個之前宣告好的物件裡面
    for (propName in config) {
        if (
            // 篩選出可以提進 props 物件裡的屬性
            hasOwnProperty.call(config, propName) &&
            !RESERVED_PROPS.hasOwnProperty(propName)
        ) {
            props[propName] = config[propName];
        }
    }
}
// childrenLength 指的是當前元素的子元素的個數,減去的 2 是 type 和 config 兩個引數佔用的長度
const childrenLength = arguments.length - 2;
// 如果拋去type和config,就只剩下一個引數,一般意味著文字節點出現了
if (childrenLength === 1) {
    // 直接把這個引數的值賦給props.children
    props.children = children;
    // 處理巢狀多個子元素的情況
} else if (childrenLength > 1) {
    // 宣告一個子元素陣列
    const childArray = Array(childrenLength);
    // 把子元素推進數組裡
    for (let i = 0; i < childrenLength; i++) {
        childArray[i] = arguments[i + 2];
    }
    // 最後把這個陣列賦值給props.children
    props.children = childArray;
}

// 處理 defaultProps
if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
        if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
        }
    }
}

// 最後返回一個呼叫ReactElement執行方法,並傳入剛才處理過的引數
return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
);

} ```

createElement並沒有十分複雜的操作,整個過程看起來更像是一個格式化的過程:將我們輸入的相對簡單清晰的結構轉化為ReactElement函式需要的格式。

ReactElement函式原始碼:

```js const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // REACT_ELEMENT_TYPE是一個常量,用來標識該物件是一個ReactElement $$typeof: REACT_ELEMENT_TYPE,

// 內建屬性賦值
type: type,
key: key,
ref: ref,
props: props,

// 記錄創造該元素的元件
_owner: owner,

}; // if (DEV) { // 這裡是一些針對 DEV 環境下的處理,對於大家理解主要邏輯意義不大,此處我直接省略掉,以免混淆視聽 } return element; }; ```

ReactElement 其實只做了一件事情,那就是“建立”,說得更精確一點,是“組裝”:ReactElement 把傳入的引數按照一定的規範,“組裝”進了 element 物件裡,並把它返回給了 React.createElement,最終 React.createElement 又把它交回到了開發者手中。

ReactElement返回的element 其實就是虛擬DOM中的一個節點:一個JS物件,這個物件包含了對真實節點的描述。

對於React虛擬DOM的理解

  • js物件,儲存在記憶體中
  • 是對真實DOM結構的對映

虛擬 DOM 的工作流程:

掛載階段:React 將結合 JSX 的描述,構建出虛擬 DOM 樹,然後通過 ReactDOM.render 實現虛擬 DOM 到真實 DOM 的對映(觸發渲染流水線);

更新階段:頁面的變化先作用於虛擬 DOM,虛擬 DOM 將在 JS 層藉助演算法先對比出具體有哪些真實 DOM 需要被改變,然後再將這些改變作用於真實 DOM。

虛擬 DOM 解決的關鍵問題有以下三個:

  • 減少 DOM 操作:虛擬 DOM 可以將多次 DOM 操作合併為一次操作
  • 研發體驗/研發效率的問題:虛擬 DOM 的出現,為資料驅動檢視這一思想提供了高度可用的載體,使得前端開發能夠基於函式式 UI 的程式設計方式實現高效的宣告式程式設計。
  • 跨平臺的問題:虛擬 DOM 是對真實渲染內容的一層抽象。同一套虛擬 DOM,可以對接不同平臺的渲染邏輯,從而實現“一次編碼,多端執行”

既然是虛擬 DOM,那就意味著它和渲染到頁面上的真實 DOM 之間還有一定的距離,這個距離通過 ReactDOM.render 方法填充:

jsx ReactDOM.render( // 需要渲染的元素(ReactElement) element, // 元素掛載的目標容器(一個真實DOM) container, // 回撥函式,可選引數,可以用來處理渲染結束後的邏輯 [callback] )

VDOM 和 DOM 的區別

  • 真實DOM存在重排和重繪,虛擬DOM不存在;
  • 虛擬 DOM 的總損耗是“虛擬 DOM 增刪改+真實 DOM 差異增刪改+排版與重繪(可能比直接操作真實DOM要少)”,真實 DOM 的總損耗是“真實 DOM 完全增刪改+排版與重繪”

傳統的原生 api 或 jQuery 去操作 DOM 時,瀏覽器會從構建 DOM 樹開始從頭到尾執行一遍流程。

當你在一次操作時,需要更新 10 個 DOM 節點,瀏覽器沒這麼智慧,收到第一個更新 DOM 請求後,並不知道後續還有 9 次更新操作,因此會馬上執行流程,最終執行 10 次流程。

而通過 VNode,同樣更新 10 個 DOM 節點,虛擬 DOM 不會立即操作 DOM,而是將這 10 次更新的 diff 內容儲存到本地的一個 js 物件中,最終將這個 js 物件一次性 attach 到 DOM 樹上,避免大量的無謂計算。

VDOM 和 DOM 優缺點

真實 DOM 的優勢: - 易用

真實 DOM 的缺點: - 效率低,解析速度慢,記憶體佔用量過高 - 效能差:頻繁操作真實 DOM,易於導致重繪與迴流

虛擬 DOM 的優勢: - 簡單方便:如果使用手動操作真實 DOM 來完成頁面,繁瑣又容易出錯,在大規模應用下維護起來也很困難 - 效能方面:使用 Virtual DOM,能夠有效避免真實 DOM 數頻繁更新,減少多次引起重繪與迴流,提高效能 - 跨平臺:React 藉助虛擬 DOM,帶來了跨平臺的能力,一套程式碼多端執行

虛擬 DOM 的缺點: - 在一些效能要求極高的應用中虛擬 DOM 無法進行鍼對性的極致優化,首次渲染大量 DOM 時,由於多了一層虛擬 DOM 的計算,速度比正常稍慢

react 的生命週期

react生命週期圖解

掛載

當元件例項被建立並插入 DOM 中時,其生命週期呼叫順序如下:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

getDerivedStateFromProps

該方法是新增的生命週期方法,是一個靜態的方法,因此不能訪問到元件的例項

執行時機:元件建立和更新階段,不論是props變化還是state變化,都會呼叫。

在每次render方法前呼叫,第一個引數為即將更新的props,第二個引數為上一個狀態的state,可以比較props 和 state來加一些限制條件,防止無用的state更新

該方法需要返回一個新的物件作為新的state或者返回null表示state狀態不需要更新

更新

當元件的 props 或 state 發生變化時會觸發更新。元件更新的生命週期呼叫順序如下:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

getSnapshotBeforeUpdate

該周期函式在render後執行,執行之時DOM元素還沒有被更新

該方法返回的一個Snapshot值(不返回報錯),作為componentDidUpdate第三個引數傳入

```js getSnapshotBeforeUpdate(prevProps, prevState) { console.log('#enter getSnapshotBeforeUpdate'); return 'foo'; }

componentDidUpdate(prevProps, prevState, snapshot) { console.log('#enter componentDidUpdate snapshot = ', snapshot); } ``` 此方法的目的在於獲取元件更新前的一些資訊,比如元件的滾動位置之類的,在元件更新後可以根據這些資訊恢復一些UI視覺上的狀態

解除安裝

當元件從 DOM 中移除時會呼叫如下方法:

  • componentWillUnmount()

錯誤處理

當渲染過程,生命週期,或子元件的建構函式中丟擲錯誤時,會呼叫如下方法:

  • static getDerivedStateFromError():更改狀態,從而顯示降級元件
  • componentDidCatch():列印錯誤資訊

各生命週期的具體作用看這裡

React父子元件的生命週期呼叫順序

```jsx //parent元件 import React from "react"; import Son from './son'

class Parent extends React.Component { constructor(props) { super(props) this.state={} console.log('parent constructor') } static getDerivedStateFromProps(){ console.log('parent getDerivedStateFromProps') return {} } componentDidMount() { console.log('parent didMount') } componentWillUnmount() { console.log('parent willUnmount') } shouldComponentUpdate(){ console.log('parent scu') return true } render() { console.log('parent render') return

parent

} }

export default Parent

//son 元件 import React from "react";

class Son extends React.Component { constructor(props) { super(props) this.state={} console.log('son constructor') } static getDerivedStateFromProps(){ console.log('son getDerivedStateFromProps') return {} } componentWillUnmount() { console.log('son willUnmount') }

componentDidMount() {
    console.log('son didMount')
}
shouldComponentUpdate(){
    console.log('son scu')
    return true
}
render() {
    console.log('son render')
    return <h3>son</h3>
}

}

export default Son 結果: parent constructor parent getDerivedStateFromProps parent render son constructor son getDerivedStateFromProps son render // 注意 son didMount parent didMount son willUnmount parent willUnmount ```

React事件和原生事件執行順序

```jsx // React 事件和原生事件的執行順序 import React from "react";

class EventRunOrder extends React.Component { constructor(props) { super(props); this.parent = null; this.child = null }

componentDidMount() {
    this.parent.addEventListener('click', (e) => {
        console.log('dom parent')
    })

    this.child.addEventListener('click', (e) => {
        console.log('dom child')
    })

    document.addEventListener("click", (e) => {
        console.log('document')
    })
}

childClick = (e) => {
    console.log('react child')
}

parentClick = (e) => {
    console.log('react parent')
}

render() {
    return (
        <div onClick={this.parentClick} ref={ref => this.parent = ref}>
            <div onClick={this.childClick} ref={ref => this.child = ref}>
                test
            </div>
        </div>
    )
}

}

export default EventRunOrder ```

dom child dom parent react child react parent document

react所有事件都掛載在document上,當真實dom觸發後冒泡到document後才會對react事件進行處理,所以:

  • 原生事件先執行
  • react合成事件再執行
  • document上掛載的事件最後執行

react的事件機制

react實現了一套自己的事件機制,包括事件註冊、事件合成、事件冒泡、事件派發等。在react中這套事件被稱為合成事件。

合成事件是 React模擬原生 DOM事件所有能力的一個事件物件,即瀏覽器原生事件的跨瀏覽器包裝器

根據 W3C規範來定義合成事件,相容所有瀏覽器,擁有與瀏覽器原生事件相同的介面,例如:

js const button = <button onClick={handleClick}>按鈕</button>

如果想要獲得原生DOM事件,可以通過e.nativeEvent屬性獲取:

js const handleClick = (e) => console.log(e.nativeEvent);; const button = <button onClick={handleClick}>按鈕</button

從上面可以看到React事件和原生事件也非常的相似,但也有一定的區別:

  • 事件名稱命名方式不同:react採用小駝峰格式
  • 事件處理函式書寫不同:react使用{},而原生事件使用雙引號

雖然onclick看似繫結到DOM元素上,但實際並不會把事件代理函式直接繫結到真實的節點上,而是把所有的事件繫結到結構的最外層,使用一個統一的事件去監聽。

這個事件監聽器上維持了一個對映來儲存所有元件內部的事件監聽和處理函式。當元件掛載或解除安裝時,只是在這個統一的事件監聽器上插入或刪除一些物件。

當事件發生時,首先被這個統一的事件監聽器處理,然後在對映裡找到真正的事件處理函式並呼叫。這樣做簡化了事件處理和回收機制,效率也有很大提升。

所以想要阻止不同時間段的冒泡行為,對應使用不同的方法,對應如下:

  • 阻止合成事件間的冒泡,用e.stopPropagation()
  • 阻止合成事件與最外層 document 上的事件間的冒泡,用e.nativeEvent.stopImmediatePropagation()
  • 阻止合成事件與除最外層document上的原生事件上的冒泡,通過判斷e.target來避免

阻止冒泡

綜上所述:

  • React 上註冊的事件最終會繫結在document這個 DOM 上,而不是 React 元件對應的 DOM(減少記憶體開銷就是因為所有的事件都繫結在 document 上,其他節點沒有繫結事件)
  • React 自身實現了一套事件冒泡機制,所以這也就是為什麼我們 event.stopPropagation()無效的原因。
  • React 通過佇列的形式,從觸發的元件向父元件回溯,然後呼叫他們 JSX 中定義的 callback
  • React 有一套自己的合成事件 SyntheticEvent

函式元件和類元件輸出差別(閉包陷阱)

以下函式元件程式碼,先alert再add,頁面顯示的值和alert的值分別是什麼

```jsx import {useState} from "react";

const FunctionComponentClosure = () => { const [value, setValue] = useState(1); const log = () => { setTimeout(() => { alert(value) }, 3000) } return (

{value}

) }

export default FunctionComponentClosure ```

alert :1

頁面顯示:2

原因:log方法內的value和點選動作觸發時的value相同,後續value的變化不會對log內部的value產生任何的影響。這種現象被稱為 閉包陷阱,即函式式元件每次render都產生一個新的log函式,這個log函式會產生一個當前階段value值的閉包。

除了閉包陷阱之外,函式元件和類元件還存在如下區別: - 寫法不同:函式元件程式碼更加簡潔 - 函式元件不需要處理this但是類元件需要 - 類元件有生命週期和state函式元件不存在(但是函式元件中可以通過hooks達到類似的效果)

如何解決閉包陷阱

```jsx const Test = () => { const [value, setValue] = useState(1); const countRef = useRef(value)

const log = function () {
    setTimeout(() => {
        alert(countRef.current)
    }, 3000)
}
useEffect(() => {
    countRef.current = value
}, [value])

return (
    <div>
        <p>{value}</p>
        <button onClick={log}>alert</button>
        <button onClick={() => setValue(value + 1)}>add</button>
    </div>
)

} ```

useRef每次render都會返回同一個引用型別物件,設定和讀取都在這個物件上處理的話,就可以得到最新的value值了。

在類元件中情況是否會相同呢?

jsx class Test extends React.Component { constructor(props) { super(props) this.state = { value: 1 } } log = () => { setTimeout(() => { alert(this.state.value) }, 3000) } render() { return ( <div> <p>{this.state.value}</p> <button onClick={this.log}>alert</button> <button onClick={() => this.setState({ value: this.state.value + 1 })}>add</button> </div> ) } } export default Test

alert和頁面顯示的值相同。

受控元件和非受控元件

受控元件:簡單理解為雙向繫結,資料和檢視的變化是同步的,受控元件一般需要初始狀態(value或者checked) 和一個 狀態更新事件函式

非受控元件:不受控制的元件,在其內部儲存自身的狀態,可以通過ref查詢DOM的當前值。初始狀態為defaultValue

推薦使用受控元件,在受控元件中資料由React元件處理。

操作DOM的情況下一般需要使用非受控元件,資料由DOM本身處理,控制能力較弱,但是程式碼量更少。

React如何實現狀態自動儲存(vue中的keep-alive)

為什麼需要狀態儲存

在React中通常使用路由去管理不同的頁面,在切換頁面時,路由將會解除安裝掉未匹配的頁面元件,所以比如從列表進入詳情頁面,等到退回列表頁面時會回到列表頁的頂部。

什麼情況下需要狀態儲存

  • 列表進入詳情
  • 已填寫但是未提交的表單
  • 管理系統中可切換和關閉的標籤

總而言之就是在互動過程中離開需要對狀態進行儲存的場景。

React為什麼不支援

狀態儲存在vue中可以使用keep-alive進行實現,但是react認為這個功能容易造成記憶體洩漏,所以暫時不支援。

如何實現

  1. 手動儲存狀態:適用於資料較少的情況

在componentWillUnmount的時候將狀態通過redux進行儲存,然後在componentDidMount週期進行資料恢復。

  1. 通過路由實現:

基本思想是,將KeepAlive中的元件也就是children取出來,渲染到一個不會被解除安裝的元件keeper中,在使用Dom操作將keeper內的真實內容移入對應的keepalive

useEffect和useLayoutEffect有什麼區別

相同點:

  • 處理副作用:函式元件內不允許操作副作用。比如:改變DOM、設定訂閱、操作定時器等
  • 底層都是呼叫mountEffectlmpl方法,基本上可以替換使用

不同點: - useEffect在畫素變化之後非同步呼叫,改變螢幕內容可能會造成頁面的閃爍 - useLayoutEffect在畫素變化之前同步呼叫,可能會造成頁面延遲顯示,但是不會閃爍:主要用於處理DOM操作、調整樣式、避免頁面閃爍等。因為是同步執行,所以要避免做大量計算,從而避免造成阻塞。 - useLayoutEffect先於useEffect執行

對react hook的理解,解決了什麼問題

官方給出的動機是解決長時間使用和維護react過程中常遇到的問題,例如: - 難以重用和共享元件中的與狀態相關的邏輯 - 邏輯複雜的元件難以開發與維護,當我們的元件需要處理多個互不相關的 local state 時,每個生命週期函式中可能會包含著各種互不相關的邏輯在裡面 - 類元件中的this增加學習成本,類元件在基於現有工具的優化上存在些許問題 - 由於業務變動,函式元件不得不改為類元件等等

在以前,函式元件也被稱為無狀態的元件,只負責渲染的一些工作

在有了hooks之後,函式元件也可以是有狀態的元件,內部也可以維護自身的狀態以及做一些邏輯方面的處理。

hooks的出現,使函式元件的功能得到了擴充,擁有了類元件相似的功能,在我們日常使用中,使用hooks能夠解決大多數問題,並且還擁有程式碼複用機制,因此優先考慮hooks。

React常用的hooks

useState

定義狀態,解決了函式元件沒有狀態的問題。

接受一個初始值(初始值可以是一個具體資料型別,也可以是一個函式,該函式只執行一次返回值作為初始值)作為引數,返回一個數組,第一項是變數,第二項是設定變數的函式。

  • 物件不可區域性更新:state是一個物件時,不能區域性更新物件屬性,useState不會合並,會把整個物件覆蓋。要用展開運算子自己進行屬性值的覆蓋。

```jsx const [state, setState] = useState({ name: 'jerry', age: 18 })

  const changeState = () => {
      setState({name:"tom"}) //覆蓋整個state
  }

```

  • 地址要變更:對於引用型別,資料地址不變的時候,認為資料沒有變化,不會更新檢視。

```jsx const [state, setState] = useState({ name: 'jerry', age: 18 })

  const changeState = () => {
      const obj = state //obj和state指向同一個地址
      obj.name = 'tom'
      setState(obj) // 地址沒有變更,不會更新
  }

```

  • useState 傳入一個函式:useState初始化是惰性的,initialState只有在初始渲染中起作用,後續渲染會被忽略,如果初始state需要通過複雜的計算獲得,可以傳入一個函式,在函式中計算並返回初始state,次函式只在初始渲染時被呼叫。

  • useState非同步回撥問題:如何獲取到更新後的state,使用useEffect,當state變化時觸發

  • 操作合併:傳入物件會被合併,傳入函式,使用preState引數不會被合併

```jsx setState({ ...state, name: state.name + '!' }) setState({ ...state, name: state.name + '!' }) setState({ ...state, name: state.name + '!' })

      setState((pre) => ({ ...state, name: pre.name + '!' }))
      setState((pre) => ({ ...state, name: pre.name + '!' }))
      setState((pre) => ({ ...state, name: pre.name + '!' }))

```

對比類元件的state

  1. 在正常的react的事件流裡(如onClick等):

  2. setState和useState是非同步執行的(不會立即更新state的結果)

  3. 多次執行setState和useState,只會呼叫一次重新渲染render

  4. 傳入物件會被合併,函式則不會被合併。可以通過setState傳入一個函式來更新state,這樣不會被合併

  5. 在setTimeout,Promise.then等非同步事件中:

  6. setState和useState是同步執行的(立即更新state的結果)

  7. 多次執行setState和useState,每一次的執行setState和useState,都會呼叫一次render

setState執行機制(類元件)

通過setState來修改元件內部的state,並且觸發render方法進行檢視的更新。

直接修改state不會引起檢視的更新,因為react沒有像vue一樣通過proxy或者definProperty監聽資料變化,必須通過setState方法來告知react元件state已經發生了改變。

關於state方法的定義是從React.Component中繼承,定義的原始碼如下:

js Component.prototype.setState = function(partialState, callback) { 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.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; 從上面可以看到setState第一個引數可以是一個物件,或者是一個函式,而第二個引數是一個回撥函式,用於可以實時的獲取到更新之後的資料。

同步非同步
  • 在元件生命週期或React合成事件中,setState是非同步。要想獲取更新後的值,可以通過setState的第二個引數傳入一個函式(函式元件通過useEffect)。
  • 在setTimeout或者原生dom事件中,setState是同步。
批量更新
  • 合成事件或者生命週期中setState傳入物件會被合併。要想避免合併可以將第一個引數寫成函式。
  • 而在setTimeout或者原生dom事件中,由於是同步的操作,所以並不會進行覆蓋現象。

useEffect

給沒有生命週期的元件新增結束渲染的訊號,在渲染結束後執行

  • 如果不接受第二個引數,那麼在第一次渲染完成之後每次更新渲染頁面的時候,都會呼叫useEffect的回撥函式。

  • 可以對第二個引數傳入一個數組,這個陣列表示的是更新執行所依賴的列表,只有依賴列表改變時(陣列中的任意一項變化時),才會觸發回撥函式

  • 第二項是一個空陣列:只在第一次渲染完成時執行。相當於didMounted

  • 清除副作用:比如綁定了自定義DOM 事件以防止記憶體洩漏

如何清除:clean-up 函式

jsx useEffect(()=>{ document.addEventListener('click',func); return ()=>{ // 在每次執行useEffect之前都會執行上一次return中內容 document.removeEventListener('click',func) } })

  • 非同步操作:useEffect返回的是clean-up函式,因此沒有辦法返回一個promise實現非同步

  • 立即執行函式:

    jsx useEffect(() => { (async function anyNameFunction() { await loadContent(); })(); }, []);

  • 在useEffect外部或者內部實現async/await函式,然後在內部呼叫

useContext

共享狀態鉤子。不同元件之間共享狀態,避免props層層傳遞

  • React.createContext
  • Context.Provider

useReducer

Action鉤子,複雜版的useState

redux的原理是使用者在頁面中發起action,從而通過reducer方法來改變state,從而實現頁面和狀態的通訊。而Reducer的形式是(state, action) => newstate。類似,我們的useReducer()是這樣的:

const [state, dispatch] = useReducer(reducer, initialState)

自己建立hooks

自己建立hooks就是一個將公共程式碼封裝的過程,比如一個hooks輸出一個滑鼠位置座標,可以如下實現:

```jsx import { useState, useEffect } from 'react' export default function useMousePosition() { const [position, setPosition] = useState({ x: 0, y: 0 }) useEffect(() => { const move = (e) => { setPosition({ x: e.x, y: e.y }) } document.addEventListener('mousemove', move) return () => { document.removeEventListener('mousemove', move) } }, []) return position }

// 使用 const position = useMousePosition() ```

useEffect的觸發時機

或者可以問: - 陣列可不可以什麼都不傳 - 數組裡邊內容如何確定

觸發機制跟第二個引數有關:

  • 第二個引數不傳時:每次渲染完成後觸發
  • 第二個引數是一個空陣列時:初始化渲染完成後觸發,相當於didMounted
  • 第二個引數是非空陣列時:陣列中資料有一項更新時觸發

陣列中的內容一般是props或者state,是普通變數時不會觸發執行

useEffect的第一個函式返回一個函式

返回一個clean-up 函式,用來清除副作用。clean-up的執行時機是每個useEffect執行前會執行上一個effect返回的clean-up函式。

hooks使用規則

  • Hooks只在函式元件頂層呼叫,不要在迴圈、條件判斷或者巢狀函式中呼叫鉤子。在類元件中無法使用。
  • 對於自定義Hooks,使用use開頭命名。

要 Hook 的呼叫順序在多次渲染之間保持一致,React 就能正確地將內部 state 和對應的 Hook 進行關聯。但如果我們將一個 Hook 呼叫放到一個條件語句中會發生什麼呢?

詳解看這裡

useMemo、memo、useCallback

他們三個的應用場景都是快取結果,當依賴值沒有改變時避免不必要的計算或者渲染。

  • useCallback 是針對函式進行“記憶”的,當它依賴項沒有發生改變時,那麼該函式的引用並不會隨著元件的重新整理而被重新賦值。當我們覺得一個函式不需要隨著元件的更新而更新引用地址的時候,我們就可以使用 useCallback 去修飾它。
  • React.memo 是對元件進行 “記憶”,當它接收的 props 沒有發生改變的時候,那麼它將返回上次渲染的結果,不會重新執行函式返回新的渲染結果。
  • React.useMemo是針對 值計算 的一種“記憶“,當依賴項沒有發生改變時,那麼無需再去計算,直接使用之前的值,對於元件而言,這帶來的一個好處就是,可以減少一些計算,避免一些多餘的渲染。當我們遇到一些資料需要在元件內部進行計算的時候,可以考慮一下 React.useMemo

useMemo與useEffect的區別

傳入 useMemo 的函式會在渲染期間執行。請不要在這個函式內部執行不應該在渲染期間內執行的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo。

useEffect在渲染後執行,可以訪問渲染後的值。

如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值。和useEffect類似,但是如果每次渲染時都計算,那就沒必要使用useMemo了。

ref使用場景

使用場景:直接使用dom元素的某個方法,或者直接使用自定義元件中的某個方法。在以下場景會用到ref:

  • 對Dom元素的焦點控制、內容選擇、控制
  • 對Dom元素的內容設定及媒體播放
  • 對Dom元素的操作和對元件例項的操作
  • 整合第三方 DOM 庫

ref作用於不同的元件時: 1. 作用於內建的html元件,得到的是真實的dom 2. ref作用於類元件,得到的是類的例項 3. ref不能作用於函式元件

使用ref的模式有:

  • 字串:傳入字串,使用時通過 this.refs.“傳入的字串”的格式獲取對應的元素。不再推薦使用,可能會被移除

  • 物件:傳入通過 React.createRef() 方式創建出來的物件,使用時獲取到建立的物件中存在 current 屬性就是對應的元素

  • 函式:ref={(el) => {this.txt = el;}}

  • 傳入hook,hook是通過 useRef() 方式建立,使用時通過生成hook物件的 current 屬性就是對應的元素

  • ref轉發:

```jsx import React, { Component } from 'react'

function A(props, ref){ console.log(props, ref) return

A元件

}

// 傳遞函式元件,得到一個新的元件,不能傳遞類元件,並且函式元件必須使用第二個 const NewA = React.forwardRef(A)

export default class App extends Component { ARef = React.createRef()

componentDidMount() {
  console.log(this.ARef) // {current: h1}
}

render() {
  return (
    <div>
      <NewA ref={this.ARef} words="sdfsd"/>
    </div>
  )
}

} ```

可以使用:useImperativeHandle定義方法

jsx useImperativeHandle(ref, () => ({ show: (title, content) => { setVisible(true); setTitle(title); setContent(content); }, hide: () => { setVisible(false); } }));

state和props有什麼區別

一個元件的資料可以來源於元件內部,也可以來源於元件外部(比如父元件)。

元件內部的狀態就是state,一般在constructor中定義。通過setState修改,會呼叫render方法重新渲染元件。 setState 還可以接受第二個引數,它是一個函式,會在 setState 呼叫完成並且元件開始重新渲染時被呼叫,可以用來監聽渲染是否完成。

元件外部定義的狀態是props,元件中的props不可以修改,只能通過傳入新的props。

相同點: - 兩者都是 JavaScript 物件 - 兩者都是用於儲存狀態 - props 和 state 都能觸發渲染更新

區別: - props 是外部傳遞給元件的,而 state 是在元件內被元件自己管理的,一般在 constructor 中初始化 - props 在元件內部是不可修改的,但 state 在元件內部可以進行修改 state 是多變的、可以修改

super和super(props)的區別

在ES6的class中: ```js class sup { constructor(name) { this.name = name; }

printName() { console.log(this.name); } }

class sub extends sup { constructor(name, age) { super(name); // super代表的是父類的建構函式 this.age = age; }

printAge() { console.log(this.age); } }

let jack = new sub("jack", 20); jack.printName(); //輸出 : jack jack.printAge(); //輸出 : 20 ```

在上面的例子中,可以看到通過 super 關鍵字實現呼叫父類,super 代替的是父類的構建函式,使用 super(name) 相當於呼叫sup.prototype.constructor.call(this,name)

如果在子類中不使用 super關鍵字,則會引發報錯,報錯的原因是子類是沒有自己的 this 物件的,它只能繼承父類的 this 物件,然後對其進行加工。

而 super() 就是將父類中的 this 物件繼承給子類的,沒有 super() 子類就得不到 this 物件。

如果先呼叫 this,再初始化 super(),同樣是禁止的行為。所以在子類 constructor 中,必須先代用 super 才能引用 this。

在 React 中,類元件是基於 ES6 的規範實現的,繼承 React.Component,因此如果用到 constructor 就必須寫 super() 才初始化 this。

這時候,在呼叫 super() 的時候,我們一般都需要傳入 props 作為引數,如果不傳進去,React 內部也會將其定義在元件例項中。 所以無論有沒有 constructor,在 render 中 this.props 都是可以使用的,這是 React 自動附帶的,是可以不寫的。

綜上所述:

  • 在 React 中,類元件基於 ES6,所以在 constructor 中必須使用 super
  • 在呼叫 super 過程,無論是否傳入 props,React 內部都會將 porps 賦值給元件例項 porps 屬性中
  • 如果只調用了 super(),那麼 this.props 在 super() 和建構函式結束之間仍是 undefined

react引入css的方式有哪些

元件式開發選擇合適的css解決方案尤為重要

通常會遵循以下規則:

  • 可以編寫區域性css,不會隨意汙染其他元件內的原生;
  • 可以編寫動態的css,可以獲取當前元件的一些狀態,根據狀態的變化生成不同的css樣式;
  • 支援所有的css特性:偽類、動畫、媒體查詢等;
  • 編寫起來簡潔方便、最好符合一貫的css風格特點

在這一方面,vue使用css起來更為簡潔: - 通過 style 標籤編寫樣式 - scoped 屬性決定編寫的樣式是否區域性有效 - lang 屬性設定前處理器 - 內聯樣式風格的方式來根據最新狀態設定和改變css

而在react中,引入CSS就不如Vue方便簡潔,其引入css的方式有很多種,各有利弊

常見的CSS引入方式有以下:

  • 行內樣式: <div style={{ width:'200px', height:'80px',      }}>測試資料</div>
  • 元件中引入 .css 檔案
  • 元件中引入 .module.css 檔案
  • CSS in JS

通過上面四種樣式的引入,各自的優缺點:

  • 在元件內直接使用css該方式編寫方便,容易能夠根據狀態修改樣式屬性,但是大量的演示編寫容易導致程式碼混亂
  • 元件中引入 .css 檔案符合我們日常的編寫習慣,但是作用域是全域性的,樣式之間會層疊
  • 引入.module.css 檔案能夠解決區域性作用域問題,但是不方便動態修改樣式,需要使用內聯的方式進行樣式的編寫
  • 通過css in js 這種方法,可以滿足大部分場景的應用,可以類似於前處理器一樣樣式巢狀、定義、修改狀態等

react事件繫結方式有哪些

繫結方式

  • render方法中使用bind
    • <div onClick={this.handleClick.bind(this)}>test</div>
    • 這種方式在元件每次render渲染的時候,都會重新進行bind的操作,影響效能
  • render方法中使用箭頭函式
    • <div onClick={e => this.handleClick(e)}>test</div>
    • 每一次render的時候都會生成新的方法,影響效能
  • constructor中bind:this.handleClick = this.handleClick.bind(this);
  • 定義階段使用箭頭函式繫結

區別

  • 編寫方面:方式一、方式二、方式四寫法簡單,方式三的編寫過於冗雜
  • 效能方面:方式一和方式二在每次元件render的時候都會生成新的方法例項,效能問題欠缺。若該函式作為屬性值傳給子元件的時候,都會導致額外的渲染。而方式三、方式四隻會生成一個方法例項

綜合上述,方式四是最優的事件繫結方式。

react元件的建立方式以及區別

建立方式

  • 函式元件:通過一個函式,return 一個jsx語法宣告的結構
  • React.createClass 方法建立:語法冗餘,目前已經不太使用
  • 繼承 React.Component 建立的類元件:最終會被編譯成createClass

區別

由於React.createClass建立的方式過於冗雜,並不建議使用。

而像函式式建立和類元件建立的區別主要在於需要建立的元件是否需要為有狀態元件:對於一些無狀態的元件建立,建議使用函式式建立的方式。

在考慮元件的選擇原則上,能用無狀態元件則用無狀態元件。

不過,由於react hooks的出現,函式式元件建立的元件通過使用hooks方法也能使之成為有狀態元件,再加上目前推崇函數語言程式設計,所以這裡建議都使用函式式的方式來建立元件。

react 中元件之間如何通訊

元件傳遞的方式有很多種,根據傳送者和接收者可以分為如下:

  • 父元件向子元件傳遞:props
  • 子元件向父元件傳遞:父元件向子元件傳一個函式,然後通過這個函式的回撥,拿到子元件傳過來的值
  • 兄弟元件之間的通訊:狀態提升,在公共的父元件中進行狀態定義
  • 父元件向後代元件傳遞:React.createContext建立一個context進行元件傳遞
  • 非關係元件傳遞:redux

React.createContext

通過使用React.createContext建立一個context js const PriceContext = React.createContext('price') context建立成功後,其下存在Provider元件用於建立資料來源,Consumer元件用於接收資料,使用例項如下:

Provider元件通過value屬性用於給後代元件傳遞資料: js <PriceContext.Provider value={100}> </PriceContext.Provider>

如果想要獲取Provider傳遞的資料,可以通過Consumer元件或者或者使用contextType屬性接收,對應分別如下:

contextType: js class MyClass extends React.Component { static contextType = PriceContext; render() { let price = this.context; /* 基於這個值進行渲染工作 */ } }

Consumer元件: js <PriceContext.Consumer> { /*這裡是一個函式*/ } { price => <div>price:{price}</div> } </PriceContext.Consumer>

React中key的作用

官網中對於diff有如下規則:

  • 對比不同型別的元素:當元素型別變化時,會銷燬重建
  • 對比同一型別的元素:當元素型別不變時,比對及更新有改變的屬性並且“在處理完當前節點之後,React 繼續對子節點進行遞迴。”
  • 對子節點進行遞迴:React 使用 key 來匹配原有樹上的子元素以及最新樹上的子元素。若key一致,則進行更新,若key不一致,就銷燬重建

react函式元件和類元件的區別

針對兩種React元件,其區別主要分成以下幾大方向:

  • 編寫形式:類元件的編寫形式更加的冗餘
  • 狀態管理:在hooks之前函式元件沒有狀態,在hooks提出之後,函式元件也可以維護自身的狀態
  • 生命週期:函式元件沒有生命週期,這是因為生命週期鉤子都來自於繼承的React.Component,但是可以通過useEffect實現類似生命週期的效果
  • 呼叫方式:函式元件通過執行函式呼叫,類元件通過例項化然後呼叫例項的render方法
  • 獲取渲染的值:函式元件存在閉包陷阱,類元件不存在(Props在 React中是不可變的所以它永遠不會改變,但是 this 總是可變的,以便您可以在 render 和生命週期函式中讀取新版本)

react高階元件以及應用場景

什麼是高階元件

js高階函式(Higher-order function),至少滿足下列一個條件的函式

  • 接受一個或多個函式作為輸入
  • 輸出一個函式

在React中,高階元件是引數為元件,返回值為新元件的函式。本質也就是一個函式,並不是一個元件。高階元件的這種實現方式,本質上是一個裝飾者設計模式。

怎麼寫高階元件

```js import React, { Component } from 'react';

export default (WrappedComponent) => { return class EnhancedComponent extends Component { // do something render() { return ; } } } ```

通過對傳入的原始元件 WrappedComponent 做一些你想要的操作(比如操作 props,提取 state,給原始元件包裹其他元素等),從而加工出想要的元件 EnhancedComponent。

通用的邏輯放在高階元件中,對元件實現一致的處理,從而實現程式碼的複用。所以,高階元件的主要功能是封裝並分離元件的通用邏輯,讓通用邏輯在元件間更好地被複用。

高階元件遵循的規則

官網

  • 不要改變原始元件,而應該使用組合
  • HOC 應該透傳與自身無關的 props
  • 包裝顯示名字以便於除錯
  • 不要在 render() 方法中使用高階元件:這將導致子樹每次渲染都會進行解除安裝,和重新掛載的操作!
  • Refs 不會被傳遞:ref 實際上並不是一個 prop(就像 key 一樣),它是由 React 專門處理的。如果將 ref 新增到 HOC 的返回元件中,則 ref 引用指向容器元件,而不是被包裝元件。

高階元件可以傳遞所有的props,但是不能傳遞ref,傳毒ref可以使用React.forwardRef: ```js function withLogging(WrappedComponent) { class Enhance extends WrappedComponent { componentWillReceiveProps() { console.log('Current props', this.props); console.log('Next props', nextProps); } render() { const {forwardedRef, ...rest} = this.props; // 把 forwardedRef 賦值給 ref return ; } };

// React.forwardRef 方法會傳入 props 和 ref 兩個引數給其回撥函式
// 所以這邊的 ref 是由 React.forwardRef 提供的
function forwardRef(props, ref) {
    return <Enhance {...props} forwardRef={ref} />
}

return React.forwardRef(forwardRef);

} const EnhancedComponent = withLogging(SomeComponent); ```

應用場景

通過上面的瞭解,高階元件能夠提高程式碼的複用性和靈活性,在實際應用中,常常用於與核心業務無關但又在多個模組使用的功能,如許可權控制、日誌記錄、資料校驗、異常處理、統計上報等。

react元件間的過度動畫如何實現

在日常開發中,頁面切換時的轉場動畫是比較基礎的一個場景。

當一個元件在顯示與消失過程中存在過渡動畫,可以很好的增加使用者的體驗。

在react中實現過渡動畫效果會有很多種選擇,如react-transition-group,react-motion,Animated,以及原生的CSS都能完成切換動畫。

在react中,react-transition-group是一種很好的解決方案,其為元素新增enter,enter-active,exit,exit-active這一系列勾子

可以幫助我們方便的實現元件的入場和離場動畫

其主要提供了三個主要的元件: - CSSTransition:在前端開發中,結合 CSS 來完成過渡動畫效果 - SwitchTransition:兩個元件顯示和隱藏切換時,使用該元件 - TransitionGroup:將多個動畫元件包裹在其中,一般用於列表中元素的動畫

安裝:

npm install react-transition-group --save npm i --save-dev @types/react-transition-group

CSSTransition

其實現動畫的原理在於,當CSSTransition的in屬性置為true時,CSSTransition首先會給其子元件加上xxx-enter、xxx-enter-active的class執行動畫

當動畫執行結束後,會移除兩個class,並且新增-enter-done的class

所以可以利用這一點,通過css的transition屬性,讓元素在兩個狀態之間平滑過渡,從而得到相應的動畫效果

當in屬性置為false時,CSSTransition會給子元件加上xxx-exit和xxx-exit-active的class,然後開始執行動畫,當動畫結束後,移除兩個class,然後新增-exit-done的class

如下例子: js import { useState } from 'react' import { CSSTransition } from 'react-transition-group' import { Button } from 'antd' const CssTransitionCom: React.FC = () => { const [show, setShow] = useState(false) const toggleShow = () => { setShow(!show) } return ( <div style={{ margin: "20px" }}> <Button type="primary" onClick={toggleShow}>toggleShow CSSTransition</Button> <CSSTransition in={show} timeout={500} classNames={'CSSTransition'} unmountOnExit={true}> <h1>hello CSSTransition</h1> </CSSTransition> </div> ) } export default CssTransitionCom

對應css樣式如下: ```css .CSSTransition-enter { opacity: 0; transform: translateX(100%); }

.CSSTransition-enter-active { opacity: 1; transform: translateX(0); transition: all 500ms; }

.CSSTransition-enter-done { background-color: cadetblue; }

.CSSTransition-exit { opacity: 1; transform: translateX(0); }

.CSSTransition-exit-active { opacity: 0; transform: translateX(-100%); transition: all 500ms; }

```

SwitchTransition

SwitchTransition可以完成兩個元件之間切換的炫酷動畫

比如有一個按鈕需要在on和off之間切換,我們希望看到on先從左側退出,off再從右側進入

SwitchTransition中主要有一個屬性mode,對應兩個值: - in-out:表示新元件先進入,舊元件再移除; - out-in:表示舊元件先移除,新元件再進入

SwitchTransition元件裡面要有CSSTransition,不能直接包裹你想要切換的元件

裡面的CSSTransition元件不再像以前那樣接受in屬性來判斷元素是何種狀態,取而代之的是key屬性

下面給出一個按鈕入場和出場的示例,如下: ```js import { SwitchTransition, CSSTransition } from "react-transition-group"; import { PureComponent } from "react"; import { Button } from "antd"; export default class SwitchAnimation extends PureComponent<{}, { isOn: boolean }> { constructor(props: {}) { super(props);

    this.state = {
        isOn: true
    }
}

btnClick() {
    this.setState({ isOn: !this.state.isOn })
}

render() {
    const { isOn } = this.state;

    return (
        <div style={{ margin: "20px" }}>
            <SwitchTransition mode="out-in">
                <CSSTransition classNames="SwitchAnimation"
                    timeout={500}
                    key={isOn ? "SwitchAnimation-on" : "SwitchAnimation-off"}>
                    <Button type="primary" onClick={this.btnClick.bind(this)}>
                        {isOn ? "SwitchAnimation-on" : "SwitchAnimation-off"}
                    </Button>
                </CSSTransition>
            </SwitchTransition>
        </div>
    )
}

} css檔案對應如下:css

.SwitchAnimation-enter { transform: translate(100%, 0); opacity: 0; }

.SwitchAnimation-enter-active { transform: translate(0, 0); opacity: 1; transition: all 500ms; }

.SwitchAnimation-exit { transform: translate(0, 0); opacity: 1; }

.SwitchAnimation-exit-active { transform: translate(-100%, 0); opacity: 0; transition: all 500ms; } ```

TransitionGroup

當有一組動畫的時候,就可將這些CSSTransition放入到一個TransitionGroup中來完成動畫

同樣CSSTransition裡面沒有in屬性,用到了key屬性

TransitionGroup在感知children發生變化的時候,先儲存移除的節點,當動畫結束後才真正移除

其處理方式如下:

  • 插入的節點,先渲染dom,然後再做動畫
  • 刪除的節點,先做動畫,然後再刪除dom

如下: ```js import { Button } from 'antd'; import React, { PureComponent } from 'react' import { CSSTransition, TransitionGroup } from 'react-transition-group';

export default class GroupAnimation extends PureComponent<{}, { friends: string[] }> { constructor(props: {}) { super(props);

    this.state = {
        friends: []
    }
}
addFriend() {
    this.setState({
        friends: [...this.state.friends, "coderwhy"]
    })
}
render() {
    return (
        <div style={{ margin: "20px" }}>
            <TransitionGroup>
                {
                    this.state.friends.map((item, index) => {
                        return (
                            <CSSTransition classNames="GroupAnimation" timeout={300} key={index}>
                                <div>{item}</div>
                            </CSSTransition>
                        )
                    })
                }
            </TransitionGroup>
            <Button type='primary' onClick={e => this.addFriend()}>+friend</Button>
        </div>
    )
}

} 對應css如下:css

.GroupAnimation-enter { transform: translate(100%, 0); opacity: 0; }

.GroupAnimation-enter-active { transform: translate(0, 0); opacity: 1; transition: all 500ms; }

.GroupAnimation-exit { transform: translate(0, 0); opacity: 1; }

.GroupAnimation-exit-active { transform: translate(-100%, 0); opacity: 0; transition: all 500ms; }

```

ReactRouter 元件的理解,常用的react router元件

react-router等前端路由的原理大致相同,可以實現無重新整理的條件下切換顯示不同的頁面。

路由的本質就是頁面的URL發生改變時,頁面的顯示結果可以根據URL的變化而變化,但是頁面不會重新整理。

因此,可以通過前端路由可以實現單頁(SPA)應用

react-router主要分成了幾個不同的包:

  • react-router: 實現了路由的核心功能
  • react-router-dom: 基於 react-router,加入了在瀏覽器執行環境下的一些功能
  • react-router-native:基於 react-router,加入了 react-native 執行環境下的一些功能
  • react-router-config: 用於配置靜態路由的工具庫

常用元件

react-router-dom的常用的一些元件:

  • BrowserRouter、HashRouter:使用兩者作為最頂層元件包裹其他元件,分別匹配history模式和hash模式
  • Route:Route用於路徑的匹配,然後進行元件的渲染,對應的屬性如下:
    • path 屬性:用於設定匹配到的路徑
    • component 屬性:設定匹配到路徑後,渲染的元件
    • render 屬性:設定匹配到路徑後,渲染的內容
    • exact 屬性:開啟精準匹配,只有精準匹配到完全一致的路徑,才會渲染對應的元件
  • Link、NavLink:通常路徑的跳轉是使用Link元件,最終會被渲染成a元素,其中屬性to代替a標題的href屬性 NavLink是在Link基礎之上增加了一些樣式屬性,例如元件被選中時,發生樣式變化,則可以設定NavLink的一下屬性:
    • activeStyle:活躍時(匹配時)的樣式
    • activeClassName:活躍時新增的class
  • switch:swich元件的作用適用於當匹配到第一個元件的時候,後面的元件就不應該繼續匹配
  • redirect:路由的重定向

hooks

除了一些路由相關的元件之外,react-router還提供一些hooks,如下: - useHistory:元件內部直接訪問history,無須通過props獲取 - useParams:獲取路由引數 - useLocation:返回當前 URL的 location物件

傳參

路由傳遞引數主要分成了三種形式:

動態路由的方式(params):

路由配置: js { path: '/detail/:id/:name', component: Detail }

路由跳轉: ```js import { useHistory,useParams } from 'react-router-dom'; const history = useHistory(); // 跳轉路由 位址列:/detail/2/zora history.push('/detail/2/zora')

this.props.history.push( '/detail/2/zora' ) ```

獲取引數: ```js // 獲取路由引數 const params = useParams()
console.log(params) // {id: "2",name:"zora"}

this.props.match.params ```

優點: - 重新整理頁面,引數不丟失

缺點: - 只能傳字串,傳值過多url會變得很長 - 引數必須在路由上配置

search傳遞引數

路由不需要特別配置

路由跳轉:

```js import { useHistory } from 'react-router-dom'; const history = useHistory(); // 路由跳轉 位址列:/detail?id=2 history.push('/detail?id=2')
// 或者 history.push({pathname:'/detail',search:'?id=2'})

```

獲取引數:所獲取的是查詢字串,所以,還需要進一步的解析,自己自行解析,也可以使用第三方模組:qs,或者nodejs裡的query-string

```js const params = useLocation()

this.props.location.search

```

優點:

  • 重新整理頁面,引數不丟失

缺點:

  • 只能傳字串,傳值過多url會變得很長,獲取引數需要自定義hooks

state傳參

路由不需要單獨配置

路由跳轉: ```js

import { useHistory,useLocation } from 'react-router-dom'; const history = useHistory(); const item = {id:1,name:"zora"} // 路由跳轉 history.push(/user/role/detail, { id: item });

this.props.history.push({pathname:"/sort ",state : { name : 'sunny' }}); ```

獲取引數: ```js // 引數獲取 const {state} = useLocation() console.log(state) // {id:1,name:"zora"}

this.props.location.state ```

優點:

  • 可以傳物件

缺點:

  • <HashRouter>重新整理頁面,引數丟失

<HashRouter>通過state傳遞引數,重新整理頁面後引數丟失,官方建議使用<BrowserRouter><BrowserRouter>頁面重新整理引數也不會丟失。

query

路由不需要特別配置

路由跳轉: js this.props.history.push({pathname:"/query",query: { name : 'sunny' }});

獲取引數: js this.props.location.query.name

優勢:

  • 傳參優雅,傳遞引數可傳物件;

缺點:

  • 重新整理位址列,引數丟失

React Router有幾種模式,實現原理是什麼

react Router 有四個庫: - react router:核心庫,封裝了Router,Route,Switch等核心元件,實現了從路由的改變到元件的更新的核心功能, - react router dom:dom環境下的router。在react-router的核心基礎上,添加了用於跳轉的Link元件,和histoy模式下的BrowserRouter和hash模式下的HashRouter元件等。所謂BrowserRouter和HashRouter,也只不過用了history庫中createBrowserHistory和createHashHistory方法

  • react router native:RN環境下的router
  • react router config

在單頁應用中,一個web專案只有一個html頁面,一旦頁面載入完成之後,就不用因為使用者的操作而進行頁面的重新載入或者跳轉,其特性如下:

  • 改變 url 且不讓瀏覽器像伺服器傳送請求
  • 在不重新整理頁面的前提下動態改變瀏覽器位址列中的URL地址

react router dom其中主要分成了兩種模式: - hash 模式:在url後面加上#,如http://127.0.0.1:5500/home/#/page1 - history 模式:允許操作瀏覽器的曾經在標籤頁或者框架裡訪問的會話歷史記錄

React Router對應的hash模式和history模式對應的元件為: - HashRouter - BrowserRouter

這兩個元件的使用都十分的簡單,作為最頂層元件包裹其他元件

原理

參考

單頁面應用路由實現原理是,切換url,監聽url變化,從而渲染不同的頁面元件。

主要的方式有history模式和hash模式。

history模式

①改變路由

history.pushState

```js history.pushState(state,title,path)

```

1 state:一個與指定網址相關的狀態物件, popstate 事件觸發時,該物件會傳入回撥函式。如果不需要可填 null。

2 title:新頁面的標題,但是所有瀏覽器目前都忽略這個值,可填 null。

3 path:新的網址,必須與當前頁面處在同一個域。瀏覽器的位址列將顯示這個地址。

history.replaceState

```js history.replaceState(state,title,path)

```

引數和pushState一樣,這個方法會修改當前的history物件記錄, history.length 的長度不會改變。

②監聽路由

popstate事件

window.addEventListener('popstate',function(e){ /* 監聽改變 */ }) 複製程式碼

同一個文件的 history 物件出現變化時,就會觸發popstate 事件 � history.pushState 可以使瀏覽器地址改變,但是無需重新整理頁面。注意⚠️的是:用 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件popstate 事件只會在瀏覽器某些行為下觸發, 比如點選後退、前進按鈕或者呼叫 history.back()、history.forward()、history.go()方法。

hash模式

①改變路由

window.location.hash

通過window.location.hash屬性獲取和設定 hash值。

hash模式下 ,history.push 底層是呼叫了window.location.href來改變路由。history.replace底層是呼叫 window.location.replace改變路由。

②監聽路由

onhashchange

window.addEventListener('hashchange',function(e){ /* 監聽改變 */ })

流程圖

image.png

當地址欄改變url,元件的更新渲染都經歷了什麼?😊😊😊 拿history模式做參考。當url改變,首先觸發histoy,呼叫事件監聽popstate事件, 觸發回撥函式handlePopState,觸發history下面的setstate方法,產生新的location物件,然後通知Router元件更新location並通過context上下文傳遞,switch通過傳遞的更新流,匹配出符合的Route元件渲染,最後有Route元件取出context內容,傳遞給渲染頁面,渲染更新。

當我們呼叫history.push方法,切換路由,元件的更新渲染又都經歷了什麼呢?

我們還是拿history模式作為參考,當我們呼叫history.push方法,首先呼叫history的push方法,通過history.pushState來改變當前url,接下來觸發history下面的setState方法,接下來的步驟就和上面一模一樣了,這裡就不一一說了。

BrowserRouter 與 HashRouter 對⽐

  • HashRouter 最簡單,每次路由變化不需要服務端接入,根據瀏覽器的hash來區分 path 就可以;BrowserRouter需要服務端解析 URL 返回頁面,因此使用BrowserRouter需要在後端配置地址對映。
  • BrowserRouter 觸發路由變化的本質是使⽤ HTML5 history API( pushState、replaceState 和 popstate 事件)
  • HashRouter 不⽀持 location.key 和 location.state,動態路由需要通過?傳遞引數。
  • Hash history 只需要服務端配置一個地址就可以上線,但線上的 web 應⽤很少使用這種方式。

對immutable的理解,如何應用在react專案中

參考

使用Immutable物件最主要的庫是immutable.js

immutable.js 是一個完全獨立的庫,無論基於什麼框架都可以用它

其出現場景在於彌補 Javascript 沒有不可變資料結構的問題,通過 structural sharing來解決的效能問題

在React中應用

使用 Immutable可以給 React 應用帶來效能的優化,主要體現在減少渲染的次數

在做react效能優化的時候,為了避免重複渲染,我們會在shouldComponentUpdate()中做對比,當返回true執行render方法

Immutable通過is方法則可以完成對比,而無需像一樣通過深度比較的方式比較

在使用redux過程中也可以結合Immutable,不使用Immutable前修改一個數據需要做一個深拷貝 ```js import '_' from 'lodash';

const Component = React.createClass({ getInitialState() { return { data: { times: 0 } } }, handleAdd() { let data = _.cloneDeep(this.state.data); data.times = data.times + 1; this.setState({ data: data }); } } ```

使用 Immutable 後: js getInitialState() { return { data: Map({ times: 0 }) } }, handleAdd() { this.setState({ data: this.state.data.update('times', v => v + 1) }); // 這時的 times 並不會改變 console.log(this.state.data.get('times')); }

react render原理,在什麼時候觸發

render存在兩種形式: - 類元件中的render方法 - 函式元件的函式本身

觸發時機: - 類元件setState - 函式元件通過useState hook修改狀態

一旦執行了setState就會執行render方法(無論值是否發生變化),useState 會判斷當前值有無發生改變確定是否執行render方法,一旦父元件發生渲染,子元件也會渲染

如何提高元件的渲染效率

在之前文章中,我們瞭解到render的觸發時機,簡單來講就是類元件通過呼叫setState方法, 就會導致render,父元件一旦發生render渲染,子元件一定也會執行render渲染

父元件渲染導致子元件渲染,子元件並沒有發生任何改變,這時候就可以從避免無謂的渲染,具體實現的方式有如下: - shouldComponentUpdate: - 通過shouldComponentUpdate生命週期函式來比對 state和 props,確定是否要重新渲染 - 預設情況下返回true表示重新渲染,如果不希望元件重新渲染,返回 false 即可 - PureComponent: - 跟shouldComponentUpdate原理基本一致,通過對 props 和 state的淺比較結果來實現 shouldComponentUpdate - React.memo - React.memo用來快取元件的渲染,避免不必要的更新,其實也是一個高階元件,與 PureComponent 十分類似。但不同的是, React.memo 只能用於函式元件 - 如果需要深層次比較,這時候可以給memo第二個引數傳遞比較函式

react diff

跟Vue一致,React通過引入Virtual DOM的概念,極大地避免無效的Dom操作,使我們的頁面的構建效率提到了極大的提升

而diff演算法就是更高效地通過對比新舊Virtual DOM來找出真正的Dom變化之處

傳統diff演算法通過迴圈遞迴對節點進行依次對比,效率低下,演算法複雜度達到 O(n^3),react將演算法進行一個優化,複雜度降為O(n)

react中diff演算法主要遵循三個層級的策略:

  • tree層級

    • DOM節點跨層級的操作不做優化,只會對相同層級的節點進行比較
    • 只有刪除、建立操作,沒有移動操作
  • conponent 層級

    • 如果是同一個類的元件,則會繼續往下diff運算,如果不是一個類的元件,那麼直接刪除這個元件下的所有子節點,建立新的
  • element 層級

    • 對於比較同一層級的節點們,每個節點在對應的層級用唯一的key作為標識
    • 提供了 3 種節點操作,分別為 INSERT_MARKUP(插入)、MOVE_EXISTING (移動)和 REMOVE_NODE (刪除)
    • 通過key可以準確地發現新舊集合中的節點都是相同的節點,因此無需進行節點刪除和建立,只需要將舊集合中節點的位置進行移動,更新為新集合中節點的位置
    • 由於dom節點的移動操作開銷是比較昂貴的,在只修改文字的情況下,沒有key的情況下要比有key的效能更好

對Fiber架構的理解,解決了什麼問題

Fiber 的結構

在 React15 以前 React 的元件更新建立虛擬 DOM 和 Diff 的過程是不可中斷,如果需要更新元件樹層級非常深的話,在 Diff 的過程會非常佔用瀏覽器的執行緒,而我們都知道瀏覽器執行JavaScript 的執行緒和渲染真實 DOM 的執行緒是互斥的,也就是同一時間內,瀏覽器要麼在執行 JavaScript 的程式碼運算,要麼在渲染頁面,如果 JavaScript 的程式碼執行時間過長則會造成頁面卡頓

基於以上原因 React 團隊在 React16 之後就改寫了整個架構,將原來陣列結構的虛擬DOM,改成叫 Fiber 的一種資料結構,基於這種 Fiber 的資料結構可以實現由原來不可中斷的更新過程變成非同步的可中斷的更新

Fiber 的資料結構主要長成以下的樣子,主要通過 Fiber 的一些屬性去儲存元件相關的資訊。

```js function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // 作為靜態資料結構的屬性 this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null;

// 用於連線其他Fiber節點形成Fiber樹 this.return = null; this.child = null; this.sibling = null; this.index = 0;

this.ref = null;

// 作為動態的工作單元的屬性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null;

this.mode = mode;

this.effectTag = NoEffect; this.nextEffect = null;

this.firstEffect = null; this.lastEffect = null;

// 排程優先順序相關 this.lanes = NoLanes; this.childLanes = NoLanes;

// 指向該fiber在另一次更新時對應的fiber this.alternate = null; } ```

Fiber 主要靠以下屬性連成一棵樹結構的資料的,也就是 Fiber 連結串列。

js // 指向父級Fiber節點 this.return = null; // 指向子Fiber節點 this.child = null; // 指向右邊第一個兄弟Fiber節點 this.sibling = null;

那麼以上的 Fiber 連結串列的資料結構有什麼特點,就是任何一個位置的 Fiber 節點,都可以非常容易知道它的父 Fiber, 第一個子元素的 Fiber,和它的兄弟節點 Fiber。卻不容易知道它前一個 Fiber 節點是誰,這就是 React 中單向連結串列 Fiber 節點的特點。也正是因為這些即便在協調的過程被中斷了,再恢復協調的時候,依然知道當前的父節點和孩子節點等資訊。

在React 16 版本中,主要做了以下的操作:

  • 為每個增加了優先順序,優先順序高的任務可以中斷低優先順序的任務。然後再重新,注意是重新執行優先順序低的任務
  • 增加了非同步任務,呼叫requestIdleCallback api,瀏覽器空閒的時候執行
  • dom diff樹變成了連結串列,一個dom對應兩個fiber(一個連結串列),對應兩個佇列,這都是為找到被中斷的任務,重新執行

從架構角度來看,Fiber 是對 React核心演算法(即調和過程)的重寫

從編碼角度來看,Fiber是 React內部所定義的一種資料結構,它是 Fiber樹結構的節點單位,也就是 React 16 新架構下的虛擬DOM

一個 fiber就是一個 JavaScript物件,包含了元素的資訊、該元素的更新操作佇列、型別.

如何解決

Fiber把渲染更新過程拆分成多個子任務,每次只做一小部分,做完看是否還有剩餘時間,如果有繼續下一個任務;如果沒有,掛起當前任務,將時間控制權交給瀏覽器(瀏覽器可以進行渲染),等瀏覽器不忙的時候再繼續執行

可以中斷與恢復,恢復後也可以複用之前的中間狀態,並給不同的任務賦予不同的優先順序,其中每個任務更新單元為 React Element 對應的 Fiber節點

實現的上述方式的是requestIdleCallback方法: window.requestIdleCallback()方法將在瀏覽器的空閒時段內呼叫的函式排隊。這使開發者能夠在主事件迴圈上執行後臺和低優先順序工作,而不會影響延遲關鍵事件,如動畫和輸入響應

Fiber 架構可以分為三層:

  • Scheduler 排程器 —— 排程任務的優先順序,高優任務優先進入 Reconciler。requestIdleCallback在排程器中用到。
  • Reconciler 協調器 —— 負責找出變化的元件
  • Renderer 渲染器 —— 負責將變化的元件渲染到頁面上

相比 React15,React16 多了Scheduler(排程器) ,排程器的作用是排程更新的優先順序。

在新的架構模式下,工作流如下:

  • 每個更新任務都會被賦予一個優先順序。
  • 當更新任務抵達排程器時,高優先順序的更新任務(記為 A)會更快地被排程進 Reconciler 層;
  • 此時若有新的更新任務(記為 B)抵達排程器,排程器會檢查它的優先順序,若發現 B 的優先順序高於當前任務 A,那麼當前處於 Reconciler 層的 A 任務就會被中斷,排程器會將 B 任務推入 Reconciler 層。
  • 當 B 任務完成渲染後,新一輪的排程開始,之前被中斷的 A 任務將會被重新推入 Reconciler 層,繼續它的渲染之旅,即“可恢復”。

Fiber 架構的核心即是”可中斷”、”可恢復”、”優先順序”

React 16 是如何解決中斷更新時 DOM 渲染不完全的問題呢?

在 React 16 中,ReconcilerRenderer不再是交替工作。當Scheduler將任務交給Reconciler後,Reconciler會為變化的虛擬 DOM 打上的標記。

export const Placement = /* */ 0b0000000000010 export const Update = /* */ 0b0000000000100 export const PlacementAndUpdate = /* */ 0b0000000000110 export const Deletion = /* */ 0b0000000001000

  • Placement表示插入操作
  • PlacementAndUpdate表示替換操作
  • Update表示更新操作
  • Deletion表示刪除操作

整個SchedulerReconciler的工作都在記憶體中進行,所以即使反覆中斷,使用者也不會看見更新不完全的 DOM。只有當所有元件都完成Reconciler的工作,才會統一交給Renderer

fiber對生命週期的影響

新老兩種架構對 React 生命週期的影響主要在 render 這個階段,這個影響是通過增加 Scheduler 層和改寫 Reconciler 層來實現的。

在 render 階段,一個龐大的更新任務被分解為了一個個的工作單元,這些工作單元有著不同的優先順序,React 可以根據優先順序的高低去實現工作單元的打斷和恢復。

從 Firber 機制 render 階段的角度看 react 即將廢除的三個生命週期的共同特點是都處於 render 階段:

componentWillMount componentWillUpdate componentWillReceiveProps

參考連結:

https://jacky-summer.github.io/2021/02/07/%E6%B5%85%E8%B0%88%E5%AF%B9-React-Fiber-%E7%9A%84%E7%90%86%E8%A7%A3/

JSX轉換成真實DOM的過程

其渲染流程如下所示:

  1. 將函式元件或者類元件中的jsx結構,通過babel轉換成React.createElement的形式,React.createElement對接收到的引數進行“格式化”,傳遞給ReactElement函式;
  2. ReactElement函式將接收到的引數進行整合,最終構造成一個虛擬DOM物件並返回;
  3. ReactDOM.render將生成好的虛擬DOM渲染到指定容器上,其中採用了批處理、事務等機制並且對特定瀏覽器進行了效能優化,最終轉換為真實DOM

react 效能優化的手段

  • 避免不必要的render:通過shouldComponentUpdate、PureComponent、React.memo

  • 使用 Immutable:在做react效能優化的時候,為了避免重複渲染,我們會在shouldComponentUpdate()中做對比,當返回true執行render方法。Immutable通過is方法則可以完成對比,而無需像一樣通過深度比較的方式比較

  • 避免使用行內函數:每次呼叫render函式時都會建立一個新的函式例項

  • 事件繫結方式:避免在render函式中宣告函式,通過在constructor繫結this,或者在宣告函式的時候使用箭頭函式

  • 使用 React Fragments 避免額外標記:使用者建立新元件時,每個元件應具有單個父標籤。這個額外標籤除了充當父標籤之外,並沒有其他作用,這時候則可以使用fragement

  • 懶載入元件:從工程方面考慮,webpack存在程式碼拆分能力,可以為應用建立多個包,並在執行時動態載入,減少初始包的大小。而在react中使用到了Suspense和 lazy元件實現程式碼拆分功能,基本使用如下: ```js const johanComponent = React.lazy(() => import(/ webpackChunkName: "johanComponent" / './myAwesome.component'));

    export const johanAsyncComponent = props => (
      <React.Suspense fallback={<Spinner />}>
        <johanComponent {...props} />
      </React.Suspense>
    );
    

    ```

  • 服務端渲染:採用服務端渲染端方式,可以使使用者更快的看到渲染完成的頁面

在React專案中如何捕獲錯誤

錯誤在我們日常編寫程式碼是非常常見的

舉個例子,在react專案中去編寫元件內JavaScript程式碼錯誤會導致 React 的內部狀態被破壞,導致整個應用崩潰,這是不應該出現的現象

作為一個框架,react也有自身對於錯誤的處理的解決方案。

為了解決出現的錯誤導致整個應用崩潰的問題,react16引用了錯誤邊界新的概念

錯誤邊界是一種 React 元件,這種元件可以捕獲發生在其子元件樹任何位置的 JavaScript 錯誤,並列印這些錯誤,同時展示降級 UI,而並不會渲染那些發生崩潰的子元件樹

錯誤邊界在渲染期間、生命週期方法和整個元件樹的建構函式中捕獲錯誤

形成錯誤邊界元件的兩個條件: - 使用了 static getDerivedStateFromError() - 使用了 componentDidCatch()

丟擲錯誤後,請使用 static getDerivedStateFromError() 渲染備用 UI ,使用 componentDidCatch() 列印錯誤資訊,如下: ```js class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; }

static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能夠顯示降級後的 UI return { hasError: true }; }

componentDidCatch(error, errorInfo) { // 你同樣可以將錯誤日誌上報給伺服器 logErrorToMyService(error, errorInfo); }

render() { if (this.state.hasError) { // 你可以自定義降級後的 UI 並渲染 return

Something went wrong.

; }

return this.props.children;

} } ```

然後就可以把自身元件的作為錯誤邊界的子元件,如下:

html <ErrorBoundary> <MyWidget /> </ErrorBoundary>

下面這些情況無法捕獲到異常:

  • 事件處理
  • 非同步程式碼
  • 服務端渲染
  • 自身丟擲來的錯誤

對於錯誤邊界無法捕獲的異常,如事件處理過程中發生問題並不會捕獲到,是因為其不會在渲染期間觸發,並不會導致渲染時候問題

這種情況可以使用js的try...catch...語法,如下: ```js class MyComponent extends React.Component { constructor(props) { super(props); this.state = { error: null }; this.handleClick = this.handleClick.bind(this); }

handleClick() { try { // 執行操作,如有錯誤則會丟擲 } catch (error) { this.setState({ error }); } }

render() { if (this.state.error) { return

Caught an error.

} return } } ```

除此之外還可以通過監聽onerror事件: js window.addEventListener('error', function(event) { ... })

react和vue渲染原理上的區別

可以看這篇文章

Redux

redux就是一個將狀態進行集中管理的容器,遵循三大基本原則:

  • 單一資料來源
  • state 是隻讀的
  • 使用純函式來執行修改

注意redux並不是只有在react中使用,還可以和其他的介面庫使用,比如vue

以下的情景可以使用redux: - 某個元件的狀態,需要共享 - 某個狀態需要在任何地方都可以拿到 - 一個元件需要改變全域性狀態 - 一個元件需要改變另一個元件的狀態

image.png

首先,使用者發出 Action。

store.dispatch(action);

然後,Store 自動呼叫 Reducer,並且傳入兩個引數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。

let nextState = todoApp(previousState, action);

State 一旦有變化,Store 就會呼叫監聽函式。

// 設定監聽函式 store.subscribe(listener);

listener可以通過store.getState()得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。

function listerner() { let newState = store.getState(); component.setState(newState); }

中介軟體

參考:https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html

中介軟體就是一個函式,對store.dispatch方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,添加了其他功能。

js let next = store.dispatch; store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action); next(action); console.log('next state', store.getState()); }

非同步操作

redux-thunk中介軟體。

預設情況下的dispatch(action)action需要是一個JavaScript的物件

redux-thunk中介軟體會判斷你當前傳進來的資料型別,如果是一個函式,將會給函式傳入引數值(dispatch,getState)

  • dispatch函式用於我們之後再次派發action
  • getState函式考慮到我們之後的一些操作需要依賴原來的狀態,用於讓我們可以獲取之前的一些狀態

所以dispatch可以寫成下述函式的形式: ```js

const getHomeMultidataAction = () => { return (dispatch) => { axios.get("http://xxx.xx.xx.xx/test").then(res => { const data = res.data.data; dispatch(changeBannersAction(data.banner.list)); dispatch(changeRecommendsAction(data.recommend.list)); }) } } ```

在react中使用

基本概念

state:普通物件,用來儲存狀態

action:普通物件,用來描述變化

reducer:接收 state 和 action,並返回新的 state 的函式,將state和action連線起來

基本使用

安裝:

npm install redux

npm install react-redux

  1. 建立store

```js import { createStore } from "redux"

const defaultState={ counter:0 }

//純函式 let reducers =(state = defaultState ,action)=>{ switch (action.type){ case "increment": console.log("increment") return { counter:state.counter+1 } case "decrement": return { counter:state.counter-1 } default : return state } } const store = createStore(reducers) export default store ```

  1. 全域性注入store

```js import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals';

import { Provider } from 'react-redux'; import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root')); root.render( );

// If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();

```

  1. react-redux將redux和react聯絡起來

```jsx import React from "react"; import { connect } from "react-redux"

class ClassCom extends React.Component { constructor(props) { super(props); }

   incre = () => {
       this.props.increment()
   }

   render() {
       return (
           <div>
               <h1>類元件</h1>
               <h1>store測試</h1>
               <div>store的值{this.props.num}</div>
               <button onClick={this.incre}>increment</button>
           </div>
       )
   }

} // export default ClassCom //該函式作為connect的第一個引數,能拿到state //對映state到組建的props上 function mapStateToProps(state) { return { num: state.counter } }

//該函式作為connect的第二個引數,能拿到dispatch //對映dispatch方法到組建的props上 function mapDispatchToProps(dispatch) { return { increment() { dispatch({ type: "increment" }) }, decrement() { dispatch({ type: "decrement" }) } } }

//connet函式執行返回一個高階元件 //呼叫這個高階元件,傳入當前元件作為引數,返回一個增強的元件 //這個增強的元件props裡有store的state和dispach方法 export default connect(mapStateToProps, mapDispatchToProps)(ClassCom) ```

這個時候元件的props中會有

  • 傳入的props
  • mapStateToProps注入的state
  • mapDispatchToProps 注入的dispatch

接下來就可以通過props去使用狀態和更新狀態

Redux和Vuex的異同點,以及用到的相同的思想

相同點

  • state共享資料
  • 流程一致:定義全域性state,觸發修改方法,修改state
  • 全域性注入store

不同點:

  • redux使用的是不可變資料,而Vuex是可變的。
  • redux每次都是用新的state替換舊的state,vuex是直接修改。
  • redux在檢測資料變化時是通過diff演算法比較差異的;vuex是通過getter/setter來比較的
  • vuex定義了state,getter,mutation,action;redux定義了state,reducer,action
  • vuex中state統一存放,方便理解;react中state依賴reducer初始值
  • vuex的mapGetters可以快捷得到state,redux中是mapStateToProps
  • vuex同步使用mutation,非同步使用action;redux同步非同步都使用reducer

相同思想

  • 單一資料來源
  • 變化可預測
  • MVVM思想