React基础知识点

摩森特沃 2021年07月08日 549次浏览

基础知识点

虚拟dom与性能提升

1.普通的数据渲染流程

  1. state数据
  2. jsx模板
  3. 数据 + 模板 结合,生成真实的dom,来显示
  4. state发生改版
  5. 数据 + 模板 结合,生成真实的dom,替换原始的dom

缺陷:

  • 第一次生成了一个完整的dom片段
  • 第二次生成了一个完整的dom片段
  • 第二次的dom替换第一次的dom,非常消耗性能

2.改良版数据渲染流程

  1. state数据
  2. jsx模板
  3. 数据 + 模板 结合,生成真实的dom,来显示
  4. state发生改版
  5. 数据 + 模板 结合,生成真实的dom,但并不直接替换原始的dom
  6. 新的dom(只是DocumentFragment,并没有发生挂载)和原始的dom做比对,找差异
  7. 找出具体发生变化的标签元素
  8. 只用新的dom中的发生变化的标签元素,替换掉旧的dom中的标签元素

缺陷:

  • 增加了dom比对的操作,性能提升并不明显

3.使用虚拟dom提升性能

  1. state数据
  2. jsx模板
  3. 数据 + 模板 生成虚拟dom(虚拟dom就是一个js对象,用它来描述真实的dom)
    ['div', {id: 'abc'}, ['span', {}, 'hello world']]
  4. 用虚拟dom的结构生成真实的dom,来显示
    <div id='abc'><span>hello world</span></div>
  5. state发生改变
  6. 数据 + 模板 生成新的虚拟dom
    ['div', {id: 'abc'}, ['span', {}, 'bye bye']]
  7. 比较原始虚拟dom和新的虚拟dom的区别,找到变化的是span中的内容
  8. 直接操作dom,改变span中的内容

特点:

  • 生成虚拟dom会有新能损耗,但由于虚拟dom实际是js对象,而创建一个js对象实际损耗是极低的
  • 虚拟dom之间的比对性能极大的优于直接进行真实dom之间的比对,使得最终的渲染效率得到了提升
  • 得益于虚拟dom的使用,可以使用react native开发原生应用,其与react的区别在于是将虚拟dom转换为真实dom还是原生组件
  • 比对虚拟dom变化时的diff算法为同层节点比较,如果同层节点不一致则不会再比较下一层级的节点

列表中的key

  • key的作用也是用在虚拟算法的diff算法中的
  • key帮助React识别哪些元素改变了,比如被添加或删除,进而提升渲染效率,如果不指定key,将会抛出警告,并且默认会以元素在数组中的下标作为key
  • 数组元素中使用的key在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的
  • key会传递信息给React,但不会传递给你的组件,因此在组件中无法获取key,如果组件中需要使用key属性的值,需要使用其他属性名显式传递这个值

组件挂载

  • 使用ReactDOM.render()方法将组件挂载到dom节点上

React的核心JSX

  • JSX:javaScript语法扩展
  • 语法核心:在{}中可使用JavaScript表达式
  • 在JSX中,false, null, undefined, true是合法的子元素,但它们并不会被渲染

JSX的属性

  • 特殊属性:className表示class,htmlFor表示for
  • 其他属性:与html属性保持一致

JSX的本质

  • 被编译为React.createElement方法,因此在render方法中也可以直接使用返回React.createElement方法调用的结果的方式,例如

carbon-1

carbon-2

  • 通过以下两种方式的对比可以知道JSX通过识别<>内首字母的大小写来确定编译的最终方式

直接使用html标签

  • 使用html标签:方法的第一个参数编译为标签名字符串

WX20210529-070114@2x

使用自定义组件

  • 首字母大写:方法的第一个参数编译为自定义组件的构造方法

WX20210529-070420@2x

组件的函数写法

  • 当一个组件没有生命周期的回调和state状态管理的时候可以使用function的形式,编写更加简单,对比如下

carbon-3

carbon-4

组件的props(属性)和state(状态)

props属性

  • props属性是只读的,不能被改变
  • 如果在使用组件时,只写了props属性却没有赋值,则会默认赋值为true
  • 属性展开,以下两个组件是等价的
function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

state状态

  • state是组件内部的数据,可以动态改变
  • 在类组件中,constructor是唯一可以直接通过赋值的方式设置state的地方,在constructor之外需要使用setState来修改state中的值
  • React 使用 Object.is 比较算法 来比较 state

异步更新

  • this.props和this.state可能会异步更新,因此以下代码可能会无法更新counter
// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
  • 为保证在调用setState后立即读取this.state或者保证逻辑在应用更新后触发,可使用以下三种方式处理
  • componentDidUpdate
  • setState(updater, callback)并在updater函数中处理的方式,updater函数中接收的state和props都保证为最新
  • setState(updater或者state对象, callback)中callback函数中处理的方式,通常更建议使用componentDidUpdate
// 示例
// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

render方法的执行

render方法的执行时机

  • 当组件的state或者props发生改变时,render函数就会重新执行,页面也会被重新渲染

  • 当父组件的render函数被执行时,它的子组件的render都将被执行

  • 每次组件更新时render方法都会被调用,但只要在相同的DOM节点中渲染<Clock />,就仅有一个Clock组件的class实例被创建使用

  • 触发组件更新的方式

  • 当组件的state或者props发生变化时,此时可使用shouldComponentUpdate函数进行控制
  • 调用forceUpdate()强制更新,此时更新不受制于shouldComponentUpdate函数,但子组件会按照正常的生命周期方法来渲染,包括会正常调用shouldComponentUpdate()方法
  • 订阅了Context后,当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染,且Provider及其内部consumer组件都不受制于shouldComponentUpdate函数

组件的生命周期

组件的生命周期

Initialization(初始化)

  • 初始化组件的数据,包括state和props,初始化的过程主要在constructor中完成

Mounting(挂载)

  • componentWillMount:在组件即将被挂载到页面的时刻自动执行
  • componentDidMount:在组件被挂载到页面之后自动执行

Updation(更新)

  • shouldComponentUpdate:在组件被更新之前自动被执行
  • componentWillUpdate:在shouldComponentUpdate之后(shouldComponentUpdate需要返回true),组件被更新之前被执行
  • componentDidUpdate:在组件被更新之后自动被执行

Unmounting(卸载)

  • componentWillUnmount:当组件即将被从页面中卸载的时候被执行

受控组件和非受控组件

  • 受控组件: React的state成为“唯一数据源”,渲染表单的React组件还控制着用户输入过程中表单发生的操作,被React以这种方式控制取值的表单输入元素就叫做“受控组件”
  • 非受控组件:将真实数据储存在DOM节点中,可以使用ref获取DOM节点的方式获取组件的值
  • 受控组件和非受控组件主要针对表单提交而言,两者有以下区别
    • 受控组件:使用state来管理状态,并为每种事件的改变编写一种处理程序
    • 非受控组件:使用ref获取DOM节点的方式,直接获取值
  • 在受控组件上给value属性指定值后会阻止用户更改输入,如果指定了value,但输入仍可编辑,则可能是将value设置为undefined或null
  • 非受控组件可以使用defaultValue/defaultChecked来为元素指定默认值
  • 在React中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制
  • 使用示例:CommentBox.jsCommentBoxNew.js
  • 参考内容

父子组件中数据的传递

向子组件中传递数据

  • 通过props传递

子组件向父组件传递数据

  • 使用父组件传递函数的方式,并在子组件中调用来影响父组件的数据,注意this的绑定问题,this需要绑定到父组件上

React开发思想

  • 状态提升(lifting state up)
  • 自上而下的数据流(top-down data flow)

Context

  • 使用示例:App.jsThemedBar.js

Context是什么

  • Props属性是自上而下单项传递的
  • Context提供了在组件中共享数据的方法,而不用在每个组件中层层传递,例如:主题,认证的用户等

Context使用

  • 设计目的是共享那些对于组件来说是全局数据的数据
  • 不要仅仅为了避免在几个层级下的组件传递props而使用context
  • 每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化
  • 当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染。Provider及其内部consumer组件都不受制于shouldComponentUpdate函数,因此当consumer组件在其祖先组件退出更新的情况下也能更新

订阅Context的两种方式

  • 使用contextType订阅

关键代码

  • Provider,然后使用它的值
  • const value = this.context;
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
// 为当前的 theme 创建一个 context(“light”为默认值)
const ThemeContext = React.createContext('light');

// 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
// 此属性可以使用 this.context 来消费最近 Context 上的那个值。可以在任何生命周期中访问到它,包括 render 函数中
class MyClass extends React.Component {
  // 指定 contextType 读取当前的 ThemeContext
  // React 会往上找到最近的 ThemeContext Provider,然后使用它的值
  static contextType = ThemeContext;
  // 上面写法与以下写法等价
  // MyClass.contextType = ThemeContext;
  render() {
    // 使用this.context直接获取context中的值
    const value = this.context;
    /* 基于这个值进行渲染工作 */
  }
}
  • 使用Consumer组件包裹订阅
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

React官方文档笔记

创建基于TypeScript的项目

  • 执行命令:npx create-react-app my-app --template typescript

添加TypeScript到现有项目中

  1. 执行命令:npm install --save-dev typescript
  2. 添加TypeScript配置文件(tsconfig.json):npx tsc --init
  3. 配置源码和编译后代码的输出位置
{
  "compilerOptions": {
    // ...
    "rootDir": "src",
    "outDir": "build"
    // ...
  },
}

严格模式

  • 使用<React.StrictMode></React.StrictMode>包裹组件可以使组件运行在严格模式下
  • 跟Fragment一样,StrictMode不会渲染任何可见的UI
  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建

PropTypes和defaultProps

  • PropTypes 仅在开发模式下对props进行类型校验
  • PropTypes和defaultProps主要用于类组件中
import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

// 对组件的prop做校验的写法
Greeting.propTypes = {
  // 在以下语句中,string表示name的类型为string,isRequired表示属性必传
  name: PropTypes.string.isRequired,
  // 其中element代表了限制的类型,此处为限制子元素
  children: PropTypes.element.isRequired, 
};

// defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。
// propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps
Greeting.defaultProps = {
  name: 'Stranger'
};
  • 为函数组件添加PropTypes
import PropTypes from 'prop-types'

function HelloWorldComponent({ name }) {
  return (
    <div>Hello, {name}</div>
  )
}

HelloWorldComponent.propTypes = {
  name: PropTypes.string
}

export default HelloWorldComponent

组件内方法调用顺序

  • 当组件被传给ReactDOM.render()方法时,React会调用组件的构造函数
  • 调用组件的render方法,然后React会更新DOM来匹配组件渲染的输出
  • 组件的输出被插入到DOM后,React就会调用ComponentDidMount()生命周期方法
  • 当组件状态有更新时,将重新调用render方法来渲染页面
  • 当组件从DOM中移除时,React会调用componentWillUnmount()生命周期方法

forceUpdate

  • 默认情况下,当组件的state或props发生变化时,组件将重新渲染。如果需要以自定义的方式让组件重新渲染,则可以调用forceUpdate()强制让组件重新渲染
  • 调用forceUpdate()将致使组件调用render()方法,此操作会跳过该组件的shouldComponentUpdate()

事件处理

  • React事件的命名采用小驼峰式(camelCase),而不是纯小写
  • 使用JSX语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 在React中不能通过返回false的方式阻止默认行为,必须显式的使用preventDefault,例如,传统的HTML中阻止链接默认打开一个新页面,可以这样写
<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

在 React 中,可能是这样的

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}
  • 通常情况下,在组件的事件回调中,如果没有在方法后面添加()使其立即执行,例如 onClick=,则应该为这个方法绑定 this,
    这是因为class的方法默认不会绑定this。如果忘记绑定this.handleClick并把它传入了onClick,当调用这个函数的时候this的值为undefined
  • 向事件处理程序传递参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
// 在这两种情况下,React的事件对象e会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过bind的方式,
// 事件对象以及更多的参数将会被隐式的进行传递

条件渲染

使用if判断进行条件渲染

if (isLoggedIn) {
  button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
  button = <LoginButton onClick={this.handleLoginClick} />;
}

使用与运算符 &&

{unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages.</h2>}
// 在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false
// 因此,如果条件是 true,&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它

使用三目运算符

{isLoggedIn
  ? <LogoutButton onClick={this.handleLogoutClick} />
  : <LoginButton onClick={this.handleLoginClick} />
}

阻止组件渲染

  • 在render方法中直接返回null可以阻止组件渲染,但其并不会影响组件的生命周期

组合和继承

  • 有的组件的展示内容需要在具体使用的时候决定,无法提前知晓,比如模态框的展示内容等,此时可以使用children prop来处理
  • 在以下示例中,<FancyBorder> JSX标签之间的所有内容都会作为一个children prop传递给FancyBorder组件。以下示例中,FancyBorder会将
    渲染在一个<div>中
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}
  • 除此之外,也有类似vue插槽的功能
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

错误边界

错误边界

  • 只有class组件才可以成为错误边界组件
  • 错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误
  • 自React 16起,任何未被错误边界捕获的错误将会导致整个React组件树被卸载

错误边界使用

  • static getDerivedStateFromError():用于在内部渲染备用UI
  • componentDidCatch():打印错误信息
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

Fragments

  • Fragments用于包裹一些其他的标签或者组件,但起本身不会被渲染到DOM节点中,其作用与Vue中的<template>标签相同
  • 可使用<> </>短语法来代替React.Fragment,但这样写后不能支持key属性
  • 用法示例
class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

// 渲染结果
<table>
  <tr>
    <td>Hello</td>
    <td>World</td>
  </tr>
</table>

Portals

  • Portals提供类似Vue中瞬间移动的功能,目的是将一个组件挂载到父组件之外的DOM节点,可用于做页面的模态框,提示框等,实际上antd组件库中的Modal组件即是使用Portals实现的
  • 补充内容:模态框:模态框描述了 UI 的一部分,如果一个元素阻挡了用户与应用的其它部分的互动,这个元素就是模态的

以下示例是使用Portals手动实现模态框的功能

---------- Portals弹框部分 ----------
interface PropsType {
  visible: boolean,
  onHide: () => void
}
export const PortalDialog: React.FC<PropsWithChildren<PropsType>> = (props) => {
  const {visible, onHide, children} = props
  return (
    <>
      {visible ? createPortal(<div>
	<div className={styles["mask"]}/>
	<div className={styles["portal"]}>
	  {children}
	  <Button onClick={onHide}>关闭弹框</Button>
	</div> 
	// 注意:以下的dialog-root与App组件对应的root元素处于同一层级
      </div>, (document.getElementById("dialog-root") as Element)) : null}
    </>
  )
}

---------- 调用弹框部分 ----------
export const DialogPage: React.FC = () => {
  const [isPortalVisible, setIsPortalVisible] = useState(false)
  const showPortal = () => {
    setIsPortalVisible(true)
  }
  const hidePortal = () => {
    setIsPortalVisible(false)
  }
  return (
    <>
      <Button onClick={showPortal}>打开弹框</Button>
      <PortalDialog visible={isPortalVisible} onHide={hidePortal}>
	// 弹框中要展示的内容
	<div>我是弹框中的内容</div>
      </PortalDialog>
    </>
  )
}

---------- 弹框样式声明部分 ----------
// 遮罩层
.mask {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: #ccc;
  opacity: .5;
}
// 弹框
.portal {
  position: absolute;
  padding: 2rem;
  width: 30rem;
  height: 20rem;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;
  border-radius: .5rem;
  border: 1px solid #ddd;
  box-shadow: 0 0 20px 2px #ddd;
  z-index: 100;
}

Refs

  • Refs提供了一种访问DOM节点或访问在render方法中创建的React元素的方式

创建或设置Refs的方式

使用createRef创建并设置Refs

该方法仅能在class组件中使用

// 访问dom节点
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
使用回调方式设置Refs
  • 使用回调方式设置Refs在React 16.3之前比较常用,使用这种方式时需要传递一个回调函数
  • React将在组件挂载时,会调用ref回调函数并传入DOM元素,当卸载时调用它并传入null,在componentDidMount或componentDidUpdate触发前,React会保证refs一定是最新的
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React 实例上(比如 this.textInput)
    return (
      <div>
        <input type="text" ref={dom => this.textInput = dom}/>
        <input type="button" value="Focus the text input" onClick={this.focusTextInput}/>
      </div>
    );
  }
}

访问Refs

  • 访问方式:const node = this.myRef.current;
  • ref 的值根据节点的类型而有所不同
  • 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref对象接收底层DOM元素作为其current属性
  • 当ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性,此时可以通过实例直接调用子组件的方法
  • 不能在函数组件上使用ref属性,因为他们没有实例
  • React会在组件挂载时给current属性传入DOM元素,并在组件卸载时传入null值。ref会在componentDidMount或componentDidUpdate生命周期钩子触发前更新

Refs与函数式组件

以上创建和使用Refs的方式均只能使用在class组件中,注意:无法在函数组件上使用 ref 属性,因为他们没有实例

function CustomTextInput(props) {
  // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

在组件之间传递Refs

Refs可以在组件之间进行传递,不管是回调形式创建的Refs还是通过 React.createRef()创建的Refs

Refs转发
  • 在Hook出现之前,Refs转发是常用的在函数式组件中使用 ref 的方式
  • Refs转发是指允许某些组件接收 ref,并将其向下传递(即转发)给子组件
  • Refs转发通常会使用forwardRef来进行获取,forwardRef的参数为一个渲染函数,渲染函数接收props和ref参数并返回一个React节点,示例如下
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 可以直接获取 DOM button 的 ref,并在必要时访问
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
通过Props传递
function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在以上示例中:Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点

Hook

Hook是什么

  • Hook是一些可以在函数组件里“钩入”React state及生命周期等特性的函数,也就是说使用Hook可以在不编写class的情况下使用state以及其他的React特性
  • Hook的本质是JavaScript函数

Hook的使用规则

  • 只能在React函数的最顶层以及任何return之前调用Hook,不要在循环,条件判断或嵌套函数中调用

补充说明:Hook只能放在顶层是因为在使用多个Hook时,react为了识别Hook与state的对应关系,是按照hook的调用顺序来对应的,如果放在条件语句中,
则可能导致有的Hook不再被执行,从而破坏了顺序导致对应关系也被破坏,这样将产生bug,因此只能在React函数的最顶层使用,如果需要增加条件判断,
则只能在Hook内部进行,为保证这条规则不被破坏,也推荐使用lint插件进行规则检查

  • 只能在React的函数组件中调用Hook,Hook在普通的JavaScript函数和class组件内部不起作用
  • 自定义Hook中也可以调用Hook
  • 可使用eslint插件强制执行规则检查,需要额外安装的eslint-plugin-react-hooks依赖,安装命令为:npm install eslint-plugin-react-hooks --save-dev,对应的ESLint配置对下
// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

useState

  • 使用useState后,由于可以在函数中管理数据的状态,从而不用再转换为class并在构造函数中声明this.state,这使得函数组件具有了class的功能
  • 一般而言,在函数退出后变量就会”消失”,而state中的变量会被React保留,React会在重复渲染时记住它当前的值,并且提供最新的值给函数组件
  • useState可以直接使用数字、字符串或者对象来声明一个状态值,如果是多个状态值,则多次调用useState即可
  • useState的返回值为:当前state(state始终为更新后最新的值)以及更新state的函数,分别表示了class中的this.state.xx和this.setState
  • 如果新的state需要通过使用先前的state计算得出,那么可以将函数传递给setState,该函数将接收先前的state,并返回一个更新后的值,参考内容
  • useState的initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始state需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用
  • 将函数传给useState时会立即执行并将结果作为state,在set方法调用时,如果传入一个函数,函数也会立即执行;所以,当要用useState保存函数时,不能直接传入要保存的函数,需要将要保存的函数作为传给useState或者set方法的函数的返回值
  • React会确保setState函数的标识是稳定的,并且不会在组件重新渲染时发生变化,因此在使用useEffect或者useCallback时可以从依赖中省去setState
  • useState返回的set函数是异步处理的
function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}
  • 读取State(以count变量为例)
  • 在class中使用this.state.count
  • 在函数中,可以直接使用count
  • 更新State(以count为例)
  • 在class中,通过this.setState()来更新
  • 在函数中,直接调用setCount方法,并传入新的值即可

useReducer

  • useReducer是useState的替代方案,在state逻辑复杂且包含多个子值或者下一个state依赖之前的state的场景下比useState更适用
  • 基本用法:const [state, dispatch] = useReducer(reducer, initialArg, init);
  • 如果 useReducer 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行(React 使用 Object.is() 比较算法 来比较 state)
  • 两种初始化useReducer state的方式
  • 方式一:将state作为第二个参数传入useReducer(最简单)
const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}
  );
  • 方式二:惰性创建初始state,需要将init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initialArg)
function init(initialCount) {
  return {count: initialCount};
}

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

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
  • useReducer跟redux的用法非常类型,但也存在以下两点主要的差异
  • useReducer 无法获取全局 store,必须要搭配 useContext 才能做到类似轻量化的 redux
  • useReducer 没有 middleware ,不能像 Redux 一样能用 thunk 或 saga 来异步处理数据

useEffect

  • useEffect是副作用函数,React会在每次渲染后调用副作用函数,包括第一次渲染的时候,React保证了每次运行effect的同时,DOM都已经更新完毕
  • useEffect是通过创建闭包的方式执行的,此时内部只能看到创建闭包时候的数据值,因此如果需要根据数据的变化来改变内部执行的逻辑,则需要加入依赖
  • useEffect可以看做是componentDidMount、componentDidUpdate、componentWillUnmount这三个生命周期函数的组合
  • 由于通常会有让组件在加载和更新时执行相同的操作,即在每次渲染后执行相同操作,但是在class组件中没有提供这样的方法,这时只能在componentDidMount和componentDidUpdate增加一样的逻辑来实现
  • effect的执行时机:与componentDidMount、componentDidUpdate不同的是,传给useEffect的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用,但会保证在任何新的渲染前执行。在开始新的更新前,React总会先清除上一轮渲染的effect
  • 需要清除和不需要清除是两种常见的副作用操作,需要清除时,可使用回调函数来指定如何“清除”副作用,回调函数会在组件销毁时执行
  • 每次重新渲染后,都会生成新的effect,替换掉之前的effect,正是基于这个原因,effect会提供变量的最新的值,而不用担心其会过期
  • Hook允许按照代码的用途来分割代码,所以跟useState一样,useEffect可以在组件中多次使用,而不是像声明周期函数那样,React将按照effect声明的顺序依次调用组件中的每一个effect
  • useEffect使用技巧
  • 如果useEffect依赖的数据为一个state且其改变频繁,则可以使用useState传入一个函数的方式去掉这个依赖
  • 不需要清除的effect
  • 与componentDidMount和componentDidUpdate不同过的是,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这会让应用看起来响应更快
  • 需要清除的effect
  • 如果需要声明一个需要清除的effect,则可以在effect中返回一个函数,React会在执行清除操作时调用它,而React会在每次重新渲染和卸载组件的时候执行清除操作
  • 通过跳过对effect函数的调用来进行性能优化
  • 在class组件中,可以在componentDidUpdate中添加比较的逻辑进行性能优化,例如
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
  • 在useEffect中,可以通过传递数组作为第二个可选参数的方式进行优化,对于有清除操作的effect同样适用
  • 特别注意,如果第二个参数传入一个空数组,意味着该hook只在组件挂载时运行一次,如果不传入第二个参数,则useEffect会在每次页面渲染的时候执行
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

useContext

  • 基本使用:const value = useContext(MyContext);
  • 接收一个context对象(React.createContext 的返回值)并返回该context的当前值,当前的context值由上层组件中举例最近的<MyContext.Provider>的value prop决定
  • 其原理是读取context的值以及订阅context的变化,具体使用时需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context
  • 当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext provider的context value值
  • useContext的参数必须是context对象本身
  • 使用步骤
  • 创建context
const defaultContextValue = {
  username: '猪小明'
}
export const appContext = React.createContext(defaultContextValue)
  • 传递context
ReactDOM.render(
  <appContext.Provider value={defaultContextValue}>
    <App/>
  </appContext.Provider>
)
  • 接收context
/**
 * 使用Consumer接收
 */
const Robot = (props) => {
  return (
    <appContext.Consumer>
      {(value) => {
        <div>
          <p>作者:{value.username}</p>
        </div>
      }}
    </appContext.Consumer>
  )
}

/**
 * 使用userContext接收
 */
const Robot = (props) => {
  const value = useContext(appContext)
  return (
    <div>
      <p>作者:{value.username}</p>
    </div>
  )
}

useRef

  • useRef返回一个可变的ref对象,其 .current 属性被初始化为传入的参数(initialValue),返回的ref对象在组件的整个生命周期内保持不变,本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”
  • useRef() 比class组件中的 ref 属性更有用,它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式
  • useRef创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象
  • 当 ref 对象内容发生变化时,useRef 并不会发出通知,变更.current属性也不会引发组件重新渲染
// 示例一:useRef的常规使用
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

// 示例二:使用useRef惰性创建一次对象的示例
function Image(props) {
  const ref = useRef(null);

  // IntersectionObserver 只会被·创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }

  // 当你需要时,调用 getObserver()
  // ...
}

useImperativeHandle

useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值,通常其应当与 forwardRef 一起使用,但在大多数情况下,应当避免使用 ref 这样的命令式代码,语法:useImperativeHandle(ref, createHandle, [deps]),示例如下

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在以上示例中,渲染 <FancyInput ref= /> 的父组件可以调用inputRef.current.focus()

useCallback

  • 返回一个memoized回调函数
  • 把内联回调函数以依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新
  • useCallback(fn, deps)相当于useMemo(() => fn, deps)
const memoizedCallback = useCallback(
  (函数参数) => {
    函数体;
  },
  [a, b],
);

useMemo

  • 返回一个 memoized 值
  • 把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算memoized值,这种优化有助于避免在每次渲染时都进行高开销的计算
  • 如果没有依赖项传入,useMemo在每次渲染时都会计算新的值
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

React是如何把对Hook的调用和组件联系起来的

  • 每个组件内部都有一个"记忆单元格"列表,它们只不过是用来存储一些数据的JavaScript对象。当用useState()调用一个Hook的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个useState()调用会得到各自独立的本地state的原因

Redux

Redux工作流程

Redux工作流程示意图

Redux形象示例

  • 以下以通知朋友修改其动态错别字的方式来类比整个模块
  • React Component:订阅了朋友圈的人
  • Action Creators:打电话或者发短信的行为
  • Store:朋友圈
  • Reducers:修改朋友圈的哪条动态以及修改方式(修改哪一个错别字,同时包含了朋友圈的初始化数据(原数据))

Redux使用步骤

  • 创建store(createStore),并同时给store传入一个修改朋友圈动态的方式(reducer)
  • 使用dispatch告诉store需要修改的朋友圈动态
  • 在reducer中设置修改朋友圈的方式(修改哪一个错别字,同时包含了朋友圈的初始化数据(原数据))
  • store订阅store中的朋友圈动态消息事件

Redux设计和使用的基本原则

  • store是唯一的
  • 只有store能改变自己的内容(reducer中虽然操作了store中的数据,但具体的改变还是store本身在执行)
  • reducer必须是纯函数(纯函数:给定固定的输入就一定会有固定的输出,而且不会有任何的副作用)

原生Redux的使用

安装命令:npm install redux -S

---------- 第一步:创建reducer ----------
import i18n from "i18next";
import {ADD_LANGUAGE, CHANGE_LANGUAGE, LanguageActionTypes} from "./languageActions";

export interface LanguageState {
  language: "en" | "zh";
  languageList: { name: string, code: string }[]
}

const defaultState: LanguageState = {
  language: "zh",
  languageList: [
    {
      name: "中文", code: "zh",
    }, {
      name: "English", code: "en"
    }
  ]
}

export const languageReducer = (state = defaultState, action: LanguageActionTypes) => {
  console.log(action)
  switch (action.type) {
    case CHANGE_LANGUAGE:
      i18n.changeLanguage(action.payload) // 此用法将产生副作用,不符合reducer的标准
      return {
        ...state,
        language: action.payload
      }
    case ADD_LANGUAGE:
      return {
        ...state,
        languageList: [...state.languageList, action.payload]
      }
  }
  return state
}

---------- 第二步:创建store ----------
import {compose, createStore} from "redux";
import {languageReducer} from "./language/languageReducer";

const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export const store = createStore(languageReducer, composeEnhancers())

---------- 第三步:获取store中的state并订阅store中的数据状态变更事件 ----------
class HeaderComponent extends React.Component<RouteComponentProps & WithTranslation, State> {

  constructor(props: RouteComponentProps & WithTranslation) {
    super(props);
    const storeState = store.getState();
    this.state = {
      language: storeState.language,
      languageList: storeState.languageList
    }
    // 订阅store
    store.subscribe(this.handleStoreChange)
  }

  handleStoreChange = () => {
    const { language, languageList } = store.getState()
    this.setState({
      language, languageList
    })
  }

  changeLanguage = () => {
    const {language, languageList} = this.state
    const otherLanguage = languageList.filter(e => e.code === language)[0]
    store.dispatch(getLanguageChangeAction(otherLanguage.code))
  }

  render() {
    return (
      <button onClick={this.changeLanguage}>切换语言</button>
    );
  }
}

Redux第三方库

  • React-Redux
  • Redux-toolkit插件,使用方式可参考这里
  • Redux中间件,主要指Redux-thunk和Redux-sage,用于处理异步请求的redux,真实项目中的Redux架构往往需要借助于中间件才能完成,常用中间件如下
  • Redux中间件往往用来对Redux本身进行扩展,只作用于Action和store的部分
  • Redux中间件是对redux的dispatch的升级

真实项目中的Redux架构图

Redux第三方库之React-Redux插件

  • React-Redux是一个第三方模块,用于在react中方便的使用redux
  • 安装命令:npm install react-redux -S
  • React-Redux使用步骤
  1. 使用与原生redux相同的方式创建reducer和store
  2. 使用React-Redux的Provider组件连接store进行数据传递
  3. 使用store,使用方法包括使用connect生成高阶组件的方式和使用React-Redux hook两种方式,由于hook的方式较为简单,此处主要介绍使用connect生成高阶组件的步骤
  • 使用connect将自定义组件和store进行连接
  • 指定连接的方式(mapStateToProps指定获取store中数据的映射规则,mapDispatchToProps指定改变store中的数据的映射规则),作为connect的参数
  • 通过上一步,可以直接像使用props中的属性的方式一样使用映射规则中指定的属性或者方法
---------- 第一步:创建reducer和store,创建方式与原生的redux方式相同,此处直接省略 ----------
---------- 第二步:使用Provider组件连接store进行数据传递 ----------
const ReactReduxApp = (
  // 将Provider和store进行连接
  <Provider store={store}>
    <ReactReduxTodoList />
  </Provider>
);

ReactDOM.render(
  ReactReduxApp,
  document.getElementById('root')
)

---------- 第三步:使用store ----------
/**
 * 使用方式一:使用connect创建高阶组件并且进行state和dispatch映射的方式获取数据
 */
// 指定连接的规则
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}

// store.dispatch to props
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange (e) {
      const action = getInputChangeAction(e.target.value)
      dispatch(action)
    }
  }
}

// 声明Props的类型
type PropsType = ReturnType<typeof mapState>
  & ReturnType<typeof mapDispatch>

const ReactReduxTodoList: React.FC<PropsType> = (props) => {
    const {inputValue, handleInputChange} = props
    // 使用inputValue和handleInputChange属性
}

// 将自定义组件和store进行连接
export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxTodoList)

/**
 * 使用方式二:在函数组件中使用hooks的方式获取数据
 */
export const Header: React.FC = () => {
  // 连接store,获取store中存储的language对象的值
  const language = useSelector(state => state.language.language)
  const languageList = useSelector(state => state.language.languageList)
  // 获取dispatch作为action转发的函数
  const dispatch = useDispatch()
  // const dispatch = useDispatch<Dispatch<LanguageActionTypes>>()

  const menuClickHandler = (e: any) => {
    if (e.key === 'new') {
      dispatch(addLanguageActionCreator("新语言", "new_lang"))
      return
    }
    dispatch(changeLanguageActionCreator(e.key))
  }
  return (
    <div  onClick={menuClickHandler} className={style['app-header']}>
    </div>
  )
}

Redux中间件之Redux-thunk

  • Redux-thunk用于实现在异步函数中使用redux,并不是代码必须,但有助于代码的关注点分离
  • Redux-thunk扩展了dispatch的支持范围,从只支持对象扩展到可以支持函数
  • 使用Redux-thunk时,要求传入的action为一个函数(redux本身要求传入的action必须为一个对象)
  • 安装命令:npm install redux-thunk -S
  • Redux-thunk使用步骤
  • 引入中间件,注意如果需要同时引入redux-devtools时,可参照redux-devtools在github文档中的写法
  • 增加处理异步请求的action,在action内部调用异步方法,同时异步方法完成后使用dispatch(传入的参数)转发真实需要处理的action
// 引入中间件
const composeEnhancers =
        typeof window === 'object' &&
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
                window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
                  // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
                }) : compose;

const enhancer = composeEnhancers(
        applyMiddleware(thunk),
);

export const store = createStore(reducer, enhancer);

//----------------------------------------

// 处理异步请求的action
export const getListAction = () => {
  return (dispatch) => {
    axios.get('http://localhost.charlesProxy.com:3000/api/list.json')
         .then(res => {
           const action = initListAction(res.data)
           dispatch(action)
         })
  }
}

Redux中间件之Redux-saga

  • Redux-saga可以将异步文件拆分到sagas这样的文件来单独管理
  • 安装命令:npm install redux-saga -S
  • Redux-saga使用步骤,示例项目
  • 引入中间件,注意如果需要同时引入redux-devtools时,可参照redux-devtools在github文档中的写法
  • 编写generator函数,并将其导出作为参数传入中间件的run方法中
// 引入中间件,注意createStore和run方法的调用顺序
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// noinspection JSUnresolvedVariable
const composeEnhancers =
  typeof window === 'object' &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
      // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
    }) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware),
);

export const store = createStore(reducer, enhancer);
// 在run方法中传入saga
sagaMiddleware.run(mySaga)

//----------------------------------------

// 捕获action并在其中加入异步获取数据的逻辑,注意
function* getInitList() {
  try {
    const res = yield axios.get('http://localhost.charlesProxy.com:3000/api/list.json')
    const action = initListAction(res.data)
    yield put(action)
  } catch (e) {
    console.log('list.json 网络请求失败');
  }
}

// saga必须导出为一个generator函数
// generator 函数
export function* mySaga() {
  // 监听redux的action,当接收到type为GET_INIT_LIST的action时,调用getInitList函数
  yield takeEvery(GET_INIT_LIST, getInitList)
}

按需异步加载组件

在访问页面时,默认会把所有源码打包的js文件一次性加载,但是实际上有很多页面的内容并不会用到,因此可以使用异步加载的方式

React16.8以后可以使用React.Lazy和React.Suspense组合的方式来使用异步加载,但写法比较繁琐,更推荐使用第三方的react-loadable库来实现异步加载,示例项目

使用react-loadable异步加载的实现步骤

  • 如果组件中需要接收外部传入的参数,由于此时外部没有直接引入组件,此时需要使用withRouter将组件包裹
class Detail extends React.Component {

  componentDidMount() {
    // 正常获取参数
    const id = this.props.match.params.id
    // 使用参数...
  }

  render() {
    const {title, content} = this.props
    return (
      <DetailWrapper>
        <Header>{title}</Header>
        <Content dangerouslySetInnerHTML = {{__html: content}} />
      </DetailWrapper>
    )
  }
}

const mapState = (state) => ({})

const mapDispatch = (dispatch) => ({})

export const MyDetail = connect(mapState, mapDispatch)(withRouter(Detail))
  • 编写loadable.js文件,在其中设置异步加载的组件,有以下两种方式
/**
 * 方式一:当需要异步加载的组件的导出方式为默认导出时
 */
export const LoadableDetail = Loadable({
  // 此处由于loadale.js文件放在组件相同的文件夹下,且组件存在的路径为:index.js文件中,因此可直接写./
  loader: () => import('./'),
  loading: () =>  <div>数据加载中</div>
})


/**
 * 方式二:当需要异步加载的组件的导出方式为命名导出时
 */
export const LoadableDetail = Loadable({
  // 指定组件的路径
  loader: () => import('./index'),
  loading: () =>  <div>数据加载中</div>,
  render: (loaded, props) => {
    // 指定需要加载的组件名称
    const MyDetail = loaded.MyDetail
    // 返回组件
    return <MyDetail/>
  }
})
  • 引用异步加载组件
function App() {
  return (
    <div className="App">
      <Provider store={store}>
        <BrowserRouter>
          <Route path='/detail/:id' exact component={LoadableDetail}/>
        </BrowserRouter>
      </Provider>
    </div>
  )
}

添加CSS样式文件

  • 直接引入css文件会对对应组件整个组件树的样式产生影响,可使用CSS-in-JS的方式解决此问题,同时也有styled-components和emotion等好用的第三方库可以更加方便的使用样式

原生的CSS-in-JS

  • 常规的css引用方式:import './index.css',使用此种方式极易造成css的全局样式污染以及命名冲突等问题
  • 使用原生的CSS-in-JS其原理是将css文件作为对象引入,通过访问对象来独立加载样式,示例:import styles from './index.module.css'
  • 使用CSS-in-JS的方式往往还需要额外的插件来支持代码的智能提示:npm install typescript-plugin-css-modules --save-dev

使用步骤

  • 编写普通的css样式文件
/**
 * App.module.css
 */
.App {
  text-align: center;
}
.App-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-items: center;
  color: white;
}
  • 引入并使用样式
import styles from './App.module.css'
function App() {
  return (
    // 对应的样式为:App.module.css文件中的.App类
    <div className={styles.App}>
    </div>
  )
}

第三方组件库的使用

styled-components

  • 安装styled-components:npm install styled-components -S
  • 使用步骤,示例项目
  • 在App.js中引入style.js
  • style.js中编写全局样式,注意从4版本开始,全局样式需要使用createGlobalStyle来创建
  • 在App.js根标签中增加<GlobalStyle/>即可影响全局的样式

emotion

  • 安装emotion:
npm install @emotion/react -S
npm install @emotion/styled -S
  • 在页面组件的顶部写上如下代码,告知组件中使用了 emotion 行内样式/* @jsxImportSource @emotion/react */
  • 行内样式编写格式:css={ /* 行内样式代码 */ }

图标使用

通过styled-components使用图标

  • 安装styled-components:npm install styled-components -S
  • 下载图标,注意下载时需要在项目中设置包含的文件,选择:woff,ttf,eot,svg,base64
  • 将图标文件(包括:.woff,.svg,.ttf,.eot,.css)拷入项目中,修改iconfont.css文件名为iconfont.js
  • 修改.woff,.svg,.ttf,.eot,.css对应的url的引用路径,改为相对路径,将class声明删除
  • 引入styled-components,使用createGlobalStyle创建全局样式并导出
  • 在项目中使用<span class="iconfont">&#x33;</span>的方式使用图标,具体使用方式可参照这里这里

以组件的形式使用svg

  • 将图标作为组件引入
import { ReactComponent as SoftwareLogo } from "logo.svg";
  • 使用图标组件
const PageHeader = () => {
  return (
    <Header between={true}>
      <ButtonNoPadding type={"link"} onClick={resetRoute}>
        <SoftwareLogo width={"5rem"} color={"rgb(38, 132, 255)"} />
      </ButtonNoPadding>
    </Header>
  );
};

immutable

  • 示例项目
  • 使用immutable的fromJS方法可以将对象转换为不可变对象
  • 此时可以直接使用对象的set方法,此方法会结合之前immutable对象的值,和设置的值,返回一个新的对象
  • 使用示例:src/common/header/store/reducer.js,src/common/header/index.js
  • 使用redux-immutable下的combineReducers方法可以使组合了多个reducer的store最终也变成immutable的对象,示例:src/store/reducer.js
  • 使用redux-immutable的fromJS包裹的对象的复合属性也会被转换为immutable的对象,此时如果需要使用外层对象来set新值给内层的immutable对象,此时的新值也需要为immutable对象,示例:src/common/header/store/actionCreators.js

路由与SPA

路由

  • 作用:类比路由器,路由器的作用是通过读取路由表,根据tcp/ip中的地址将数据包按照最佳路线传输到指定地点,在这个过程中,数据传输的路线的计算过程就是路由(routing)

传统的网站的路由方式

  • 当浏览器的url发生变化时,浏览器页面相应的发生改变,此种方式路由系统难以管理且会暴露资源的路径

现代路由方式

  • js,css,html会被打包为一个大文件,一次性传输给浏览器
  • js通过劫持浏览器路由,生成虚拟路由来动态渲染页面dom元素,路由与实际的文件没有对应关系

react-router

  • react-router 和 react-router-dom 的关系类似于 react 和 react-dom、react-native
  • react-router 用于进行路由的计算和路由状态的管理,react-router-dom 则用于消费 react-router 的计算结果,用于 dom 的呈现
  • react-router-dom
  • 安装react-router-dom会自动安装react-router这个核心框架
  • <Link />组件可以渲染出<a/>标签
  • <BrowserRouter />组件可以利用H5 API实现路由切换
  • <HashRouter />组件则利用原生JS中的window.location.hash来实现路由切换
  • 使用react-router实现路由的要求
  • 路由导航与原生浏览器操作行为一致:<BrowerRouter/>
  • 路由的路径解析原理与原生浏览器一致,可以自动识别url路径:<Route/>
  • 路径的切换以页面为单位,不发生页面堆叠(只匹配一个Route):<Switch/> + Route中的exact属性
  • 使用react-router实现页面跳转
/**
 * 第一种方式:使用withRouter高阶组件实现页面跳转,并在组件中获取参数实现跳转
 */
interface PropsType extends RouteComponentProps {
  id: number | string;
}

const ProductImageComponent: React.FC<PropsType> = ({ id, history }) => {
  return (
    <div onClick={() => history.push(`detail/${id}`)}>
      内容
    </div>
  )
}

export const ProductImage = withRouter(ProductImageComponent)

/**
 * 第二种方式:使用useRouter获取history实现跳转,本质上跟第一种方式差不多,只是获取history的方式不一样
 */
export const Header: React.FC = () => {
  const history = useHistory()

  return (
    <Button.Group className={style['button-group']}>
      <Button onClick={() => history.push('/register')}>注册</Button>
      <Button onClick={() => history.push('/signIn')}>登录</Button>
    </Button.Group>
  )
}

/**
 * 第三种方式:使用Link超链接的方式跳转
 */
export const Header: React.FC = () => {
  return (
    <Layout.Header className={style['main-header']}>
      <Link to='/'>
        <img src={logo} alt="" className={style['App-logo']}/>
        <Typography.Title level={3} className={style['title']}>环球旅游网</Typography.Title>
      </Link>
    </Layout.Header>
  )
}

获取路由参数

/**
 * 第一种方式:直接通过对象的链式调用获取
 */
import {RouteComponentProps} from 'react-router-dom'

interface MatchParams {
  touristId: string;
}

export const DetailPage: React.FC<RouteComponentProps<MatchParams>> = (props) => {
  return (
    <div>路线id:{props.match.params.touristId}</div>
  )
}

/**
 * 第二种方式:使用hook的方式获取
 */
import {RouteComponentProps, useParams} from 'react-router-dom'

interface MatchParams {
  touristId: string;
}

export const DetailPage: React.FC<RouteComponentProps<MatchParams>> = (props) => {
  const {touristId} = useParams<MatchParams>();
  return (
    <div>路线id:{touristId}</div>
  )
}