1️⃣脚手架 × 组件化开发(上)
00 分钟
2024-1-18
2024-8-1

一、脚手架

(一)前端工程的复杂化

如果我们只是开发几个小的demo程序,那么永远不需要考虑一些复杂的问题:
  • 比如目录结构如何组织划分;
  • 比如如何管理文件之间的相互依赖;
  • 比如如何管理第三方模块的依赖;
  • 比如项目发布前如何压缩、打包项目;
  • 等等...
现代的前端项目已经越来越复杂了:
  • 不会再是在HTML中引入几个css文件,引入几个编写的js文件或者第三方的js文件这么简单;
  • 比如css可能是使用less、sass等预处理器进行编写,我们需要将它们转成普通的css才能被浏览器解析;
  • 比如JavaScript代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖;
  • 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等);
为了解决上面这些问题,我们需要再去学习一些工具:
  • 比如babel、webpack、gulp。配置它们转换规则、打包依赖、热更新等等一些的内容;
  • 脚手架的出现,就是帮助我们解决这一系列问题的;
 

(二)React脚手架准备工作

💡
脚手架
传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;
notion image
编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构。
每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的,既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板。不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可,这样也可以间接保证项目的基本机构一致性,方便后期的维护。
总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;
对于现在比较流行的三大框架都有属于自己的脚手架:Vue的脚手架:vue-cli、Angular的脚手架:angular-cli、React的脚手架:create-react-app
目前这些脚手架都是使用node编写的,并且都是基于webpack的。所以我们必须在自己的电脑上安装node环境。
 
与脚手架相关的是包管理工具,通常是叫npm,全称Node Package Manager,即“node包管理器”,帮助我们管理一下依赖的工具包(比如react、react-dom、axios、babel、webpack等等),作者开发的目的就是为了解决“模块管理很糟糕”的问题。
但是另外的还有一个大名鼎鼎的node包管理工具yarn,Yarn是由Facebook、Google、Exponent 和Tilde 联合推出了一个新的JS 包管理工具,为了弥补npm 的一些缺陷而出现的,早期的npm存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题,虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用yarn。
React脚手架默认也是使用yarn,全局安装:npm install -g yarn
 
附Yarn和npm命令对比
notion image
 
最后我们创建React项目的脚手架:npm install -g create-react-app
 

(三)创建项目

💡
需要安装好 Node.js 和 yarn 作为前置工作
现在,我们就可以通过脚手架来创建React项目了。
  • 创建React项目的命令如下:create-react-app 项目名称 ⚠️注意:项目名称不能包含大写字母,另外还有更多创建项目的方式,可以参考GitHub的readme
  • 创建完成后,进入对应的目录,就可以将项目跑起来:cd 项目根文件夹yarn start
 
创建项目后我们可以看到部分文件比较特殊
  1. ⏺ reportWebVitals.js
    1. reportWebVitals.js 是一个在React项目中的性能监控工具。它是由Google发起的,旨在提供各种质量信号的统一指南,以提供出色的网络用户体验。该文件的主要作用是收集和报告有关网站性能的数据,例如页面加载时间、交互时间、首次内容绘制时间等。这些数据可以帮助开发人员识别和解决性能问题,从而提高网站的性能。
      其中,getCLSgetFIDgetFCPgetLCPgetTTFB 是从web-vitals库中导入的函数,用于获取关键指标和辅助指标的数据。
       
      使用:项目中的 index.js 文件最后一行改为 reportWebVitals(console.log);,或者参考脚手架官网
       
      该文件只在使用create-react-app脚手架创建的React项目中才会出现
       
  1. ⏺ serviceWorker.js ⇒ PWA
    1. 💡
      PWA全称Progressive Web App,即渐进式WEB应用。一个PWA应用首先是一个网页, 可以通过Web技术编写出一个网页应用。随后添加上 App Manifest 和Service Worker 来实现PWA 的安装和离线等功能;这种Web存在的形式,我们也称之为是Web App;
      PWA解决的问题:
      • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
      • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
      • 实现了消息推送;
      • 等等一系列类似于Native App相关的功能;
      serviceWorker.js 是一个用于实现离线缓存和网络恢复能力的文件。它是一个JavaScript文件,可以在Web应用程序中运行,以提供离线支持和网络恢复能力。在使用create-react-app脚手架创建React项目时,serviceWorker.js文件不会自动创建。但是,您可以手动创建该文件并将其添加到项目中,以便在您的React应用程序中使用离线缓存和网络恢复功能。
       
      使用:
      🔎参考源码代码:
       
  1. ⏺ robots.txt
    1. robots.txt文件是一个纯文本文件,用于告诉搜索引擎抓取工具(蜘蛛)哪些页面可以被抓取,哪些页面不应该被抓取。该文件通常用于阻止搜索引擎抓取网站的某些部分,或者指定搜索引擎只收录指定的内容。
  1. ⏺ manifest.json
    1. manifest.json是一个提供有关Web应用的信息的JSON文本文件,它对于Web应用被下载并呈现给用户类似于原生应用(例如,被安装在设备的主屏幕上,为用户提供更快的访问和更丰富的体验)是必要的
 

(四)Webpack

React的脚手架是基于Webpack来配置的,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler);当webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle;
notion image
 
但是,我们并没有在目录结构中看到任何webpack相关的内容,原因是React脚手架将webpack相关的配置隐藏起来了(其实从Vue CLI3开始,也是进行了隐藏);
如果我们希望看到webpack的配置信息,我们可以执行一个package.json文件中的一个脚本:"eject": "react-scripts eject",但这个操作是不可逆的,所以在执行过程中会给与我们提示(而且需要将文件先把修改的文件提交到git
我们发现项目多了 config 和 scripts 配置文件对webpack进行配置,且修改配置需要重新打包。
 

(五)编写代码

在src目录下,创建一个index.js文件,因为这是webpack打包的入口。
 
如果我们不希望直接在 ReactDOM.render 中编写过多的代码,就可以单独抽取一个组件App.js:
 

二、组件化开发(上)

(一)分而治之思想

人面对复杂问题的处理方式,任何一个人处理信息的逻辑能力都是有限的。所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。但是,我们人有一种天生的能力,就是将问题进行拆解。如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
notion image
上面的思想就是分而治之的思想,分而治之是软件工程的重要思想,是复杂系统开发和维护的基石,而前端目前的模块化和组件化都是基于分而治之的思想;
 
notion image
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
 
我们需要通过组件化的思想来思考整个应用程序:
  • 我们将一个完整的页面分成很多个组件;
  • 每个组件都用于实现页面的一个功能块;
  • 而每一个组件又可以进行细分;
  • 而组件本身又可以在多个地方进行复用;
 

(二)React 组件化

组件化是React的核心思想。前面我们封装的App本身就是一个组件,组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用,任何的应用都会被抽象成一颗组件树。
notion image
React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
  • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
💡
这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:函数组件、无状态组件、展示型组件主要关注UI的展示;类组件、有状态组件、容器型组件主要关注数据逻辑。
 

(三)组件定义

1. 类组件

类组件的定义有如下要求:
  • 组件的名称是大写字符开头(无论类组件还是函数组件
  • 类组件需要继承React.Component
  • 类组件必须实现 render 函数
在ES6之前,可以通过 create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义
 
使用class定义一个组件:
  • constructor是可选的,我们通常在 constructor 中初始化一些数据;
  • this.state 中维护的就是我们组件内部的数据;
  • render() 方法是 class 组件中唯一必须实现的方法;
💡
render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一
  • React 元素:
    • 通常通过JSX 创建。例如,<div /> 会被 React 渲染为DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件;
      无论是 <div /> 还是 <MyComponent /> 均为React元素。
  • 数组或fragments:使得render 方法可以返回多个元素。
  • Portals:可以渲染子节点到不同的DOM 子树中。
  • 字符串或数值类型:它们在DOM 中会被渲染为文本节点。
  • 布尔类型或null:什么都不渲染。
 
 

2. 函数组件(推荐)

函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。
函数组件有自己的特点
  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
  • 没有this(组件实例);
  • 没有内部状态(state);
 
我们来定义一个函数组件:
 

(四)生命周期基础

💡
生命周期生命周期函数的关系
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段。比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;比如卸载过程(Unmount),组件从DOM树中被移除的过程;
 
React 内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
  • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
  • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
  • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调
我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能。
⚠️ 我们谈React生命周期时,主要谈的的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
 
最常用的生命周期函数
最常用的生命周期函数
生命周期函数(包含不常用生命周期函数)
生命周期函数(包含不常用生命周期函数)
1️⃣ 首先我们通过代码实验:组件的初始化和组件的更新
 
控制台效果
控制台效果
 
2️⃣ 然后我们测试组件的其他生命周期函数
 

(五)生命周期应用

  • Constructor
    • 如果不初始化 state 或不进行方法绑定,则不需要为React组件实现构造函数。在constructor中通常只做两件事情:1️⃣ 通过给this.state 赋值对象来初始化内部的state;2️⃣ 为事件绑定实例(this);
  • componentDidMount
    • componentDidMount() 会在组件挂载后(插入DOM 树中)立即调用。在该函数中通常执行以下操作:1️⃣ 依赖于DOM的操作可以在这里进行;2️⃣ 发送网络请求(官方建议);3️⃣ 添加一些订阅,E.g.监听(需要在 componentWillUnmount 取消订阅);
  • componentDidUpdate
    • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。建议操作:1️⃣ 当组件更新后,可以在此处对DOM 进行操作;2️⃣ 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
  • componentWillUnmount
    • componentWillUnmount() 会在组件卸载及销毁之前直接调用,建议操作:1️⃣ 在此方法中执行必要的清理操作;2️⃣ 例如,清除timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
 
还有一些不常用的生命周期函数
  • getDerivedStateFromPropsstate 的值在任何时候都依赖于 props 时使用(二者保持一致);该方法返回一个对象来更新 state
  • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以用某个对象保存信息,后期可获取DOM更新前的一些信息(比如说滚动位置);
  • shouldComponentUpdate:该生命周期函数很常用,详见性能优化
 
其他详见:
 

(六)组件嵌套与数据传递

1. 组件的嵌套

组件之间存在嵌套关系。在之前的案例中,我们只是创建了一个组件App,如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护。所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;再将这些组件组合嵌套在一起,最终形成我们的应用程序;
E.g.
notion image
上面的嵌套逻辑如下,它们存在如下关系:
App组件是Header、Main、Footer组件的父组件; Main组件是Banner、ProductList组件的父组件;
 
在开发过程中,我们会经常遇到需要组件之间相互进行通信。E.g. 1️⃣ App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;2️⃣ 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;3️⃣ 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
总结父组件通过 属性=值 的形式来传递给子组件数据;子组件通过 props 参数获取父组件传递过来的数据;
 

2. 父组件数据传递子组件

 
补充
🔎实际上 constructor 不实现代码也可以正常运行。我们查看React的 Component 源代码,因为通过在 constructor 中调用 super()props 传给父类并保存,所以子类才能实现继承
notion image
 
那有一个问题?为什么能实现继承。我们以以下代码为例
🔎以下是类继承通过Babel转成ES5的源码
 
最后补充父传子函数组件
 

3. 参数propTypes

假设我们需要对传递的数据进行类型验证,项目中默认使用了 Flow 或者 TypeScript 可以直接进行类型验证,没有则通过 prop-types 库来进行参数验证。从React v15.5 开始,React.PropTypes 已移入另一个包中——prop-types 库
 

4.子组件数据传递父组件

某些情况,我们也需要子组件向父组件传递消息。在vue中是通过自定义事件来完成的;在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
案例如下:
 

5. 组件通信案例

需求:实现一个类似于AntDesign的顶部水平导航
 

6. 实现slot插槽

以京东手机端的导航栏为例,不同类型的页面的导航栏可能细节不一样,但是整体结构有一定的相似之处,所以可以通过插槽实现导航栏的细分
JD首页导航栏
JD首页导航栏
JD分类页导航栏
JD分类页导航栏
 
以下提供两种React的实现方式
1️⃣ this.props.children 实现,缺陷是对传入slot顺序有要求,例如传入一个元素可以使用此方式。
2️⃣ 通过 props 传递插槽信息
调用方式
 

7. 跨组件通信Context

在开发中,比较常见的数据传递方式是通过 props 属性自上而下(由父到子)进行传递。但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
 
实现1️⃣:props 自上而上传递(不推荐❎)
 
实现2️⃣:Context
React提供了一个API:Context。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props;其设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
  • React.createContext()
    • 首先创建一个需要共享的 Context 对象。如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值。
  • Context.Provider()
    • 每个Context对象都会返回一个Provider React 组件,它允许消费组件订阅context的变化。Provider 接收一个value属性,传递给消费组件;一个 Provider 可以和多个消费组件有对应关系;多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;当 Provider 的value 值发生变化时,它内部的所有消费组件都会重新渲染;
  • Class.contextType
    • 挂载在class上的contextType属性会被重赋值为一个由 React.createContext() 创建的Context 对象:这能让你使用 this.context 来消费最近Context 上的那个值;我们可以在任何生命周期中访问到它,包括 render 函数中;
  • Context.Consumer
    • 这里,React 组件也可以订阅到 context 变更。这可以使在函数式组件中完成订阅context。这里需要函数作为子元素(function as child)这种做法,这个函数接收当前的context 值,返回一个 React 节点;
 
上述案例 Context 使用改写为以下代码:
 

8. 跨组件通信多个Context

9. 全局事件传递(兄弟组件数据传递)

前面通过 Context 主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?
  • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus),来完成操作;
  • 在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;
 
首先安装 events 库:yarn add event
events常用的API:
  • 创建EventEmitter对象:eventBus对象;
  • 发出事件:eventBus.emit("事件名称", 参数列表);
  • 监听事件:eventBus.addListener("事件名称", 监听函数);
  • 移除事件:eventBus.removeListener("事件名称", 监听函数);
 
使用案例:我们在Profile组件中给Home组件(兄弟组件)传递一些数据
 

(七)setState

1. 使用setState的原因

开发中我们并不能直接通过修改 state 的值来让界面发生更新:因为我们修改了 state 之后,希望React根据最新的 state 来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化。因为React并没有实现类似于Vue2中的 Object.defineProperty 或者Vue3中的 Proxy 的方式来监听数据的变化,我们必须通过 setState 来告知React数据已经发生了变化。(与微信小程序类似)
💡
组件中并没有实现setState的方法,但是我们可以调用的原因是 setState 方法是从Component中继承过来的
notion image
 

2. setState异步情况

在组件生命周期或React合成事件中,setState是异步。
 

3. setState同步情况

在setTimeout或者原生dom事件中,setState是同步
 

4. 🔎源码角度(包含Lane)解析

总结:在组件生命周期或React合成事件中,setState异步;在setTimeout或者原生dom事件中,setState同步
以下从源码的角度解析同步与异步问题:
Component 中的 updater 是与 ReactFiberClassComponent.js 中的 classComponentUpdater 对象相对应的,其包含三个方法:
1️⃣ enqueueSetState() 用于将新的状态更新排入队列。它首先获取组件实例的fiber节点,然后创建一个更新,并将新的状态(payload)和回调函数(callback)附加到更新上。然后,它将更新排入队列,并安排更新。
2️⃣ enqueueReplaceState() 这个方法与 enqueueSetState() 类似,但它用于替换组件的当前状态,而不是将新的状态合并到当前状态。
3️⃣ enqueueForceUpdate() 这个方法用于强制更新组件,即使状态没有改变。它的工作方式与 enqueueSetState()enqueueReplaceState() 类似,但它不需要新的状态(payload)。
 

Lane 是what?

enqueueSetState() 引出一个问题,Lane到底是什么?
lane 是React@17中用于表示任务的优先级,是对 expirationTime 的重构。其是一个32位的二进制数,每个二进制位表示 1 种优先级,优先级最高的 SyncLane 为 1,其次为 2、4、8 等(理解为越靠右优先级越高)⚠️ 注意:lane 的长度是31位,react这么做是为了避免符号位参与运算。
lanes 是一个整数,该整数所有为二进制位为 1 对应的优先级任务都将被执行。
 

Lane 八种操作

  1. lane & lane
  1. lane & lanes
  1. lanes & ~lane
  1. lanes1 & lanes2
  1. lane | lane
  1. lanes2 |= lanes1 & lane
  1. lane *= 2 和 lane <<= 1
  1. lanes & -lanes
在下面将会举例详细介绍这些操作,这里先介绍一下 lane 的值:
lanes 值为 0b0000000011111111111111110000000,表示有多个任务的优先级。
TransitionLane1 值为 0b0000000000000000000000010000000,表示单个任务的优先级
TransitionLane2 值为 0b0000000000000000000000100000000,表示单个任务的优先级
 

  • lane & lane
    • 用来判断是不是同一个 lane,两个 lane 是否有相同的位为 1(取交集) 👉 判断单个任务是否相同
      比如:lane & TransitionLane1,如果 lane 的值为 0b0000000000000000000000010000000,则输出 0b0000000000000000000000010000000,否则输出 0
      用于 getLabelForLane 函数
       
  • lane & lanes
    • 用来判断是不是同一个 lane,两个 lane 是否有相同的位为 1(取交集) 👉 判断单个任务是否存在
      如果想判断 lanes 中是否有 lane,进行如下计算:
      将 TransitionLane1 和 lanes 进行按位与,得到 lane & lanes,它的值是 0b0000000000000000000000010000000,和 TransitionLane1 值相同,说明 lanes 中有 TransitionLane1 任务
      用于 isTransitionLane 等函数
       
  • lanes & ~lane
    • 用来从 lanes 中删除 lane(取差集) 👉 删除单个任务
      如果想去从 lanes 中删掉 lane,具体步骤如下:
      1. 对 TransitionLane1 取反,得到 ~lane,即 0b1111111111111111111111101111111
      1. 对 lanes 和 ~lane 进行按位与运算,得到 lanes & ~lane,即 0b0000000011111111111111100000000
      1. 这样就把 lanes 中的 TransitionLane1 置为了 0,也就是去掉了这个任务
      用于 getNextLanes 等函数
       
  • lanes1 & lanes2
    • 用于判断 lanes1 中是否有 lane 属于 lanes2(取交集) 👉 判断单个任务在两个队列中是否存在交集
      如果想判断 lanes1 中是否有 lane 属于 lanes2,进行如下计算:
      1. 假设 lanes2 为 SyncDefaultLanes,它是由 InputContinuousHydrationLane | InputContinuousLane |DefaultHydrationLane | DefaultLane 组成的,即 0b0000000000000000000000000111100
      1. 当 lanes1 的 3 ~ 6 位为 1,即 lanes1 为 0b0000000000000000000000000111100
      1. 则 lanes1 & lanes2 的值为 lanes1,即 0b0000000000000000000000000111100
      1. 说明 lanes1 中有 lanes2 中的 lane
      这种用法有种变形:lanes & (lane | lane)
       
  • lane | lane
    • 用于将多个 lane 合并为一个 lanes(取并集) 👉 合并单个任务为队列
      合并两个 laneTransitionLane1 | TransitionLane2,得到的值为 0b0000000000000000000000110000000
      用于 markHiddenUpdate 等函数
       
  • lanes2 |= lanes1 & lane
    • 用于将 lanes1 中的 lane 合并到 lanes2 中(先取交集,再取并集) 👉 队列间的任务的移动
      💡 这种写法等于:lanes2 = lanes2 | (lanes1 & lane)
      如果想从 lanes1 中取出 lane,并将它合并到 lanes2 中,进行如下计算:
      1. lanes1 为 InputContinuousHydrationLane | InputContinuousLane,即 0b0000000000000000000000000001100
      1. lanes2 为 DefaultHydrationLane | DefaultLane,即 0b0000000000000000000000000110000
      1. lane 为 InputContinuousLane,即 0b0000000000000000000000000001000
      1. lanes1 & lane 的值为 InputContinuousLane,即 0b0000000000000000000000000001000
      1. lanes2 |= lanes1 & lane 的值为 DefaultHydrationLane | DefaultLane | InputContinuousLane,即 0b0000000000000000000000000111100
      1. lanes2 中多了 InputContinuousLane 这个任务
      用于 markRootMutableReadmarkRootPinged 等函数
       
  • lane *= 2 和 lane <<= 1
    • 都是将 lane 左移一位,一般来说位运算比乘法运算快
      TransitionLane1 *= 2 和 TransitionLane1 <<= 1 的结果都是 0b0000000000000000000000100000000
      用于 getLaneLabelMapclaimNextRetryLane 等函数
       
  • lanes & -lanes
    • 从 lanes 中找出最高优先级的 lane 👉 找出优先级最高的任务
      如果想找出 lanes 中最高优先级的 lane,进行如下计算:
      1. 对 lanes 取反,得到 ~lanes,即 0b1111111100000000000000001111111
      1. 末尾加 1,得到 lanes,即 0b1111111100000000000000010000000
      1. 对 lanes 和 lanes 进行按位与运算,得到 lanes & -lanes,即 0b0000000000000000000000010000000
      1. 这样就找出了 lanes 中最高优先级的 lane
      用于 getHighestPriorityLane 函数
 
最后补充
  1. 在 js 中对于二进制数操作要特别小心:~ 是按位取反(末尾不加一),- 取反末尾加一
  1. lane === (~lane + 1)
 

5. setState数据合并

通过 setState({}) 修改某个属性的值,并不会对其他对象产生影响。源码中其实是有对原对象和新对象进行合并的,如下所示:
 

6. 多个state的合并

💡 我们可以看到 incrementNoReturn() 每次只加2,而 incrementReturn() 每次能加4,造成这样的差别在于传入函数和对象的区别
对于 incrementNoReturn 方法,直接传递了一个对象给setState。在这种情况下,如果在一个事件处理函数中连续调用多次setState,React会将这些更新合并成一个更新,然后一次性应用。因此,即使调用了三次setState,实际上只有最后一次调用才会生效,所以计数器只增加了2。
对于 incrementReturn 方法,传递了一个函数给setState。在这种情况下,React会依次执行每个函数,每个函数都会接收到前一个状态作为参数,然后返回一个新的状态。因此,每次调用setState都会增加计数器的值,所以总共增加了4。
我们可以从源码的角度看看:
 

7. state不可变的数据

我们可以通过案例代码进行探讨,对两种修改state的方法进行比较
而且,即使我们改用继承 PureComponent 还是无法正常触发更新的
 
假如我们想实现通过按钮单独使某个人的年龄+1呢,具体实现方法如下:
 
 
 
notion image

上一篇
React入门
下一篇
组件化开发(下)

评论
Loading...