简介
React Hooks自从推出后就一直深受前端开发者的喜爱,优雅的函数组件,让开发变的更加轻量,更关注业务本身。本文将介绍React Hooks中的常用的几个hooks手动实现模拟一下,更好的理解React Hooks的原理。
通过这篇文章,可以更好理解下面这些问题:
1.为什么useState不能在循环、判断和子函数里面使用
2.useEffect如何模拟class中的生命周期
3.如何实现一个自定义hooks
useState
官方定义:
/**
* Returns a stateful value, and a function to update it.
*/
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
// convenience overload when first argument is omitted
/**
* Returns a stateful value, and a function to update it.
*/
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
从以上接口来看,useState接收一个初始值,返回一个被state管理的值和一个更新函数。
useState的作用:React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数
使用方法
让我们先看看使用的例子:
基于create-react-app 创建一个简单的工程
function App() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
点击
</Button>
</div>
);
}
这样当点击按钮时,页面会根据点击次数逐步增加:
那么这个useState干了什么呢?这是官方的一个解释
调用 useState 方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫
count, 但是我们可以叫他任何名字,比如name。这是一种在函数调用时保存变量的方式 ——useState是一种新方法,它与 class 里面的this.state提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState 需要哪些参数?
useState()方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了0作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用useState()两次即可。)
useState 方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写
const [count, setCount] = useState()的原因。这与 class 里面this.state.count和this.setState类似,唯一区别就是你需要成对的获取它们。
手动实现
下面我们简单的手写一个useState的实现
既需要每次刷新都保留原有值,又能在给新值的时候主动刷新页面
var _memoizedState // 在全局存储的state
function useState(initialState) {
var state = _memoizedState||initialState;
function setState(newState) {
_memoizedState = newState;
render();
}
return [state, setState];
}
//实现一个基础的render函数来触发刷新
const rootElement = document.getElementById("root");
function render() {
ReactDOM.render(<App />, rootElement);
}
render();
运行起来看到跟原来实际的useState效果一致,那接下来我们看下useEffect
useEffect
官方定义
/**
* Accepts a function that contains imperative, possibly effectful code.
*
* @param effect Imperative function that can return a cleanup function
* @param deps If present, effect will only activate if the values in the list change.
*
*/
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
可以看到useEffect接收一个callback和一个ReadonlyArray<any>的DependencyList数组,useEffect的官方用法描述为:可以让你在函数组件中执行副作用操作,所谓副作用就是数据获取,设置订阅以及手动更改 React 组件中的 DOM,比如修改document.title或者获取网络数据等等。
使用例子
function App() {
const [count, setCount] = useState(0);
useEffect(()=>{
document.title = `You clicked ${count} times`;
},[count])
return (
<div>
<div>{count}</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
点击
</Button>
</div>
);
}
基于上面useState的例子,我们增加一个useEffect,其中第二个参数是[count],这样当count变更的时候页面标题就会变成You clicked ${count} times:
useEffect的用法:
- 如果 DependencyList 不存在或者为[],那么 callback 每次 render 都会执行
- 如果 DependencyList 存在,只有当数字内部发生了变化, callback 才会执行
官方对useEffect的工作流程解释如下:
useEffect做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用
useEffect? 将useEffect放在组件内部让我们可以在 effect 中直接访问countstate 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
手动实现
var _deps;
function useEffect(callback, deps) {
/* 如果 deps 不存在,或者 deps 有变化,或者第一次_deps不存在*/
if (!_deps ||!deps||!deps.every((el, i) => el === _deps[i])) {
callback();
_deps = deps;
}
}
运行后即可看到实际效果,符合预期
至此我们简单的实现了一下useState和useEffect,大家也可能发现一个问题,就是当多个state的时候,比如:
const [count, setCount] = useState(0);
const [name, setName] = useState('leo');
我们的useState就失效了,接下来我们做下简单的改造
支持多state改造
let memoizedState = [];
let index = 0; //memoizedState计数
//基础实现useEffect
function useEffect(callback, deps) {
const hasNoDeps = !deps;
const _deps = memoizedState[index];
const hasChangedDeps = _deps
? !deps.every((el, i) => el === _deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[index] = deps;
}
index++;
}
//基础实现useState
function useState(initialState) {
memoizedState[index] = memoizedState[index] || initialState;
const currentIndex = index;
function setState(newState) {
memoizedState[currentIndex] = newState;
render();
}
return [memoizedState[index++], setState];
}
const rootElement = document.getElementById("root");
function render() {
index = 0;
ReactDOM.render(<App />, rootElement);
}
1、初始化时会把所有的useState和所有的useEffect按照顺序存储到memoizedState,同时闭包保存currentIndex
2、渲染时会依次拿出上一次值
这里全局采用一个数组和一个计数来存储state,同时内部采用闭包来记录他存在的index。同时也解释了最上面的问题:
为什么useState不能在循环、判断和子函数里面使用 如果放到循环判断和子函数内,这个index就会错乱,无法再维护原因的memoizedState
回答问题
1.为什么useState不能在循环、判断和子函数里面使用
答:如果放到循环判断和子函数内,这个index就会错乱,无法再维护原因的memoizedState
2.useEffect如何模拟class中的生命周期
答: 当useEffect没有第二个参数时,就类似class模式中的componentDidMount 和 componentDidUpdate
当useEffect第二个参数为[]时,类似class模式中的componentDidMount
同时useEffect还有个返回函数,用来当组件被卸载的时候做一些取消注册作用
useEffect(()=>{
return ()=>{
//componentWillUnmount
}
})
3.如何实现一个自定义hooks
答: 方法1:可以实际操作与useState一样的memoizedState数组来实现自己的hooks(需要知道内部实现和变量名,不推荐)
方法2:内部包装官方的hooks来定制自己的hooks(推荐做法,后续会更新文章讲自定义hooks)
目前官方的实现:
采用了一个单向链表结构,通过next定位下一个
export type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
|};
export type Effect = {|
tag: HookFlags,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
|};
总结
至此,React hooks的基本原理已经介绍完了,同时手动实现了一下useState和useEffect。当然官方还有很多常用的hooks,比如useMemo、useCallback、useReducer等,大家可以在官网查看具体使用方式 zh-hans.reactjs.org/docs/hooks-…
后面会更新一些常用的hooks使用和一些自定义hooks,感谢大家阅读,有问题欢迎留言和私信。