函数组件实质时一个render函数,只能传递props,无法维持state,也不具备生命周期,通过hooks可以实现对函数组件的增强。
通过HOC可以实现类组件状态逻辑复用,但HOC增加了组件嵌套层级;通过hooks可以实现函数组件状态逻辑复用且不改变组件结构。
在类组件中对于每一个生命周期的逻辑,会随着其复杂度提升而变得维护困难,容易产生bug;使用hooks则可以将组件中相互关联的部分拆分成更小的函数实现,而并非强制按照生命周期划分。
hooks使用注意事项:
- 仅在函数组件内部调用:外部调用不会被执行
- 仅在函数组件顶层调用:保证调用次数和次序相同
useState
useState使用示例:
function Counter() {
const [state, setState] = useState(0)
setState(state => state ++)
return (
<h1 onClick={() => setState(num => num ++)}>
Count: {state}
</h1>
)
}
在上面的示例中,使用 useState方法 获取 state值和更新state方法。
useState原理示例:
// 当前fiber
let wipFiber = null
// hook索引
let hookIndex = null
// 函数组件更新方法
function updateFunctionComponent(fiber) {
// 更新当前fiber
wipFiber = fiber
// 重置hook索引
hookIndex = 0
// 重置当前fiber的hooks数组
wipFiber.hooks = []
// 执行函数组件方法返回一个虚拟DOM对象,创建数组作为children
const children = [fiber.type(fiber.props)]
// children 执行 reconcile ,进行diffing计算...
reconcileChildren(fiber, children)
}
// useState 钩子函数
function useState(initial){
// 从当前fiber的上一次更新的fiber的hooks数组的对应hook获取老hook
const oldHook =
wipFiber.alternate && // 获取上一次更新的fiber
wipFiber.alternate.hooks && // 获取上一次更新的fiber的hooks数组
wipFiber.alternate.hooks[hookIndex] // 根据hook索引获取对应hook
// 首次渲染时,使用初始值,
// 下一次渲染开始,使用上一次更新的fiber的hook的state值
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [], // 重置当前hook的回调函数队列queue数组
}
// 首次渲染时,actions为空数组,下一次渲染时,actions为此次hook的queue
const actions = oldHook ? oldHook.queue : []
// 通过遍历actions来逐个顺序执行action
actions.forEach(action => {
hook.state = action(hook.state) // 传入state执行setState的回调函数
})
// 更新state的方法
const setState = action => {
// 将传入setState的回调函数加入hook的回调函数队列queue数组中
hook.queue.push(action)
/*
注意:在下面的代码中,执行了与render方法类似的逻辑
更新wipRoot并设置为nextUnitOfWork,使workLoop从下一个工作单元开始新render
这将导致 如果在render过程中接受到setState的更新操作,
会将当前工作中的fiber tree直接丢弃,从setState创建的新fiber tree开始重新渲染。
事实上,React使用expiration timestamp来标记每一次更新并以此确定其更新的优先级。
*/
// 使用上一次更新的根fiber的创建新fiber
wipRoot = { // 将新fiber更新到工作中的根fiber
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
// 将新fiber设置为下一个工作单元
nextUnitOfWork = wipRoot
// 清空将要删除的fiber列表
deletions = []
}
// 更新当前fiber的hooks数组
wipFiber.hooks.push(hook)
// 更新hook索引
hookIndex++
return [hook.state, setState]
}
const React = { useState };
在上面的代码示例中,当一个函数组件内的useState被调用时,将这次useState调用记录创建为一个名为hook的对象,hook对象保存了一次useState调用返回的state值和queue数组,queue数组保存了当前render过程中针对当前useState的所有action,一个action为执行一次useState后返回的setState方法的一次调用,通过在该组件的fiber上维护的名为hooks的数组,找出该组件函数执行时将会被调用的全部useState记录的hook,使用hookIndex可以从hooks数组中找到当前正在执行的useState记录的hook,通过该组件fiber中保存的上一次更新的fiber的hooks数组中的hookIndex对应的useState调用记录的hook,通过遍历执行hook的queue中所有setState调用的action并传入hook的state值,从而获取更新后的state值,最后返回state值和setState方法。
使用示例解析:
function Counter() {
const [state, setState] = useState(0)
/*
此次调用useState,对应类组件constructor生命周期,
效果为初始化state为0:
初始化当前hook为{state: 0, queue: []},
设置组件fiber的hooks数组为[{state: 0, queue: []}],
此次调用的hookIndex为0。
*/
setState(state => state ++)
/*
此次调用setState,对应类组件getDerivedStateFromProps生命周期,
效果为更新state值为1:
更新当前hook为{state: 0, queue: [state => state ++]},
中断当前render执行,立即开始一次新的render,
在新的render运行到当前fiber时,
从组件fiber找到上一次更新时的fiber的hooks中hookIndex为0的hook,
即{state: 0, queue: [state => state ++]},
初始化当前hook为{state: 0, queue: []},
运行上一次更新时的hook中的queue中的全部action,即state => state ++,
更新当前hook为{state: 1, queue: []},
设置组件fiber的hooks数组为[{state: 1, queue: []}]。
注意:由于此操作会导致丢弃当前render,重新执行render,应尽量避免。
*/
return (
<h1 onClick={() => setState(num => num ++)}>
Count: {state}
</h1>
)
/*
页面渲染完成,h1的对应fiber的hooks为[{state: 1, queue: []}],
当用户点击h1时,回调函数调用setState方法:
更新当前hook为{state: 1, queue: [num => num ++]},
开始重新render,在运行到当前fiber时,
从组件fiber找到上一次更新时的fiber的hooks中hookIndex为0的hook,
即{state: 1, queue: [num => num ++]},
初始化当前hook为{state: 1, queue: []},
运行上一次更新时的hook中的queue中的全部action,即num => num ++,
更新当前hook为{state: 2, queue: []},
设置组件fiber的hooks数组为[{state: 2, queue: []}]。
*/
}
对于维护复杂状态,推荐使用useReducer,用法和useState基本一致。
useReducer使用示例:
function reducer(state, action) {
switch (action.type) {
case 'add':
state.push( action.item)
return [...state];
case 'remove':
state.splice(action.index, 1);
return [...state];
default:
throw new Error();
}
}
function ItemList() {
const [state, dispatch] = useReducer(reducer, [{index: 0, value: 'value0'}]);
return [
<button onClick={()=>{
const index = state.length;
dispatch({type: 'add', item: {index, value: 'value'+index}})
}}>add</button>,
<ul>
{state.map((item, key) => <li onClick={()=>{
dispatch({type: 'remove', index: key})
}}>{item}</li>)}
</ul>
]
}
useEffect
useEffect使用示例:
function Counter({num}) {
useEffect(()=>{
consolr.log('component mounted or updated');
return ()=>{
consolr.log('component unmounted');
}
})
useEffect(()=>{
consolr.log('component mounted');
}, [])
useEffect(()=>{
consolr.log('props num changed');
}, [num])
return (
<h1>
Count: {num}
</h1>
)
}
在上面的示例中,使用 useEffect 方法在函数组件中实现类似类组件生命周期函数中的逻辑。
useEffect原理示例:
// useState 钩子函数
function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
){
// 从当前fiber的上一次更新的fiber的hooks数组的对应hook获取老hook
const oldHook =
wipFiber.alternate && // 获取上一次更新的fiber
wipFiber.alternate.hooks && // 获取上一次更新的fiber的hooks数组
wipFiber.alternate.hooks[hookIndex] // 根据hook索引获取对应hook
// 从老Hook获取老effect
const oldEffect = oldHook ? oldHook.effect : null
// 初始化deps参数
const nextDeps = deps === undefined ? null : deps;
// 首次渲染时,创建新effect,下一次渲染开始,effect为当前的fiber的hook的effect
const hook = {
effect: {
tag: 'NO_EFFECT',
create, // 传入的create
destroy: oldEffect ? oldEffect.destroy: undefined,
nextDeps // 传入的deps
}
}
if (
wibHook === null || // 首次渲染
nextDeps === null || // 没有传入deps
nextDeps !== wibHook.deps // deps发生变化
) {
/*
此处的判断逻辑符合使用示例中的预期结果:
首次渲染时,运行effect回调,即mount
首次渲染或更新渲染时,不传入deps时,运行effect回调,即mount和update
更新渲染时,传入deps且deps发生变化,运行effect回调,即deps变化时的update
更新渲染时,传入deps且deps没有变化,不运行,即传入deps为[]
*/
effect.effectTag = 'HOOK_EFFECT';
}
wipFiber.hooks.push(hook)
hookIndex++
/*
注意:
事实上,React将所有useEffect创建在hook上的effect的保存在memorizeState属性上,
并构建成名为componentUpdateQueue环形链表。
*/
}
function destroyEffects(fiber) {
if (fiber.hooks) {
fiber.hooks
.filter(
hook => hook.effect.tag === "HOOK_EFFECT" && hook.effect.destroy
)
.forEach(effectHook => {
effectHook.effect.destroy()
})
}
}
function createEffects(fiber) {
if (fiber.hooks) {
fiber.hooks
.filter(
hook => hook.effect.tag === "HOOK_EFFECT" && hook.effect.create
)
.forEach(effectHook => {
effectHook.effect.destroy = effectHook.effect.create()
})
}
}
// 提交fiber
function commitWork(fiber) {
if (!fiber) { // fiber tree 遍历完成
return
}
let domParentFiber = fiber.parent // 父fiber
// 函数组件和类组件自身没有对应的真实DOM
while (!domParentFiber.dom) { // 判断是否存在真实DOM
// 查找最近的具有真实DOM的父fiber,作为当前fiber的真实DOM的容器
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom // 父fiber真实DOM
if (fiber.effectTag === "PLACEMENT") {
if(fiber.dom !== null){
domParent.appendChild(fiber.dom) // 添加当前fiber真实DOM到父级
}
createEffects(fiber)
} else if (fiber.effectTag === "UPDATE") {
destroyEffects(fiber)
if(fiber.dom !== null){
updateDom( // 根据当前fiber更新真实DOM
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
createEffects(fiber)
} else if (fiber.effectTag === "DELETION") {
destroyEffects(fiber)
commitDeletion(fiber, domParent) // 移除需要删除的真实DOM
}
/*
通过两次递归调用commitWork,
先后分别传入子fiber和兄弟fiber,
实现深度优先遍历,
即有子fiber则先遍历子fiber,直到fiber tree最深层级子fiber,
此时没有子fiber,开始遍历兄弟fiber,直到同级fiber遍历完成,
此时没有兄弟fiber,如此直到整个fiber tree遍历完成。
React在fiber tree中实际发生改变的fiber上创建effect,
将effect构建成一个收集了全部fiber变更的effect list链表,
在commit阶段执行effect list的全部effect,
实现了只对实际发生改变的fiber的对应DOM进行更新,
避免了遍历整个fiber tree造成的性能浪费。
*/
commitWork(fiber.child) // 提交第一个子fiber
commitWork(fiber.sibling) // 提交下一个兄弟fiber
}
const React = { useState };
在上面的代码示例中,当一个函数组件内的useEffect被调用时,会将这次useEffect调用记录在组件fiber中创建为一个名为effect的对象,effect对象保存了一次useEffect调用的tag标签、create回调函数、destroy回调函数、deps依赖数据数组,tag标签用来标记effect操作类型,create为传入useEffect的第一个参数,destroy为create函数return值,即组件的unmount回调函数、deps为传入useEffect的第二个参数。通过在该组件的fiber上维护的名为hooks的数组,找出该组件函数执行时将会被调用的全部useEffect记录的hook,使用hookIndex可以从hooks数组中找到当前正在执行的useEffect记录的hook。
在commit时,区分fiber中不同的更新操作类型,通过遍历该组件fiber中的hooks数组中的hook,找出hook中的effect,对于mount和update组件:执行tag为’HOOK_EFFECT’的effect的create回调函数,将返回值更新到该effect的destroy;对于unmount组件:执行tag为’HOOK_EFFECT’的effect的destroy回调函数。
使用示例解析:
function Counter({num}) {
useEffect(()=>{
console.log('mounted or updated');
return ()=>{
console.log('unmounted');
}
})
/*
此次调用useEffect效果为:
componentDidMount和componentDidUpdate输出'mounted or updated',
componentDidUpdate输出'unmounted'。
初始化当前hook为{effect: {
tag: 'HOOK_EFFECT',
create: ()=>{console.log('mounted or updated')
return ()=>{console.log('unmounted')}
},
destroy: undefined,
nextDeps: null,
}},
设置组件fiber的hooks数组为[{effect: {...}}],
此次调用的hookIndex为0。
*/
useEffect(()=>{
console.log('mounted');
}, [])
/*
此次调用useEffect效果为:
componentDidMount输出'mounted'。
初始化当前hook为{effect: {
tag: 'HOOK_EFFECT',
create: ()=>{console.log('mounted')},
destroy: undefined,
nextDeps: [],
}},
设置组件fiber的hooks数组为[{effect: {...}}, {effect: {...}}],
此次调用的hookIndex为1。
*/
useEffect(()=>{
console.log('props num changed');
}, [num])
/*
此次调用useEffect效果为:
num属性变更后的componentDidMount输出'props num changed'。
假设传入的num属性值为0,则初始化当前hook为{effect: {
tag: 'HOOK_EFFECT',
create: ()=>{console.log('props num changed')},
destroy: undefined,
nextDeps: [0],
}},
设置组件fiber的hooks数组为[{effect: {...}},{effect: {...}},{effect: {...}}
],
此次调用的hookIndex为2。
*/
return (
<h1>
Count: {num}
</h1>
)
/*
所有fiber运行完成,会从根fiber进行commit,
在commit当前fiber时,进行"PLACEMENT"操作,
执行createEffects方法,遍历Counter组件fiber的hooks的hook,
执行effect.tag为'HOOK_EFFECT'的effect.create方法,
并将返回赋值到effect.destroy,
执行效果为:
输出'mounted or updated'
hooks[0].effect.destroy更新为()=>{console.log('unmounted')}
输出'mounted'
hooks[1].effect.destroy重新赋值为undefined
输出'props num changed'
hooks[2].effect.destroy重新赋值为undefined
*/
/*
当num属性更新时,开始重新render,
假设当前num属性值为1,在运行到当前fiber时,
hooks[1].effect.tag更新为'NO_EFFECT'
hooks[2].effect.nextDeps更新为[1]
所有fiber运行完成,再次commit时,进行"UPDATE"操作,
执行destroyEffects和createEffects方法,执行效果为:
输出'unmounted'
输出'mounted or updated'
hooks[0].effect.destroy重新赋值为()=>{console.log('unmounted')}
输出'props num changed'
hooks[2].effect.destroy重新赋值为undefined
*/
/*
当组件unmount时,fiber运行commit进行"DELETION"操作,
执行destroyEffects方法,执行效果为:
输出'unmounted'
*/
}
通过useEffect自定义hooks方法分离组件逻辑示例:
function useLoginStatus({account}) {
const [isLogin, setIsLogin] = useState(false);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
fetch('/checkLogin',{account}).then(handleStatusChange);
}, [account]);
return isLogin;
}
function User({userInfo}){
const isLogin = useLoginStatus(userInfo.account)
return <div className={isLogin? 'online':'offline'}>
{userInfo.username}
</div>
}
在上面的代码示例中,useLoginStatus会在account属性变化时重新获取用户登录状态并保存到isLogin,在User组件中使用useLoginStatus的返回值更新用户切换账号时的展示样式,useLoginStatus也可以复用到其他函数组件中,实现逻辑复用和分离,同时也不会增加组件结构的嵌套层级,User组件自身更加简洁,有利于维护。