# 从源码看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)