Navigation
阅读进度0%
No headings found.

React 核心技术:Ref、Hook 与组件设计模式

December 19, 2024 (1y ago)

React
JavaScript
TypeScript

ref和DOM

在众多的组件库中,ref是非常重要的,对于一些特殊场景和 陈旧组件,ref有大用处,

为什么使用ref和注意事项

当前,我们学习 父子组件的交互仅仅是通过props控制的,但是props会带来重新渲染,有时候会影响性能,或者在一些特定的场景下,props并不是非常我们进行代码组织,这时候ref就有用了

比如如下的场景使用ref就非常的合适

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画
  • 集成第三方 DOM 库。

但是也需要避免过度使用ref。反而使用状态提升也是不错的方式

使用指南

场景1-给DOM加ref

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // 创建一个 ref 来存储 textInput 的 DOM 元素
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }
 
  focusTextInput() {
    // 直接使用原生 API 使 text 输入框获得焦点
    // 注意:我们通过 "current" 来访问 DOM 节点
    this.textInput.current.focus();
  }
 
  render() {
    // 告诉 React 我们想把 <input> ref 关联到
    // 构造器里创建的 `textInput` 上
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
 
// react会在 组件挂载时给给ref 中的current赋值存入的DOM元素,在卸载的时候传入null,ref优先于
 componentDidMount 或 componentDidUpdate更新

场景2-给Class组件加ref

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // 创建一个 ref 来存储 textInput 的 DOM 元素
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }
 
  focusTextInput() {
    // 直接使用原生 API 使 text 输入框获得焦点
    // 注意:我们通过 "current" 来访问 DOM 节点
    this.textInput.current.focus();
  }
 
  render() {
    // 告诉 React 我们想把 <input> ref 关联到
    // 构造器里创建的 `textInput` 上
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
 
// 给class
class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
 
  componentDidMount() {
    this.textInput.current.focusTextInput();
  }
 
  render() {
    return (
      <CustomTextInput ref={this.textInput} />
    );
  }
}

函数组件就没有机会了吗?

有机会!但是不能直接给ref需要转化,并且结合hook

// 这个博客写的内容和实在

函数组件的Rrf如何写?

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
 
// forwardRef方法是react中的,具体用于透传ref
// 父组件如何调用指南
<FancyInput ref={inputRef} />
  inputRef.current.focus()。
  
  
 
  
 
 

组件中某些DOM暴露ref给父组件

关于转发ref 很重要!,具体在前文 骨灰教程三中有详细描述,这个自己暴露的实现 主要还是依赖转发,在HOC中的实现代码如下,在antd中的部分源码也是使用该方式

// hoc中的实现
function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
 
    render() {
      const {forwardedRef, ...rest} = this.props;
 
      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }
 
 
 
  
  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}
 
// fc中的实现
 

回调的形式使用ref

这种形式不常见,

// 这里列举了两种哈 一种是class组件的一种是FN组件的
// 作用就是确保refs是最新的,在组件挂载之前可以拿到最新的refs
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
 
    this.textInput = null;
 
    this.setTextInputRef = element => {
      this.textInput = element;
    };
 
    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={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
 
 
 
function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}
 
class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

协调react内部的一些算法

在官方的文档中,一大幅文字的形式,解释了react内部是如何进行diffing算法计算的,我们这里引用了一篇优秀博客来讲解,这里不打算将哈,打算新开一个解析react源码系列进行讲解,

严格模式和静态类型检查以及PropTypes类型检查

静态类型检查Flow

首先哈Flow是Facebook 研发的,用于对js进行静态类型检查,使用方式如下,

需要注意的是,我们都是在create-react-app下进行的操作,由于Flow属于js语法拓展。浏览器并不支持它,你需要在构建的时候去掉它,但是好消息是,create-react-app内部已经去掉了,如果你不是用create-react-app,你需要百度一下了哈,

  1. 安装Flow
yarn add --dev flow-bin
 
npm install --save-dev flow-bin
  1. 添加一个script

packjson中

{
  // ...
  "scripts": {
    "flow": "flow",
    // ...
  },
  // ...
}
  1. 初始化
yarn run flow init
npm run flow init
 
# 完成初始化会生成一个flow配置文件,
  1. 运行检查器
yarn flow
npm run flow
 
如果你看到了该信息,说明,已经检查通过
No errors!
✨  Done in 0.17s.
  1. 一些简单语法。如 类型注释
// @flow
function concat(a: string, b: string) {
  return a + b;
}
 
concat("A", "B"); // Works!
concat(1, 2); // Error!
 

create-react-app 下配置Typescript

在大型项目中我们更推荐是哟ts,而不是这些各种各样的检测器,ts也是未来js的方向!

主要安装要点

  • 将 TypeScript 添加到你的项目依赖中。
  • 配置 TypeScript 编译选项
  • 使用正确的文件扩展名
  • 为你使用的库添加定义

直接使用cli内置的模板,这样最方便哈

npx create-react-app my-app --template typescript
# 这样就好了!

如果是后加呢?

  1. 安装ts
yarn add --dev typescript
npm install --save-dev typescript

注意阿,还没有完,在编译的时候,我们build是需要执行tsc编译ts文件的,因此需要给build的script加上tsc命令,packjson中

{
  // ...
  "scripts": {
    "build": "tsc",
    // ...
  },
  // ...
}
  1. 配置tsc编译的时候去编译ts

如果没有tsc配置文件,那么tsc将会是毫无卵用的,

yarn run tsc --init
npx tsc --init

执行上述命令会在项目根目录初始化一个tsconfig.json 文件,配置它很重要,下面是一般的配置,个性化配置需要看文档 tsconfig文档 [tsconfig](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)

// tsconfig.json
 
{
  "compilerOptions": {
    // ...
    "rootDir": "src",  需要tsc编译的文件来自 src下
    "outDir": "build"  编译输出到 build下
    // ...
  },
}
 
 
// antdProp的tsconfig.json
{
  "compilerOptions": {
    "outDir": "build/dist",
    "module": "esnext",
    "target": "esnext",
    "lib": ["esnext", "dom"],
    "sourceMap": true,
    "baseUrl": ".",
    "jsx": "react",
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "allowJs": true,
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "strict": true,
    "paths": {
      "@/*": ["./src/*"],
      "@@/*": ["./src/.umi/*"]
    }
  },
  "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
}
 

之后就是正常开发使用.ts 使用tsx.打包一样打,运行一样run start

安装react的types声明,更多的声明文件type上看它和npm类似

# yarn
yarn add --dev @types/react
 
# npm
npm i --save-dev @types/react

如果没有你需要的声明文件,那你需要自己定义一个

具体方法

  1. 新建declarations.d.ts 
  2. 编码
declare module 'querystring' {
  export function stringify(val: object): string
  export function parse(val: string): object
}

其它扩展

基于社区的,我们还有很多扩展。这里不详细的说明了,感兴趣的同学可以去看看react官方文档,

PropsTypes类型检查

实际上这个我们现在用的比较少了,基本来都往ts上走,这里不过多说明,在基础教程里有说明

严格模式

用来突出显示应用程序中潜在问题的工具,这里不过多讲解,实际项目中很少应用

非受控组件设计模式

大多数情况下我们是不推荐使用非受控组件来干,受控组件干的事,但是偏要实现也是有方法的。那就是让DOM来完成,看下面的例子

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }
 
  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }
 
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
 
// 还有更多的方式你应该值得注意,比如select上的默认value不是value,input为file;类型时如何处理等问题。
下面还有一个例子就是其实现
class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }
 
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}
 
ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

web Componets

wC实际上是一个基于html的Components。详细请查看MDN或者软大大的教程,说白了就是用户自定义标签

react 和vue和wC所运用的场景不一样,没有谁最NB。下面的例子我们来讲解一下react中如何使用使用wc wc中如何使用react

// react中使用wc
 
// 注意class="demo"是对的不是classname哈!
function BrickFlipbox() {
  return (
    <brick-flipbox class="demo">
      <div>front</div>
      <div>back</div>
    </brick-flipbox>
  );
}
 
class HelloMessage extends React.Component {
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}
 
 
//wc中使用react 
class XSearch extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('span');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
 
    const name = this.getAttribute('name');
    const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
    ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
  }
}
customElements.define('x-search', XSearch);
 

Hook介绍和基础原理

hook实际上是16.8出来的新特性,让fn也可以管理state这样一来,传统的oop的心智模式将会发生变化,FN变炒成了主流,详细的对比我再 ===> 面试篇中有讲可以点击这里进行翻阅 链接

基础hook

基础hook的用法十分的简单,这里呢,直接上代码

useState

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>
            <button onClick={() => setCount(
          		( prevCount,crerruntCount ) =>{
              retrun prevCount + crerruntCount + ''
          	  }
            )
    			}>
              +</button>
    </>
  );
}
 
 
// 需要强调的一个点就是,这里的useState的源码是如何运作的需要了解一下下
// 自己实现一个useState

useEffect

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);
 
// 这个实际上也是接受两个参数,第一个是函数,第二个依赖也就是distance,当依赖有变就去调用函数
 
第一个函数retrun出来的函数会在,组件销毁时调用,如果只想初始化的时候调用一次那么跨域传递一个空数组给到这个
 
依赖 []  类似于下面这样
 
useEffect(
  () => {
   fetch.....
  },
  [],
);
 
 
 
// 非常常见的面试题就是useEffect和useLayout的区别,实际上useEffect是异步的,。useLayoutEffect是在DOM 更新完之后触发的。就像next.tick一样,

useContext

// 实际上这个东西就是为了践行,Provider  consumer 模式,
 
 
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
 
const ThemeContext = React.createContext(themes.light);
 
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
 
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

扩展hook

useReducer

// 注意,如果你希望了解该内容,那么你需要先来了解Redux ,该hook是是使用是特殊场景下的useState替代方案
// 该案例使用惰性加载,好处就是:将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
 
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>
    </>
  );
}
 

userReducer + useContext

使用这两个API 可以在一定的范围之内,创建一个非常简单且完善 “状态管理器”

最好的例子 详情 这个文章 React hooks(useContext、useReducer、自定义hooks) - 掘金

import React, { useReducer, useContext } from 'react'
 
const InitStateContext = createContext({
  name: '',
  page: '', 
  message: '',
  list: [],
  data: '',
});
 
const reducer = (state, action) => {
  switch (action.type) {
    case 'changeTheme':
      return {
        ...state,
        ...action.payload,
      };
    default:
      return {
        ...state,
        ...action.payload,
      };
  }
};
 
const useInitState = () => {
  const initStateCtx = useContext(InitStateContext);
  const [state = {}, dispatch = null] = initStateCtx;
  return [state, dispatch];
};
 
 
const GlobalContext = React.createContext()
 
export default function App() {
  const [state, dispatch] = useReducer(reducer, {});
 
 
  return (
    <InitStateContext.Provider value={[state, dispatch]}>  
        <div>
            <Child1></Child1>
            <Child2></Child2>
            <Child3></Child3>
        </div>
    </InitStateContext.Provider>
  )
}
 
function Child1() {
    const [dispatch] = useInitState();
    return (
        <div>
            <button onClick={() => {
                dispatch({
                    type: 'child2'
                })
            }}>改变child2</button>
            <button onClick={() => {
                dispatch({
                    type: 'child3'
                })
            }}>改变child3</button>
        </div>
    )
}
 
function Child2() {
     const [state] = useInitState();
 
    return (
        <div>
            {state.name}
        </div>
    )
}
 
function Child3() {
       const [state] = useInitState();
 
    return (
        <div>
            {state.name}
        </div>
    )
}
 
 
// 如果更近一步,我们还可以简化成自定义的 hook 
// 也就是可以子啊hooks 中直接使用 

useCallback

// 这个东西做优化的时候非常有用。具体的用法如下,仅仅有a,b这些依赖,发生变化时采取调用函数
// 返回一个函数 但是不会执行它!
 
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useMemo

// 同样的,这个hook作为优化手段也是有奇效,
// 该函数会在render期间 执行, 不能在这个函数中做其他与渲染进程无关的操作。返回的是 一个值,这个值它可以是组件
 
// 调用函数,并且返回函数的执行知乎的结果
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]);

useRef

// 一个令人头疼的问题在于,fn组件中,ref是不能传递的,要使用ref就要挂载到指定的dom上
 
function TextInputWithFocusButton() {
  const inputEl = useRef(null);  // 返回一个不可变的ref对象
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle

// 可以让你在使用 ref 时,自定义暴露给父组件的实例值。
 
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
 
// 在父组件上,父组件就能调用,子组件的focus方法
inputRef.current.focus()。
 
 

useDebugValue

// 可用于在 React 开发者工具中显示自定义 hook 的标签,我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。当然了再deBug时也有奇效
// 注意,这个东西只有在自定义hook组件上有用
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
 
  // ...
 
  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');
 
  return isOnline;
}
 
 

来点高级的自定义hook

自定义的hooks还是比较好玩的

这里只是列举了相对比较常见的的方式,也是官方给出的,当然了社区中也出现了很多各种花里胡哨的封装,但是逻辑实际上是差不多的,

简单的封装   

这里的场景还是官方的提供的,有两个组件,他们都想中的各自的“好友”是否在线,于是最初的版本

import React, { useState, useEffect } from 'react';
 
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
 
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
 
// 组件2
import React, { useState, useEffect } from 'react';
 
function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
 
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

使用自定义hook可以抽取公用逻辑

// 自定义的封装hook
import { useState, useEffect } from 'react';
 
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
 
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
 
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
 
  return isOnline;
 
  // 改造后的两个组件
  function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
 
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
  
  function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
 
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

hook实际上就是函数,所以可以和组件自己的进行通信,而且要注意的是由于函数是天生的无状态,所以,这里的hook实际上是两个不同的“状态快照”,而不是一个,需要特别说明,两个组件用的不是一个

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];
 
function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);
 
  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}

集成reducer

如果要实现比较复杂的抽离封装,我们可以使用reducer,这样在小范围内还是比较nice的,但是!如果你选择大面积使用,那么你应该引入 全局的状态管理器,入rxjs dva mbox等

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);
 
  // 继续使用reducer进行更多的复杂操作
  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }
 
  return [state, dispatch];
}
 
function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, []);
 
  function handleAddClick(text) {
    dispatch({ type: 'add', text });
  }
 
  // ...
}