React Hooks 简介

React Hooks 是 React 16.8 以及之后版本的产物,React Hooks 就是一堆钩子函数,不同的钩子函数提供了不同的功能,React 通过这些钩子函数对函数型组件进行增强。Hooks 允许你在不编写 class 的情况下使用状态(state)和其他 React 特性。 你还可以构建自己的 Hooks, 跨组件共享可重用的有状态逻辑。

践行代数效应

React 核心成员 Sebastian Markbåge (Hooks 的发明者)曾说:我们在 React 中做的就是践行代数效应。

代数效应是函数式编程的一个概念,它所解决的一部分问题只在纯函数式编程中才存在,是为了分离一些副作用。代数效应实际上是一个比较领先的理念(写这篇博客为止),这种感觉就像你在写回调去解决异步问题,突然有一天有一个人告诉你有一个叫 async/await 的东西!
有人看到这可能就会说了,我不关心什么破代数效应,不要跟我讲大道理,直接给老子上代码就行了≦(._.)≧
代数效应看起来像是很高深的原理,下面我们用一些虚构的伪代码来解释一下什么是代数效应。

假设我们有一个 getName 是根据 id 获取用户信息的函数,还有一个 makeFriends 函数用来处理 getName 返回的用户信息

function getName(id) {// ... 获取用户数据
}function makeFriends(id1, id2) {const user1 = getName(id1)const user2 = getName(id2)return `${user1.name}和${user2.name}变成了好朋友`
}

现在我们将 getName 变成异步函数

async function getName(id) {// ... 异步操作
}async function makeFriends(id1, id2) {const user1 = await getName(id1)const user2 = await getName(id2)return `${user1.name}和${user2.name}变成了好朋友`
}

可以看到,由于 getName 函数变成了异步函数,导致 makeFriends 也需要变成异步函数去获取 getName 返回的数据,getName 变成异步函数破坏了 makeFriends 函数的同步特性。但是其实以正常编程思想,我才不想关注 getName 是怎样的实现的,我只在乎 getName 返回的数据。

这其实就是 getName 变成异步函数导致的副作用。
我们尝试虚构一个类似 try…catch 的语法 —— try…handle 和两个操作符 perform、resume 去分离一下这样的副作用

function getName(id) {const user = perform axios.get(id)return user
}function makeFriends(id1, id2) {const user1 = getName(id1)const user2 = getName(id2)return `${user1.name}和${user2.name}变成了好朋友`
}try {makeFriends('9527', '9528')
} handle (user) {if (user) {resume with user} else {resume with {name: '毛小星'}}
}

当 makeFriends 执行到 getName 方法的时候,会执行 perform 后面的语句,perform 跳出当时的执行栈,try…handle 会捕获 perform 执行的结果,这就是一个效应。这种语法看起来非常像 try…catch,但是一旦 catch 到了 Error,那么当前执行的这条逻辑就完成了,当前的调用栈就销毁了,那么我们能不能使用一种语法“回到”之前的执行逻辑中去呢?try…handle 让这种场景成为了可能,handle 捕获了 perform 的执行结果后,依然会捕获下一个 perform。
上面这段伪代码就是代数效应的思想,是不是很简单,其实代数效应就是将副作用从函数中分离,让函数变得更加纯粹一些,这也是函数式编程的核心思想。

代数效应在 React 中的实践

React 16.8 中的 Hooks 就是在践行代数效应,像 useState、useReducer、useRef 等,我们不需要关注函数式组件中的 state 在 Hooks 中是如果保存的,React 会为我们处理。你可以把 useState 看做成是一个 perform State(),这个效应就被 React 处理了,这样我们就直接使用 useState 返回的 state,编写我们的业务逻辑即可。
下面我们看看 useState 的基本使用

useState 的基本使用

在 16.8 之前的版本,函数型组件基本只负责展示数据,不负责状态的保存。useState 的出现就可以让函数型组件保存状态了,下面我们来看看 useState 是怎样的使用的

  1. useState 可以接收一个初始的值
  2. 返回值为数组,数组中存储状态值和更改状态值的方法
  3. useState 方法可以被调用多次,用来保存不同状态值
  4. 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值的情况
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'const userMap = new Map([['9527', { name: '毛小星' }],['9528', { name: '杨秘书' }],
])const Friend = () => {const [count, setCount] = useState(() => 0)const [id1] = useState('9527')const [id2] = useState('9528')const [content, setContent] = useState('')const getName = (_id) => {return userMap.get(_id)}const makeFriend = (_id1, _id2) => {const user1 = getName(_id1)const user2 = getName(_id2)const result = `${user1.name} 和 ${user2.name} 变成了好朋友`setContent(result)}useEffect(() => {makeFriend(id1, id2)}, [id1, id2, count])return (<div><p>{count && content}</p><Button type="primary" onClick={() => setCount((_count) => _count + 1)}>increment</Button></div>)
}export default Friend

上面这段代码,是不是和 try…handle 非常像,有点那个味了,是不是?没错,useState 和 useEffect 的组合就是 React 践行代数效应的最好示例。

useState 原理

上面我们在讲代数效应的时候,我们说不用关心 useState 里面做了什么,我们只需要使用 useState 返回给我们的 state 即可。诶,但是我就是个好奇宝宝,我就想知道 useState 内部到底是怎样实现的。

Hooks 架子

下面我们自己实现一个简易版的 useState,来了解一下 Hooks 内部的基本原理。
我们先来准备一些基础代码,也就是我们要写的 useState 的架子,写代码之前,我们先来梳理一下我们需要干什么。

  • 首先我们要模拟 useState ,那么我们肯定要先声明一个 useState 函数
  • 然后我们需要一个组件来测试我们实现的 hook,我们暂且叫它 App
  • 我们都知道 React 16.8 之后采用了新的 Fiber 架构,Fiber 也就是一个对象用来存储组件的信息,一般组件都会被存储在 stateNode 这个属性上,而 Hooks 的 state 会用链表结构被存储在 Fiber 的 memoizedState 这个属性上。
  • React 设计最精妙之处就在于它的调度,我们需要一个调度函数 schedule
  • 组件是需要区分生命周期的,首次渲染和更新阶段是不一样的,我们使用一个 isMount 字段去标识
  • 最后我们需要一个 workInprogressHook 来处理最近的一个 hook

下面我们就通过上面的思路来把 hooks 的框架搭出来

let isMount = true // 是否渲染
let workInprogressHook = null // 当前处理 hook// Fiber对象
const fiber = {stateNode: App,memoizedState: null, // 用链表去存储 hook
}function useState (initialState) {// todo 实现 useState
}// 调度
function schedule() {// 初始化 当前处理 hookworkInprogressHook = fiber.memoizedStateconst app = fiber.stateNode()isMount = falsereturn app
}// 测试组件
// 为了简化流程,我们忽略 DOM 更新
function App () {const [num, setNum] = useState(0)const [count, setCount] = useState(0)return {onClick() {setNum(num => num + 1)},updateCount() {setCount(count => count + 1)}}
}// 将调度挂载到 window 对象上,方便测试点击效果
window.app = schedule()

初始状态的 useState

上面我们在 useState 函数中留了一个 todo项,在实现 useState 函数之前,我们先来思考一个问题,我们应该怎样存储 useState 生成的状态呢,通常我们调用 useState 是像下面这样的

const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)

在 React 源码中,React 是通过链表结构来存储这些 hook 的,我们要把所有的 state 通过链表的形式存储,并且我们要将 workInprogressHook 指向当前 hook 方便我们处理,下面我们来试着实现 useState

function useState(initialState) {let hook // 当前 hook 节点if (typeof initialState === 'function') {initialState = initialState();}if (isMount) {hook = {memoizedState: initialState,next: null,}// 创建 hook 链表// 如果没有初始化的 hook 则初始化 hook 节点,并将当前处理节点(workInprogressHook)指向当前 hook// 如果不是初始化的话,则将 当前处理节点(workInprogressHook)的下一个节点指向 hookif (!fiber.memoizedState) {fiber.memoizedState = hook} else {workInprogressHook.next = hook}workInprogressHook = hook}// todo 实现更新逻辑
}

更新 state

在完善 useState 的更新逻辑,我们先来想想,既然 state 是需要用链表来存储的,那么update 函数也得需要对应一个链表来存储啊,我们来看看为什么需要链表来存储

const [count, setCount] = useState(0)return (<p onClick={() => {setCount(num => num + 1)setCount(num => num + 1)setCount(num => num + 1)}}>{num}</p>
)

可以看到更新函数 setCount 可能不是只调用一次,在 React 中,这些 update 函数被环状链表组合在了一起。这时我们就需要在 hook 上增加一个 queue 属性来存储 update 函数

hook = {memoizedState: initialState,next: null,// 保存改变的状态// 队列是因为 有可能有多个更新函数// setCount(num => num + 1)// setCount(num => num + 1)// setCount(num => num + 1)queue: {pending: null,}
}

在 React 源码中,更新阶段会调用 dispatchAction.bind(null, hook.queue) 这个函数来更新 state,我们先来看看是怎样实现的

function dispatchAction(queue, action) {// 更新节点const update = {action,next: null,}// 构建更新链表 环状链表// queue.pending === null 还没有触发更新,创建第一个更新if (queue.pending === null) {// u0 -> u0 -> u0update.next = update} else {// u0 -> u0// u1 -> u0 -> u1update.next = queue.pending.nextqueue.pending.next = update}queue.pending = update// 触发更新schedule()
}

环状链表的操作可能不太容易理解,下面我们来详细讲解下。

  • 首先,当产生第一个 update 的时候(我们叫它 u0),此时queue.pending === null。update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。然后queue.pending = update;即queue.pending = u0
queue.pending = u0 ---> u0^       ||       |---------
  • 当产生第二个update(我们叫他u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0。queue.pending.next = update;,即u0.next = u1。然后queue.pending = update;即queue.pending = u1
queue.pending = u1 ---> u0   ^       ||       |---------

这样做的好处就是,当我们需要遍历 update 时,queue.pending.next指向第一个插入的update,方便我们去操作 update 函数。逻辑还是比较清晰明了的,如果上面看不懂的话,需要去好好补一下数据结构了哦。

完善 useState

在 dispatchAction 中,我们将 update 构建成环状链表后, 接着我们就可以继续实现 useState 中的更新逻辑,当我们需要更新 state 时,我们就需要遍历环状链表,将新的状态更新到 update 函数中去,当遍历完,我们将链表清空,最后我们将新的 state 和 update 函数返回即可。

function useState(initialState) {let hook // 当前 hook 节点if (typeof initialState === 'function') {initialState = initialState();}if (isMount) {... mount 阶段} else {// 如果是 update 的情况,则将 hook 指向 workInprogressHook// workInprogressHook 指向 hook 链表的下一个节点hook = workInprogressHookworkInprogressHook = workInprogressHook.next}// 处理更新 遍历更新函数的环状链表// 获取初始状态let baseState = hook.memoizedStateif (hook.queue.pending) {let firstUpdate = hook.queue.pending.nextdo {const action = firstUpdate.action// 处理更新状态baseState = action(baseState)firstUpdate = firstUpdate.next} while (firstUpdate !== hook.queue.pending.next) // 遍历完环状链表// 清空链表hook.queue.pending = null}hook.memoizedState = baseStatereturn [baseState, dispatchAction.bind(null, hook.queue)]
}

浅析 Hooks 源码

上面我们实现一个简单的 useState,我们使用 isMount 来判断更新时机,但是 React 中没有这么 low,React 中使用了不同的 hash 值来标识不同的 hooks 的状态

本篇博客 React 源码为 16.12.0 版本

// 利用 hash 来存储不同状态的方法
// mount 阶段
const HooksDispatcherOnMount: Dispatcher = {readContext,useCallback: mountCallback,useContext: readContext,useEffect: mountEffect,useImperativeHandle: mountImperativeHandle,useLayoutEffect: mountLayoutEffect,useMemo: mountMemo,useReducer: mountReducer,useRef: mountRef,useState: mountState,useDebugValue: mountDebugValue,useResponder: createDeprecatedResponderListener,useDeferredValue: mountDeferredValue,useTransition: mountTransition,
};// update 阶段
const HooksDispatcherOnUpdate: Dispatcher = {readContext,useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useImperativeHandle: updateImperativeHandle,useLayoutEffect: updateLayoutEffect,useMemo: updateMemo,useReducer: updateReducer,useRef: updateRef,useState: updateState,useDebugValue: updateDebugValue,useResponder: createDeprecatedResponderListener,useDeferredValue: updateDeferredValue,useTransition: updateTransition,
};

Redux 的作者 Dan Abramov 在加入 React 团队中也是将 Redux 的思想带入了 React 中,useState 和 useReducer 这两个 hook 就是他的代表作,而且从本质来说,useState 不过就是预置了 reducer 的 useReducer,下面的源码会印证这点。

mount 阶段的 useState 和 useReducer

在 mount 阶段,useState 会调用 mountState, 而 useReducer 则会调用 mountReducer
下面我们来看看这两个方法

// \react\packages\react-reconciler\src\ReactFiberHooks.jsfunction mountState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {// 创建hook对象const hook = mountWorkInProgressHook();if (typeof initialState === 'function') {// $FlowFixMe: Flow doesn't like mixed typesinitialState = initialState();}hook.memoizedState = hook.baseState = initialState;const queue = (hook.queue = {pending: null,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: (initialState: any),});const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchAction.bind(null,currentlyRenderingFiber,queue,): any));return [hook.memoizedState, dispatch];
}function mountReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] {const hook = mountWorkInProgressHook();let initialState;if (init !== undefined) {initialState = init(initialArg);} else {initialState = ((initialArg: any): S);}hook.memoizedState = hook.baseState = initialState;const queue = (hook.queue = {pending: null,dispatch: null,lastRenderedReducer: reducer,lastRenderedState: (initialState: any),});const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(null,currentlyRenderingFiber,queue,): any));return [hook.memoizedState, dispatch];
}

能看到 mountState 和 mountReducer 的区别就是 queue 中 lastRenderedReducer 字段

const queue = (hook.queue = {// 与极简实现中的同名字段意义相同,保存update对象pending: null,// 保存dispatchAction.bind()的值dispatch: null,// 上一次render时使用的reducerlastRenderedReducer: reducer,// 上一次render时的statelastRenderedState: (initialState: any),
});

mountReducer 的 lastRenderedReducer 接收的就是传入你自定义的 reducer;而 mountState 接收的 lastRenderedReducer 是一个预置的 basicStateReducer。
下面我们来看看 basicStateReducer 的实现

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {// $FlowFixMe: Flow doesn't like mixed typesreturn typeof action === 'function' ? action(state) : action;
}

这也直接证明了 useState 即 reducer 为 basicStateReducer 的 useReducer。

update 阶段的 useState 和 useReducer

在 update 阶段 updateState 则是直接调用了 updateReducer 方法,更加证明了 useState 就是特殊的 useReducer

function updateState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any));
}

下面我们来看看 updateReducer 是怎样实现的

function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] {// 获取当前hookconst hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;// ...计算 newState 的过程hook.memoizedState = newState;const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];
}

源码这个部分比较长,我只保留了一些主干代码,大致流程就是重新计算新的 state,然后将新的 state 返回。

调用更新函数

我们在使用 setCount((count) => count + 1) 这样的更新函数更新 state 的时候,会触发 dispatchAction 函数,这个时候当前的函数组件对应的 Fiber 和 对应的更新方法(hook.queue)就通过调用 dispatchAction.bind 传入了方法内

function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,
) {const currentTime = requestCurrentTimeForUpdate();const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update: Update<S, A> = {expirationTime,suspenseConfig,action,eagerReducer: null,eagerState: null,next: (null: any),};// 构建 update 环状链表// Append the update to the end of the list.const pending = queue.pending;if (pending === null) {// This is the first update. Create a circular list.update.next = update;} else {update.next = pending.next;pending.next = update;}queue.pending = update;const alternate = fiber.alternate;if (fiber === currentlyRenderingFiber ||(alternate !== null && alternate === currentlyRenderingFiber)) {// This is a render phase update. Stash it in a lazily-created map of// queue -> linked list of updates. After this render pass, we'll restart// and apply the stashed updates on top of the work-in-progress hook.didScheduleRenderPhaseUpdate = true;update.expirationTime = renderExpirationTime;currentlyRenderingFiber.expirationTime = renderExpirationTime;} else {if (fiber.expirationTime === NoWork &&(alternate === null || alternate.expirationTime === NoWork)) {// 只保留核心代码// ...优化调度渲染const currentState: S = (queue.lastRenderedState: any);const eagerState = lastRenderedReducer(currentState, action);update.eagerReducer = lastRenderedReducer;update.eagerState = eagerState;if (is(eagerState, currentState)) {// Fast path. We can bail out without scheduling React to re-render.// It's still possible that we'll need to rebase this update later,// if the component re-renders for a different reason and by that// time the reducer has changed.return;}}// 调度scheduleWork(fiber, expirationTime);}
}

dispatchAction 函数我只留了一些主干代码,总结一下:将 update 加入 queue.pending,构建环状链表,在优化渲染后,开启调度。

if…else… 是 React 的一些优化手段,if 内:

if (fiber === currentlyRenderingFiber ||(alternate !== null && alternate === currentlyRenderingFiber)
)

这是需要 render 阶段触发的更新,所以需要给当前的更新放到一个延迟队列中,在渲染阶段,再重新启用 workInProgress 去触发更新

而下面的 else…if

else if (fiber.expirationTime === NoWork &&(alternate === null || alternate.expirationTime === NoWork))

fiber.expirationTime 保存的是 fiber 对象的 update的优先级,fiber.expirationTime === NoWork 则意味着 fiber 对象上不存在 update。
通过源码的学习,我们已经知道了 update 计算 state 是在 hook 的声明阶段,在调用阶段还通过内置的 reducer 重新计算 state,如果调用阶段的 state 和声明阶段的 state 是相等的,那么就完全不需要重新开启一次新的调度了。

到此我们就了解了 hooks 的理念,其实 React 就是在践行函数式编程,如果你觉得这篇“人类高质量文章”写的不错就点个赞吧!

  • 写给那些搞不懂代数效应的我们(翻译)
  • React技术揭秘

浅析React Hooks原理相关推荐

  1. React Hooks 原理理解

    文章目录 Hooks hooks与fiber(workInProgress) 状态派发--useState(useReducer)原理 处理副作用--useEffect(useLayoutEffect ...

  2. 【React】946- 一文吃透 React Hooks 原理

    一 前言 之前的两篇文章,分别介绍了react-hooks如何使用,以及自定义hooks设计模式及其实战,本篇文章主要从react-hooks起源,原理,源码角度,开始剖析react-hooks运行机 ...

  3. React Hooks 原理探究、Three.js之3D乒乓球小游戏 | 每日掘金第 195 期

    如果你经常关注前端基础建设,相信你一定对抽象语法树(Abstract Syntax Tree,AST)技术并不陌生.在 Babel.Webpack.Eslint 等工具中,AST 都发挥着重要的作用, ...

  4. jsbridge原理_React Hooks 原理与最佳实践

    1. 前言 React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性.React Hooks 要解决的问题是状态共享,是继 r ...

  5. React hooks中swr的原理和源码解析

    授权转载自:JonyYu https://github.com/forthealllight/blog/issues/61 swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目 ...

  6. React Hooks核心原理与实战

    React Hooks核心原理与实战 一.Hooks的优点 1.1 Hooks的含义 1.2 优点 二.常用的Hooks 2.1 useState 2.2 useEffect 2.3 useCallb ...

  7. 浅析React之事件系统(二)

    上篇文章中,我们谈到了React事件系统的实现方式,和在React中使用原生事件的方法,那么这篇文章我们来继续分析下,看看React中合成事件和原生事件混用的各种情况. 上一个例子 在上篇文章中,我们 ...

  8. 使用React Hooks你可能会忽视的作用域问题

    前言 其实React Hooks已经推出来一段时间了,直到前一阵子才去尝试了下,看到的一些博客都是以API的使用居多,还有一些是对于原理的解析.而我这篇文章想写的是关于React Hooks使用中的作 ...

  9. dw按钮图片滚动js_使用 React Hooks 实现仿石墨的图片预览插件(巨详细)

    点击上方"前端教程",选择"星标" 每天前端开发干货第一时间送达! 作者:DARRELL https://juejin.im/post/5e9bf299f265 ...

最新文章

  1. hmm 求隐藏序列_统计学习方法--HMM回顾
  2. 转:android实时语音问题分析
  3. python读写csv确定编码格式_Python使用utf8编码读写csv文件
  4. mysql 外链接 后面的on_mysql数据库中关于内连接、外链接中on where having的用法。(转载)...
  5. python 编程入门-python编程入门(第3版)
  6. html中样式控制的问题
  7. Pycharm运行时找不到文件问题
  8. 设计模式之_Strategy_03
  9. ElasticSearch Java Api(二) -检索索引库
  10. Linux中Postfix邮件发送配置(三)
  11. Mac-安装Homebrew报错error: could not lock config file .git/config:
  12. byte转换为string乱码_每日一课 | 如何将int转换为String
  13. 从CMO到龙泉寺弟子:佛法改变人生
  14. AngularJS2.0教程(一)快速上手之基础知识
  15. 深入浅出之正则表达式(一)(转)
  16. cv2.add和cv2.subtract图像加减法运算
  17. python 类初始化函数_C类初始化函数
  18. 调试器无法终止一个或多个进程_EXCEL按多个分隔符号无法分列,没关系,一个M函数全搞定...
  19. mysql做时间判断_mysql关于时间函数的判断
  20. Ubuntu18 Cuda10升级Cuda11+Cudnn8+TensorRT7.1+Opencv3

热门文章

  1. 朋友圈功能合集来咯!定时发朋友圈,查看朋友圈,朋友圈跟圈,一键转发朋友圈,延迟评论
  2. 喜欢一个女生应该如何表达
  3. 计算机冷启动和热启动的区别
  4. 蓝桥1+x java中级 看了就过关
  5. 抖音直播怎么赚钱?普通人如何做好抖音直播带货?
  6. 城市新农人,撑起了一个170亿的绿植赛道
  7. Stratifyd 发布《奢侈品 · 服饰客户体验洞察报告》附下载
  8. Haskell列表操作
  9. c语言为什么是2的15次方,在C语言中为什么int型的取址范围为-2的15次方到2的15次方减1?...
  10. ipython介绍_IPython的介绍与使用