设计思想
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,
但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
好处:
1、跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
2、类定义更为复杂:
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注this的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
3、状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意事项
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
- 可以基于这条约束,窥探猜测下react的hook机制实现原理
- 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
- 不能在useEffect中使用useState,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
猜测下 在函数里使用 this.state
即赋予函数 状态变量的能力,这种能力可以想到是如何实现的?
===> 闭包。因为 对象是有行为的数据;而闭包是有数据的行为 哈哈
理解hook
loading逻辑在组件里作为通用逻辑,难以复用,即如何实现组件的逻辑复用?
1、render props
2、HOC
底层都是高阶函数
弊端:嵌套地狱
React Hooks 则可以完美解决上面的嵌套问题,它拥有下面这几个特性。
- 多个状态不会产生嵌套,写法还是平铺的
- 允许函数组件使用 state 和部分生命周期
- 更容易将组件的 UI 与状态分离
funciton hook VS class state
- function state 的粒度更细,class state 过于无脑。
- function state 保存的是快照,class state 保存的是最新值。
- 引用类型的情况下,class state 不需要传入新的引用,而 function state 必须保证是个新的引用。
在 1s 内频繁点击10次按钮,上面两种写法的执行表现是一样吗?
在第一个例子中,连续点击十次,页面上的数字会从0增长到10。
而第二个例子中,连续点击十次,页面上的数字只会从0增长到1。
===> 主要是引用和闭包的区别
class 组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。
function component 里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了1次。
问题解决:
useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个。
useRef 有下面这几个特点:
- useRef 是一个只能用于函数组件的方法。
- useRef 是除字符串 ref、函数 ref、createRef 之外的第四种获取 ref 的方法。
- useRef 在渲染周期内永远不会变,因此可以用来引用某些数据。
- 修改 ref.current 不会引发组件重新渲染。
想让函数组件也是从0加到10, 传入对象每次对象更新行不行,不行,即使 state 是个对象,但每次更新的时候,要传一个新的引用进去,这样的引用依然是没有意义
const [state, setState] = useState({ count: 0 })
setState({
count: state.count + 1
})
useEffect
useEffect 比较重要,它主要有这几个作用:
- 代替部分生命周期,如 componentDidMount、componentDidUpdate、componentWillUnmount。
- 更加 reactive,类似 mobx 的 reaction 和 vue 的 watch。
- 从命令式变成声明式,不需要再关注应该在哪一步做某些操作,只需要关注依赖数据。
- 通过 useEffect 和 useState 可以编写一系列自定义的 Hook。
useLayoutEffect
useEffect VS useLayoutEffect:
- useEffect 不会 block 浏览器渲染,而 useLayoutEffect 会。
- useEffect 会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行。
实现原理
hook属性依赖于组件,一个函数组件有唯一的hook属性
_hooks 属性里面保存了我们执行一个组件的时候,里面所有 Hook 方法相关的信息。
组件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放进 _list => currentIndex++ => 渲染结束
组件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,获取 _list[currentIndex]=> currentIndex++ => 重复上面步骤 => 更新结束
为什么 hooks 方法不能放在条件语句里面了。因为每次进入这个函数的时候,都是要和 currentIndex 一一匹配的,如果更新前后少了一个 Hook 方法,那么就完全对不上了,导致出现大问题。
- 每个组件实例上挂载一个 _hooks 属性,保证了组件之间不会影响。
- 每当遇到一个 hooks 方法,就将其 push 到 currentComponent._hooks._list 中,且 currentIndex 加一。
- 每次渲染进入一个组件的时候,都会从将 currentIndex 重置为 0 。遇到 hooks 方法时,currentIndex 重复第二步。这样可以把 currentIndex 和 currentComponent._hooks._list 中的对应项匹配起来,直接取上次缓存的值。
- 函数组件每次重新执行后,useState 中还能保持上一次的值,就是来自于步骤3中的缓存。
- 由于依赖了 currentComponent 实例,所以 hooks 不能用于普通函数中。
useEffect
useEffect(() => {
console.log(count);
}, [count]);
- 有两个参数 callback 和 dependencies 数组
- 如果 dependencies 不存在,那么 callback 每次 render 都会执行
- 如果 dependencies 存在,只有当它发生了变化, callback 才会执行
# useState
var _state; // 把 state 存储在外面
function useState(initialValue) {
_state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
# deps依赖
let _deps; // _deps 记录 useEffect 上一次的 依赖
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
# 比较depArray 依赖,当发生改变时候 再执行cb
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
问题:useEffect的执行
是每次render都会调用hook函数,但是函数绑定的cb不一定都执行
我们已经实现了可以工作的 useState 和 useEffect。
但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。(闭包内的单例变量是一个引用)所以外界通过useXX得到的数据,内部依赖使用的同一个,那么外界就会同更新
const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
使用数组,来解决 Hooks 的复用问题
关键
- 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
- 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。
A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A:共享同一个 memoizedState,共享同一个顺序。
Q:“Capture Value” 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
真正react实现
虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。
单链表顺序串联hook
React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
type Effect = {
tag: HookEffectTag, # effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
// 看吧 果然是 帮用户订阅数据变化,再将回调放到生命周期里
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
memoizedState和cursor存在哪里
我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
和react生命周期结合
var useState = (initState) => {
let data = initState;
const dispatch = (newData) => {
data = newData;
}
return [data, dispatch];
}
在每次渲染的过程中,函数都会被重新调用而重新初始化,这并不是我们期望的。因此我们需要一个数据结构对每次执行的 state 进行存储,同时还需要区分初始化和更新状态的不同执行方式。
- 对于同一个 state,在 mounted 和 updated 的不同状态下,hook 是如何共享的 =>挂在FiberNode节点下
- dispatchAction 中的 storeUpdateActions 和 updateState 中的 updateMemorizedState 是如何运作的
==> 在我们实际场景中,普遍存在着一个更新周期中多次调用的 re-render 行为
function Demo () {
const [count, setCount] = useState(0);
return <div onClick={() => {
setCount(count++);
setCount(count++)
setCount(count++)
}}>{count}<div>
}
实际上组件不会渲染3次,而是根据最后的状态渲染。
这意味着在调用 dispatch 更新的时候,我们并不是直接进行更新逻辑,而是将其存储进行update时统一的调度更新,根据执行的有序性,我们采用队列存储一个 hook 的多次调用。
采用链表形式进行存储:存储同个组件内的多个hook;(包括相同的 Hook 多次调用)
type Hook = {
memoizedState: any, // 上一次完整更新之后的最终状态值
queue: UpdateQueue<any, any> | null, // 更新队列
next: any // 下一个 hook
}
整个链表在 mounted 的时候构建,在 update 时按照顺序执行。
因此不能在条件循环等场景下使用。
useEffect同理:
在mount时创建一个 hook 对象,新建一个 effectQueue,以单向链表的方式存储每一个 effect,将 effectQueue 绑定在 fiberNode 上,并在完成渲染之后依次执行该队列中存储的 effect 函数
与 useState 不同的一点是,useEffect 拥有一个 deps 依赖数组。当依赖数组变更的时候,一个新的副作用函数会被追加至链尾。