一、React-router

(一)路由的由来

路由是一个网络工程里面的术语。路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动
路由器提供了两种机制:路由(路由是决定数据包从来源到目的地的路径)和转送(转送将输入端的数据转移到合适的输出端
路由中有一个非常重要的概念叫路由表,路由表本质上就是一个映射表,决定了数据包的指向。
路由的概念出现最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
 
1️⃣ 后端路由阶段
早期的网站开发整个HTML页面是由服务器来渲染的,服务器直接生产渲染好对应的HTML页面,返回给客户端进行展示。并且,一个页面有自己对应的网址,也就是URL。URL会发送到服务器,服务器会通过正则对该URL进行匹配,并且最后交给一个Controller进行处理。Controller进行各种处理,最终生成HTML或者数据,返回给前端,这就完成了一个IO操作。
上面的这种操作,就是后端路由。当我们页面中需要请求不同的路径内容时,交给服务器来进行处理,服务器渲染好整个页面,并且将页面返回给客户顿。这种情况下渲染好的页面,不需要单独加载任何的js和css,可以直接交给浏览器展示,这样也有利于SEO的优化。
后端路由的缺点
一种情况是整个页面的模块由后端人员来编写和维护的。另一种情况是前端开发人员如果要开发页面,需要通过PHP和Java等语言来编写页面代码,而且通常情况下HTML代码和数据以及对应的逻辑会混在一起,编写和维护都是非常糟糕的事情。
 
2️⃣ 前后端分离阶段
前端渲染的理解
每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染。需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件。同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了。
前后端分离阶段:
随着Ajax的出现, 有了前后端分离的开发模式。后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中。这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上,并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可;目前很多的网站依然采用这种模式开发(jQuery开发模式);
 
3️⃣ 单页面富应用(SPA)
单页面富应用的理解
单页面富应用的英文是single-page application,简称SPA。整个Web应用只有实际上只有一个页面,当URL发生改变时,并不会从服务器请求新的静态资源,而是通过JavaScript监听URL的改变,并且根据URL的不同去渲染新的页面。
如何可以应用URL和渲染的页面呢 👉 前端路由
前端路由维护着URL和渲染页面的映射关系,路由可以根据不同的URL,最终让我们的框架(比如Vue、React、Angular)去渲染不同的组件,最终我们在页面上看到的实际就是渲染的一个个组件页面。
 

(二)前端路由底层原理

前端路由是如何做到URL和内容进行映射呢?监听URL的改变。URL发生变化,同时不引起页面的刷新有两个办法:1️⃣ 通过URL的hash改变URL;2️⃣ 通过HTML5中的history模式修改URL;
当监听到URL发生变化时,我们可以通过自己判断当前的URL,决定到底渲染什么样的内容。
 
1️⃣ URL的hash
URL的hash也就是锚点(#),本质上是改变 window.locationhref 属性;我们可以通过直接赋值 location.hash 来改变 href,但是页面不发生刷新。
⚠️ 注意:hash的优势就是兼容性更好,在老版IE中都可以运行;但是缺陷是有一个 #,显得不像一个真实的路径;
 
2️⃣ HTML5的history
history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面
  • replaceState:替换原来的路径;
  • pushState:使用新的路径;
  • popState:路径的回退;
  • go:向前或向后改变路径;
  • forward:向前改变路径;
  • back:向后改变路径;
以下是案例代码:
 

(三)React-router 引入

目前前端流行的三大框架, 都有自己的路由实现:
  • Angular的ngRouter
  • React的react-router
  • Vue的vue-router
React Router的版本4开始,路由不再集中在一个包中进行管理了:
  • react-router是router的核心部分代码;
  • react-router-dom是用于浏览器的;
  • react-router-native是用于原生应用的;
 
安装react-router:yarn add react-router-dom,安装react-router-dom会自动帮助我们安装react-router的依赖
 

(四)主要API × 基本使用

react-router最主要的API是给我们提供的一些组件:
  • <BrowserRouter><HashRouter>
    • Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件。<BrowserRouter> 使用history模式<HashRouter> 使用hash模式
  • <Link><NavLink>
    • 通常路径的跳转是使用Link组件,最终会被渲染成a元素;NavLink是在Link基础之上增加了一些样式属性(后续学习);
      to 属性:Link中最重要的属性,用于设置跳转到的路径;
  • <Route>
    • <Route> 用于路径的匹配;
      path 属性:用于设置匹配到的路径;
      component 属性:设置匹配到路径后,渲染的组件;
      exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
 
以下是案例代码,首先需要创建页面管理文件夹pages,在其中创建几个组件Home、About、Profile,然后在App.js中加入以下代码。而且我们需要知道路由渲染的位置与 <Route> 组件放置的位置有关(占位)。
 

(五)<NavLink>

需求:路径选中时,对应的 <a> 元素变为红色。
原生方法实现:在state中定义一个对象数组存放路由信息和一个记录当前 index的变量,并通过map渲染 <Link> 进行展示,并且当 index === this.state.currentIndex 时赋值一个控制红色的className。
 
以下使用更方便的方法 <NavLink> 实现
 
事实上在默认匹配成功时,<NavLink> 就会添加上一个动态的 active class。加入我们担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class。我们通过设置 activeClassName 属性即可实现。
 

(六)<Switch>

以下代码为例,我们可以发现出部分问题:第23、24行代码中组件也渲染了。
 
原因:默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染。但是实际开发中,我们往往希望有一种排他的思想,只要匹配到了第一个,那么后面的就不应该继续匹配了,这个时候我们可以使用 <Switch> 来将所有的Route进行包裹即可,具体代码如下:
 

(七)<Redirect>

<Redirect> 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中。💡需要注意的是通常这种逻辑会放在Redux中
以下是一个案例代码:
根据user代码我们可以知道,当 isLoginfalse,就会重定向到 /login 地址对应的组件;当为 true,则显示账号信息。
 

(八)路由的嵌套

在开发中,路由之间是存在嵌套关系的。以下假设about页面中有多个子页面。点击不同的链接可以跳转到不同的地方,显示不同的内容,以下是案例代码:
 

(九)手动路由跳转与源码阅读

目前我们实现的跳转主要是通过 <Link> 或者 <NavLink> 进行跳转的。实际上我们也可以通过JavaScript代码进行跳转,但有一个前提:必须获取到history对象
💡
需要注意的是此处的history对象是由路由创建的,并不是我们上述说的路由模式中的history模式,我们可以通过 props.history(函数组件)/ this.props.history(类组件) 进行获取,此外可以获取 locationmatch 属性。
获取history对象有两种方式:
1️⃣ 如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
2️⃣ 如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象。
 
以下是情况1️⃣
 
情况2️⃣:通过高阶组件可以在组件中添加想要的属性,此时我们使 withRouter 高阶组件帮助我们添加新的属性。但必须要满足两个条件:其一,App组件必须包裹在 Router组件 之内;其二,App组件使用 withRouter高阶组件 包裹。
以下需要修该index.js文件:
 
🔎 withRouter() 源码解读
首先我们找到 node_modules/react-router-dom/index.js,可以看到根据环境进行引入不同引入不同的文件。
 
然后我们跳转到 node_modules/react-router-dom/cjs/react-router-dom.js 文件
 
然后我们找到 react-router 库,按照类似逻辑,找到 withRouter() 函数定义的地方。
 
context 中的数据又源自于我们在开发中使用的 <BrowserRouter><HashRouter>,以 <BrowserRouter> 为例:
 
我们追踪到另一个库 history,react-router-dom本质上使用的是这个 history。
 
我们重新回到 react-router-dom.js 中,React.createElement 第一个传入的元素是 reactRouter.Router
 

(十)参数传递与动态路由

传递参数有三种方式:
  • 动态路由的方式;
  • search传递参数;
  • Link中to传入对象
动态路由的概念指的是路由中的路径并不会固定:例如 /detail 的path对应一个组件Detail,这是固定的。如果我们将path在Route匹配时写成 /detail/:id,那么 /detail/abc/detail/123 都可以匹配到该Route,并且进行显示。这个匹配规则,我们就称之为动态路由。通常情况下,使用动态路由可以为路由传递参数。
 
1️⃣ 动态路由的方式
路由数据的获取:props.match
 
2️⃣ search传递参数❎
假设我们需要传递更多的数据需要用到 search 参数,例如 /detail?id=1242&ok=false,以下是获取数据的示例代码:
💡
url中?后面的参数在服务器端称之为query。
 
3️⃣ Link中 to 传入对象✅
 

(十一)路由统一配置react-router-config

目前我们所有的路由定义都是直接使用 <Route> 组件,并且添加属性来完成的。但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理,这个时候可以使用react-router-config来完成。
安装react-router-config:yarn add react-router-config
 
首先我们需要配置路由映射的关系数组
 
其次我们需要修改App.js文件
 
about.js 页面也因子路由也需要作出相应的修改
 
🔎 renderRoutes() 源码解读
 

二、React Hooks

(一)Hooks的出现

Hook 是[email protected] 的新增特性,它可以让我们在不编写 class 的情况下使用 state 以及其他的React特性(比如生命周期)
class组件相对于函数式组件的优势
  • class组件可以定义自己的 state,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑,比如在 componentDidMount 中发送网络请求,并且该生命周期函数只会执行一次;函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class组件可以在状态改变时只会重新执行 render 函数以及我们希望重新调用的生命周期函数 componentDidUpdate 等;函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
 
class组件存在的问题
  • 复杂组件变得难以理解:
    • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
    • 比如 componentDidMount 中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount 中移除);
    • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  • 难以理解的class:
    • 很多人发现学习ES6的class是学习React的一个障碍。
    • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
    • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
  • 组件复用状态很难:
    • 在前面为了一些状态的复用我们需要通过高阶组件或render props;
    • 像我们之前学习的redux中 connect 或者react-router中的 withRouter,这些高阶组件设计的目的就是为了状态的复用;
    • 或者类似于 ProviderConsumer 来共享一些状态,但是多次使用 Consumer 时,我们的代码就会存在很多嵌套;
    • 这些代码让我们不管是编写和设计上来说,都变得非常困难;
 
💡 Hook的出现
Hook的出现,可以解决上面提到的这些问题。它可以让我们在不编写 class 的情况下使用 state 以及其他的React特性。同时我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决。
Hook的使用场景:
  • Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景);
  • 但是如果是一个旧的项目,并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
 

(二)计数器案例对比与useState()

我们通过传统的计数器案例,对比class组件和函数式组件结合hooks的对比
我们可以看到上面的代码差异非常大:函数式组件结合hooks让整个代码变得非常简洁,并且再也不用考虑this相关的问题。
 
useState() 来自 react 库,需要从react中导入,它是一个hook。其参数是初始化值,如果不设置为 undefined;其返回值是个数组,包含两个元素:元素一是当前状态的值(第一调用为初始化值)、元素二是设置状态值的函数。
当我们点击button后,会完成两件事情:1️⃣调用 setCount,设置一个新的值;2️⃣组件重新渲染,并且根据新的值返回DOM结构。
Hook 就是JavaScript 函数,这个函数可以帮助你钩入(hook into) React State以及生命周期等特性。
 
⚠️ 但是使用它们会有两个额外的规则
  • 只能在函数最外层调用Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在React的函数组件中调用Hook。不要在其他JavaScript函数中调用。
💡
FAQ:为什么叫 useState 而不叫 createState
“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state,如果每次都创建新的变量,它就不是“state”了。这也是Hook的名字总是以use开头的一个原因。
 

(三)useState() 复杂场景

1️⃣ 当我们需要用到多个状态时:
 
2️⃣ 如果我们需要对复杂状态进行修改:
 

(四)useEffect()

💡
useEffect 分为四个部分:1.基本使用2.清除操作3.多个使用4.性能优化
Effect Hook可以帮助我们来完成一些类似于class中生命周期的功能。💡事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects),所以对于完成这些功能的Hook被称之为Effect Hook。
 
1️⃣ useEffect 基本使用
案例:假如我们现在有一个需求,页面的title总是显示counter的数字,分别使用class组件和Hook实现:
useEffect() 的解析:
  • 通过 useEffect() 的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect() 要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个回调函数
 
2️⃣ useEffect 的清除操作
以上是最基本的使用。我们在class组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除(比如我们之前的事件总线或Redux中手动调用subscribe), useEffect() 函数的返回值得回调函数刚刚好能帮我们完成这个操作。
以下我们使用 useEffect() 模拟实现以前类组件的订阅和取消订阅。
💡
为什么要在 useEffect 中返回一个函数?
这是 useEffect 可选的清除机制。每个 useEffect 都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑放在一起,它们都属于effect的一部分。但需要注意的是,React会在组件更新和卸载的时候执行清除操作,正如之前说到的,useEffect 在每次渲染的时候都会执行。
 
3️⃣ 多个 useEffect 的使用
根据以上类组件的逻辑,我们在 componentDidMount 中实现了修改DOM、订阅事件和网络请求。此时我们使用Effect Hook,我们可以将这些不同逻辑分离到不同的 useEffect 中(从定义的顺序依次执行)
 
4️⃣ Effect 性能优化
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题
  • 某些代码我们只是希望执行一次即可,类似于 componentDidMountcomponentWillUnmount 中完成的事情(比如网络请求、订阅和取消订阅);
  • 另外,多次执行也会导致一定的性能问题
我们如何决定 useEffect 在什么时候应该执行和什么时候不应该执行。参数一是执行的回调函数;参数二是useEffect在哪些state发生变化时,才重新执行(受谁的影响)
 

(五)useContext()

在之前的开发中,我们要在组件中使用共享的Context有两种方式:
  • 类组件可以通过 类名.contextType = MyContext 方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
 
但是有个问题,如果是多个Context共享时就会存在大量的嵌套,优化方案就是通过Context Hook,其允许我们通过Hook来直接获取某个Context的值。⚠️但是需要注意:当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给MyContext provider的 context value 值。以下是案例代码:
 

(六)useReducer()

useReducer() 仅仅是 useState() 的一种替代方案。在某些场景下,如果state的处理逻辑比较复杂,我们可以通过 useReducer() 来对其进行拆分,或者这次修改的state需要依赖之前的state时,也可以使用。
 
⚠️ 但是需要注意,数据是不会共享的,它们只是使用了相同的 counterReducer 的函数而已。(我们可以创建一个新的组件进行测试,看Home组件的数据加减是否会影响新的组件)
实验证明,counter在两个组件中是相互独立的。所以,useReducer() 只是 useState() 的一种替代品,并不能替代Redux。
 

(七)useCallback()

useCallback() 实际的目的是为了进行性能的优化。其具体的优化措施是:useCallback() 会返回一个函数的memoized(记忆的)值,在依赖不变的情况下,多次定义的时候,返回的值是相同的。
 
案例1️⃣:使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化
 
案例2️⃣:使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化。结论:通常使用 useCallback() 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存。
案例1️⃣中 useCallback() 并没有做到性能优化,以下是性能优化的案例。
 

(八)useMemo()

useMemo() 实际的目的也是为了进行性能的优化。
性能优化步骤:useMemo() 返回的也是一个memoized(记忆的)值,在依赖不变的情况下,多次定义的时候,返回的值是相同的;
 
案例1️⃣:进行大量的计算操作,是否有必须要每次渲染时都重新计算
 
案例2️⃣:对子组件传递相同内容的对象时,使用 useMemo() 进行性能的优化
 
💡
总结
useMemo() 是对返回值做优化,useCallback() 传入的回调函数做优化。其实useMemo()可以转化为 useCallback(),以下是根据useCallback()案例修改后的代码
 

(九)useRef()

在之前类组件的学习中,我们知道refs用于操作DOM。其hook useRef() 返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
最常用的ref两种用法:
用法1️⃣:引入DOM(或者组件,但是需要是class组件)元素
 
用法2️⃣:保存一个数据(前一次的值),这个对象在整个生命周期中可以保存不变
 

(十)useImperativeHandle()

useImperativeHandle()forwardRef() 有关系,我们通过 forwardRef() 可以将ref转发到子组件,子组件拿到父组件中创建的ref,绑定到自己的某一个元素中,具体解析见之前代码
 
forwardRef() 的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件,其导致的问题是带来某些情况的不可控,父组件可以拿到DOM后进行任意的操作。但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;
 

(十一)useLayoutEffect()

useLayoutEffect() 看起来和 useEffect() 非常的相似,事实上他们也只有一点区别而已:
  • useEffect() 会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect() 会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新
所以,如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect()
notion image
 
以下是案例代码:
 

(十二)customHook-自定义Hooks

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。以下以所有的组件在创建和销毁时都进行打印为案例,展示自定义Hook。
 

(十三)自定义hook案例

1️⃣ Context共享
例如,我们很多组件都需要获取user和token数据,则我们可以封装到一个hook中。
 
2️⃣ 获取滚动的位置
 
3️⃣ localStorage数据存储
 

三、Hooks 原理

(一)引入Fiber

Fiber与浏览器渲染有关系,浏览器渲染与GPU有关系。
浏览器与GPU的交互主要通过WebGPU API实现。WebGPU API使web开发人员能够使用底层系统的GPU(图形处理器)进行高性能计算并绘制可在浏览器中渲染的复杂图形。WebGPU是WebGL的继任者,为现代GPU提供更好的兼容、支持更通用的GPU计算、更快的操作以及能够访问到更高级的GPU特性。
在多进程架构的浏览器如Chromium中,有一个专门的进程(或者线程)和GPU设备进行交互,执行GL操作。任何一条GL命令都必须经由这个进程处理后才提交给GPU驱动,这个进程就是GPU进程。Renderer进程或者Browser进程必须通过IPC才能与GPU进程交互,告诉GPU进程需要执行哪些GL命令。
在这种架构中,多个GPU客户端可能会同时访问GPU服务,而GPU客户端之间可能存在数据依赖关系,因此必须提供一种同步机制保证GPU操作的先后次序。这就涉及到了同步点 (SyncPoint)机制的基本原理。GPU客户端与GPU进程之间的交互封装在 GpuCommandBufferProxy 类中,这个代理类会向GPU服务端的 GpuCommandBufferStub 发送IPC消息请求执行某些GPU操作。
总的来说,浏览器与GPU的交互和同步信号的关系是通过一系列的API和同步机制来实现的,以确保GPU操作的正确性和效率。
⚠️ 而且GUI渲染和Js代码的执行是在同一个线程,即二者是互斥的,过长的Js代码执行时间会阻塞GUI的渲染造成页面的卡顿。当然在该线程中还包含有例如用户事件的响应、键盘事件的响应、Js代码的执行、Layout布局、Paint和raf(request Animation FrameCall)
 
React为了优化浏览器渲染,将部分的操作(E.g. diff算法、更新)放到了一个算法中——reconciliation(协同),通过该算法将操作切分成了许多的Fiber,最终形成一个Fiber树,而Fiber树也是根据ReactElement树生成的,Fiber可以理解为一个执行单元(碎片)。
然后先让浏览器执行一些执行的操作(上述的用户事件的响应、键盘事件的响应…),当浏览器闲置时,通过 requestIdleCallback() 传入我们的Fiber碎片,执行React的部分代码,即充分使用剩余剩余的时间。💡 requestIdleCallback() 在浏览器中兼容性并不好,并且如果在 requestIdleCallback() 内部进行DOM更改操作,可能会超出浏览器提供的deadline。对此React 实现了一个类似的机制。他们使用了 requestAnimationFrameMessageChannel 来模拟一帧的空闲判断。requestAnimationFrame 是由系统来决定回调函数的执行时机,它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随屏幕的刷新频率,不会引起丢帧和卡顿。
💡
加上我们之前说的Lane概念,可以得出以下结论:
Fiber
  • Fiber是React的核心算法,也被称为Fiber Reconciliation。
  • Fiber是一种数据结构,可以用一个纯JS对象来表示。它是React架构运行时动态的工作单元。
  • Fiber的存在使异步可中断的更新成为可能。每次执行完一个执行单元,React就会检查还剩多少时间,如果没有时间就将控制权让出去。
  • Fiber是一种将reconciliation(递归diff)拆分成无数个小任务的算法;它随时能够停止,恢复。
Lane
  • Lane是React 18中引入的一种新的优先级机制,用于判断具体任务属于哪个优先级。
  • React使用二进制来表示优先级,然后在运行时会将Lane优先级转换成Scheduler优先级。
Fiber和Lane都是React内部的关键机制,它们共同支持React的并发模式和时间切片功能,从而提高了React的性能和用户体验。
 

(二)Hooks原理

上一节提到Fiber的原因是,hooks存储的数据实际上是挂载到fiber上面,使用fiber进行存储,从而生成fiber树。下文以 useState() 为案例分析hooks的底层原理。
我们查看React@18的源码,从 packages/react/index.js 入手,跳转到 packages/react/src/ReactClient.js,最后发现 useState() 源自于此:
 
以上实际上只是声明了函数的类型,具体实现需要到 packages/react-reconciler/src/ReactFiberHooks.js 文件中
 
以上是第一次调用 useState() 的大致执行流程,第二次执行就会执行类似的 HooksDispatcherOnUpdate() 函数,最终发现是调用 updateReducer() 函数
💡 updateWorkInProgressHook() 也解释了为什么 useState() 不能使用if条件判断,因为当条件中止时,链表结构就会断裂对后续执行造成影响。
 

四、Redux Hooks

Redux的hooks是React Redux库提供的一种新的API,它允许你在函数组件中直接访问Redux store和dispatch actions。这些hooks在v7.1.0版本中首次添加。以下是Redux的主要hooks:
  • useSelector():这个hook允许你从Redux store的state中提取数据,使用一个选择器函数。选择器函数应该是纯函数,因为它可能会被执行多次并在任意时间点执行。选择器将会用整个Redux store state作为其唯一的参数。选择器可以返回任何结果,包括直接返回嵌套在state内部的值,或者派生新的值。当一个action被dispatch时,useSelector() 会进行引用比较,比较前一个选择器结果值和当前结果值。如果它们不同,组件将被强制重新渲染。如果它们相同,组件将不会重新渲染。⚠️ useSelector() 默认使用严格的 === 引用相等检查,而不是浅相等。
  • useDispatch():这个hook返回一个dispatch函数,允许你在组件中直接dispatch一个action。
  • useStore():这个hook返回Redux store的引用,你可以用它来访问store的state或者dispatch actions。但是,你应该尽量避免使用这个hook,因为它会导致组件重新渲染
 
以下是一些使用Redux hooks的例子:
在这个例子中,我们创建了一个 Counter 组件,它从Redux store中获取 counter 值,并使用 dispatch 函数来发送 INCREMENTDECREMENT 动作。
这个例子前提是假设你的Redux store中有一个 counter 属性,以及对应的 INCREMENTDECREMENT 动作。
 
useStore 通常不推荐使用,因为它会导致组件重新渲染。但是,如果确实需要访问store,可以这样做:
在这个例子中,我们使用 useStore 获取了Redux store的引用,然后使用 getStatedispatch 方法。
 
 
notion image

 
React-Transition-Group & Redux仿网易云 Demo
Loading...
😈Zabanya
😈Zabanya
一名喜欢瞎折腾选手
公告
 
部分教程类文章篇幅过大,可能会导致加载时间稍微偏长,非常感谢您的耐心等待 ~ 🎉