长文预警,若是以为前戏太长可直接从第三章开始看~javascript
本文基于 React 16.8.6 进行讲解css
使用的示例代码:html
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 调用三次setCount便于查看更新队列的状况
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
)
}
复制代码
代码很是简单,点击button使count+3,count的值会显示在屏幕上。前端
本节参考:How Are Function Components Different from Classes?java
本节主要概念:react
咱们来看一个简单的Greeting组件,它支持定义成类和函数两种性质。在使用它时,不用关心他是如何定义的。git
// 是类仍是函数 —— 无所谓
<Greeting /> // <p>Hello</p>
复制代码
若是 Greeting
是一个函数,React 须要调用它。github
// Greeting.js
function Greeting() {
return <p>Hello</p>;
}
// React 内部
const result = Greeting(props); // <p>Hello</p>
复制代码
但若是 Greeting
是一个类,React 须要先将其实例化,再调用刚才生成实例的 render
方法:react-native
// Greeting.js
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// React 内部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
复制代码
React经过如下方式来判断组件的类型:数组
// React 内部
class Component {}
Component.prototype.isReactComponent = {};
// 检查方式
class Greeting extends React.Component {}
console.log(Greeting.prototype.isReactComponent); // {}
复制代码
本节主要概念(了解便可):
Fiber(可译为丝)比线程还细的控制粒度,是React 16中的新特性,旨在对渲染过程作更精细的调整。
产生缘由:
mount/update
,没法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就没法当即获得处理,影响体验React Fiber的方式:
把一个耗时长的任务分红不少小片,每个小片的运行时间很短,虽然总时间依然很长,可是在每一个小片执行完以后,都给其余任务一个执行的机会,这样惟一的线程就不会被独占,其余任务依然有运行的机会。
React Fiber把更新过程碎片化,执行过程以下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其余紧急任务要作,若是没有就继续去更新,若是有紧急任务,那就去作紧急任务。
维护每个分片的数据结构,就是Fiber。
有了分片以后,更新过程的调用栈以下图所示,中间每个波谷表明深刻某个分片的执行过程,每一个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情
Fiber的调度过程分为如下两个阶段:
render/reconciliation阶段 — 里面的全部生命周期函数均可能被执行屡次,因此尽可能保证状态不变
Commit阶段 — 不能被打断,只会执行一次
Fiber的增量更新须要更多的上下文信息,以前的vDOM tree显然难以知足,因此扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)
与Fiber有关的全部代码位于packages/react-reconciler中,一个Fiber节点的详细定义以下:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
// Instance
this.tag = tag; this.key = key; this.elementType = null;
this.type = null; 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.contextDependencies = null; this.mode = mode;
// Effects
/** 细节略 **/
}
复制代码
咱们只关注一下this.memoizedState
这个key
用来存储在上次渲染过程当中最终得到的节点的state
,每次render
以前,React会计算出当前组件最新的state
而后赋值给组件,再执行render
。— 类组件和使用useState的函数组件均适用。
记住上面这句话,后面还会常常提到memoizedState
有关Fiber每一个key的具体含义能够参见源码的注释
本节主要概念:
因为React体系的复杂性以及目标平台的多样性。react
包只暴露一些定义组件的API。绝大多数React的实现都存在于 渲染器(renderers)中。
react-dom
、react-dom/server
、 react-native
、 react-test-renderer
、 react-art
都是常见的渲染器
这就是为何无论目标平台是什么,react
包都是可用的。从react
包中导出的一切,好比React.Component
、React.createElement
、 React.Children
和 Hooks
都是独立于目标平台的。不管运行React DOM,仍是 React DOM Server,或是 React Native,组件均可以使用一样的方式导入和使用。
因此当咱们想使用新特性时,react
和 react-dom
都须要被更新。
例如,当React 16.3添加了Context API,
React.createContext()
API会被React包暴露出来。 可是React.createContext()
其实并无_实现_ context。由于在React DOM 和 React DOM Server 中一样一个 API 应当有不一样的实现。因此createContext()
只返回了一些普通对象: **因此,若是你将react升级到了16.3+,可是不更新react-dom,那么你就使用了一个尚不知道Provider 和 Consumer类型的渲染器。**这就是为何老版本的react-dom
会报错说这些类型是无效的。
这就是setState
尽管定义在React包中,调用时却可以更新DOM的缘由。它读取由React DOM设置的this.updater
,让React DOM安排并处理更新。
Component.setState = function(partialState, callback) {
// setState所作的一切就是委托渲染器建立这个组件的实例
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
各个渲染器中的updater触发不一样平台的更新渲染
// React DOM 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
复制代码
至于updater的具体实现,就不是这里重点要讨论的内容了,下面让咱们正式进入本文的主题:React Hooks
本节主要概念:
全部的Hooks在React.js
中被引入,挂载在React对象中
// React.js
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactHooks';
复制代码
咱们进入ReactHooks.js
来看看,发现useState
的实现居然异常简单,只有短短两行
// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
复制代码
看来重点都在这个dispatcher
上,dispatcher
经过resolveDispatcher()
来获取,这个函数一样也很简单,只是将ReactCurrentDispatcher.current
的值赋给了dispatcher
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
复制代码
因此useState(xxx)
等价于 ReactCurrentDispatcher.current.useState(xxx)
看到这里,咱们回顾一下第一章第三小节所讲的React渲染器与setState,是否是发现有点似曾相识。
与updater是setState可以触发更新的核心相似,ReactCurrentDispatcher.current.useState
是useState
可以触发更新的关键缘由,这个方法的实现并不在react包内。下面咱们就来分析一个具体更新的例子。
以全文开头给出的代码为例。
咱们从Fiber调度的开始:ReactFiberBeginwork
来谈起
以前已经说过,React有能力区分不一样的组件,因此它会给不一样的组件类型打上不一样的tag, 详见shared/ReactWorkTags.js。
因此在beginWork的函数中,就能够根据workInProgess(就是个Fiber节点)上的tag值来走不一样的方法来加载或者更新组件。
// ReactFiberBeginWork.js
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
/** 省略与本文无关的部分 **/
// 根据不一样的组件类型走不一样的方法
switch (workInProgress.tag) {
// 不肯定组件
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
// 加载初始组件
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
// 函数组件
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 更新函数组件
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 类组件
case ClassComponent {
/** 细节略 **/
}
}
复制代码
下面咱们来找出useState发挥做用的地方。
mount过程执行mountIndeterminateComponent
时,会执行到renderWithHooks
这个函数
function mountIndeterminateComponent( _current, workInProgress, Component, renderExpirationTime, ) {
/** 省略准备阶段代码 **/
// value就是渲染出来的APP组件
let value;
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
/** 省略无关代码 **/
}
workInProgress.tag = FunctionComponent;
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}
复制代码
执行前: nextChildren = value
执行后: value= 组件的虚拟DOM表示
至于这个value是如何被渲染成真实的DOM节点,咱们并不关心,state值咱们已经经过renderWithHooks取到并渲染
点击一下按钮:此时count从0变为3
更新过程执行的是updateFunctionComponent函数,一样会执行到renderWithHooks这个函数,咱们来看一下这个函数执行先后发生的变化:
执行前: nextChildren = undefined
**执行后:**nextChildren=更新后的组件的虚拟DOM表示
一样的,至于这个nextChildren是如何被渲染成真实的DOM节点,咱们并不关心,最新的state值咱们已经经过renderWithHooks取到并渲染
因此,renderWithHooks
函数就是处理各类hooks逻辑的核心部分
ReactFiberHooks.js包含着各类关于Hooks逻辑的处理,本章中的代码均来自该文件。
在以前的章节有介绍过,Fiber中的memorizedStated
用来存储state
在类组件中state
是一整个对象,能够和memoizedState
一一对应。可是在Hooks
中,React并不知道咱们调用了几回useState
,因此React经过将一个Hook对象挂载在memorizedStated
上来保存函数组件的state
Hook对象的结构以下:
// ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
复制代码
重点关注memoizedState
和next
memoizedState
是用来记录当前useState
应该返回的结果的queue
:缓存队列,存储屡次更新行为next
:指向下一次useState
对应的Hook对象。结合示例代码来看:
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 调用三次setCount便于查看更新队列的状况
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
)
}
复制代码
第一次点击按钮触发更新时,memoizedState的结构以下
只是符合以前对Hook对象结构的分析,只是queue中的结构貌似有点奇怪,咱们将在第三章第2节中进行分析。
renderWithHooks的运行过程以下:
// ReactFiberHooks.js
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
// 若是current的值为空,说明尚未hook对象被挂载
// 而根据hook对象结构可知,current.memoizedState指向下一个current
nextCurrentHook = current !== null ? current.memoizedState : null;
// 用nextCurrentHook的值来区分mount和update,设置不一样的dispatcher
ReactCurrentDispatcher.current =
nextCurrentHook === null
// 初始化时
? HooksDispatcherOnMount
// 更新时
: HooksDispatcherOnUpdate;
// 此时已经有了新的dispatcher,在调用Component时就能够拿到新的对象
let children = Component(props, refOrContext);
// 重置
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
// 更新memoizedState和updateQueue
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.updateQueue = (componentUpdateQueue: any);
/** 省略与本文无关的部分代码,便于理解 **/
}
复制代码
核心: 建立一个新的hook,初始化state, 并绑定触发器
初始化阶段ReactCurrentDispatcher.current
会指向HooksDispatcherOnMount
对象
// ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
/** 省略其它Hooks **/
useState: mountState,
};
// 因此调用useState(0)返回的就是HooksDispatcherOnMount.useState(0),也就是mountState(0)
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 访问Hook链表的下一个节点,获取到新的Hook对象
const hook = mountWorkInProgressHook();
//若是入参是function则会调用,可是不提供参数
if (typeof initialState === 'function') {
initialState = initialState();
}
// 进行state的初始化工做
hook.memoizedState = hook.baseState = initialState;
// 进行queue的初始化工做
const queue = (hook.queue = {
last: null,
dispatch: null,
eagerReducer: basicStateReducer, // useState使用基础reducer
eagerState: (initialState: any),
});
// 返回触发器
const dispatch: Dispatch<BasicStateAction<S>,>
= (queue.dispatch = (dispatchAction.bind(
null,
//绑定当前fiber结点和queue
((currentlyRenderingFiber: any): Fiber),
queue,
));
// 返回初始state和触发器
return [hook.memoizedState, dispatch];
}
// 对于useState触发的update action来讲(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
复制代码
重点讲一下返回的这个更新函数 dispatchAction
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) {
/** 省略Fiber调度相关代码 **/
// 建立新的新的update, action就是咱们setCount里面的值(count+1, count+2, count+3…)
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 重点:构建query
// queue.last是最近的一次更新,而后last.next开始是每一次的action
const last = queue.last;
if (last === null) {
// 只有一个update, 本身指本身-造成环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
update.next = first;
}
last.next = update;
}
queue.last = update;
/** 省略特殊状况相关代码 **/
// 建立一个更新任务
scheduleWork(fiber, expirationTime);
}
复制代码
在dispatchAction
中维护了一份query的数据结构。
query是一个有环链表,规则:
因此每次插入新update时,就须要将原来的first指向query.last.next。再将update指向query.next,最后将query.last指向update.
下面结合示例代码来画图说明一下:
前面给出了第一次点击按钮更新时,memorizedState中的query值
其构建过程以下图所示:
即保证query.last始终为最新的action, 而query.last.next始终为action: 1
核心:获取该Hook对象中的 queue,内部存有本次更新的一系列数据,进行更新
更新阶段 ReactCurrentDispatcher.current
会指向HooksDispatcherOnUpdate
对象
// ReactFiberHooks.js
// 因此调用useState(0)返回的就是HooksDispatcherOnUpdate.useState(0),也就是updateReducer(basicStateReducer, 0)
const HooksDispatcherOnUpdate: Dispatcher = {
/** 省略其它Hooks **/
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
// 能够看到updateReducer的过程与传的initalState已经无关了,因此初始值只在第一次被使用
// 为了方便阅读,删去了一些无关代码
// 查看完整代码:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606
function updateReducer(reducer, initialArg, init) {
// 获取初始化时的 hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 开始渲染更新
if (numberOfReRenders > 0) {
const dispatch = queue.dispatch;
if (renderPhaseUpdates !== null) {
// 获取Hook对象上的 queue,内部存有本次更新的一系列数据
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
// 获取更新后的state
do {
const action = update.action;
// 此时的reducer是basicStateReducer,直接返回action的值
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
// 对 更新hook.memoized
hook.memoizedState = newState;
// 返回新的 state,及更新 hook 的 dispatch 方法
return [newState, dispatch];
}
}
}
// 对于useState触发的update action来讲(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
复制代码
单个hooks的更新行为全都挂在Hooks.queue下,因此可以管理好queue的核心就在于
结合示例代码:
[count, setCount] = useState(0)
时,建立一个queuesetCount(x)
,就dispach一个内容为x的action(action的表现为:将count设为x),action存储在queue中,之前面讲述的有环链表规则来维护updateReducer
中被调用,更新到memorizedState
上,使咱们可以获取到最新的state值。官方文档对于使用hooks有如下两点要求:
以useState为例:
和类组件存储state不一样,React并不知道咱们调用了几回useState
,对hooks的存储是按顺序的(参见Hook结构),一个hook对象的next指向下一个hooks。因此当咱们创建示例代码中的对应关系后,Hook的结构以下:
// hook1: const [count, setCount] = useState(0) — 拿到state1
{
memorizedState: 0
next : {
// hook2: const [name, setName] = useState('Star') - 拿到state2
memorizedState: 'Star'
next : {
null
}
}
}
// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState
复制代码
因此若是把hook1放到一个if语句中,当这个没有执行时,hook2拿到的state实际上是上一次hook1执行后的state(而不是上一次hook2执行后的)。这样显然会发生错误。
关于这块内容若是想了解更多能够看一下这篇文章
只有函数组件的更新才会触发renderWithHooks函数,处理Hooks相关逻辑。
仍是以setState为例,类组件和函数组件从新渲染的逻辑不一样 :
类组件: 用setState触发updater,从新执行组件中的render方法
函数组件: 用useState返回的setter函数来dispatch一个update action,触发更新(dispatchAction最后的scheduleWork),用updateReducer处理更新逻辑,返回最新的state值(与Redux比较像)
说了这么多,最后再简要总结下useState的执行流程~
初始化: 构建dispatcher函数和初始值
更新时:
ReactCurrentDispatcher.current
指向负责更新的DispatcheruseState
会被从新执行,在resolve dispatcher的阶段拿到了负责更新的dispatcher。useState
会拿到Hook对象,Hook.query
中存储了更新队列,依次进行更新后,便可拿到最新的statememorizedState
也被设置为最新的state关于咱们:
咱们是蚂蚁保险体验技术团队,来自蚂蚁金服保险事业群。咱们是一个年轻的团队(没有历史技术栈包袱),目前平均年龄92年(去除一个最高分8x年-团队leader,去除一个最低分97年-实习小老弟)。咱们支持了阿里集团几乎全部的保险业务。18年咱们产出的相互宝轰动保险界,19年咱们更有多个重量级项目筹备动员中。现伴随着事业群的高速发展,团队也在迅速扩张,欢迎各位前端高手加入咱们~
咱们但愿你是:技术上基础扎实、某领域深刻(Node/互动营销/数据可视化等);学习上善于沉淀、持续学习;性格上乐观开朗、活泼外向。
若有兴趣加入咱们,欢迎发送简历至邮箱:xingyan.hyx@antfin.com
本文做者:蚂蚁保险-体验技术组-星焰
掘金地址:STAR🌟