1.自变量(src):useState、useReducer、useContext
页面state很简单,可以直接使用useState
页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
2.因变量(e=mc):useMemo、useEffect、useCallback
3.第三方变量或者dom定位:useRef
1.1useState自定义变量,设置变量和修改变量的方法,初始值。
1.2如果说可以用 Context 来替代 useState() 的话,Reducer 可以替代事件方法,如果单独写三个方法会显得冗余,这时可以使用 Reducer 的 dispatch 机制。
useReducer是进阶版的useState,把多个useState合并为一个,redux中使用
例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
// reducer 计数器
const initialState = {count: 0};
const 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);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
1.3useContext跨级组件中使用,类似vue provide&inject
不论中间隔着几层,createContext(0) ->context.Provider ->useContext。
import React, { useState ,useContext, createContext} from 'react';
// 1.创建一个 context
const Context = createContext(0)
function App () {
const [ count, setCount ] = useState(0)
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
// 2.使用CountContext.Provider包裹需要接收参数的子组件,并通过value传值
<Context.Provider value={count}>
{/* <Item1></Item1>
<Item2></Item2> */}
<Item3></Item3>
</Context.Provider>
</div>
)
}
// 组件一, useContext 写法
function Item3 () {
// 3.创建子组件Counter,通过useContext把刚刚创建好的CountContext作为参数传进去,并读取count值
const count = useContext(Context);
return (
<div>{ count }</div>
)
}
export default App;
1.4context+consumer模拟redux
1.4.1.创建一个 context 对象 (React.createContext),
1.4.2.定义useReducer方法const [state, dispatch] = useReducer(loginReducer, initialData);
1.4.3.context.Provider订阅了这个 Context 对象的组件,value绑定reducer的dispatch方法,包裹下面子组件
1.4.3.需要消费的子组件用useContext接受dispatch方法,再调用
2.1useMemo和useCallback定义无副作用的因变量,useEffect是有副作用的因变量
useMemo 的函数会在渲染期间执行,所以可以使用useMemo解决渲染DOM期间会执行不相关函数的需求
2.2useMemo返回变量,useCallback返回函数
类组件,可以继承PureComponent
React.memo是一个高阶组件,useMemo是一个hook,共同点:它们都可以用来缓存数据,避免子组件的无效重复渲染。
import React, {useMemo, useRef} from "react";
function ReactMemoChild() {
const ref = useRef(0);
return (
<>
<p>页面渲染次数:{ref.current++}</p>
</>
);
}
export default React.memo(ReactMemoChild);
useMemo常用在以下两种场景的优化中:1)引用类型的变量 2)需要大量时间执行的计算函数。
const UseMemoDemo = () => {
// 调用这个函数需要大量时间去计算
const slowFunction = (number) => {
console.log('calling slow function')
for (let i = 0; i <= 1000000000; i++) {
}
return number * 2
}
const [inputNumber, setInputNumber] = useState(1)
const [dark, setDark] = useState(true)
// 场景1:执行某函数需要大量时间,使用useMemo来优化,在不必要执行函数的时候不执行函数
const doubleNumber = useMemo(() => {
return slowFunction(inputNumber)
}, [inputNumber])
// 场景2:每次组件更新会重新执行,内部的引用类型变量会重新创建,这会导致使用到引用类型变量的组件重新渲染,使用useMemo来让每次的变量相同
const themeStyle = useMemo(() => {
return {
background: dark ? 'black' : 'white',
color: dark ? 'white' : 'black'
}
}, [dark])
useEffect(() => {
console.log('themeStyle changed')
}, [themeStyle])
const handleChange = (e) => {
setInputNumber(parseInt(e.target.value))
}
return (
<>
<input type='text' value={inputNumber} onChange={handleChange}/>
<button onClick={() => {
setDark(prevDark => !prevDark)
}}>change theme
</button>
<p style={themeStyle}>{doubleNumber}</p>
</>
)
}
export default UseMemoDemo;
2.3useEffect、useMemo、useCallback中很多相似之处,如第二个参数的依赖属性一样。
都是闭包。需要释放内存
2.4 useEffect
useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
第二个参数3种情况:
a.不设置,任何改变都会触发第一个参数;
b.[],第一次空的时候执行,后面不执行。相当于componnetDidMount生命周期;
c.[参数名字],首次执行+数据变化时执行。相当于vue的watch,并且immediate为true
d.componentWillUnmount
const timer = setInterval(() => { setCount(count + 1) }, 1000) // useEffect方法的第一个参数是一个函数,函数可以return一个方法,这个方法就是在组件销毁的时候会被调用 useEffect(() => { return () => { clearInterval(timer) } }, [])
- 使用useEffect时,若有多个副作用,则应该调用多个useEffect,而不是写在一个里面;
比如请求数据,操作dom,修改标题都用useEffect - useEffect第一个参数可以返回一个函数,这个函数会在组件卸载时(也就是render了,生成新的快照时)执行,可以用来清除副作用里的操作;
- useLayoutEffect是在render前同步执行的(和componentDidMount等价),useEffect是在render后异步执行的;
useLayoutEffect相当于防抖,一般在useEffect执行有问题(多次执行),再用这个api
useLayoutEffect与componentDidMount,componentDidUpdate的调用阶段是一样的;
import React, { useEffect, useLayoutEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [state, setState] = useState("hello world")
useEffect(() => {
let i = 0;
while(i <= 100000000) {
i++;
};
setState("world hello");
}, []);
// useLayoutEffect(() => {
// let i = 0;
// while(i <= 100000000) {
// i++;
// };
// setState("world hello");
// }, []);
return (
<>
<div>{state}</div>
</>
);
}
export default App;
总结:1.优先使用 useEffect,因为它是异步执行的,不会阻塞渲染
2.会影响到渲染的操作尽量放到 useLayoutEffect中去,避免出现闪烁问题
3.useLayoutEffect和componentDidMount是等价的,会同步调用,阻塞渲染
因为 useEffect 是渲染完之后异步执行的,所以会导致 hello world 先被渲染到了屏幕上,再变成 world hello,就会出现闪烁现象。而 useLayoutEffect 是渲染之前同步执行的,所以会等它执行完再渲染上去,就避免了闪烁现象。也就是说我们最好把操作 dom 的相关操作放到 useLayouteEffect 中去,避免导致闪烁。
- useEffect里面用定时器,会产生闭包陷阱。用useRef的ref.current存储变量
闭包陷阱产生的原因就是 useEffect 等 hook 里用到了某个 state,但是没有加到 deps 数组里,这样导致 state 变了却没有执行新传入的函数,依然引用的之前的 state。
闭包陷阱的解决也很简单,正确设置 deps 数组就可以了,这样每次用到的 state 变了就会执行新函数,引用新的 state。不过还要注意要清理下上次的定时器、事件监听器等。
import { useEffect, useLayoutEffect, useRef } from 'react';
const App = () => {
const [count,setCount] = useState(0);
const fn = () => {
//还可以做一些其他逻辑操作
console.log(count);
};
const ref = useRef(()=>{});
useEffect(() => {
setInterval(() => {
//最关键的一步,使用函数,接受一个旧的state,得到新的state
//所以就会render
setCount(count => count + 1);
}, 1000);
}, []);
//每次在render前都给ref赋值新的fn,这个fn里的state是最新值
useLayoutEffect(() => {
ref.current = fn;
});
useEffect(() => {
setInterval(() => ref.current(), 1000);
}, []);
return <div>count: {count}</div>;
}
export default App;
useEffect第二个参数如果是引用类型,发生变化监测不到,所以需要重写
const [a, setA] = useState({
b: 'dx',
c: '18',
})
const changeA = () => {
setA((old) => {
const newA = {...old}
newA .b = 'yx'
return newA
})
}
useEffect(() => {
/** 当组件挂载时执行一次changeA */
changeA ()
},[])
/**当changeA执行打印 {b:'yx',c:'18'} */
useEffect(() => {
/** 执行逻辑 */
console.log(a)
},[a])
useRef扩展
我们知道useRef返回的就是一个普通的JS对象{current:undefined},所以我们直接创建一个js对象,也可以代替useRef()。
区别:
我们创建的对象,组件每次重新渲染都会创建一个新对象。如useR
useRef()创建的对象,可以确保每次渲染获取到的都是同一个对象。
当你需要一个对象不会因为组件的重新渲染而改变时,就可以使用useRef()。
- useCallback返回函数,应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。
给子组件传函数时,父组件用useCallback(function(){},deps), 子组件用React.memo包裹
React.useCallback(function helloWorld(){}, []);
// …功能相当于:
React.useMemo(() => function helloWorld(){}, []);
const handleMegaBoost = React.useMemo(() => {
return function() {
setCount((currentValue) => currentValue + 1234);
}
}, []);
const handleMegaBoost = React.useCallback(() => {
setCount((currentValue) => currentValue + 1234);
}, []);
但是,根据闭包的理解,useCallback传递的是一个闭包函数,所以存在一定的风险。除非是计算量特别大的子组件这种极端情况,否则不推荐使用。
useEffect
触发两次引起的bug
在研究这个问题后得到了大概三种解决方案:
1.取消react.strictMode模式
2.在设置的参数的useEffect中加非空判断
3.把初始化放到useReducer里面
在这里插入图片描述
常见10个hooks
react生命周期
useId
import { useId } from ‘react’;
function PasswordField() {
const passwordHintId = useId(‘duan’);
}
无论有多少个PasswordField组件,ID都不一样
forwardRef()
useImperativeHandle
相当于vue defineExpose,暴露方法
import { forwardRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
useImperativeHandle(ref, () => {
return {
// ... your methods ...
};
}, []);
// ...
react v16.8常用库
redux: 著名JavaScript状态管理容器
redux-thunk: 处理异步逻辑的redux中间件
immutable: Facebook历时三年开发出的进行持久性数据结构处理的库
react-lazyload: react懒加载库
better-scroll: 提升移动端滑动体验的知名库
styled-components: 处理样式,体现css in js的前端工程化神器(详情请移步我之前的文章styled-components:前端组件拆分新思路)
axios: 用来请求后端api的数据