[react] hooks

woow_wu7發表於2020-01-10

導航

[深入01] 執行上下文
[深入02] 原型鏈
[深入03] 繼承
[深入04] 事件迴圈
[深入05] 柯里化 偏函式 函式記憶
[深入06] 隱式轉換 和 運算子
[深入07] 瀏覽器快取機制(http快取機制)
[深入08] 前端安全
[深入09] 深淺拷貝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模組化
[深入13] 觀察者模式 釋出訂閱模式 雙向資料繫結
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue專案
[部署03] gitlab-CI

react-hooks的實戰總結

大綱

useState
useEffect
useLayoutEffect
useCallback
useMemo
useRef(useImperativeHandle
useImperativeHandle(useRef)
useReducer
useContext
Context
React.memo
--
useFetch
如何封裝一個useFetch
hooks中父元件如何獲取子元件中的方法 `useImperativeHandle` `forwardRef`
--
複習:
js中的註釋規範
手寫call,applay,bind
邏輯運算子 &&     ||
複製程式碼

useState

  • function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
useState

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);

- 引數
  - 引數是一個值或者函式
  - 引數是函式時,返回值作為初始值,(當初始值需要複雜運算時使用)
    - 該函式僅在初始渲染時執行一次,以獲得初始狀態。在以後的元件渲染中,不會再呼叫,從而跳過昂貴的操作。!!!!
- 返回值
  - 返回值是一個陣列
  - 第一個成員:返回設定過後的state
  - 第二個成員:setter函式(本質上就是一個dispatcher,底層就是useReducer)
    - 該函式的引數可以是一個值,或者是一個簽名為 (prevState: S) => S 的函式
  - 即返回一個狀態值和一個函式來更新狀態
- 注意事項
  - 僅在頂層呼叫hook,不能在迴圈,條件,巢狀函式中 useState(),在多個useState()呼叫中,渲染之間的呼叫順序必須相同。
    - 為什麼要在頂層順序呼叫,而不能在迴圈,條件,巢狀中呼叫???????
      - 因為內部的各個state都會用一個佇列來維護(類似陣列)
      - 並且佇列中的state和setter函式是一一對應的,在上面三種情況中可以會改變一一對應的關係,造成錯誤的預期
  - !!!
  - 過時的狀態,閉包是一個從外部作用域捕獲變數的函式。(解決辦法,通過函式返回值,如setCount(count => count+1))
  - 閉包(例如事件處理程式,回撥)可能會從函式元件作用域中捕獲狀態變數。 
    由於狀態變數在渲染之間變化,因此閉包應捕獲具有最新狀態值的變數。否則,如果閉包捕獲了過時的狀態值,則可能會遇到過時的狀態問題。
- 複雜的狀態
  - 當有多個狀態完成同一項任務時,或者狀態過多時,建議使用 useReducer
- 關於react元件的重新渲染
  -  class  中重新渲染:是render()和willmout dimount等鉤子函式重新執行
  - function中重新渲染:整個元件函式重新執行
- 思考
  - 當函式執行完後,所有記憶體都會被釋放掉,函式中宣告的變數也會被釋放掉,那useState如何能儲存狀態呢???????
  - 或者說,每次重新渲染,為什麼useState能返回記住的狀態????
- 答案
  - useState利用閉包,在函式內部建立一個當前函式元件的狀態。並提供一個修改該狀態的方法。






- 例項
------------------
(1) 例子1
  - 過時的狀態
  - setCount引數是值和函式的區別

<div  onClick={() => setTimeout(() => setCount(count+1), 3000)}> ----------- // 在3秒內多次點選,多次渲染,但始終 count =1
  點選執行定時器,set函式引數是一個值的情況
</div>
<div onClick={() => setTimeout(() => setCount(count => count+1), 3000)}> --- // 在3秒內多次點選,時間到後,會記錄每次點選的值
  點選執行定時器,set函式引數是一個函式的情況
</div>
<div>{count}</div>





------------------
(2) 例子2
  - 如果是同一個引用,元件不會重新渲染

const [isReRender, setIsReRender] = useState({render: true});
setIsReRender(isReRender) -------------------- 不會重新渲染,內部使用函式記憶,快取了上一次的值,相同直接使用
-
setIsReRender(() => ({render: true})) -------- 會重新渲染,因為是一個新的不同引用的物件
-
isReRender.render = false;
setIsReRender(isReRender) -------------------- 不會重新渲染,賦值之後再比較,引用沒變





------------------
(3) 例子3
- useState返回的state在整個生命週期中保持不變,所以可以用來固定變數的引用,在不使用改變函式的時候
- 當然也可以使用useRef去固定變數
const [a] = useState(0); // 注意,這隻使用賦值state,不需要改變函式





------------------
(4) 例子4!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
function CaseOne() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      console.log(count, 'in')
      setCount(count+1)
      // 這裡有外層函式CaseOne,裡層函式setInterval的回撥函式,並且裡層使用了外層的count,setCount兩個變數,形成閉包
      // 定時器每次執行,都是使用的第一次執行時宣告並賦值的count,count是第一次渲染中的閉包中的count,值永遠是0 
      // 所以 setCount的引數永遠是1,在這個例子中
    }, 1000)
  }, [])
  console.log(count, 'out')
  return (
    <div style={{background: 'yellow'}}>
      <div>經典的use-state</div>
    </div>
  )
}
解析:
1. useState()的初始引數,只會在第一次執行的時候,賦值給count變數,以後的渲染就不再有作用
2. useState()的返回值count,在除了第一次使用useState的初始引數外,以後的渲染將使用上一次的setCount的返回值
具體過程:
第一次渲染
  - useState(0)執行,返回[0, setter函式]賦值給了count和setCount
  - 外層count => 0
  - 1s後定時器執行,形成閉包,閉包中的變數 count是0,setCount(0+1)
第二次渲染:
  - count和setCount從新宣告並賦值,count是上一次setCount的返回值,即 1
  - 外層count => 1
  - 注意:定時器再次執行,count還是引用的第一次執行生成的閉包中的count是0,setCount(0+1)
  - 注意:第二次的setCount和第一次的setCount的引數都是0+1,即引數沒有發生變化,這種情況,react將不會在重新渲染,即不會有第三次渲染了
         即外層的count將不再列印,保持1





------------------
(5) 例子5
- useState在不用setter函式修改,而是直接修改state時,可以用來快取引用型別的變數
  - 如果是原始型別的值,由於是const不能被修改,如果用let每次都是新的引用,重渲染會被初始化
function CaseTwo() {
  const [a] = useState({count: 1}) // 引用型別的變數
  let [c] = useState(10) // 原始型別的值,使用的是let
  const [b, setB] = useState(0)
  return (
    <div style={{background: 'yellow'}}>
      <div>CaseTwo</div>
      <div onClick={() => {
        a.count = 22 // 直接修改引用型別的值,注意不是用useState返回的setter函式修改
        c = 33 // 直接修改原始型別的值
        setB(b => b+1) // 重新渲染
      }}>點選重新渲染</div>
      <div>{a.count}</div> // 列印22
      <div>{b}</div> // 列印10,每次被重新初始化,這種情況要固定住值的話,可以使用useRef !!!!!!!!!!!
      <div>{c}</div>
    </div>
  )
}
複製程式碼

經典的例子:juejin.im/post/5d7110…

juejin.im/post/5dd1df…

mp.weixin.qq.com/s/Bo0Q6FDn4…

解析:imweb.io/topic/5c7d5…

useState原始碼:juejin.im/post/5dc6e1…

使用介紹:juejin.im/post/5d985d…

useEffect

  • 執行副作用的hook
  • 什麼是副作用:除了和渲染ui相關的意外的邏輯都是副作用(當dom渲染完執行的和ui無關的邏輯)
useEffect
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  - type EffectCallback = () => (void | (() => void | undefined));
  - type DependencyList = ReadonlyArray<any>;


引數:
- 第一個引數:函式
  - 每一次render之後,執行副作用,和清除上一次副作用
  - 該函式的返回值就是清除函式(清除上一次的副作用)
- 第二個引數:陣列
  - 當這幾個依賴有一個要更新,effect裡面也會重新生成一個新的副作用並執行副作用
  - 如果沒有更新,則不會執行。
  - 如果第二個引數不傳,表示沒有依賴項,則每次render後都會執行
  - 注意:第二個引數是一個空陣列 [ ] 時!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    - 如果傳一個空陣列,表示useEffect的回撥只執行一次,類似於componentDidMount()
    - useEffect每次渲染後都會執行,只是useEffect依賴是空陣列,每次對比沒有變化,所以Effect的回撥不再執行
    - 是useEffect的回撥只執行一次,而不是useEffect只執行一次,useEffect每次渲染後都會執行
    - 依賴的比較,只是簡單的比較值或者引用是否相等
    - 所以
      - 很明顯,useEffect可以模仿(didMount,didUpdate),返回值可以模仿(willUnmount)
- 什麼是副作用
  - 和渲染ui無關的邏輯都是副作用
  - 當DOM渲染完成之後,我還要執行一段別的邏輯,這一段別的邏輯就是副作用


useEffect執行的時機:
- 每次渲染完成後,執行useEffect
  - setEffect有點類似於componentDidMount,componentDidUpdate,componentWillUnmount的結合



注意事項:
1. useEffect中用到的狀態,官網推薦都應該寫到依賴陣列中去,不管引用的是基礎型別值、還是物件甚至是函式。
2. useEffect的第一個引數函式,如果返回一個函式,該函式就是清理函式

3.清除函式執行的時機:
- useEffect的第一個引數函式,如果返回一個函式,該函式就是清理函式(清除一些副作用,如定時器等)
- 執行的時機:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- 第一次渲染,不會執行清除函式,所以 31 -----------------------------------(只不過定時器的緣故,132一起列印了)
- 第二次渲染,在渲染完成後,立即執行清除函式,再執行Effect函式的回撥函式,所以 321
- 總結:再每次渲染完成後,立即執行清除函式清除上一次的副作用(第一次渲染除外)

列印順序為:31 321 321 321
const [count, setCount] = useState(0);
useEffect(() => {
  const timer = setInterval(() => {
    console.log('1=======')
    setCount(count+1)
  }, 1000)
  return () => {
    console.log('2=======')
    clearInterval(timer)
  }
}, [count])
console.log('3=======')
複製程式碼

juejin.im/post/5cd839… juejin.im/post/5d9707…

useLayoutEffect

  • 在所有的 DOM 變更之後同步呼叫 effect,可以使用它來讀取 DOM 佈局並同步觸發重渲染,在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。
  • useLayoutEffect 與 componentDidMount、componentDidUpdate 的呼叫階段是一樣的,注意只是呼叫時機一樣
  • seLayoutEffect是同步地,而useEffect是非同步的
  • useLayoutEffect可以解決螢幕跳動的問題
useEffect

function App2 () {
  const [count, setCount] = useState(0)
  useEffect(() => {
    if (count===0) {
      setCount(count => Math.random())
    }
  }, [count])
  return (
    <div>
      <div onClick={() => setCount(count => 0)}>點選更新</div>
      {count}
    </div>
  )
}
結果:渲染結果是先變成0,後變成隨機數
過程:
1.  setCount(count => 0),從新render,渲染0,執行useEffect,setCount(count => Math.random()),重新渲染得到隨機數







----------------------
useLayoutEffect

function App2 () {
  const [count, setCount] = useState(0)
  useLayoutEffect(() => {
    if (count===0) {
      setCount(count => Math.random())
    }
  }, [count])
  return (
    <div>
      <div onClick={() => setCount(count => 0)}>點選更新</div>
      {count}
    </div>
  )
}
結果:不會變成0,而是直接顯示隨機數字
過程:
1.  setCount(count => 0),重新render,瀏覽器並沒有渲染完成,
2. useLayoutEffect執行,阻塞渲染,setCount(count => Math.random()),重新render,
3. useLayoutEffect執行,沒有邏輯,渲染隨機數

複製程式碼

useCallback

  • 快取函式(或者說快取住函式變數),(或者說固定函式的引用)
useCallback

- 固定函式的引用,即快取函式
- function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
- 引數:
  - 第一個引數:回撥函式
  - 第二個引數:依賴項
- 返回值
  - 返回值是一個T型別的函式


- 說明
  - 一般情況下,元件重新渲染,函式就會被重新宣告
  - 但是用useCallback包裝的函式,只有在依賴項改變的時候,才'相當於'重新宣告,未改變時,是快取的上一個的函式引用


- 注意點
1. const a = useCallback(() => {...}, [])
  - 這種情況 a 永遠不會重新宣告,因為它的依賴項是空


- 效能優化 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  - 配合React.memo優化
  - React.memo
    - React.memo是當props改變時才會重新渲染,僅僅依賴於被傳入的props
    - 而當props中包含函式的時候,函式的引用可以用useCallback控制,即useCallback可以控制props中的函式是否改變!!!!!!


案例
(1) 配合React.memo做效能優化
父元件:
function UseCallback() {
  const [count, setCount] = useState(0)
  const [color, setColor] = useState('yellow')
  const callBackChangeColor = useCallback(() => { 
    // callBackChangeColor 函式的更新僅僅依賴於color改變
    // 如果color不更新的話,callBackChangeColor的引用就不會改變
    // 又因為子元件更新僅僅依賴於callBackChangeColor函式的更新
    const index = Math.floor(Math.random()*4)
    setColor(['black', 'white', 'blue', 'green', 'Purple'][index])
  }, [color])
  return (
    <div style={{background: '#FF4500', padding: '20px', margin: '10px 0'}}>
      <div style={{color}}>USE_CALLBACK</div>
      <div onClick={() => setCount(count => count+1)}>父元件改變count</div> // 當count改變時,子元件並不會重新渲染
      <div>{count}</div>
      <ChildUseCallback setColor={callBackChangeColor} /> // 子元件
    </div>
  )
}
子元件:
const ChildUseCallback = React.memo(({setColor}) => { // React.memo元件的更新僅僅依賴於setColor函式的改變
  const changeColor = () => {
    setColor()
  }
  console.log('子元件執行了')
  return (
    <div style={{background: '#FA8072'}}>
      <div>CHILD_USE_CALLBACK</div>
      <div onClick={changeColor}>改變顏色</div>
    </div>
  )
})
複製程式碼

useMemo

  • 快取變數,類似於計算屬性
  • 使用的時候要考慮兩點
    • 要記住的函式開銷大嗎?大才考慮使用useMemo
    • 返回的是原始值嗎?不是原始值時才考慮useMemo
  • 當useEffect的函式中要使用不會再改變的props,但是不在依賴項時,ESLint Hook會發出警告
    • 解決辦法是,用useRef固定住不需要再改變的某些props=> 具體請點選下面的連結
useMemo
- function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
- 引數
  - 第一個引數是一個函式,返回值型別是T型別
    - 通常情況下類似這樣 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
    - 注意函式返回函式,內層函式必須有返回值!!!!!!!!!!!!!
  - 第二個引數是陣列或者undefined,即依賴項
- 返回值
  - T型別的變數



案例:
父元件:
function UseCallback() {
  const [count, setCount] = useState(0)
  const [color, setColor] = useState('yellow')
  const [isBoolean, setIsBoolean] = useState(false)
  const callBackChangeColor = useCallback(() => {
    const index = Math.floor(Math.random()*4)
    setColor(['black', 'white', 'blue', 'green', 'Purple'][index])
  }, [color])
  const aOrBFn = () => {
    return isBoolean ? 'A' : 'B'
  }
  const cacheIsBoolean = useMemo(() => aOrBFn(), [isBoolean])  // useMemo
  // useMemo,當isBoolean改變時重新執行aOrBFn函式,同時在子元件沒有依賴的state變化時,不重新渲染
  // 例如:
  //   1. 當count變化時,父元件重新渲染,但是子元件沒有依賴cont,不希望重新渲染
  //   2. 子元件重新渲染只依賴兩個,一是 cacheIsBoolean 變數,二是callBackChangeColor函式
  //   3. 只希望依賴的兩個props改變,才重新渲染子元件,所以使用useCallback固定函式,用useMemo固定變數,同時用React.memo優化子元件
  return (
    <div style={{background: '#FF4500', padding: '20px', margin: '10px 0'}}>
      <div style={{color}}>USE_CALLBACK</div>
      <div onClick={() => setCount(count => count+1)}>父元件改變 => Count</div>
      <div>{count}</div>
      <div onClick={() => setIsBoolean(boo => !boo)}>父元件改變 => Boolean</div> // isBoolean改變,觸發useMemo的引數函式重新計算
      <div>{isBoolean + ''}</div>
      <ChildUseCallback setColor={callBackChangeColor} cacheIsBoolean={cacheIsBoolean}/> // 子元件依賴cacheIsBoolean
    </div>
  )
}
子元件:
const ChildUseCallback = React.memo(({setColor, cacheIsBoolean}) => {
  const changeColor = () => {
    setColor()
  }
  console.log('子元件執行了')
  return (
    <div style={{background: '#FA8072'}}>
      <div>CHILD_USE_CALLBACK</div>
      <div onClick={changeColor}>改變顏色</div>
    </div>
  )
})
複製程式碼

哪些情況要避免使用useMemo www.infoq.cn/article/mM5…

useRef

useRef

- const refContainer = useRef(initialValue);
- function useRef<T = undefined>(): MutableRefObject<T | undefined>;
- current屬性
  - useRef.current屬性,被初始化為傳入的引數(initialValue)
- 返回值
  - 返回一個可變的ref物件
  - 返回的ref物件,在元件的整個生命週期內保持不變


(1) 作用
- 儲存任何可變的值,在元件整個生命週期內保持不變(每次渲染時,返回同一個ref物件)
- 即還可以繫結DOM節點和子元件 (儲存任何可變值包括DOM節點,子元件等)
- 相當於class中的普通屬性


(2) 注意點
- 改變 useRef.current 屬性不會觸發元件重新渲染,類似於class元件中宣告的屬性


(3) 例項
- 主要測試:繫結DOM 和 固定變數
function UseRef() {
  const renderTimes = useRef(0); // 元件渲染的次數測試,useRef.current的初始值時0,(相當於外部的全域性變數,能固定住值)
  renderTimes.current++; // 每次render都 +1
  const inputRef = useRef(null) // 繫結dom測試
  console.log('renderTime:', renderTimes.current)  // 1  2
  console.log('useRef.current:', inputRef.current) // null <input />
  const focus = () => {
    inputRef.current.focus()
  }
  return (
    <div style={{background: '#FFBBFF'}}>
      <div>USE_REF</div>
      <input ref={inputRef} />
      <button onClick={focus}>獲取input焦點</button>
    </div>
  )
}

複製程式碼

useRef使用場景 juejin.im/post/5d27cd…

useContext

  • 注意:在function元件中,必須必須使用useContext才能拿到context例項中的值,而不能像在class元件中那樣直接獲取
useContext
- 注意:在function元件中,必須必須使用useContext才能拿到context例項中的值,而不能像在class元件中那樣直接獲取

function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;
interface Context<T> {
  Provider: Provider<T>;
  Consumer: Consumer<T>;
  displayName?: string;
}


(1)
const value = useContext(MyContext);
- 引數
  - 一個context物件,(React.createContext的返回值)
- 返回值
  - context的當前值
  - 注意:context的值由( 上層元件 )中距離最近的 <MyContext.Provider> 的 value prop 決定
- 原理:
  - 當上層最近的<MyContext.Provider>更新時,該hoos會觸發 `重新渲染`,並使用最新的 value 值
- 場景
  - 避免資料的層層傳遞
- 本質
  - useContext實際上是class中的 <context例項.Consumer> 或者 static contextType = context例項  的語法糖
  - useContext只是讓你能夠讀取context的值,以及訂閱context的變化



---------------
例子:

import React, {useContext, useState} from 'react';
const TestContext = React.createContext('100') // React.createContext('100')建立一個context例項,初始值為‘100’
// 父元件
function UseContext() {
  const [fontWeight, setFontWight] = useState('400')
  return (
    <div style={{background: '#1E90FF'}}>
      <div>USE_CONTEXT</div>
      <div onClick={() => setFontWight('700')}>點選改變context</div>
      <TestContext.Provider value={fontWeight}> // context例項提供Provider元件,元件提供value值,當value變化時,元件會重新渲染
        <ContextChildOne />
      </TestContext.Provider>
    </div>
  )
}
// 子元件
function ContextChildOne() {
  return (
    <div>
      <div>ContextChildOne</div>
      <ContextChildTwo />
    </div>
  )
}
// 孫子元件
function ContextChildTwo() {
  const fontWeightTwo = useContext(TestContext) // 使用useContext
  return (
    <div>
      <div style={{fontWeight: fontWeightTwo}}>ContextChildTwo</div>
    </div>
  )
}

export default UseContext;
複製程式碼

class元件中如何使用context:react.docschina.org/docs/contex… 官網useContext react.docschina.org/docs/hooks-…

Context

  • React.createContext
  • Class.contextType
  • Context.Provider
  • Context.Consumer
  • Context.displayName
Context
- context提供了一個無需為每層元件手動新增props,就能在元件樹之間進行資料傳遞的方法
- 注意:如果元件的屬性需要層層傳遞,而且只有最後一個元件需要使用到傳遞的屬性,可以使用控制反轉
  - 控制反轉:直接傳遞整個在最後需要使用的元件,減少props的個數和傳遞的層數
- api


1. React.createContext
  - 建立一個context物件,注意是物件
  - 當react渲染一個( 訂閱 )了這個( context物件 )的元件時,這個元件會從離自身最近的(Provider )中讀取到當前的context
  - 只有訂閱了這個context物件的元件所在的元件樹中沒有匹配到 Provider 時,defaultValue才會生效。
  - 注意:將undefined傳遞給provider的value,消費元件的defaultValue不會生效


2. Context.Provider
- <Context.Provider value={...}>
- 每個Context物件都有一個Provider物件元件,它允許消費元件`訂閱context的變化`
- Provider接收一個value屬性,傳遞給消費元件
- 一個Provider可以和多個消費元件有對應關係
- 多個Provider也可以巢狀使用,裡層會覆蓋外層的資料
- 注意:當Provider的value值發生變化時,它內部的所有消費元件都會從新渲染。!!!!!!!!!!!!!!!!!
- Provider 及其內部 consumer 元件都不受制於 shouldComponentUpdate 函式,因此當 consumer 元件在其祖先元件退出更新的情況下也能更新。


--------------------
3. Class.contextType     
- `提供給this.context來消費`
- MyClass.contextType = MyContext; // MyContext是context例項物件,MyClass是元件,contextType元件的靜態屬性
- contextType屬性也可以寫在元件內部,static ContextType = MyContext
- 說明:
- 掛載在 class 上的 `contextType` 屬性會被重賦值為一個由 React.createContext() 建立的 Context 物件。
- 這能讓你使用 this.context 來消費最近 Context 上的那個值。你可以在任何生命週期中訪問到它,包括 render 函式中。
 - 如果你正在使用實驗性的 public class fields 語法,你可以使用 `static` 這個類屬性來初始化你的 `contextType`。
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基於這個值進行渲染工作 */
  }
}




--------------------
4. Context.Consumer
- React 元件也可以訂閱到 context 變更。這能讓你在 函式式元件 中完成訂閱 context
- 程式碼案例:

const GlobalThem = React.createContext({color: 'red'}) // 建立context物件,初始值引數只是在消費元件沒有找到Provider時生效
function App2() {
  return (
    <div>
      <GlobalThem.Provider value={{color: 'blue'}}> 
        // Provider提供給消費元件訂閱context,把value傳遞給消費元件,並在value變化時重新渲染
        <Child />
      </GlobalThem.Provider>
    </div>
  )
}
function Child() {
  return (
    <div>
      <Grandson />
    </div>
  )
}
function Grandson() {
  return (
    <GlobalThem.Consumer> 
      // Consumer主要用於函式式元件,在class元件中可以使用Class.contextType包裝,用this.context獲取
      // 消費元件訂閱context,下面的value引數就是context的值
      {
        value => (
          <div> // 返回一個元件
            {value.color}
          </div>
        )
      }
    </GlobalThem.Consumer>
  )


--------------------
5. Context.displayName
- context 物件接受一個名為 displayName 的 property,型別為字串。
- React DevTools 使用該字串來確定 context 要顯示的內容。

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
複製程式碼

React.memo

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

- React.memo()為高階元件,與React.pureComponent()相似,但是memo只作用於函式元件,不適用於class元件
- React.memo()在props相同的情況下,並不會重新渲染
  - 即 React 將跳過渲染元件的操作並直接複用最近一次渲染的結果
  - 即 ( 函式記憶 ),快取結果值
複製程式碼

useReducer

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
- 引數
  - reducer
    - (state, action) => newState,接受一個state和action,返回一個新的state
  - initialArg
    - reducer中的state的初始值,大多數情況下是一個物件作為合理的資料型別
- 返回值
  - 返回當前的 state 和 dispatch 函式
- 使用場景
  - 當元件中state比較多時,useReducer可以聚合各個state
  - 當各個層級元件中的資料需要頻繁的共享時

--------------
例項:
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState); // useReducer
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button> // 點選觸發dispatch函式,派發一個action
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
複製程式碼

如何封裝一個useFetch

useFetch


(1) 版本1
export function useFetch(fetchFn, fnParams={}) {
  const [data, setData] = useState([])
  const [params, setParams] = useState(fnParams)
  const [isError, setIsError] = useState(false)
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    const fetchData = async() => {
      setIsLoading(true)
      setIsError(false)
      try {
        const res = await fetchFn(params)
        setData(res.data)
      } catch(err) {
        setIsError(true)
      }
      setIsLoading(false)
    }

    fetchData()
  }, [params]) // fetchData依賴params的更新,params改變則重新執行fetchData

  const doFetch = (params) => { // 對外暴露更新params的函式
    setParams(params) // params改變觸發useEffect更新
  }

  return {data, isError, isLoading, doFetch}
}




------------------------------------------------------------------------------------
(2)版本2
- 版本1中各個state都是完成同一個副作用,可以將他們聚合在一個state中
- 使用 useReducer 優化

(use-fetch.js)
import {useState, useEffect, useReducer} from 'react';
import {FetchReducer, fetchType} from '../../store/reducers/fetch-reducer';
export function useFetch(fetchFn, fnParams={}) {
  const [params, setParams] = useState(fnParams)
  const [state, dispatch] = useReducer(FetchReducer, {
    data: [],
    isError: false,
    isLoading: false
  })
  useEffect(() => {
    const fetchData = async() => {
      dispatch({type: fetchType.FETCH_INIT}) // 開始,dispatch => action
      try {
        const res = await fetchFn(params)
        dispatch({type: fetchType.FETCH_SUCCESS, payload: res.data}) // 成功,資料payload
      } catch(err) {
        dispatch({type: fetchType.FETCH_FAIL}) // 失敗
      }
    }
    fetchData()
  }, [params])
  const doFetch = (params) => {
    setParams(params)
  }
  return {doFetch, ...state} // 展開state,暴露給外部
}

(fetch-reducer.js)
export const fetchType = {
  FETCH_INIT: 'FETCH_INIT',
  FETCH_SUCCESS: 'FETCH_SUCCESS',
  FETCH_FAIL: 'FETCH_FAIL'
}

export const FetchReducer = (state, action) => {
  switch(action.type) {
    case fetchType.FETCH_INIT:
      return {
        ...state,
        isError: false,
        isLoading: true
      }
    case fetchType.FETCH_SUCCESS:
      return {
        ...state,
        isError: false,
        isLoading: false,
        data: action.payload
      }
    case fetchType.FETCH_SUCCESS:
      return {
        ...state,
        isError: true,
        isLoading: false
      }
    default:
      return {
        ...state
      }
  }
}

複製程式碼

hooks中父元件如何獲取子元件中的方法

  • useImperativeHandle
  • forwardRef
  • imperative:勢在必行的,急切,急迫
hooks中父元件如何獲取子元件中的方法
- useImperativeHandle
- forwardRef


(1) useImperativeHandle
- useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父元件的例項值。
- imperative:勢在必行的,急切,急迫
- `注意:useImperativeHandle需要和forwardRef一起使用`

(2) forwardRef
- `React.forwardRef` 會建立一個React元件,這個元件能夠將其接受的 ref 屬性轉發到其元件樹下的另一個元件中。
- 即傳遞轉發 ref 屬性給子元件,`這樣父元件就能直接繫結子元件的子元件或者孫子元件`
- 引數:一個渲染元件
- 渲染元件的引數:props和ref



import React, {useRef, useImperativeHandle, forwardRef} from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

// 父元件
const App2 = () => {
  const childRef = useRef(null)
  const getChildFn = () => {
    childRef.current.go() // 在current中直接獲取子元件暴露出來的方法
  }
  return (
    <>
      <div>父元件</div>
      <div onClick={getChildFn}>點選獲取子元件中的方法</div>
      <Child ref={childRef}></Child> // 通過ref繫結子元件
    </>
  )
}


// 子元件
const Child =  forwardRef((props, ref) => { // 用forwardRef包裝的元件,接收props,和ref引數
  useImperativeHandle(ref, () => ({ // 暴露給父元件的方法go
    go: () => {
      console.log('用forwardRef高階元件包裝後,元件能接收props和ref');
      console.log('useImperativeHandle的回撥中返回的物件中的方法提供給父元件使用');
    }
  }))
  return (
    <div>
      子元件
    </div>
  )
})
複製程式碼

zh-hans.reactjs.org/docs/react-…

複習:

js中的註釋規範

@version 16.8.0
@see https://reactjs.org/docs/hooks-reference.html#usecontext


函式相關
@param  {string}  address  地址
@param {number} [num] 中括號表示引數可選
@returns  void
@desc  描述
@deprecated  指示一個函式已經廢棄,而且在將來的程式碼版本中將徹底刪除。要避免使用這段程式碼


@date     2014-04-12
@author   QETHAN<qinbinyang@zuijiao.net>
@copyright程式碼的版權資訊
@overview對當前程式碼檔案的描述。
複製程式碼

www.jsphp.net/js/show-9-2… segmentfault.com/a/119000000…

複習

手寫call

1. 原生的call
- Function.prototype.call()
- 引數:
  - 第一個引數:this將要繫結的物件,當第一個引數是空,null,undefined,則預設傳入全域性物件
  - 後面的引數:傳入函式的引數
- 主要的作用:
  - 繫結函式中 this 所指定的物件
  - 執行函式(把繫結this的物件以外的引數,傳入使用的函式)
  - 注意:fn是可以有返回值的
- 注意點:
  - eval(string)將字串當作語句來執行,引數是一個字串


- 實現思路:
- 在物件上新增一個函式
- 在物件上執行這個函式,此時函式中的this指向的就是這個物件(this執行時才能確定,執行時所在的物件)
- 執行完後,刪除這個函式屬性


簡單版:
useEffect(() => {
  const obj = {
    name: 'wu',
    age: 20
  };
  function fn (address) {
    return this.name + this.age + address
  }
  Function.prototype._call = function(obj) {
    if (typeof this !== 'function') { // _call方法必須在函式物件上呼叫
      throw new Error('the _call method must called with a function object')
    }
    const context = obj ? obj : window; // 如果引數是null,undefined,或者為空,則會預設傳入全域性物件,比如window
    context.fn = this // 因為下面呼叫時,this指向了fn
    const res = context.fn(...Array.from(arguments).slice(1)) // 除去第一個引數,執行函式,如果有返回值,需要返回
    delete context.fn // 執行後,刪除
    return res
  }
  const res = fn._call(obj, 'hangzhou')
  console.log(res, 'res')
}, [])
複製程式碼

手寫 apply

apply
- 和call方法大致相同,繫結this,除了繫結的物件之外的引數傳入函式,並執行函式
- 和call的區別:傳入函式的引數是一個陣列,所以理論上call的效能比apply好

useEffect(() => {
  const obj = {name: 'wang', age: 20}
  function fn (name, age) {
    return {
      name: name || this.name,
      age: age || this.age
    }
  }
  Function.prototype._apply = function(obj) {
   if (typeof this !== 'function') { // _call方法必須在函式物件上呼叫
      throw new Error('the _apply method must called with a function object')
    }
    const context = obj ? obj : window;
    context.fn = this
    const res = context.fn(...[...arguments].slice(1).flat(Infinity))
    // 陣列例項.flat(Infinity)表示展開多層陣列,最後只剩一層
    // 注意:Infinity首字母要大寫
    delete context.fn
    return res
  }
  const res = fn._apply(obj, 'zhang')
  console.log(res, 'res')
}, [])
複製程式碼

手寫bind

bind
- 將函式體內的this繫結到某個物件上,並返回一個新的函式
- 引數:
  - 第一個引數:方法所要繫結的物件
  - 後面的引數:將會作為引數傳遞給函式
  - 注意:返回的函式仍然可以傳參
    - 如果bind方法傳遞兩個引數(傳給函式的就是一個),那麼返回的函式的第一個引數,將作為第二個引數傳遞給函式
- bind函式的特點:
  - 繫結this
  - 返回新函式
  - 可以傳參(bind函式傳參,返回的新函式也可以傳參)
  - 返回的是新函式,還可以通過 new 命令呼叫




- 程式碼實現:

(1) 簡單版
useEffect(() => {
  // bind模擬實現
  const obj = {
    name: 'wang',
    age: 20
  };
  function fn(address, sex) {
    return this.name + this.age + address + sex;
  }
  Function.prototype._bind = function(obj) {
    const context = this; // 這裡的this指向 fn 函式(this在呼叫時確定指向,_bind是供fn呼叫)
    const bindParams = Array.prototype.slice.call(arguments, 1); 
    // 將類陣列物件轉成真是的陣列,call除了繫結this,還可以向fn傳參
    // _bind函式的引數收集
    return function() { 
      // _bind返回一個新函式
      const resParams = Array.prototype.slice(arguments); 
      // 返回的函式接收的引數收集
      return context.apply(obj, bindParams.concat(bindParams)) // 拼接_bind和返回函式的引數,因為是陣列,用apply
    }
  }
  const newFn = fn.bind(obj, 'chongqing')
  const res = newFn('man');
  console.log(res, 'res')
}, [])





(2) 完善版 
_bind返回的引數,考慮new命令呼叫的情況
- 特點:
- 當bind返回的新函式,使用new操作符呼叫時,bind所指定的this物件會失效,因為建構函式中的this指向的是例項物件
- 雖然this指向失效,但是引數仍然是有效的
useEffect(() => {
  const obj = {
    name: 'wang',
    age: 20
  }
  function fn(address, sex) {
    console.log(this.name, 'this.name')
    return this.name + this.age + address + sex
  }
  Function.prototype._bind = function(obj) {
    // _bind方法只能在函式物件上呼叫
    if (typeof this !== 'function') {
      throw new Error('bind must be called on the funciton object')
    }
    // 寄生繼承,防止例項和原型上重複的屬性和方法,和修改 prototype
    // 原理:用中間函式來中轉,
      // closure.prototype => ParasiticFn例項
      // ParasiticFn.prototype => fn.prototype
        // 這樣 closure的例項可以訪問closure函式中的屬性,訪問ParasiticFn.protype上的屬性
        // 而ParasiticFn本身沒有任何屬性和方法
    // parasitic:寄生
    const ParasiticFn = function(){};
    ParasiticFn.prototype = this.prototype
    closure.prototype = new ParasiticFn()

    const bindParams = Array.prototype.slice.call(arguments, 1)
    const context = this // fn
    function closure() {
      const closureParams = Array.prototype.slice.call(arguments)
      return context.apply(this instanceof ParasiticFn ? this : obj, bindParams.concat(closureParams)) // 組合引數,並呼叫,返回返回值
      // 注意:
      // 1. closure() 函式中的this,根據呼叫方式的不同,this的指向會不同, (注意這裡的this和closure函式外的this指向也不同)
      // 2. 通過new命令呼叫時,closure被當作建構函式,this指向例項物件
      // 3. 通過普通函式呼叫時候,this指向的是傳入的需要繫結的物件
      // 4. this instanceof ParasiticFn ? this : obj的意思是:通過new呼叫,繫結例項物件,其他情況繫結obj物件
      // 5. // 通過什麼方式呼叫新函式,就繫結什麼物件
    }

    return closure
  }
  const newFn = fn._bind(obj, 'chongqign')

  const resOrdinary = newFn('man')
  console.log(resOrdinary, 'resOrdinary')
  // wang this.name
  // wang20chongqignman resOrdinary

  const resConstructor = new newFn('woman')
  console.log(resConstructor, 'resConstructor')
  // undefined "this.name"
  // {}
})
複製程式碼

github.com/mqyqingfeng…

邏輯運算子

&&

&&
運算規則:
- 如果第一個運運算元的布林值是true  => 返回第二個運運算元的值 
- 如果第一個運運算元的布林值是false => 返回第一個運運算元的值,且不對第二個運運算元求值
短路:
- 跳過第二個運運算元的運算叫做短路
多個連用:
- 返回第一個為false的表示式的值


總結:
- 執行到哪裡就返回當前的值
- 判斷的是布林值,返回的是值
false && 2 => false // 執行到false就不執行了,返回布林值是false的表示式的值 false
'' && 2    => '' // 執行到第一個表達是布林值是false,不再執行,返回當前表示式的值 ''
true && 1 && [] && 0 && {} => 0 // 執行到 0 時,布林值時false,不再執行,返回當前的值 0
複製程式碼

&&

&&
運算規則
- 如果第一個運運算元的布林值是true,則返回第一個表示式的值,且不再對後面的運運算元求值
- 如果第一個運運算元的布林值是false,則返回第二個運運算元的值(如果只有兩個運運算元時)
複製程式碼

相關文章