# 从源码看Hooks
React 17-alpha中新增了新功能:Hooks。总结他的功能就是:让FunctionalComponent具有ClassComponent的功能。
import React, { useState, useEffect } from 'react'
function FunComp(props) {
const [data, setData] = useStat('initialState')
function handleChange(e) {
setData(e.target.value)
}
useEffect(() => {
subscribeToSomething()
return () => {
unSubscribeToSomething()
}
})
return (
<input value={data} onChange={handleChange} />
)
}
按照Dan的说法,设计Hooks主要是解决ClassComponent的几个问题:
- 很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深
- 会产生巨大的组件(指很多代码必须写在类里面)
- 类组件很难理解,比如方法需要
bind,this指向不明确
这些确实是存在的问题,比如我们如果用了react-router+redux+material-ui,很可能随便一个组件最后export出去的代码是酱紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
这就是一个4层嵌套的HOC组件
同时,如果你的组件内事件多,那么你的constructor里面可能会酱紫:
class MyComponent extends React.Component {
constructor() {
// initiallize
this.handler1 = this.handler1.bind(this)
this.handler2 = this.handler2.bind(this)
this.handler3 = this.handler3.bind(this)
this.handler4 = this.handler4.bind(this)
this.handler5 = this.handler5.bind(this)
// ...more
}
}
虽然最新的class语法可以用handler = () => {}来快捷绑定,但也就解决了一个声明的问题,整体的复杂度还是在的。
然后还有在componentDidMount和componentDidUpdate中订阅内容,还需要在componentWillUnmount中取消订阅的代码,里面会存在很多重复性工作。最重要的是,在一个ClassComponent中的生命周期方法中的代码,是很难在其他组件中复用的,这就导致了了代码复用率低的问题。
还有就是class代码对于打包工具来说,很难被压缩,比如方法名称。
更多详细的大家可以去看ReactConf的视频 (opens new window),我这里就不多讲了,这篇文章的主题是从源码的角度讲讲Hooks是如何实现的
# 先来了解一些基础概念
首先useState是一个方法,它本身是无法存储状态的
其次,他运行在FunctionalComponent里面,本身也是无法保存状态的
useState只接收一个参数initial value,并看不出有什么特殊的地方。所以React在一次重新渲染的时候如何获取之前更新过的state呢?
在开始讲解源码之前,大家先要建立一些概念:
# React Element
JSX翻译过来之后是React.createElement,他最终返回的是一个ReactElement对象,他的数据解构如下:
const element = {
$$typeof: REACT_ELEMENT_TYPE, // 是否是普通Element_Type
// Built-in properties that belong on the element
type: type, // 我们的组件,比如`class MyComponent`
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
这其中需要注意的是type,在我们写<MyClassComponent {...props} />的时候,他的值就是MyClassComponent这个class,而不是他的实例,实例是在后续渲染的过程中创建的。
# Fiber
每个节点都会有一个对应的Fiber对象,他的数据解构如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null; // 就是ReactElement的`$$typeof`
this.type = null; // 就是ReactElement的type
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
在这里我们需要注意的是this.memoizedState,这个key就是用来存储在上次渲染过程中最终获得的节点的state的,每次执行render方法之前,React会计算出当前组件最新的state然后赋值给class的实例,再调用render。
所以很多不是很清楚React原理的同学会对React的ClassComponent有误解,认为state和lifeCycle都是自己主动调用的,因为我们继承了React.Component,它里面肯定有很多相关逻辑。事实上如果有兴趣可以去看一下Component的源码,大概也就是100多行,非常简单。所以在React中,class仅仅是一个载体,让我们写组件的时候更容易理解一点,毕竟组件和class都是封闭性较强的
# 开始讲源码
既然我们知道了存储state和props的是在Fiber对象上,那么我们就不需要担心上面提到的FunctionalComponent无法存储状态的问题了,所以接下去就让我们一步步看看useState具体做了啥吧。
由于是alpha版本,所以在gitgub上没找到源码,似乎React团队的发布策略是正式版发布才会把代码更新到master分支。所以只能看打包过的源码了
// react中的useState
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
var dispatcher = ReactCurrentOwner.currentDispatcher;
!(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
return dispatcher;
}
到这里React中的useState就结束了,剩下的就是ReactCurrentOwner.currentDispatcher是个啥,这个东西会在react-dom中渲染的过程中变化,所以我们要到react-dom中寻找
在renderRoot中我们找到了:
function renderRoot(root, isYieldy, isExpired) {
// ...
ReactCurrentOwner$2.currentDispatcher = Dispatcher;
// workLoop
ReactCurrentOwner$2.currentDispatcher = null;
resetHooks();
}
var Dispatcher = {
readContext: readContext,
useCallback: useCallback,
useContext: useContext,
useEffect: useEffect,
useImperativeMethods: useImperativeMethods,
useLayoutEffect: useLayoutEffect,
useMemo: useMemo,
useMutationEffect: useMutationEffect,
useReducer: useReducer,
useRef: useRef,
useState: useState
};
renderRoot这个方法是React开始渲染整棵树的入口,讲起来就超级长了,所以有机会做React源码分析的时候再详细讲,可以关注一波获取最新动态。在这里我们只需要知道,在开始渲染的时候给他设置了currentDispatcher,而渲染的过程中是会调用每个节点的,也就是说执行到我们有Hooks的组件的时候自然会调用Dispatcher.useState
这里还有一个方法叫resetHooks,我们后面再讲。
useState的代码:
function useState(initialState) {
return useReducer(basicStateReducer,
// useReducer has a special case to support lazy useState initializers
initialState);
}
useReducer的代码较长,我放在gist里
这里主要做了什么呢?首先要找到workInProgressHook,这个就跟Fiber跟新的时候的workInProgress类似,指代的是当前正在工作的对象。在这里就是执行到了哪个useState,要找到他对应的Hook对象,数据结构如下:
{
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null
};
跟Fiber对象一样,他也有一个memoizedState,在使用Hooks的时候,我们把ClassComponent中以对象存储的state,拆成一个个key对应的Hook的关系,所以每个Hook会对应一个memoizedState
找到workInProgressHook使用的是createWorkInProgressHook,在React中存在着这种current => workInProgress的关系,这个一下两下讲不清楚,就不在这详细展开了。这里只需要记住两个关键点:
- 如果第一次渲染,每个
workInProgressHook都会重新创建 - 如果之前有创建过,会从之前保存的上面复制一个
然后reducer会在这个workInProgressHook上执行更新,这部分代码也比较复杂,就不详细展开了,直接讲一下重点:
queue是调用useState返回的那个方法的时候生成的,我们可能一次性调用很多次,所以queue是个链状数据结构
# updateFunctionalComponent
看到这里我们先停一下,我们先来了解一下什么时候才会执行到useState,答案就是在FunctionalComponent被执行的时候,那么也就是updateFunctionalComponent的时候(这个也有机会分析源码的时候再详细讲)。在这里我们看到这么一句代码:
// Component就是FunctionalComponent本身
// nextProps就是新的props
nextChildren = finishHooks(Component, nextProps, nextChildren, context);
finishHooks代码如下:
didScheduleRenderPhaseUpdate在调用useState返回的更新方法的时候会设置为true,我们先默认他为true
# 注意
- 目前
react-hot-loader不能和hooks一起使用,详情 (opens new window)