对函数型组件进⾏增强, 让函数型组件可以存储状态, 可以拥有处理副作⽤的能⼒. 让开发者在不使⽤类组件的情况下, 实现相同的功能.
- 缺少逻辑复⽤机制
- 为了复⽤逻辑增加⽆实际渲染效果的组件(HOC或者render props),增加了组件层级,变得⼗分臃肿,嵌套地狱。
- 增加了调试的难度以及运⾏效率的降低
- 类组件经常会变得很复杂难以维护
- 将⼀组相⼲的业务逻辑拆分到了多个⽣命周期函数中
- 在⼀个⽣命周期函数内存在多个不相⼲的业务逻辑
- 类成员⽅法不能保证this指向的正确性
useReducer是另⼀种让函数组件保存状态的⽅式.
const INCREMENT = 'increment';
const reducer = (state, action) => {
switch(action.type) {
case INCREMENT:
return state + 1;
}
}
const [count, dispatch] = useReducer(reducer, 0);
const handleIncrement = () => dispatch({ type: INCREMENT });
在跨组件层级获取数据时简化获取数据的代码
import { createContext, useContext } from 'react';
const countContext = createContext();
const { Provider } = countContext;
const App = () => (
<Provider value={ 100 }>
<Foo />
</Provider>
);
const Foo = () => {
const value = useContext(countContext); // 100
// ...
}
让函数型组件拥有处理副作⽤的能⼒.类似⽣命周期函数,可以把 useEffect
看做 componentDidMount
, componentDidUpdate
和 componentWillUnmount
这三个函数的组合.
useEffect(() => {})
=>componentDidMount
,componentDidUpdate
useEffect(() => {}, [])
=>componentDidMount
useEffect(() => () => {})
=>componentWillUnMount
- 按照⽤途将代码进⾏分类 (将⼀组相⼲的业务逻辑归置到了同⼀个副作⽤函数中)
- 简化重复代码, 使组件内部代码更加清晰
// count变化的时候把网页标题也变化
useEffect(() => {
document.title = count;
}, [count]);
useEffect中的参数函数不能是异步函数, 因为useEffect函数要返回清理资源的函数, 如果是异步函数就变成了返回Promise。所以需要用个IIFE把异步函数包起来。
useEffect(() => {
(async () => {
await axios.get();
})();
});
useMemo 的⾏为类似Vue中的计算属性, 可以监测某个值的变化, 根据变化值计算新值。useMemo 会缓存计算结果. 如果监测值没有发⽣变化, 即使组件重新渲染, 也不会重新计算. 此⾏为可以有助于避免在每个渲染上进⾏昂贵的计算.
const res = useMemo(
() => expensiveCalc(count)
}, [count]); // 如果count变化此函数重新执行
memo还能用来性能优化, 如果本组件中的数据没有发⽣变化, 阻⽌组件更新. 类似类组件中的 PureComponent 和 shouldComponentUpdate
import React, { memo } from 'react';
const App = () => (<div></div>);
const MemoApp = memo(App);
缓存函数, 使组件重新渲染时得到相同的函数实例,以实现性能优化。
const App = () => {
const [count, setCount] = useState(0);
const resetCount = useCallback(() => setCount(0), [setCount]);
// <Test />组件不会频繁更新(假设Test是purecomponent),因为拿到的resetCount是经过缓存的, 是同一个实例
return (<Test resetCount={ resetCount }/>)
}
可以用来获取DOM元素对象
const App = () => {
const username = useRef();
const log = () => console.log(username); // { current: input }
return (<input ref={ username } onChange={ log }/>);
}
还可以用来跨组件周期保存数据。 即使组件重新渲染, 保存的数据仍然还在. 保存的数据被更改不会触发组件重新渲染.
// 这样是不行的,因为每次重新渲染,App() 重新被调用,timer又被设置成null了,就拿不到之前周期的那个timer。
const App = () => {
let timer = null;
useEffect(() => {
timer = setInterval(() => { ... }, 1000);
}, []);
const stopTimer = () => {
clearInterval(timer);
};
};
const App = () => {
let timer = useRef(); // 通过useRef跨周期保存timer的reference
useEffect(() => {
timer.current = setInterval(() => { ... }, 1000);
}, []);
const stopTimer = () => {
clearInterval(timer.current);
};
};
// useState相关
let state = [];
let setters = [];
let stateIdx = 0;
// useEffect相关
let prevDeps = [];
let effectIdx = 0;
function reRender () {
stateIdx = 0;
effectIdx = 0;
ReactDOM.render(<App />, document.getElementById('root'));
}
function createSetter (index) {
return newState => {
state[index] = newState;
reRender();
}
}
function useState (initialState) {
state[stateIdx] = state[stateIdx] ? state[stateIdx] : initialState;
setters[stateIdx] = setters[stateIdx] ? setters[stateIdx] : createSetter(stateIdx);
const value = state[stateIdx];
const setter = setters[stateIdx];
stateIdx++;
return [value, setter];
}
const isFunction = x => Object.prototype.toString.call(x) === '[object Function]';
const isArray = x => Object.prototype.toString.call(x) === '[object Array]';
function useEffect(cb, deps) {
if (!isFunction(cb)) throw new Error('useEffect函数的第一个参数必须是函数');
if (typeof deps === 'undefined') {
cb(); // 没有deps,每次都直接调用cb()
} else {
if (!isArray(deps)) throw new Error('useEffect函数的第二个参数必须是数组');
const prevDeps = prevDeps[effectIdx];
const hasChanged = (
!prevDeps || // 初次渲染
deps.some((dep, index) => dep !== prevDeps[index]) // deps变化
);
hasChanged && cb();
// 同步依赖值
prevDeps[effectIdx] = deps;
effectIdx++;
}
}
function useReducer (reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch (action) {
const newState = reducer(state, action);
setState(newState);
}
return [state, dispatch];
}
在hook出现之前,类组件的能力边界明显强于函数组件,因为它有自己的state和this,还自带了各种生命周期,所以可以处理很多状态相关的逻辑。函数组件更多的就是作为纯渲染组件来用。
但是,类组件这样又带来了一些劣势:
- 整个逻辑是在生命周期和this.state耦合在一起的,逻辑难以拆分,复用性很差。
- 整个类组件里面的繁杂的逻辑,导致学习成本的提高,开发的效率变低。
实际上,类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React 框架的设计理念,组件本身的定位就是函数,一个输入数据、输出 UI 的函数。UI = f(data)
。
为了能让开发者更好的的去编写函数式组件,产生了hook。这是一套能够使函数组件更强大、更灵活的“钩子”,能够钩入各种函数组件本来缺失的能力,比如state,比如生命周期。
所以说,功能上来讲,函数组件 + hook = 类组件,但这样的好处就是,可以自己选择钩入需要的逻辑,按需导入,而不是像类组件一样,还没有用很多逻辑,就已经导入了。这样让整个设计更加灵活,也增加了逻辑的可复用性。
总结,hook的好处:
- 函数组件更符合react设计思想
- 可以按需导入
- 逻辑的复用
- 减少逻辑的耦合性,更小颗粒度的逻辑
- 必须始终在React函数的顶层使用Hook
- 不要在循环、条件或嵌套函数中调用 Hook
- 在 React 的函数组件中调用 Hook
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。
共同点
useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。
不同点
useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
useEffect是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM后渲染),不会产生闪烁。useLayoutEffect总是比useEffect先执行。
setState的时候,必须要给一个新的对象,才能触发改变。这点跟redux很想,reducer必须要给一个新对象,不然redux就以为没有改变,因为是通过oldData === newData来对比的。
但是class的setState就没关系,因为在React.component里面, setState里面显式调用了render和diff。
let [num, setNums] = useState([0,1,2,3])
const test = () => {
num.push(1)
setNums(num) // num不会改变,如果用num = [...num ,1]就可以
}
- fiber本身就用链表链接sibling和child,这样可以实现深度优先的遍历
- 副作用链effect list:深度优先的遍历fiber树就是为了收集这条链,渲染commit阶段就通过遍历副作用链完成 DOM 更新
- hook链表:存储了按顺序执行的hook的信息
无状态组件中fiber对象memoizedState保存当前的hooks形成的链表。每个hook都是一个对象,也是链表中的一个node。
const fiber: Fiber = {
memoizedState: null, // 指向当前函数的hooks形成的链表
}
const hook: Hook = {
// useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null, // 指向下一个hook
};
mount的初始化阶段,在一个函数组件第一次渲染执行上下文过程中,每个react-hooks执行,都会产生一个hook对象,并形成链表结构,绑定在workInProgress的memoizedState属性上,然后react-hooks上的状态,绑定在当前hooks对象的memoizedState属性上。对于effect副作用钩子,会绑定在workInProgress.updateQueue上,等到commit阶段,dom树构建完成,在执行每个 effect 副作用钩子。
在组件初始化的时候,每一次hooks执行,都会调用mountWorkInProgressHook。
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) { // 第一个hook,创建一个hook链表
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else { // 把hook挂到下一个hook上
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
对于useXXX函数,都会调用mountXXX,结构大概是
function mountXXX(...args) {
const hook = mountWorkInProgressHook();
hook.memoizedState = ... // 保存了状态
hook.queue = ... // 保存了负责更新的信息
return ... // 如果有需要的话,就return,useEffect就没有return
}
对于useState
function mountState(initialState){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// 如果 useState 第一个参数为函数,执行函数得到state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 带更新的
dispatch: null, // 负责更新函数
lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
lastRenderedState: initialState, // 最后一次得到的 state
});
const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
// dispatchAction就是useState里面那个setter
// fiber和queue都被bind成当前的fiber,只用传入action
function dispatchAction(fiber, queue, action) {}
- hook的出现为了解决函数组件相对于类组件确实的那些功能,包括状态保存,还有生命周期。
- 类组件的缺点:
- 逻辑复用性不方便,耦合性强:需要HOC或者render props,这样会产生回调地狱
- 同一组相关联的逻辑散落在组件各个地方,太分散。hook可以把逻辑写成一组。有点类似vue的composite API,也有人把composite api叫vue hooks哈哈哈
- 整个react充满函数式编程的思想,UI = f(data),函数组件更贴合这种设计:输入prop(data),输出UI。类组件更像是面向对象的编程思想。vue 2里面就很不函数,耦合性特别强,总是喜欢什么东西都往vue实例的vm身上挂,vm像个木流牛马一样传来传去,包括webpack也是,在实例化的时候会把compiler传来穿去,往上面挂东西。
从功能上来讲,函数组件 + hook = 类组件。这样把一个大的,耦合性强的类组件,拆分成了更小颗粒度的逻辑,就像积木一样,可以按需钩入需要的逻辑,大大优化了开发者体验。