React Hooks 实践

118 阅读3分钟

场景一 : hook的挂载和卸载顺序

常用的两种情况

情况一 :

// 父组件
const App = () => {
  const [count,setCount]= useState<number>(0)
  useEffect(() => {
    console.log(" ===== root")
    return () => {
      console.log(" ===== unroot")
    }
  })
  return (
  <div>
    <p>{count}</p>
    <Button onClick={() => {
      setCount(() => {
        return count + 1
      })
    }}>
      +
    </Button>
    <Child count={count}></Child>
  </div>)
}

// 子组件
const Child: React.FC<IProps> = (props) => {
  useEffect(() => {
    console.log(" ===== child1")
    return () => {
      console.log(" ====== unchild1")
    }
  })
  return (
    <div>
      Child: {props.count}
      <ChildButton></ChildButton>
    </div>
  )
}
// 子组件 Button
const ChildButton = () => {
  useEffect(() => {
    console.log(" ====== child1-button");
    return () => {
      console.log(" ====== un child1-button");
    }
  })
  return <div>Button</div>
}
export default App

挂载

微信图片_20230628113850.png

卸载

微信图片_20230628114359.png

情况二 :

const App = () => {
  const [count,setCount]= useState<number>(0)
  useEffect(() => {
    console.log(" ====== root")
    return () => {
      console.log(" ====== unroot")
    }
  })
  return (
  <div>
    <p>{count}</p>
    <Button onClick={() => {
      setCount(() => {
        return count + 1
      })
    }}>
      +
    </Button>
    {count>=2 && count<=3 ? <Child2 count={count}></Child2>: <span>占位</span>}
  </div>)
}

// 子组件
const Child2: React.FC<IProps> = (props) => {
  useEffect(() => {
    console.log(" ===== child2")
    return () => {
      console.log(" ===== unchild2")
    }
  })
  return (
    <div>
      Child: {props.count}
      <Child2Button></Child2Button>
    </div>
  )
}
// 子组件 Button
const Child2Button = () => {
  useEffect(() => {
    console.log(" ===== child2-button");
    return () => {
      console.log(" ===== un child2-button");
    }
  })
  return <div>Button</div>
}
export default App

挂载

微信图片_20230628142116.png

卸载

**总结: **

1 - 组件挂载时先执行子组件的hook, 再执行父组件的hook

2 - 组件卸载时分两种情况:

- 仅发生渲染时,先执行子组件的hook,再执行父组件的hook

- 直接卸载时,先执行父组件的hook,再执行子组件的hook

3 - 组件会先标记要卸载的组件,先执行要卸载的组件 hook(unmount),再执行仅渲染的组件 hook (unmount)

场景二 : useMemo useCallback优化

普通场景优化 :

const Child: React.FC<IProps> = ({count}) => {
  const value = useMemo(() => {
    console.log("====== compute", count);
    return count + 1
  },[count])
  const clickMe = useCallback(() => {
    console.log("====== click me", count);
  },[count])
  return (
    <div>
      计算的值: {value}
      <Button onClick={clickMe}></Button>
    </div>
  )
}

useCallback二次优化 :

const Child: React.FC<IProps> = ({count}) => {
  const value = useMemo(() => {
    console.log("====== compute", count);
    return count + 1
  },[count])
  const clickMe = useCallback((data:number) => {
    console.log("====== click me", data);
  },[])
  return (
    <div>
      计算的值: {value}
      <Button onClick={clickMe.bind(this,count)}></Button>
    </div>
  )
}

**总结: **

1 - useMemo: 只有在依赖项改变的时候重新计算值,避免组件每次渲染的时候重新计算

2 - useCallback: 只有在依赖项改变的时候返回最新的函数,节省内存空间,优化性能

场景三 : hooks 依赖项数组中每项是通过 Object.is 比较的

const App = () => {
  const [data, setData] = useState<{ [key: string]: number }[]>([]);
    const newData = useMemo(() => {
      console.log("======= memo", data);
      return JSON.stringify(data);
   }, [data])

    // 期望:数组中值发生改变时,才重新计算
    useEffect(() => {
      const keys = Object.keys(newData);
      console.log("========== root", keys);
    }, [newData])
    
    console.log("====== rerender...");
    return (
      <div>
        <p>{JSON.stringify(data, null, 2)}</p>
        <Button
          onClick={() => {
            console.log("===== click1", data);
            // 1. 引用没改变,值改变,组件没有发生重新渲染
            setData(() => {
              data.push({ a: 1 });
              return data
            })
          }}
        >
          +
        </Button>
        <Button
          onClick={() => {
            console.log("===== click2", data);
            // 2. 引用改变,值没改变,做了无用的渲染
            setData([...data])
          }}
        >
          -
        </Button>
      </div>
    );
}

**总结: **

1 - useState 前后两次的值是通过Object.is 去比较的

2 - useMemo useCallback 依赖项中的值对比策略也是 Object.is

场景四 : 有多个state 需要调用多个setState 可以用useReducer优化

const App = () => {
  const [title,setTitle] = useState('')
  const [text,setText] = useState('')
  const [open,setOpen] = useState(false)
  const handleChange = (value:number) => {
    switch (value) {
      case 11:
        setTitle('新闻联播')
        setText("《新闻联播》是中国中央电视台每日晚间播出的一档新闻节目,被称为“中国政坛的风向标”,节目宗旨为“宣传党和政府的声音,传播天下大事")
        setOpen(true)
        break;
      case 4:
        setTitle('共同关注')
        setText("《共同关注》是CCTV-13新闻频道一档以公益慈善为品牌特色的日播专题栏目")
        setOpen(true)
        break;
      default:
        setTitle('')
        setText('')
        setOpen(false)
        break;
    }
  }
    return (
    <div>
      <Radio.Group onChange={(e) => handleChange(e.target.value)} defaultValue={0}>
      <Radio value={0}>关闭</Radio>
      <Radio value={11}>CCTV11</Radio>
      <Radio value={4}>CCTV4</Radio>
      </Radio.Group>
      <h2>电源:{open?'开':'关'}</h2>
      <h3>{title}</h3>
      <h4>{text}</h4>
    </div>
  )
}
export default App 

优化后

import { useReducer } from "react"
import {Radio} from 'antd'
const App = () => {
  interface IData{
    title:string,
    text:string,
    open:boolean
  }
  type Action = {
    type:0
  } | {
    type:11
  } | {
    type:4
  }
  const reducer = (state:IData,action:Action) => {
    switch (action.type) {
          case 11:
           return {
            title:'新闻联播',
            text:'"《新闻联播》是中国中央电视台每日晚间播出的一档新闻节目,被称为“中国政坛的风向标”,节目宗旨为“宣传党和政府的声音,传播天下大事"',
            open:true
            }
          case 4:
           return {
            title:'共同关注',
            text:"《共同关注》是CCTV-13新闻频道一档以公益慈善为品牌特色的日播专题栏目",
            open:true
           }
          default:
           return {
            title:'',
            text:'',
            open:false
           }
        }
  }
  const [data,dispatch] = useReducer(reducer,{
    title:'',
    text:'',
    open:false
  })
    return (
    <div>
      <Radio.Group onChange={(e) => dispatch({type:e.target.value})}  defaultValue={0}>
      <Radio value={0}>关闭</Radio>
      <Radio value={11}>CCTV11</Radio>
      <Radio value={4}>CCTV4</Radio>
      </Radio.Group>
      <h2>电源:{data.open?'开':'关'}</h2>
      <h3>{data.title}</h3>
      <h4>{data.text}</h4>
    </div>
  )
}
export default App 

**总结: **

1 - useReducer 可以合并多个 useState 有两点好处:

1. 使代码更优雅,好比如设计模式中的策略模式

2. 减少组件多次的重复渲染