手把手帶你用react hook擼一遍class組件的特性

前言

react hook是繼16.6的Suspense、lazy、memo後的又一巨大的使人興奮的特性。而後有各類文章說了hook的優缺點,其中缺點包括:沒有直接替代getSnapshotBeforeUpdate、componentDidUpdate生命週期的hook、不能像class組件那樣寫this、函數太大。這只是表面的現象,只要稍微思考一下,hook實際上是無所不能的,我甚至相信將來挑不出hook的毛病來。今天手把手帶你們過一遍如何實現class組件特性。javascript

基本用法可見官網,閱讀本文須要先了解useStateuseEffectuseRefuseLayoutEffect的使用方法。本文核心hook——useRef,本文也算是一篇useRef的應用文章。當你知道核心是基於useRef的時候,或許已經想到實現辦法了,很好,咱們心有靈犀 「握個手」前端

useRef

useRef傳入一個參數initValue,並建立一個對象{ current: initValue }給函數組件使用,在整個生命週期中該對象保持不變。因此,聽到它名字叫作useRef的時候,很天然就想到它就是用來作元素的ref的:java

const divRef = useRef();
return  <div ref={divRef}>; 複製代碼

最基本的使用方法,接着想進行dom操做,那就這樣玩:react

if (divRef.current) {
  divRef.current.addEventListener(...);
}
複製代碼

函數組件的執行,整個函數體全部的必然躲不掉從新執行,那麼若是但願有一個不從新走一遍的變量,咱們一般會把它放函數組件外面去:數組

let isMount = false;
function C(){
  useEffect(() => { isMount= true; return () => { isMount= false; } }, []);
  return <div /> } 複製代碼

這就是一個判斷組件有沒有掛載到頁面的實現方法,若是咱們用useRef,顯然優雅不少了,並且是否是有點this的感受閉包

function C(){
  const mount = useRef({}).current;
  useEffect(() => { mount.isMount= true; return () => { mount.isMount= false; } }, []);
  return <div /> } 複製代碼

ok,如今假的this要原形畢露了:dom

export default () => {
  const _this = useRef({
    state: { a: 1, b: 0 },
  }).current;
	return (
		<div> a: {_this.state.a} / b : {_this.state.b} </div>
	)
}
複製代碼

state更新相關的邏輯實現

useState就至關於hook版本的setStateconst [state, setState] = useState(initState);,state利用了函數組件從新執行,從閉包讀取函數記憶的結果。調用hook的setState,則會更新state的值而後從新執行一遍整個函數組件。此處再次感嘆一下,hook真的沒什麼黑魔法,少一點套路多一點真誠。函數

好比有一個這樣子的組件:post

function T(){
  const [count, setCount] = useState(0);
  return <span onClick={() => setCount(count + 1)}>{count}</span>
}
複製代碼

第一次執行函數組件,最後渲染就至關於:學習

function T(){
  const count = 0
  return <span>{count}</span>
}
複製代碼

點擊一下,count+1,也就是至關於執行了一個這樣子的函數組件:

function T(){
  const count = 1
  return <span>{count}</span>
}
複製代碼

因此,真沒有什麼黑魔法,就是讀前一個值而後+1展現而已。好了,回到正題,函數組件的更新就是useState,那強制更新呢?如何實現一個forceUpdate?其實也很簡單,dispatcher(useState返回值第二個元素)傳入一個函數,相似於class組件的setState傳入一個函數同樣,能夠拿到當前的state值:

const useForceUpdate = () => {
  const forceUpdate = useState(0)[1];
  return () => forceUpdate(x => x + 1);
}

export default () => {
  const forceUpdate = useForceUpdate(); // 先定義好,後面想用就用
  // ...
	return (
		<div /> ) } 複製代碼

咱們已經知道了如何模擬this和state初始化了,那咱們能夠實現一個相似class組件的setState了:給ref裏面的屬性賦值,再forceUpdate。

本文只是但願所有收攏在useRef,而後修改狀態的方法純粹一點,固然能夠用useState對着一個個state值進行修改

export default () => {
  const forceUpdate = useForceUpdate();
  const _this = useRef({
    state: { a: 1, b: 0 },
    setState(f) {
      console.log(this.state)
      this.state = {
        ...this.state,
        ...(typeof f === 'function' ? f(this.state) : f) // 兩種方法都考慮一下
      };
      forceUpdate();
    },
    forceUpdate,
  }).current;
  return (
    <div> a: {_this.state.a} / b : {_this.state.b} <button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a傳state</button> <button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b傳函數</button> </div>
  );
}
複製代碼

到此,咱們已經實現了class組件的thissetStateforceUpdate

didmount、didupdate、willunmount的實現

其實我上一篇文章已經實現過,這裏再糅合到ref裏面從新實現一遍。仍是同樣的方法,基於兩個useEffect實現三個生命週期:

export default () => {
  const forceUpdate = useForceUpdate();
  const isMounted = useRef(); // 掛載標記
  const _this = useRef({
    state: { a: 1, b: 0 },
    setState(f) {
      console.log(this.state)
      this.state = {
        ...this.state,
        ...(typeof f === 'function' ? f(this.state) : f) // 兩種方法都考慮一下
      };
      forceUpdate();
    },
    forceUpdate,
    componentDidMount() {
      console.log('didmount')
    },
    componentDidUpdate() {
      console.warn('didupdate');
    },
    componentWillUnMount() {
      console.log('unmount')
    },
  }).current;
  useEffect(() => {
    _this.componentDidMount();
    return _this.componentWillUnMount;
  }, [_this]);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      _this.componentDidUpdate();
    }
  })
  return (
    <div> a: {_this.state.a} / b : {_this.state.b} <button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a傳state</button> <button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b傳函數</button> </div>
  )
}
複製代碼

記錄上一次狀態

有人可能也注意到了,上面的componentDidUpdate是沒有傳入上一次props和state的。是的,getDerivedStateFromProps也要上一個state的。因此咱們還須要一個ref存上一個狀態:

export default (props) => {
  const forceUpdate = useForceUpdate();
  const isMounted = useRef();
  const magic = useRef({ prevProps: props, prevState: {}, snapshot: null }).current;
  magic.currentProps = props; // 先把當前父組件傳入的props記錄一下
  const _this = useRef({
    state: { a: 1, b: 0 },
    setState(f) {
      console.log(this.state)
      this.state = {
        ...this.state,
        ...(typeof f === 'function' ? f(this.state) : f)
      };
      forceUpdate();
    },
    componentDidMount() {
      console.log('didmount')
    },
    getDerivedStateFromProps(newProps, currentState) {
        // 先放這裏,反正等下要實現的
    },
    componentDidUpdate(prevProps, prevState, snapshot) {
      console.warn('didupdate');
      console.table([
        { k: '上一個props', v: JSON.stringify(prevProps) },
        { k: 'this.props', v: JSON.stringify(magic.currentProps) },
        { k: '上一個state', v: JSON.stringify(prevState) },
        { k: 'this.state', v: JSON.stringify(_this.state) },
      ])
    },
    componentWillUnMount() {
      console.log('unmount')
    }
  }).current;

  useEffect(() => {
    _this.componentDidMount();
    // 後面都是賦值操做,防止同一個引用對象,實際上應該深拷貝的。這裏爲了方便,但至少要淺拷
    magic.prevProps = { ...props };  // 記錄當前的,做爲上一個props給下一次用
    magic.prevState = { ..._this.state }; // 同理
    return _this.componentWillUnMount;
  }, [_this, magic]);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
    // 這裏就拿到了上一個props、state了,snapshot也先留個空位給他吧
      _this.componentDidUpdate(magic.prevProps, magic.prevState, magic.snapshot || null);
    // 拿完就繼續重複操做,給下一次用
      magic.prevProps = { ...props };
      magic.prevState = { ..._this.state };
    }
  })
    return (
      <div> props: {props.p}/ a: {_this.state.a} / b : {_this.state.b} <button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a傳state</button> <button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b傳函數</button> </div>
    );
}
複製代碼

這下,能夠去控制檯作一些操做state和改變props的操做了,並看下打印的結果

getDerivedStateFromProps

這個函數的原意就是但願props能夠做爲初始化state或者在渲染以前修改state,那麼根據它的意圖,很容易就能夠實現這個生命週期,我這裏getDerivedStateFromProps還能夠用假this哦。其實這個生命週期應該是最容易實現和想出來的了:

// 基於前面的組件直接加上這段代碼
  const newState = _this.getDerivedStateFromProps(props, magic.prevState);
  if (newState) {
    _this.state = { ..._this.state, ...newState }; // 這裏不要再更新組件了,直接改state就收了
  }
複製代碼

getSnapshotBeforeUpdate

到了一個hook不能直接替代的生命週期了。這裏再看一下useLayoutEffect和useEffect執行的時機對比:

注意到,下一個useLayoutEffect執行以前,先執行上一個useLayoutEffect的clean up函數,並且都是同步,能夠作到近似模擬willupdate或者getSnapshotBeforeUpdate了

// 再增長一段代碼
  useLayoutEffect(() => {
    return () => {
      // 上一個props、state也傳進來,而後magic.snapshot 前面已經傳入了componentDidUpdate
      magic.snapshot = _this.getSnapshotBeforeUpdate(magic.prevProps, magic.prevState);
    }
  })
複製代碼

componentDidCatch

另外一個不能用hook直接替代的生命週期,說到錯誤,這個生命週期也是捕捉函數render執行的時候的錯誤。那些編譯不過的,非函數渲染時候報的錯,它沒法捕獲的哦。基於這個前提,咱們仍是基於try-catch大法實現一波:

// 對最後的return 修改,這裏還能夠個性化一下fallback ui呢
  try {
    return (
      <div> props: {props.p}/ a: {_this.state.a} / b : {_this.state.b} <button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a傳state</button> <button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b傳函數</button> </div>
    )
  } catch (e) {
    _this.componentDidCatch(e)
    return <div>some err accured</div>;
  }
複製代碼

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索