目录
React Hook 新出现背景
类组件的问题
函数组件的局限
React Hook
Hook 的使用和实践
useState 和 Hook 的闭包机制
函数组件渲染拆解
让函数式组件也可以输出 3 3 3
useEffect
useEffect 的函数返回值
useEffect 和 类组件生命周期
useEffect 和 闭包问题
闭包问题的切入点和发生场景
尝试解决闭包问题-监听state变化
尝试解决闭包问题 - setState 另外一种更新组件状态的方式
解决闭包问题最佳实践-useState和useRef
useRef 和 useState 的最佳实践
例如以下的例子
useCallback
React.memo
React.useCallback 和 React.memo
React.useCallback 和 React.memo 最佳实践
Raect.memo 的局限
React.useMemo 代替 React.momo
React.momo 和 React.useMemo
useReducer useContext
其他 Hook
组件复用
高阶组件
高阶组件编写和使用
自定义 Hook 的编写和使用
函数式式组件和类组件默认属性
类组件的问题被解决了么?
简单总结
使用感受
个人使用方面
团队协作方面
React Hook
新出现背景providers customers
,等一堆工具都是为了解决这个问题,但是造成了很严重的理解成本和组件嵌套地狱state
和 生命周期,导致使用场景有限React Hook
Hooks
是 React 16.8
新增的特性,它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性,无需转化成类组件
Hook
的使用和实践useState
和 Hook
的闭包机制// hook 组件
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
You clicked {count} times
);
}
// 等效的类组件
class Counter extends Component {
state = { count: 0 };
log = () => {
this.setState({
count: this.state.count + 1,
});
setTimeout(() => {
console.log(this.state.count);
}, 3000);
};
render() {
return (
You clicked {this.state.count} times
);
}
}
快速点击下的情况下,想想 Hook
组件和函数式组件控制台打印出来的是什么?
3 3 3
Class
组件的 state
是不可变的,通过 setState
返回一个新的引用,this.state
指向一个新的引用setTimeout
执行的时候,通过 this
获取最新的 state
引用,所以这个输出都是 3
0 1 2
props
和 state
既然每次渲染都是一个独立的闭包,可以尝试代码拆解函数式组件的渲染过程
// 第一次点击
function Counter() {
const [0, setCount] = useState(0);
const log = () => {
setCount(0 + 1);
// 只能获取这次点击按钮的 state
setTimeout(() => {
console.log(0);
}, 3000);
};
}
// 第二次点击
function Counter() {
const [1, setCount] = useState(0);
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(1);
}, 3000);
};
}
// 第三次点击
function Counter() {
const [2, setCount] = useState(0);
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(2);
}, 3000);
};
}
4
次渲染,count
从 0
变为 3
count = 0
count = 0
,count
变成 1
, 第二次渲染,渲染后页面看到 count = 1
,对应上述代码第一次点击count = 1
,count
变成 2
, 第三次渲染,渲染后页面看到 count = 2
,对应上述代码第二次点击count = 2
,count
变成 3
, 第四次渲染,渲染后页面看到 count = 3
,对应上述代码第三次点击3 3 3
有种比较简单并且能解决问题的方案,借用 useRef
useRef
返回一个可变的 ref
对象,其 current
属性被初始化为传入的参数(initialValue)
useRef
返回的 ref
对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref
对象都是同一个useRef
可以类比成类组件实例化后的 this
,在组件没有销毁的返回的引用都是同一个function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
You clicked {count.current} times
);
}
3 3 3
Ref
对象整个生命周期都不变,修改 current
属性也只是修改属性,那除了打印,这里的 You clicked 0 times
,点击三次,会变成 3
么?3
次,但是不会像 useState
一样,渲染 4
次,这里只会渲染 1
次,然后看到的都是 You clicked 0 times
参考React实战视频讲解:进入学习
通过 useRef
虽然能解决打印的问题,但是页面渲染是不对的,这里还是使用 useState
的方案,配合 useEffect
可以实现我们想要的效果
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
useEffect
的签名,effect
是函数类型,并且必填, 还有第二个可选参数,类型是只读数组useEffect
是处理副作用的,其执行时机在 每次 Render
渲染完毕后,换句话说就是每次渲染都会执行,在真实 DOM
操作完毕后。配合这个 hook
, 如果每次 state
改变后渲染完之后,把 ref
里面的值更新,然后控制台打印 ref
的值,
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() => {
currentCount.current = count;
});
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
You clicked {count} times
);
}
这样子写可以符合我们的预期效果,页面展示从 0 1 2 3
, 然后控制台输出 3 3 3
,然后我们拆解下渲染过程。
4
次渲染,count
从 0
变为 3
count = 0
, currentCount.current = 0
, 页面显示 0
, 渲染完成,触发 useEffect
, currentCount.current = 0
count = 0
, 渲染完成后,count = 1
, 页面显示 1
,触发 useEffect
,currentCount.current = 1
count = 1
, 渲染完成后,count = 2
, 页面显示 2
,触发 useEffect
,currentCount.current = 2
count = 2
, 渲染完成后,count = 3
, 页面显示 3
,触发 useEffect
,currentCount.current = 3
currentCount.current = 3
,第四次渲染,页面看到 count = 3
, setTimeout
中调用的是 currentCount
这 useEffect(() => {
console.log('after render');
return () => {
console.log('last time effect return');
};
});
对象,输出都是 3
useEffect
的函数返回值type EffectCallback = () => void | (() => void | undefined);
useEffect
的回调函数可以返回空,也可以返回一个函数,如果返回一个函数的话,在 effect
执行回调函数的时候,会先执行上一次 effect
回调函数返回的函数
这个 useEffect
,每次渲染完之后,控制台会先输出 last time effect return
,然后再输出 after render
之前提到,useEffct
有两个参数,第二参数是个可选参数,是 effect
的依赖列表, React
根据这些列表的值是否有改变,决定渲染完之后,是否执行这个副作用的回调
如果不传这个参数,React
会认为这个 effect
每次渲染然之后都要执行,等同于 componentDidUpdate
这个生命周期无约束执行
useEffect(() => {
currentCount.current = count;
});
componentDidUpdate() {
currentCount.current = this.state.count;
}
如果这个参数是空数组,React
会认为组件内任何状态和属性改变,都不会触发这个 effect
,相当于这个 effect
是仅仅在组件渲染完之后,执行一次,后面组件任何更新都不会触发这个 effect
,等同 componentDidMount
useEffect(() => {
currentCount.current = count;
}, []);
componentDidMount() {
currentCount.current = this.state.count;
}
如果配合 useEffect
回调函数的返回函数,可以实现类似 componentWillUnmount
的效果,因为如果是空数组的话,组件任何更新都不会触发 effect
,所以回调函数的返回函数只能在组件销毁的时候执行
useEffect(() => {
return () => {
console.log('will trigger ar willUnmount')
}
}, []);
componentWillUnmount() {
console.log('will trigger ar willUnmount')
}
如果依赖列表里面有值,则类似componentDidMount
有条件约束更新,只有当上一次的状态和这次的不一样,才执行
useEffect(() => {
currentCount.current = count;
}, [count]);
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
currentCount.current = this.state.count;
}
}
useEffect
和 闭包问题假设组件需要在初始化的时候,定义一个定时器,让 count
自增,自然而然的可以写出以下的代码
// 初始化的 count = 0
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
componentDidMount() {
setInterval(() => {
this.setState({ count: this.state.count + 1 });
}, 1000);
}
但是实际运行的时候,类组件展示是对的,函数组件从 0
递增到 1
之后,页面渲染就再也不变了
this
这个引用,很容易通过 state
拿到最新的值[]
,所以只有首次渲染后,才会这行这个 effect
,首次渲染后, count
就是 0
,所以 setCount(count + 1)
每次都是执行 setCount(0 + 1)
,所以定时器工作是正常的,不过取的值有问题。闭包问题,大多发生在,有些回调函数执行,依赖到组件的某些状态,但是这些状态并没有写到 useEffect
的依赖列表里面。导致执行回调函数的时候,拿到组件的状态不是最新的。
主要的场景有:
Observer
的回调这些场景,通常只要在组件初始化渲染完之后,定义一次回调函数就好,但是如果回调函数依赖到组件的转态或者属性,这时候就要小心,闭包问题
function Router() {
const [state, setState] = useState('');
useEffect(() => {
window.addEventListener<'hashchange'>(
'hashchange',
() => {
// 监听 hash 变化,这里依赖到 state
},
false
);
}, []);
}
例如这里的写法,在组件渲染完监听 hashchange
,回调函数是拿不到后续更新的 state
的,只能能到初始化时候的空字符串。
state
变化既然回调函数要每次都拿到最新的 state
,可以监听 state
的变化,state
变化的时候,重新定义事件监听器,改写一下
function Router() {
const [state, setState] = useState('');
useEffect(() => {
window.addEventListener(
'hashchange',
() => {
// 监听 hash 变化,这里依赖到 state
},
false
);
}, [state]);
}
以上代码能用,但是 state
每次改变,就会重新定义一个 hashchange
回调函数,但是上一次的 hashchange
的事件监听器并没有清除,代码能跑,但是内存泄漏也太严重了,可以配合 useEffect
回调函数返回的函数配合清掉上一次的事件监听器
function Router() {
const [state, setState] = useState('');
useEffect(() => {
const callback = () => {};
window.addEventListener('hashchange', callback, false);
return () => window.removeEventListener('hashchange', callback, false);
}, [state]);
}
这样内存泄漏的问题被解决了,但是这种事情监听,正常来说设置一次就好,没必要重新定义,还有别的更好的方法么?
setState
另外一种更新组件状态的方式useState
返回的更新状态的函数,除了可以传一个值,还可以传一个回调函数,回调函数带一个参数,这个参数是最新的 state
,像这样的话,之前那个定时器的例子,可以修改成这样。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// setCount(count + 1)
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return {count}
;
}
这里我们改了一行代码,setCount(count + 1)
改成了 setCount((c) => c + 1)
,这样修改之后,其实定时器回调已经没有依赖到 count
这个值了,由 setCount
里面的回调函数,返回最新的 count
的值,就是setCount((c) => c + 1)
,里面的 c
.
同样的,对于事件监听器里面,我们也可以通过这个方式去获取最新的 state
,但是这里有几个问题
state
,所以在调用 setState
的时候,拿到最新的值的同时,记得把 setState
的值,设置成和当前同一个,如果没有返回,那调用 setState
之后, state
的值会变成 undefined
setState
返回一个同样的值,会不会导致组件和它的子组件重新渲染?找了下文档说明是这样的:调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心function Router() {
const [state, setState] = useState('');
useEffect(() => {
const callback = () => {
let latestState = ‘’;
setState((stateCallback) => {
// stateCallback 是最新的 state
latestState = stateCallback;
// 记得把 stateCallback 返回去,不然 state 会被改成 undefined
return stateCallback;
});
// latestState 已经被赋值为最新的 state 了
};
window.addEventListener<'hashchange'>('hashchange', callback, false);
}, [])
}
这样基本就没问题了,做到了只定义了一次回调,然后也可以获取最新的 state
,一举两得,但是还是有问题的
setState
回调函数如果不写 return stateCallback;
这段代码,会导致 state
莫名其妙被设置成 undefined
,而且非常不好发现,维护性太差setState
是用来改变组件的 state
的,不是让你这样用的的,虽然这样用完全没问题。但是可维护性太差了,如果你的代码被接手,别人就会疑惑这里为什么要这么写,无注释和变量命名太糟糕的情况下,代码可以维护性基本为 0
state
,虽然不会导致子组件重新渲染,但是本组件还是有可能重新渲染的,按官网的说法这个方案不完美。思路再发散一下?执行回调函数的时候,需要获取到最新的 state
,能不能用一个不变的值缓存 state
? 等等?? 不变的值???
useState
和useRef
useRef
的返回是在整个组件生命周期都是不变的一个对象,可以借助 useRef
来获得最新的 state
。例如这个例子可以改成这样:
function Router() {
const [state, setState] = useState('');
const stateRef = useRef(state);
// 这样,可以把 stateRef 和最新的 state 绑定起来
stateRef.current = state;
// 或者这样,可以把 stateRef 和最新的 state 绑定起来
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
const callback = () => {
const latestState = stateRef.current;
};
window.addEventListener<'hashchange'>('hashchange', callback, false);
}, []);
}
stateRef.current
上面两种写法,都可以获得最新的 count
,回调函数里面里面直接读取 stateRef.current
的值,可以拿到最新的 state
闭包问题的最优解,节本就是这样了。
useRef
和 useState
的最佳实践useState
和 useRef
仔细想想和和类组件的什么属相很相似?是不是和 this.state
和 this
的属性很像this
上就好了,如果需要参与渲染的属性,挂在 this.state
上Hook
中,useRef
和 useState
可以实现类似效果// 函数组件
const Child = React.memo(() => {
// count 参与页面渲染
const [count, setCount] = useState(0);
// userInfo 不参与渲染
const userInfo = useRef(null);
});
// 类组件
class Child extends React.PureComponent {
constructor(props) {
super(props);
// 不参与渲染
this.userInfo = null;
// 参与渲染的属性
this.state = {
count: 0,
};
}
}
再看看 useEffect
回调函数的返回值
type EffectCallback = () => (void | (() => void | undefined));
return void | (() => void | undefined)
确定是没有返回或者返回一个函数,所以下面这种写法是有问题的,虽然也没有明显标明返回体,就是没有返回一样,但是这个回调函数是异步函数,异步返回默认返回一个 Promise
对象,所以这种写法是不提倡的
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'url‘, ); setData(result.data);}, []);
为了规避这个问题,可以修改一下写法
useEffect(() => {
const fetchData = async () => {
const result = await axios('url');
setData(result.data);
};
fetchData();
}, []);
useCallback
把函数写进里面没什么问题,官方也推荐,但是万一我的副作用里面需要处理多个函数或者一个超长的函数的话,一个是不美观,一个是太难维护
这个适用可以利用 useCallback
把函数抽离出去,useCallback
返回一个记忆化的函数,当且仅当依赖列表有任何属性改变的时候,它才会返回一个新的函数,所以这个特性比较适合传给子组件的回调函数
function Counter() {
const [count, setCount] = useState(0);
const getFetchUrl = useCallback(() => {
return 'https://v?query=' + count;
}, [count]);
useEffect(() => {
getFetchUrl();
}, [getFetchUrl]);
return {count}
;
}
这里如果 count
改变的时候,getFetchUrl
的值也会改变,从而导致 useEffect
也触发
React.memo
React.memo()
返回一个记忆化的值,如果 React 内部会判定,如果重新渲染
props` 不相等,就会重新渲染,如果没有改变,就不会触发组件渲染
这个特性比较有用,因为如果父组件重新渲染的时候,子组件就会重新渲染,使用这个特性可以减少不必要的子组件重新渲染
const Child = memo((props) => {
useEffect(() => {
}, [])
return (
// ...
)
}, (prevProps, nextProps) => {
// 判定相等的逻辑
// 假如某些属性的改变不需要重新渲染
// 可以编写这个函数
})
React.useCallback
和 React.memo
useCallback
要把 memo
拎出来讲,想一下 useCallback
的作用,返回一个缓存的函数,在函数组件里面,每次渲染都会执行一次组件函数,组件函数每次执行,在组件内部的函数都会重新定义,这样的话,父组件传给子组件的回调函数每次渲染都会变memo
的角度去看,父组件每次渲染,子函数组件如果不加 memo
的话,就算是子组件无任何依赖,属性都不变的情况下,子组件也会重新渲染useCallback
,这样可以避免回调函数重新定义,但是子组件如果不用 memo
包裹,就算任何子组件属性没改变,还是会导致子组件重新渲染;memo
包裹,父组件每次渲染,重新定义回调函数,还是会导致重新memo
和 useCallback
必须都用上,不然是没用的,不仅达不到优化的效果,而且会加重 React 比较的负担。要不就别用,要不就都用上。React.useCallback
和 React.memo
最佳实践父组件用 useCallback
包裹函数,子组件用 memo
包裹组件,要不就都不用
// 子组件
// callback 为父组件传过来的回调函数
const Child = ({ callback }) => {}
// 子组件用 React.memo 包裹
export default React.memo(Child);
// 父组件const Parent = () => {
// 子组件的回调函数用 useCallback 包裹
const callback = React.useCallback(() => {}, []);
return
};
Raect.memo
的局限React.memo
包裹在组件上,可以对传给组件的属性进行判定,父组件导致子组件重新渲染的时候, memo
包裹的组件,会判定属性是否和上次渲染时候否改变,如果有改变,子组件重新渲染,否则不会重新渲染。React.memo
有个局限,只能防止来源于外部的属性,如果是来源于内部的属性,React.memo
是无作用的,例如通过 useContext
直接注入组件内部的属性,它没法防止,可以看下下面这个简单的例子export const Store = React.createContext(null);
export default function Parent() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });
return (
);
}
export const Count = memo(() => {
const { state, dispatch } = useContext(Store);
const setCount = () => dispatch({ type: 'changeCount' });
return (
<>
count: {state.count}
>
);
});
export const Step = memo(() => {
const { state, dispatch } = useContext(Store);
const setCount = () => dispatch({ type: 'changeStep' });
return (
<>
count: {state.step}
>
);
});
count
或者 step
任意这个属性改变,都会导致两个子组件重新渲染,这显然是不对的。React.useMemo
代替 React.momo
useMemo
和 memo
一样,返回一个记忆化的值,如果依赖项没有改变,会返回上一次渲染的结果,它和 useCallback
的差别就在一个是返回记忆化的函数,一个是返回记忆化的值,如果 useMemo
的回调函数执行返回一个函数,那它的效果和 useCallback
是一样的。
因而上面的组件可以改一下,下面这种写法就可以防止任意一个属性改变会导致两个子组件重新渲染的问题
export const Count = () => {
const { state, dispatch } = useContext(Store);
const setCount = () => dispatch({ type: 'changeCount' });
return useMemo(
() => (
<>
count: {state.count}
>
),
[state.count]
);
};
export const Step = () => {
const { state, dispatch } = useContext(Store);
const setStep = () => dispatch({ type: 'changeStep' });
return useMemo(
() => (
<>
step: {state.step}
>
),
[state.step]
);
};
React.momo
和 React.useMemo
React.momo
在防止子组件重新渲染方面,是最简单的,在类组件里面有个 React.PureComponent
,其作用也是。但是它无法检测函数内部的状态变化,并且防止重新渲染,例如 useContext
注入的状态。不过它自动比较全部属性,使用起来方面。React.memo
按照依赖列表是否有属性改变,决定是否返回新的值,一定程度上和 Vue
的计算属性类似,但是需要说动声明依赖的属性。相比 React.momo
,它的控制的粒度更细,但是一般的外部属性变化,用这个明显没有 React.memo
方便useReducer
useContext
useReducer
是 useState
的一种替代方案,useState
的内部实现就是 useReducer
redux
一样,一个是 reducer
, 一个是初始值,有两个返回,一直是当前的 state
,一个是 dispatch
dispatch
调用 action
就可以修改 state
里面的数据Action
,就可以修改数据,由于数据不在组件内部,也不用处理内部 state
变化带来的 effect
。useContext
和 useReducer
结合,一定程度上可以实现一个 React Redux
Hook
useImperativeHandle
,搭配 useRef
和 forwardRefs
可以实现定制父组件可以引用子组件的属性和方法,而不是直接引用整个子组件的实例,在父组件需要调用子组件属性和方法,但是又不想全部属性和方法都给父组件调用的时候使用useLayoutEffect
使用的不多,作用和 useEffect
一样,但是这个 hook
是在组件变化后, DOM
节点生成后,渲染之前调用,区别于 useEffect
是渲染之后调用,不太推荐使用,会阻塞渲染useDebugValue
可用于在 React
开发者工具中显示自定义 hook
的标签。类似 Vue
组件用的 name
或者 React
组件中的 displayName
,不影响代码运行React Hook
有自定义 Hook
,React
类组件有高阶组件或者渲染属性
有个比较常见的场景,进入页面需要调用后端接口的问题,如果每个组件都写一次,很繁琐,假设处理数据的接口长这样子
interface Response {
/** 是否有错误 */
hasError: boolean;
/** 是否有数据 */
hasData: boolean;
/** 是否请求中 */
Loading: boolean;
/** 请求回来的数据 */
data: T;
}
高阶组件(HOC)
是 React
中用于复用组件逻辑的一种高级技巧。HOC
自身不是 React API
的一部分,它是一种基于 React
的组合特性而形成的设计模式。这种组件复用还挺常见的,比如 React-redux
里面的 connect
,React Router
的 withRouter
它可以做到:
用处
按上面请求的需求,做一个组件渲染完之后,就立即开始请求初始数据
function requestWrapper(options) {
return (Component) => {
return class WapperComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
hasError: false,
hasData: false,
data: null,
};
}
httpRequest = async () => {
this.setState({ loading: true });
// 这里做一些请求工作,我这里就不写了,没必要
this.setState({ hasData: true });
this.setState({ hasError: false });
this.setState({
data: {
/** 这里是请求回来的数据 */
},
});
this.setState({ loading: false });
};
componentDidMount() {
this.httpRequest();
}
render() {
// 透传外部传过来的属性,然后合并 this.state,传给包裹组件
const combineProps = { ...this.props, ...this.state };
return this.state.loading ? (
) : (
);
}
};
};
}
使用方面,高阶组件可以修饰类组件或者函数组件
function Page(props) {
// props 包含 loading hasError hasData data prop1
// 其中 prop1 来源于外部属性,其他的属性来源于包裹的组件
}
// 类组件使用,使用装饰器
@requestWrapper({url: '', param: {} })
class ClassComponent extends React.PureComponent {
}
// 类组件使用,不适用装饰器
export default requestWrapper({url: '', param: {} })(ClassComponent);
// 函数式组件使用
const WrapPage = requestWrapper({ url: '', param: {} })(Page);
export default WrapPage;
// 使用
;
Hook
的编写和使用自定义 Hook
的编写,一个简单的数据请求的 Hook
function useRequest(options) {
const [loading, setLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [hasData, setHasData] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
async function reqeust() {
setLoading(true);
/** 这里依旧是请求,没写 */
setHasError(false);
setHasData(true);
setData({
/** 请求回来的数据 */
});
setLoading(false);
}
reqeust();
}, []);
return { loading, hasError, hasData, data };
}
自定义 hook
只能在函数式组件使用,不能在类组件里面用
function Page(props) {
// 这次的 props 只有 prop1 这个属性
const { loading, hasError, hasData, data } = useRequest({
url: ‘’,
param: {},
});
}
;
class Button extends React.PureComponent {
static defaultProps = { type: "primary", onChange: () => {} };
}
// 不论是函数式还是类都可以这么玩,这也是类静态属性的另外一种写法
Button.defaultProps = {
type: "primary",
onChange: () => {}
};
function Button({ type, onChange }) {}
// 这样写看起来没问题,但是实际上,如果父组件没传 onChange,onChange
// 每次组件渲染都会生成一个新的函数,引用类型都有这个问题
function Button({ type = "primary", onChange = () => {} }) {}
// 这很OK,你可真是个小机灵鬼
const changeMet = () => {}
function Button({ type = “primary”, onChange = changeMet }) {}
复用组件状态逻辑难
Hook
编写需要对 Hook
运行机制非常了解,门槛并不比高阶组件低生命周期带来的负面影响,逻辑拆分严重
Hook
里面切实被解决了,不会存在同一个逻辑被拆分在 N
个生命周期里面了This
的指向问题
this
,就不会有 this
的问题,但是相对的,如果需要一个不变的对象,请使用 useRef
useState
可以实现类似 state
和 setState
的效果useEffect
可以实现 componentDidMount
componentDidUpdate
componentWillUnmount
这几个生命周期的功能,并且写法更加简单,在每次渲染后都会触发,触发的条件是依赖项有改变useRef
返回一个引用,每次渲染都返回同一个对象,和类组件 this
属性一致useCallback
返回一个记忆化的回调函数,在依赖项改变的时候,回调函数会修改,否则返回之前的回调函数,对于一些需要传给子组件的函数,可以使用这个,避免子组件因为回调函数改变而改变useMemo
返回一个记忆化的值,依赖项改变,返回的值才会变,可用来记忆化值,和 Vue
计算属性类似,避免重复计算,避免重复渲染Hook
是实现状态和逻辑复用,作用和高阶组件还有渲染属性差不多useReducer
是 useState
的底层实现,可以管理多个 state
,把 state
从组件内部抽离出来useContext
可以实现批量传值,注入多个组件,和 useReducer
useMemo
使用可以实现 Redux
的功能Hook
中的闭包问题,大多还是由于依赖项没有填写导致This
的更加恼人,主要调试不好发现问题,填不填依赖项也是一个让人纠结的活Hook
的依赖不能自动识别,必须手动声明,虽然有插件辅助添加,但是使用起来还是不如 Vue
的 Hook
Hook
的机制的情况下,Hook
开发体验还是比类组件好很多Hook
的时候,团队成员的 Hook
水平是不太一致的,很多人员就遇到了闭包问题,还有依赖死循环的问题,这个可能大大小小都遇到过,就好像上面提到的,解决闭包问题,方式五花八门,其实也是我自己摸索过来的,然后看到团队成员其实差不多还使用者 state
更新之后,重新设置监听的方式,这个并不是太好,只能说闭包问题解决了React
官方也没有总结太多最佳实践,很多都靠自己实践过来的,所以团队成员在刚接触 Hook
的时候,都是 useEffect
useState
两把 API
,甚至在 React Hook
的官方文档里面 Hook 简介,对于这两个 Hook
介绍的很多Hook
,比如 useRef
和 useCallback
使用场景其实没有太好的例子去支撑这些 API
的使用。倒是其实团队里面不少成员,面对着不参与渲染的属性,也是用 useState
,而不是使用 useRef
。就是很多新人接触 Hook
容易犯的一个错误。React
自动检测依赖项的插件没有生效,这无疑会给本身就难以发现的闭包问题加了一层霜React Hook
的同学,直接用类组件,类组件虽然代码写起来繁琐,但是起码没有闭包这些问题,而且代码被接手之后容易读懂,React Hook
只是一个工具,会使用会给你加分,但是不会使用只会用类组件,也不会对其他人代码有影响,比较类组件和函数组件是可以共存的