IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> React Hooks原理探究 -> 正文阅读

[JavaScript知识库]React Hooks原理探究

概览
React 中通常使用 类定义 或者 函数定义 创建组件:

在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

好处:

1、跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;

2、类定义更为复杂: 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理; 时刻需要关注this的指向问题; 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;

3、状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

注意:

避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定; 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用; 不能在useEffect中使用useState,React 会报错提示; 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;

重要钩子:

useState: 用于定义组件的 State,对标到类组件中this.state的功能
useEffect:通过依赖触发的钩子函数,常用于模拟类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法
其它内置钩子:useContext: 获取 context 对象
useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux,并不是持久化存储,会随着组件被销毁而销毁;属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;配合useContext的全局性,可以完成一个轻量级的 Redux
useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
useRef: 获取组件的真实节点;
useLayoutEffect:DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同。useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;可以获取更新后的 state;
自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。
利用数组模拟useState的实现原理
我们可以使用Array模拟useState的原理,正如文章React hooks: not magic, just arrays所说,但是React底层真实的实现,是利用的链表,这里我们下面会说到

当调用 useState 的时候,会返回形如 (变量, 函数) 的一个元祖。并且 state 的初始值就是外部调用 useState 的时候,传入的参数。

理清楚了传参和返回值,再来看下 useState 还做了些什么。正如下面代码所示,当点击按钮的时候,执行setNum,状态 num 被更新,并且 UI 视图更新。显然,useState 返回的用于更改状态的函数,自动调用了render方法来触发视图更新。

function App() {
const [num, setNum] = useState(0);

return (


num: {num}

<button onClick={() => setNum(num + 1)}>加 1

);
}

复制代码
初步模拟

function render() {
ReactDOM.render(, document.getElementById(“root”));
}

let state;

function useState(initialState){
state = state || initialState;

function setState(newState) {
state = newState;
render();
}

return [state, setState];
}

render(); // 首次渲染
复制代码
初步模拟让我们发现了Hooks的第一个核心原理:闭包,是的Hooks返回的state和setState方法,在hooks内部都是利用闭包实现的

但是真实的useXXX都是可以多次声明使用的,所以我们这里的初步实现并不支持对多个变量声明

为什么不能在循环、判断内部使用Hook
首先,利用Array模拟React Hook原理

前面 useState 的简单实现里,初始的状态是保存在一个全局变量中的。以此类推,多个状态,应该是保存在一个专门的全局容器中。这个容器,就是一个朴实无华的 Array 对象。具体过程如下:

第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。
举例:

function RenderFunctionComponent() {
const [firstName, setFirstName] = useState(“Rudi”);
const [lastName, setLastName] = useState(“Yardley”);

return (
<Button onClick={() => setFirstName(“Fred”)}>Fred
);
}
复制代码
上面代码的创建流程

1)初始化

创建两个Array, setters and state,设置游标cursor = 0;

hooks_create_1.png

  1. 首次渲染

遍历所有的useState,将setterspush进入数组,将statepush进入状态数组

hooks_create_2.png

3)重渲染

后续的每次重渲染都会重置游标cursor = 0,并依次从数组中取出之前的state

4)事件触发

每个事件都有对应游标的state值,任何state事件触发,都会修改state数组中对应的state值

hooks_create_3.png

完整模拟useState

import React from “react”;
import ReactDOM from “react-dom”;

const states = [];
let cursor = 0;

function useState(initialState) {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 检查是否渲染过

function setState(newState) {
states[currenCursor] = newState;
render();
}

cursor+=1; // 更新游标
return [states[currenCursor], setState];
}

function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(1);

return (


count1: {count1}


<button onClick={() => setCount1(count1 + 1)}>add1 1
<button onClick={() => setCount1(count1 - 1)}>delete1 1



num2: {num2}


<button onClick={() => setCount2(count2 + 1)}>add2 1
<button onClick={() => setCount2(count2 - 1)}>delete2 1


);
}

function render() {
ReactDOM.render(, document.getElementById(“root”));
cursor = 0; // 重置cursor
}

render(); // 首次渲染
复制代码
如果在循环,判断中使用Hooks

let firstRender = true;

function RenderFunctionComponent() {
let initName;

if(firstRender){
[initName] = useState(“Rudi”);
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState(“Yardley”);

return (
<Button onClick={() => setFirstName(“Fred”)}>Fred
);
}
复制代码
创建流程示意图

hooks_create_4.png

重渲染

hooks_create_5.png

可以看到,因为firstRender的条件判断,游标为0的state值会被异常设置成useState(initName),游标为1的state会被异常设置成useState(“Yardley”),而其实Yardley是游标为2的state的值

也就是说初始化组件的时候,hooks会直接维护一套数组,对应相应的state和setState方法,如果在条件渲染中使用,会导致重渲染的时候,异常的游标对应,异常的游标对应也会导致调用的setState方法失效

模拟useEffect的实现原理
useEffect可能是我们在使用hooks的时候,使用频率仅次于useState的的钩子方法了,它的作用是副作用,说直白就是某些state或者props变化的时候,需要监听并执行相应的操作,那么我们就需要使用useEffect了,对标就是Class组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合

模拟实现(依然是利用Array + Cursor的思路)

const allDeps = [];
let effectCursor = 0;

function useEffect(callback, deps = []) {
if (!allDeps[effectCursor]) {
// 初次渲染:赋值 + 调用回调函数
allDeps[effectCursor] = deps;
effectCursor+=1;
callback();
return;
}

const currenEffectCursor = effectCursor;
const rawDeps = allDeps[currenEffectCursor];
// 检测依赖项是否发生变化,发生变化需要重新render
const isChanged = rawDeps.some(
(dep,index) => dep !== deps[index]
);
// 依赖变化
if (isChanged) {
// 执行回调
callback();
// 修改新的依赖
allDeps[effectCursor] = deps;
}
// 游标递增
effectCursor+=1;
}

function render() {
ReactDOM.render(, document.getElementById(“root”));
effectCursor = 0; // 注意将 effectCursor 重置为0
}
复制代码
真实的React实现
我们用数组模拟出了Hooks的实现原理,但是React的真实实现是用单链表的形式代替数组的,通过next串联起所有的hook

首先让我们看一张图

hooks_6.png

Dispatcher
dispatcher 是一个包含了 hooks 函数的共享对象。它将基于 ReactDOM 的渲染阶段被动态地分配或清理,并且它将确保用户无法在React组件外访问到Hooks,源码参考

hooks在启用时被一个叫做enableHooks 的标志位变量启用或禁用,在渲染根组件时,判断该标志位并简单的切换到合适的 dispatcher 上,源码参考

部分源码

function renderRoot(root: FiberRoot, isYieldy: boolean): void {
invariant(
!isWorking,
'renderRoot was called recursively. This error is likely caused ’ +
‘by a bug in React. Please file an issue.’,
);

flushPassiveEffects();

isWorking = true;
// 控制hooks的当前Dispatcher
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
} else {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}


复制代码
当完成渲染后,dispatcher将被置为null,这是为了防止在ReactDOM的渲染外被异常访问,源码参考

部分源码

// We’re done performing work. Time to clean up.
isWorking = false;
ReactCurrentOwner.currentDispatcher = null;
resetContextDependences();
resetHooks();
复制代码
在Hooks内部,使用resolveDispatcher方法解析当前的dispatcher引用,如果当前的dispatcher异常,则会报错

部分源码

function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
invariant(
dispatcher !== null,
‘Hooks can only be called inside the body of a function component.’,
);
return dispatcher;
}
复制代码
真正的Hooks
可以说Dispatcher是Hooks机制下的对外统一暴露控制器,渲染过程中,通过flag标志控制当前的上下文dispatcher,核心意义就是严格控制hooks的调用渲染,防止hooks在异常的地方被调用了

hooks queue
hooks的表现是:按照调用顺序被链接在一起的节点(nodes)。总结一下hooks的一些属性

初次渲染创建初始状态
状态值可以被更新
React会在重渲染后,记住之前的状态值
React会按照调用顺序,取得和更新正确的状态
React知道当前的hook是属于哪个fiber的
所以我们看Hooks的时候,就不能单纯的认为每个hook节点是一个对象,而是一个链表节点,而整个hooks模型,则是一个队列

{
memoizedState: ‘foo’,
next: {
memoizedState: ‘bar’,
next: {
memoizedState: ‘baz’,
next: null
}
}
}
复制代码
我们可以看到源码对于一个Hook和Effect模型的定义,源码

export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update | null,
queue: UpdateQueue | null,
next: Hook | null,
};

type Effect = {
tag: HookEffectTag,
create: () => mixed,
destroy: (() => mixed) | null,
inputs: Array,
next: Effect,
};

export function useState(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
}
复制代码
首先,可以看到useState的实现就是useReducer的某一种情况的实现,所以在官方文档上,也说了useReducer是useState的另外一种实现方案,结合了Redux的思想,可以避免过多的传递回调函数,而可以直接传递dispatch到深层次的组件中去 官网关于useReducer的说明

这里我还是贴上关于useReducer的用法案例,其实主要是能理解redux或者dva的原理和用法,就可以对标useReducer的用法

const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case ‘increment’:
return {count: state.count + 1};
case ‘decrement’:
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: ‘decrement’})}>-
<button onClick={() => dispatch({type: ‘increment’})}>+
</>
);
}
复制代码
回到Hook的定义,我们现在就可以对每个参数进行说明了

memoizedState:hook更新后的缓存state
baseState:初始化initialState
baseUpdate:最近一次调用更新state方法的action
queue:调度操作的队列,等待进入reducer
next:link到下一个hook,通过next串联每个hook
结合fiber看hooks
对于fiber实现,这里不做详细解释,我们这里只需要知道,React在V16中,对组件构建渲染的机制,从栈模式改为了fiber模式,变成了具有链表和指针的单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步和下一步,从而使遍历变得可以被暂停或者重启 这里的理解就是一种任务分割调度算法,将原先同步更新渲染的任务分割成一个个独立的小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的时间循环机制

fiber简单说概念,就是组件渲染的一个基础任务切割单元,里面包含了当前组件构建的最基础的一个任务内容单元

其中,要提到一个很重要的概念memoizedState,这个字段是不是很眼熟,上面关于hook的定义里面,也有这个字段,是的,fiber数据结构中,也有这个字段,在fiber中,memoizedState的意义就是指向属于这个fiber的hooks队列的首个hook,而hook中的memoizedState则指的是当前hook缓存的state值(这里笔者在看一些博客的时候,发现有的博主把两个数据结构中的同名字段搞混淆了)

我们可以看源码

// There’s no existing queue, so this is the initial render.
if (reducer === basicStateReducer) {
// Special case for useState.
if (typeof initialState === ‘function’) {
initialState = initialState();
}
} else if (initialAction !== undefined && initialAction !== null) {
initialState = reducer(initialState, initialAction);
}
// 注意:重点
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
复制代码
上面可以看到,initialState作为初始state值,被同时赋值给了baseState和memoizedState

再看三段段段源码,源码链接

// Hooks are stored as a linked list on the fiber’s memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
复制代码
export function prepareToUseHooks(
current: Fiber | null,
workInProgress: Fiber,
nextRenderExpirationTime: ExpirationTime,
): void {
if (!enableHooks) {
return;
}
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
firstCurrentHook = current !== null ? current.memoizedState : null;

// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;

// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;

// isReRender = false;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
}
复制代码
export function finishHooks(
Component: any,
props: any,
children: any,
refOrContext: any,
): any {
if (!enableHooks) {
return children;
}

// This must be called after every function component to prevent hooks from
// being used in classes.

while (didScheduleRenderPhaseUpdate) {
// Updates were scheduled during the render phase. They are stored in
// the renderPhaseUpdates map. Call the component again, reusing the
// work-in-progress hooks and applying the additional updates on top. Keep
// restarting until no more updates are scheduled.
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;

// Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;

children = Component(props, refOrContext);

}
renderPhaseUpdates = null;
numberOfReRenders = 0;

const renderedWork: Fiber = (currentlyRenderingFiber: any);

renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);

const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;

renderExpirationTime = NoWork;
currentlyRenderingFiber = null;

firstCurrentHook = null;
currentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;

remainingExpirationTime = NoWork;
componentUpdateQueue = null;


复制代码
其中一段源码有这么一段注释:Hooks are stored as a linked list on the fiber’s memoizedState field.,大概意思是Hooks是以链表的形式储存在fiber的memoizedState字段中

第二段代码是fiber中,hook执行前置函数

第三段代码是fiber中,hook执行后置函数,方法中有这么一句renderedWork.memoizedState = firstWorkInProgressHook;

所以我们来总结一下
1)Hook数据结构中和fiber数据结构中都有memoizedState字段,但是表达的意义不同,Hook中是作为缓存的state值,但是fiber中是指向的当前fiber下的hooks队列的首个hook(hook是链表结构,指向首个,就意味着可以访问整个hooks队列)

2)fiber中调用hook的时候,会先调用一个前置函数,其中 currentlyRenderingFiber = workInProgress;

firstCurrentHook = current !== null ? current.memoizedState : null;

这两句代码分别将当前渲染的fiber和当前执行的hooks队列的首个hook赋值给了当前的全局变量currentlyRenderingFiber和firstCurrentHook

再看下关于currentlyRenderingFiber变量的源码说明

// The work-in-progress fiber. I’ve named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber | null = null;
复制代码
currentlyRenderingFiber就是定义当前正在渲染中的fiber结构

3)fiber调用hooks结束的时候,会调用finishHooks方法,可以看到,会将当前fiber的memoizedState字段存入firstWorkInProgressHook,也就是将hooks队列的首个hook存入,然后将currentlyRenderingFiber字段置为null

Class or Hooks
从当下的环境来看,Hooks已经逐渐成为主流组件方式,比如Ant4.x的组件,已经全面推荐Hooks模式,Hooks的有点主要在于精简的编码模式,函数式编程思想,而Calss组件的主要优点在于【完整】,【精准】的组件流程控制,包括可以使用shouldComponentUpdate等生命周期对渲染做严格控制

Class组件
在业务开发时的思考模式是:【先做什么,再做什么】,this.setState第二个回调的参数,就是这种思想的绝对体现,然后配合【生命周期函数】完成一整个组件的功能,对于组件封装和复用的角度,HOC模式也必须依赖Class实现

Hooks
对标Class组件,使用Hooks需要有一个编程思路上的转变,Hooks的业务开发的思考模式是:【依赖】,【副作用】

所有的状态维护好后,需要思考的就是围绕这些状态产生的【副作用】,我的什么state或者props变了之后,对应的【副作用】需要干什么事,也是这种设计理念下,useEffect的功能可以直接对标Class组件的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合

但是Class在目前来说任有不可替代性,因为Class拥有完整的生命周期控制,比如shouldComponentUpdate等生命周期,而Hooks则无法做到如此精细化的控制

通过 Hooks 我们可以对 state 逻辑进行良好的封装,轻松做到隔离和复用,优点主要体现在:

复用代码更容易:hooks 是普通的 JavaScript 函数,所以开发者可以将内置的 hooks 组合到处理 state 逻辑的自定义 hooks中,这样复杂的问题可以转化一个单一职责的函数,并可以被整个应用或者 React 社区所使用;
使用组合方式更优雅:不同于 render props 或高阶组件等的模式,hooks 不会在组件树中引入不必要的嵌套,也不会受到 mixins 的负面影响;
更少的代码量:一个 useEffect 执行单一职责,可以干掉生命周期函数中的重复代码。避免将同一职责代码分拆在几个生命周期函数中,更好的复用能力可以帮助优秀的开发者最大限度降低代码量;
代码逻辑更清晰:hooks 帮助开发者将组件拆分为功能独立的函数单元,轻松做到“分离关注点”,代码逻辑更加清晰易懂;

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-17 12:40:05  更:2021-11-17 12:41:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:08:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码