快来跟我一起学 React(Day7)

简介

我们继续上一节的内容,开始分析 React 官网:https://reactjs.org/docs/accessibility.html 的 “高级指引” 部分,这一部分会涉及到 Refs 转发、Fragments、高阶组件等概念的分析,比前面章节的难度还是略微大一些的,所以一定要跟上节奏哦,我们一起出发吧!

知识点

  • Refs 转发
  • Fragments
  • 高阶组件
  • 深入 JSX

准备

我们直接用上一节中的 react-demo-day5 项目来作为我们的 Demo 项目,还没有创建的小伙伴可以直接执行以下命令 clone 一份代码:

git clone -b dev https://gitee.com/vv_bug/react-demo-day5.git

接着进入到项目根目录 react-demo-day5 ,并执行以下命令来安装依赖与启动项目:

npm install --registry https://registry.npm.taobao.org && npm start
1-1.png

等项目打包编译成功,浏览器会自动打开项目入口,看到上面截图的效果的时候,我们的准备工作就完成了。

https://gitee.com/vv_bug/react-demo-day5/tree/dev)

Refs 转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。

解释起来有点抽象,我们还是用 Demo 来演示一下。

转发 refs 到 DOM 组件

因为上一节测试 “错误边界” 组件的时候抛了一个错误, src/advanced-guides 目录下的模块都变成了 “Something went wrong”,所以我们先修改一下 src/advanced-guides/error.tsx 组件,让它不要再报错了:

function ErrorCom(): null{
  return null;
  // throw new Error("报错啦!");
}
export default ErrorCom;

我们在 src/advanced-guides 目录下创建一个 forward-ref 目录:

mkdir ./src/advanced-guides/forward-ref

然后我们在 src/advanced-guides/forward-ref 目录下创建一个 index.tsx 文件:

import React from "react";
import CusInput from "./cus-input";

function ForwardRef() {
  let cusInputRef: any;
  const handleInputRef = (ref: any) => {
    cusInputRef = ref;
  };
    /*
        让 input 元素聚焦
    */
  function focusInput() {
    cusInputRef && cusInputRef.focus();
  }

  return (
    
      {/* 自定义 input 组件  */ }
      
      
    
  );
}

export default ForwardRef;

可以看到,我们自定义了一个 CusInput,然后获取了 CusInput 元素的引用 ref,最后通过 ref 让自定义的 CusInput 元素自动获取焦点。

ok,然后我们在 src/advanced-guides/forward-ref 目录下创建一个 cus-input.tsx 组件:

import React from "react";
function CusInput(props:any, ref: any) {
  return (
    
); } export default React.forwardRef(CusInput);

可以看到,我们定义了一个函数式组件 CusInput,并且通过 React.forwardRef 方法把 CusInput 组件的 ref 指向了其子元素 input

最后我们 src/advanced-guides/index.tsx 组件中引入 src/advanced-guides/forward-ref/index.tsx 组件:

/**
 * 核心概念列表
 */
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
import ForwardRef from "./forward-ref";

function AdvancedGuides() {
  return (
    
      
{/* 代码分割 */ } {/* Context */ } {/* 报错的组件 */ } {/* Refs 转发 */ }
); }; export default AdvancedGuides;

我们重新运行项目看结果:

npm start
1-2.gif

可以看到,当我们点击 “聚焦 input” 按钮的时候,input 元素自动被聚焦了。

接下来我们用类组件的形式来实现一下 src/advanced-guides/forward-ref/cus-input.tsx 组件。

我们在 src/advanced-guides/forward-ref 目录下创建一个 cus-input.com.tsx 文件:

import React from "react";
import PropTypes from "prop-types";

type Prop = {
  handleRef: (ref: any) => void
};

class CusInputCom extends React.Component {
  static propTypes = {
    handleRef: PropTypes.func
  }

  render() {
    return (
      
); } } export default React.forwardRef((props, ref: any) => { return ; });

然后在 src/advanced-guides/forward-ref/index.tsx 组件中引入 src/advanced-guides/forward-ref/cus-input.com.tsx 组件:

import React from "react";
import CusInput from "./cus-input";
import CusInputCom from "./cus-input.com";

function ForwardRef() {
  let cusInputRef: any;
  let cusInputRef2: any;
  const handleInputRef = (ref: any) => {
    cusInputRef = ref;
  };
  const handleInputRef2 = (ref: any) => {
    cusInputRef2 = ref;
  };

  function focusInput() {
    cusInputRef && cusInputRef.focus();
  }
  function focusInput2() {
    cusInputRef2 && cusInputRef2.focus();
  }

  return (
    
      {/* 自定义 input 组件  */ }
      
      {/* 自定义 input 组件  */ }
      
      
      
    
  );
}

export default ForwardRef;

我们重新运行项目看结果:

npm start
1-3.gif

可以看到,我们分别用 “函数组件”、“类组件” 实现了 CusInput 组件。

React.createRef

我们上面用的都是使用了一个方法去接受组件的 ref 属性:

let cusInputRef: any;
const handleInputRef = (ref: any) => {
  cusInputRef = ref;
}; 
{/* 自定义 input 组件  */ }
 

其实接受一个组件的 ref 属性,除了利用函数外,我们还可以利用 React 提供的 createRef 方法。

我们来改造一下 src/advanced-guides/forward-ref/index.tsx 组件:

import React from "react";
import CusInput from "./cus-input";
import CusInputCom from "./cus-input.com";

function ForwardRef() {
  // let cusInputRef: any;
  // let cusInputRef2: any;
  let cusInputRef = React.createRef();
  let cusInputRef2 = React.createRef();
  function focusInput() {
    cusInputRef?.current?.focus();
  }
  function focusInput2() {
    cusInputRef2?.current?.focus();
  }

  return (
    
      {/* 自定义 input 组件  */ }
      
      {/* 自定义 input 组件  */ }
      
      
      
    
  );
}

export default ForwardRef;

我们还需要简单的修改一下 src/advanced-guides/forward-ref/cus-input.com.tsx 组件:

import React from "react";
import PropTypes from "prop-types";

type Prop = {
  handleRef: React.RefObject
};

class CusInputCom extends React.Component {
  static propTypes = {
    handleRef: PropTypes.object
  }
  render() {
    return (
      
); } } export default React.forwardRef((props, ref: any) => { return ; });

可以看到,我们把之前的函数接受 ref 全改成了 React.createRef() 方式:

let cusInputRef = React.createRef();
  let cusInputRef2 = React.createRef();

React 会自动把 ref 挂载到传入对象的 current 属性中:

 function focusInput() {
    cusInputRef?.current?.focus();
  }
  function focusInput2() {
    cusInputRef2?.current?.focus();
  }

效果跟上面一样,我就不演示了。

欢迎志同道合的小伙伴一起交流,一起学习。

Fragments

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

其实我们在 Demo 中已经使用过 Fragment 组件了,比如我们的 src/advanced-guides/forward-ref/index.tsx 组件:

return (
    
      {/* 自定义 input 组件  */ }
      
      {/* 自定义 input 组件  */ }
      
      
      
    
  );

我们可以试着把 src/advanced-guides/forward-ref/index.tsx 组件中的 React.Fragment 组件去掉:

1-4.png

可以看到,IDE 就直接报错了,说 “JSX 语法必须包含一个父元素”。

有童鞋要说了,“我们可以直接定一个 div 元素或者其它元素呀”,是的!你可以这样做,但是当我们定义的是 div 元素的时候,最后是会被渲染到 DOM 中的,而我们想要的是不需要渲染到 DOM 中,所以我们就可以使用 Fragment 组件。

短语法

你可以使用一种新的,且更简短的语法来声明 Fragments。它看起来像空标签:

return (
    <>
      {/* 自定义 input 组件  */ }
      
      {/* 自定义 input 组件  */ }
      
      
      
    
  );

带 key 的 Fragments

使用显式 React.Fragment 语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:

function Glossary(props) {
  return (
    
{props.items.map(item => ( // 没有`key`,React 会发出一个关键警告
{item.term}
{item.description}
))}
); }

key 是唯一可以传递给 Fragment 组件的属性。

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

使用 HOC 解决横切关注点问题

组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。

例如我们之前的切换主题组件 src/advanced-guides/context/context.func.tsx ,如果我们需要获取到 Context 对象中的 themetoggleTheme,我们需要这样做:

import {AppContext} from "../../app-context";
import React from "react";

function ContextFunc() {
  return (
    
{ ({toggleTheme}) => }
); } export default ContextFunc;

我们需要利用 AppContext.Consumer 方式,或者类组件中的静态 contextType 属性方式来获取到 Context 对象中的数据。

小伙伴有没有想过,我们的自定义组件需要关心 Context 对象怎么获取吗?能不能有一种方式直接把 Context 中的数据直接通过 props 传递给我呢?我每次写一个组件为了去获取 Context 还得去写这么多代码。

ok,“高阶组件” 来了!

我们首先在 src/advanced-guides 目录下创建一个 hoc 目录:

mkdir ./src/advanced-guides/hoc

然后在 src/advanced-guides/hoc 目录下创建一个高级组件 with-theme.tsx 组件:

touch ./src/advanced-guides/hoc/with-theme.tsx

然后将以下内容写入到 src/advanced-guides/hoc/with-theme.tsx 组件:

import React from "react";
import {AppContext, AppContextType, Themes} from "../../app-context";

export type ThemeType = {
    theme: Themes,
    toggleTheme: () => void
};
export type getThemeDataType = {
    (appContext: AppContextType): ThemeType;
};

/**
 * 带主题的高阶组件
 * @param getThemeData
 */
function withTheme(getThemeData: getThemeDataType) {
    return function (WrappedComponent: typeof React.Component | React.FunctionComponent) {
        // 转发 ref 函数组件
        const RefComponent = (props: any, ref: any) => {
            class ThemeComponent extends React.Component {
                render() {
                    return (
                        
                            {(appContext) => (
                                
                            )}
                        
                    );
                };
            }

            return (
                
            );
        };
        // 转发 ref
        return React.forwardRef(RefComponent);
    };
}

export default withTheme;

这里我们做了几步工作:

  1. 定义了一个带主题的高阶组件 withTheme 函数。
  2. 定义了一个转发 ref 函数组件 RefComponent
  3. 定义了一个带主题的组件 ThemeComponent
  4. 利用 AppContext.Consumer 获取到了 AppContext 对象中的数据。

这已经算是一个比较复杂的高阶组件了,因为里面还包含了高阶组件的 ref 转发等功能(算是对前面 forwad-ref 内容的复习了,不熟悉的童鞋记得去看一下前面的文章哦 )。

接着我们修改一下 src/advanced-guides/context/context.com.tsx 组件:

import React from "react";
import withTheme,{ThemeType} from "../hoc/with-theme";
type Prop =ThemeType & {
};
class ContextCom extends React.Component {
  render() {
    return (
      
); } } // 构造一个带主题功能的组件 export default withTheme((appContext) => appContext)(ContextCom);

同样修改一下 src/advanced-guides/context/context.func.tsx 组件:

import React from "react";
import withTheme from "../hoc/with-theme";

function ContextFunc(props: any) {
    return (
        
); } // 构造一个带主题功能的组件 export default withTheme((appContext) => appContext)(ContextFunc);

可以看到,是不是变得很简答了呢?我们不需要再考虑 “AppContext 该怎么获取”了,我们只需要利用 withTheme 高阶函数,它就会自动的把 AppContext 中跟主题相关的数据给到这个组件,我们直接在组件中通过 Props 就可以访问了。

1-5.gif

可以看到,效果跟我们之前的一样。

高阶组件的约定

  1. 不要改变原始组件,高级组件应该是一个纯函数。

    ...
    /**
     * 带主题的高阶组件
     * @param getThemeData
     */
    function withTheme(getThemeData: getThemeDataType) {
        return function (WrappedComponent: typeof React.Component | React.FunctionComponent) {
            // 转发 ref 函数组件
            const RefComponent = (props: any, ref: any) => {
                class ThemeComponent extends React.Component {
                    render() {
                        return (
                            
                                {(appContext) => (
                                    
                                )}
                            
                        );
                    };
                }
    
                return (
                    
                );
            };
            // 转发 ref
            return React.forwardRef(RefComponent);
        };
    }
    
    export default withTheme;
    

    可以看到,我们并没有对传入的 WrappedComponent 组件做任何额外的操作。

  2. 将不相关的 props 传递给被包裹的组件。

     
    

    可以看到,我们保留了 WrappedComponent 组件自身的 props,只是额外通过 getThemeData 方法添加了一些参数。

  3. 最大化可组合性。

    我们定义的 withTheme 是一个返回高阶组件的函数,可以接受 getThemeData 参数,提供给使用者自定义 props 里面的内容。

  4. 约定:包装显示名称以便轻松调试。

    我们直接定义了一个叫 ThemeComponent 的组件,方便调试。

深入 JSX

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。如下 JSX 代码:


  Click Me

会编译为:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

如果没有子节点,你还可以使用自闭合的标签形式,如:

会编译为:

React.createElement(
  'div',
  {className: 'sidebar'}
)

指定 React 元素类型

JSX 标签的第一部分指定了 React 元素的类型。

大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX 表达式时,Foo 必须包含在作用域内。

React 必须在作用域内

由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。

例如,在如下代码中,虽然 ReactCustomButton 并没有被直接使用,但还是需要导入:

import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
 // return React.createElement(CustomButton, {color: 'red'}, null);
  return ;
}

如果你不使用 JavaScript 打包工具而是直接通过

你可能感兴趣的:(快来跟我一起学 React(Day7))