开篇有奖
如果你最近一年出去面过试,很可能面临这些问题:
- react 16到底做了哪些更新;
- react hooks用过么,知道其原理么;
第一个问题如果你提到了Fiber reconciler,fiber,链表,新的什么周期,可能在面试官眼里这仅仅是一个及格的回答。以下是我整理的,自我感觉还良好的回答:
分三步:
- react作为一个ui库,将前端编程由传统的
命令式
编程转变为声明式
编程,即所谓的数据驱动视图,但如果简单粗暴的操作,比如讲生成的html直接采用innerHtml替换,会带来重绘重排
之类的性能问题。为了尽量提高性能,React团队引入了虚拟dom,即采用js对象来描述dom树,通过对比前后两次的虚拟对象,来找到最小的dom操作(vdom diff),以此提高性能。 - 上面提到的vDom diff,在react 16之前,这个过程我们称之为
stack reconciler
,它是一个递归的过程,在树很深的时候,单次diff时间过长会造成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。所以为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将以前的stack reconciler替换为fiber reconciler
。采用增量式渲染
。引入了任务优先级(expiration)
和requestIdleCallback
的循环调度算法,简单来说就是将以前的一根筋diff更新,首先拆分成两个阶段:reconciliation
与commit
;第一个reconciliation
阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期做小任务diff。然后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。 - 由于reconciliation是可以被打断的,且存在任务优先级的问题,所以会导致commit前的一些生命周期函数多次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官方已申明这些问题,并将其标记为unsafe,在React17中将会移除
- 由于每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存
workInprogressTree
(future vdom) 与oldTree
(current vdom)两个链表,两个链表相互引用。这无形中又解决了另一个问题,当workInprogressTree生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在。
以上就是我上半年面试自己不断总结迭代出的答案,希望能对你有所启发。
接着来回答第二个问题,hooks本质是什么?
hooks 为什么出现
当我们在谈论React这个UI库时,最先想到的是,数据驱动视图,简单来讲就是下面这个公式:
view = fn(state)
我们开发的整个应用,都是很多组件组合而成,这些组件是纯粹,不具备扩展的。因为React不能像普通类一样直接继承,从而达到功能扩展的目的。
出现前的逻辑复用
在用react实现业务时,我们复用一些组件逻辑去扩展另一个组件,最常见比如Connect,Form.create, Modal。这类组件通常是一个容器,容器内部封装了一些通用的功能(非视觉的占多数),容器里面的内容由被包装的组件自己定制,从而达到一定程度的逻辑复用。
在hooks 出现之前,解决这类需求最常用的就两种模式:HOC高阶组件
和 Render Props
。
高阶组件类似于JS中的高阶函数,即输入一个函数,返回一个新的
函数, 比如React-Redux中的Connect:
class Home extends React.Component {
// UI
}
export default Connect()(Home);
高阶组件由于每次都会返回一个新的组件,对于react来说,这是不利于diff和状态复用的,所以高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。而Render Props模式的出现就完美解决了这个问题,其原理就是将要包裹的组件作为props属性传入,然后容器组件调用这个属性,并向其传参, 最常见的用props.children
来做这个属性。举个?:
class Home extends React.Component {
// UI
}
<Route path = "/home" render= {(props) => <Home {...props} } />
更多关于render 与 Hoc,可以参见以前写的一片弱文:React进阶,写中后台也能写出花
已存方案的问题
嵌套地狱
上面提到的高阶组件和RenderProps, 看似解决了逻辑复用的问题,但面对复杂需求时,即一个组件需要使用多个复用包裹时,两种方案都会让我们的代码陷入常见的嵌套地狱
, 比如:
class Home extends React.Component {
// UI
}
export default Connect()(Form.create()(Home));
除了嵌套地狱的写法让人困惑,但更致命的深度会直接影响react组件更新时的diff性能。
函数式编程的普及
Hooks 出现前的函数式组件只是以模板函数存在,而前面两种方案,某种程度都是依赖类组件来完成。而提到了类,就不得不想到下面这些痛点:
- JS中的this是一个神仙级的存在, 是很多入门开发趟不过的坑;
- 生命周期的复杂性,很多时候我们需要在多个生命周期同时编写同一个逻辑
- 写法臃肿,什么constructor,super,render
所以React团队回归view = fn(state)
的初心,希望函数式组件也能拥有状态管理的能力,让逻辑复用变得更简单,更纯粹。
架构的更新
为什么在React 16前,函数式组件不能拥有状态管理?其本质是因为16以前只有类组件在更新时存在实例,而16以后Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力,下面会详讲。
hooks 的本质
有可能,你听到过Hooks的本质就是闭包
。但是,如果满分100的话,这个说法最多只能得60分。
哪满分答案是什么呢?闭包 + 两级链表
。
下面就来一一分解, 下面都以useState来举例剖析。
闭包
JS 中闭包是难点,也是必考点,概括的讲就是:
闭包是指有权访问另一个函数作用域中变量或方法
的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法
传递到外部。
export default function Hooks() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(18);
const self = useRef(0);
const onClick = useCallback(() => {
setAge(19);
setAge(20);
setAge(21);
}, []);
console.log('self', self.current);
return (
<div>
<h2>年龄: {age} <a onClick={onClick}>增加</a></h2>
<h3>轮次: {count} <a onClick={() => setCount(count => count + 1)}>增加</a></h3>
</div>
);
}
以上面的示例来讲,闭包就是setAge这个函数,何以见得呢,看组件挂载阶段hook执行的源码:
// packages/react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init) {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg;
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
});
// 重点
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
return [hook.memoizedState, dispatch];
}
所以这个函数就是mountReducer,而产生的闭包就是dispatch函数(对应上面的setAge),被闭包引用的变量就是currentlyRenderingFiber
与 queue
。
- currentlyRenderingFiber: 其实就是workInProgressTree, 即更新时链表当前正在遍历的fiber节点(源码注释:
The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook
); - queue: 指向hook.queue,保存当前hook操作相关的reducer 和 状态的对象,其来源于mountWorkInProgressHook这个函数,下面重点讲;
这个闭包将 fiber节点与action, action 与 state很好的串联起来了,举上面的例子就是:
- 当点击增加执行setAge, 执行后,新的state更新任务就储存在fiber节点的hook.queue上,并触发更新;
- 当节点更新时,会遍历queue上的state任务链表,计算最终的state,并进行渲染;
ok,到这,闭包就讲完了。
第一个链表:hooks
在ReactFiberHooks文件开头声明currentHook变量的源码有这样一段注释。
/*
Hooks are stored as a linked list on the fiber's memoizedState field.
hooks 以链表的形式存储在fiber节点的memoizedState属性上
The current hook list is the list that belongs to the current fiber.
当前的hook链表就是当前正在遍历的fiber节点上的
The work-in-progress hook list is a new list that will be added to the work-in-progress fiber.
work-in-progress hook 就是即将被添加到正在遍历fiber节点的hooks新链表
*/
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
从上面的源码注释可以看出hooks链表与fiber链表是极其相似的;也得知hooks 链表是保存在fiber节点的memoizedState属性的, 而赋值是在renderWithHooks函数具体实现的;
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
// 获取当前节点的hooks 链表;
nextCurrentHook = current !== null ? current.memoizedState : null;
// ...省略一万行
}
有可能代码贴了这么多,你还没反应过来这个hooks 链表具体指什么?
其实就是指一个组件包含的hooks, 比如上面示例中的:
const [count, setCount] = useState(0);
const [age, setAge] = useState(18);
const self = useRef(0);
const onClick = useCallback(() => {
setAge(19);
setAge(20);
setAge(21);
}, []);
形成的链表就是下面这样的:
所以在下一次更新时,再次执行hook,就会去获取当前运行节点的hooks链表;
const hook = updateWorkInProgressHook();
// updateWorkInProgressHook 就是一个纯链表的操作:指向下一个 hook节点
到这 hooks 链表是什么,应该就明白了;这时你可能会更明白,为什么hooks不能在循环,判断语句中调用,而只能在函数最外层使用,因为挂载或则更新时,这个队列需要是一致的,才能保证hooks的结果正确。
第二个链表:state
其实state 链表不是hooks独有的,类操作的setState也存在,正是由于这个链表存在,所以有一个经(sa)典(bi)React 面试题:
setState为什么默认是异步,什么时候是同步?
结合实例来看,当点击增加会执行三次setAge
const onClick = useCallback(() => {
setAge(19);
setAge(20);
setAge(21);
}, []);
第一次执行完dispatch后,会形成一个状态待执行任务链表:
如果仔细观察,会发现这个链表还是一个环
(会在updateReducer后断开), 这一块设计相当有意思,我现在也还没搞明白为什么需要环,值得细品,而建立这个链表的逻辑就在dispatchAction函数中。
function dispatchAction(fiber, queue, action) {
// 只贴了相关代码
const update = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// 触发更新
scheduleWork(fiber, expirationTime);
}
上面已经说了,执行setAge 只是形成了状态待执行任务链表,真正得到最终状态,其实是在下一次更新(获取状态)时,即:
// 读取最新age
const [age, setAge] = useState(18);
而获取最新状态的相关代码逻辑存在于updateReducer中:
function updateReducer(reducer, initialArg,init?) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// ...隐藏一百行
// 找出第一个未被执行的任务;
let first;
// baseUpdate 只有在updateReducer执行一次后才会有值
if (baseUpdate !== null) {
// 在baseUpdate有值后,会有一次解环的操作;
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
// do while 遍历待执行任务的状态链表
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// 优先级不足,先标记,后面再更新
} else {
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
// Process this update.
if (update.eagerReducer === reducer) {
// 简单的说就是状态已经计算过,那就直接用
newState = update.eagerState;
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
// 终止条件是指针为空 或 环已遍历完
} while (update !== null && update !== first);
// ...省略100行
return [newState, dispatch];
}
}
最后来看,状态更新的逻辑似乎是最绕的。但如果看过setState,这一块可能就比较容易。至此,第二个链表state就理清楚了。
结
读到这里,你就应该明白hooks 到底是怎么实现的:
闭包加两级链表
虽然我这里只站在useState这个hooks做了剖析,但其他hooks的实现基本类似。
另外分享一下在我眼中的hooks,与类组件到底到底是什么联系:
- useState: 状态的存储及更新,状态更新会触发组件更新,和类的state类似,只不过setState更新时是采用Object.assign(oldstate, newstate); 而useState的set是直接替代式的
- useEffect: 类似于以前的componentDidMount 和 componentDidUpdate生命周期钩子(即render 执行后,再执行Effect, 所以当组件与子组件都有Effect时,
子组件的Effect先执行
), Update需要deps依赖来唤起; - useRefs: 用法类似于以前直接挂在类的this上,像this.selfCount 这种,用于变量的临时存储,而又不至于受函数更新,而被重定义;与useState的区别就是,refs的更新不会导致Rerender
- useMemo: 用法同以前的componentWillReceiveProps与getDerivedStateFromProps中,根据state和props计算出一个新的属性值:计算属性
- useCallback: 类似于类组件中constructor的bind,但比bind更强大,避免回调函数每次render造成回调函数重复声明,进而造成不必要的diff;但需要注意deps,不然会掉进
闭包
的坑 - useReducer: 和redux中的Reducer相像,和useState一样,执行后可以唤起
Rerender
第一次写源码解析,出发点主要两点:
- 最近半年自己在react确实下了一些功夫,有一个输出也是为了自己以后更好的回忆;
- 网上太多的人用一个闭包来概括hooks,我觉得这是对技术的亵渎(个人意见);
文章中若有不详或不对之处,欢迎斧正;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。