一般介绍一个新的东西(知识),分三个部分:是什么?为什么?怎么用?
React Hook是什么?
官方解释:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
翻译一下:从现在开始,react函数式组件不再是无状态组件,你可以通过hook直接让函数式组件拥有class组件的能力。
为什么要用React Hook?
看官方的意思:不是替代class组件吧,也至少是想引导用户把函数式的组件写起来。那么为什么要往这个方面引导呢?
class组件有什么问题?
- 随时间推移,组件臃肿难以维护和复用
- 声明周期里的函数比较乱
- class的特殊书写形式(this绑定)
- class编译会变大,性能不好,热加载不稳定
- class自身复杂度高
其中复用
是最棘手的问题,也是hook出现的最根本的原因。
没有hook的时候,我们用什么办法实现class组件的复用?
- 属性渲染
- 高阶组件
这两种方式都受限于class组件的本身问题,也不能根本解决提升组件复用性的问题,只是提供了复用的手段。
函数式组件结合hook的方式,让组件可以有自己的状态,并且可以高复用,页面里class组件减少也会更好维护。
怎么就体现了好的复用性呢?具体的优势有哪些?
- 函数组件没有生命周期
- 不存在特殊的书写方式
- hook的注入让其拥有class组件的能力,但同时又有简洁的书写方式
- 复用的本质是那些封装起来的hook能力,例如自定义的hook,那是一部分独立的功能组件
仔细回味一下,在实际开发中是不是碰到过不得已将函数式组件更改为class组件,从而进入到了难为维护和复用的世界里。
怎么使用React Hook?
常规用法
先看一个例子,就不从一个简单的使用api开始了,直接看封装Hook的玩法
const useUserList = () => {
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState([]);
const loadUsers = async params => {
setLoading(true);
setUsers([]);
const users = await loadUsers('/request', params);
setUsers(users);
setLoading(false);
};
const addUsers = useCallback(
user => setUsers(users => users.concat(user)),
[]
);
const deleteUsers = useCallback(
use => setUsers(users => without(users, user)),
[]
);
return [users, {loading, loadUsers, addUsers, deleteUsers}];
}
上面是一段简单的封装hook操作,业务逻辑中经常会使用到。
先体会一下这个hook的功能:
- 提供用户列表
- 提供加载、添加和删除操作
- 还有加载状态,这可能影响页面的行为
这是在干什么?管理状态和关联的行为,是状态和行为的封装,这是复用的本质,未来只要用到用户方面的数据和操作复用这个hook好了。所以看到这里,复用的不是UI,而是能力。
如何合理使用
当然上面的书写方式对我来说还没能感受到“真香”的现场,只是换了一个方式抽离逻辑的书写,那么试着分析看看上面的例子,在我看来,更加合理的使用是让hook发挥其更强的复用性价值,如果做到这一点那么Hook才是最有意义的。看看下面的硬核现场分析。
从上面这个例子看,封装是状态和关联的行为的打包,那么可以理解hook就是“给我一些变量和方法,我给你封装一个hook”。从最底层通用的角度翻译成代码如下:
export const useMethods = (initialValue, methods) => {
const [value, setValue] = useState(initialValue);
const boundMethods = useMemo(
() => Object.entries(methods).reduce(
(methods, [name, fn]) => {
const method = (...args) => {
setValue(value => fn(value, ...args));
};
methods[name] = method;
return methods;
},
{}
),
[methods]
)
return [value, boundMethods];
}
上述只是最底层的hook封装办法,想要解决业务逻辑还需要其他通用Hook来调用。
既然Hook是解决状态和关联行为的复用能力,那么从状态和行为两个方面下手:状态是变量,变量是简单的数据结构,而行为其实是过程,是一些更为通用Hook和方法的组合。
拿例子里用到的User数组类型来对数据结构进行封装,看代码
const arrayMethods = {
push(list, item) {
return list.concat(item)
},
pop(list) {
return list.slice(0, -1);
},
slice(list, start, end) {
return list.slice(start, end);
}
...
}
export const useArray = (initialValue = []) => {
assert(Array.isArray(initialValue), 'initialValue must be an array');
return useMethods(initialValue, arrayMethods);
}
再来看看行为的封装,拿上面的例子的请求数据进行封装
const useTaskPending = task => {
const [pending, setPending] = useState(false);
const taskWithPending = useCallback(
async (...args) => {
setPending(true);
const result = await task(...args);
setPending(false);
return result;
},
[task, setPending]
);
return [taskWithPending, pending];
}
除了过程的封装,还要针对结果进行状态同步管理
const useTaskPendingState = (task, state) => {
const [taskWithPending, pending] = useTaskPending(task);
const callAndStore = useCallback(
async () => {
const result = await taskWithPending();
state(result);
},
[taskWithPending, state]
)
return [callAndStore, pending];
}
现在我们将上述例子里的hook过程经过数据结构和过程封装以后,代码修改成如下的结果:
const useUserList = () => {
const [users, {push, pop, slice}] = useArray([]);
const [load, pending] = useTaskPendingState(getListUser, state);
return [users, {pending, load, addUser: push}]
}
再看看上述的表达,极简到震惊脸。而且useArray和useTaskPendingState可以重复被使用,越是通用底层的Hook,越是复用性强。
品到这里,可以深刻感受到hook的“香”在哪里,只要你理解到Hook解决的问题是什么?本质是复用什么?针对这个复用我们可以极致到什么程度?
其实目前业务代码里不太可能按照上述分析的方式进行书写,但是可以通过分析感受到Hook的使用原则,以及面对工作里业务逻辑怎么思考Hook的使用和封装。现阶段,能够写出第一个例子的情况已经很优秀了。
Hook的实现原理
hook的使用上面有两个原则
- 不要再循环、条件判断和嵌套函数里使用
- 只在react的函数式组件里使用
原理部分拿useState举例:状态管理都是关于数组
先看useState怎么工作的?
function renderFunction() {
const [firstName, setFirstName] = useState('qingming');
const [lastName, setLastName] = useState('dong');
return (
<Button onClick={() => setFirstName('damu')}>SET</Button>
)
}
代码逻辑不解释,那么React怎么去工作的呢?注意上面的黄色文字,模拟实现步骤:
- 创建两个空数组,state一个,setter一个,一个下标变量cursor
- 第一次渲染
- cursor = 0 state = ['first'] setters = [setFirstName]
- cursor = 1 state = ['first', 'last'] setters = [setFirstName, setLastName]
- 第二次渲染(第N次渲染)每一次重新渲染cursor都会重置为0
- cursor = 0 state = ['first'] setters = [setFirstName]
- cursor = 1 state = ['first', 'last'] setters = [setFirstName, setLastName]
- 事件处理
- setFirstName('damu')
- 找到setter里对应的方法,记录当前的cursor
- 找到state里对应cursor的状态值
- 替换state里对应位置的值
useState底层实现大概可以如下:
let state = []
let setters = []
let cursor = 0
let firstRun = true
// 实现cursor与setter关联,调用方法直接找到cursor对象的state进行修改
function createSetter(cursor) {
return function setterWithCursor(newVal) {
state[cursor] = newVal
}
}
export function useState(initValue){
// 初次渲染进行数组的更新,其他情况下不需要操作数组
if (firstRun) {
state.push(initValue)
setters.push(createSetter(cursor))
firstRun = false
}
const value = state(cursor)
const setter = setters(cursor)
cursor++
return [value, setter];
}
最后解释一下hook为什么不能再循环、条件语句和函数嵌套里使用。拿条件语句来看
let firstRender = true;
function RenderFunctionComponent() {
let initName;
if(firstRender){
[initName] = useState("qingming");
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState("dong");
return (
<Button onClick={() => setFirstName("damu")}>Fred</Button>
);
}
将上面的代码逻辑,使用useState底层实现来翻译一遍是这样的:
- 第一次执行
- cursor = 0 state = ['first'] setters = [setter_0]
- cursor = 1 state = ['first', 'first'] setters = [setter_0, setFirstName]
- cursor = 2 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]
- 第二次执行,不再执行条件语句,直接执行两次初始化
- cursor = 0 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]
- cursor = 1 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]
此时我们的firstName = lastName = 'first',而我们的代码意思确实firstName = 'first' !== lastName = 'last'。
由此可见,数组管理的严谨性,为什么不灵活管理这些下标呢?可能有性能问题吗?看未来会不会变化? 相信梳理到这里,针对hook你应该理解了,更多Hook api和实现底层快去实践和学习吧。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!