写了3个月React,我学到了什么?

趁你还年轻
English

image.png

原文链接:
React那些事儿
React hooks那些事儿

新环境从Vue转到了React技术栈,这个过程还是比较有趣的。

在React中会看到与Vue很多相似的地方,也有一些不同的地方,学习过程中遇到一些疑惑,做了记录。

  • useRef如何解决空指针问题?
  • useEffect与useCallback(useMemo)的区别是什么?
  • React除了可以通过props传递数据以外,如何通过context方式传递数据?
  • React.createElement(Input, props)中的React.createElement如何理解?
  • react中的FC是什么?FC<[interface]>是什么意思?主要用处及最简写法是怎样的?
  • React中FC的形参的props, context, propTypes, contextTypes, defaultProps, displayName是什么?
  • import { MouseEvent } from 'react'是什么意思?SyntheticEvent是什么类型?
  • React.forwardRef是什么意思?useImperativeHandle是什么意思?

useRef如何解决空指针问题?

通常来说,useRef用于引用组件的Dom节点。Vue中的ref则是引用一个vue组件。与Vue不同,react中的ref不仅仅是引用Dom节点,还可以生成一个内存不变的对象引用。

使用useState导致的空指针示例

const [foo, setFoo] = useState(null);

const handler = () => {
    setFoo("hello")
}

useEffect(() => {
    return () => {
      // 无论怎样foo都是null,给useEffect的deps加入foo也不行
      if (foo === "hello") {
          // do something...
      }
    }
}, [])

使用useRef的正确示例(解决事件处理器中对象为null的问题)

const foo = useRef(null)

const handler = () => {
    foo.current = "hello"
}

useEffect(() => {

    return () => {
      // foo.current为hello
      if (foo.current === "hello") {
          // do something...
      }
    }
}, [])

useRef解决空指针问题的原因是什么?

  • 组件生命周期期间,useRef指向的对象都是一直存在的
  • 每次渲染时,useRef都指向同一个引用的对象

总结起来就是:useRef生成的对象,在组件生命周期期间内存地址都是不变的。

const refContainer = useRef(initialValue);

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.

总结一下会使用到useRef解决空指针问题的场景:

  • 事件处理器
  • setTimeout,setInterval

useEffect与useCallback(useMemo)的区别是什么?

浏览器执行阶段:可见修改(DOM操作,动画,过渡)->样式规则计算->计算空间和位置->绘制像素内容->多个层合成
前四个阶段都是针对元素的,最后一个是针对层的。由点到面。
image

执行时间不同

useEffect在渲染完成后执行函数,更加准确的来说是在layout和paint完成之后。

The function passed to useEffect will run after the render is committed to the screen.Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint

useCallback(useMemo)在渲染过程中执行函数。

Remember that the function passed to useMemo runs during rendering.

哪些适合在渲染完成后执行,哪些适合在渲染过程中执行

渲染完成后执行:Mutations(DOM操作), subscriptions(订阅), timers, logging
渲染过程中执行:用于不依赖渲染完成的性能优化,状态一变更立即执行

一个例子阐明useEffect和useMemo的区别

useMemo最主要解决的问题:怎么在DOM改变的时候,控制某些函数不被触发。
例如下面这个例子,在name变更的时候,useEffect会在DOM渲染完成后出发price的函数,而useMemo可以精准的只触发更新name的函数。

这是一个非常非常好的例子,更加详细的博文在这里:useMemo和useEffect有什么区别?怎么使用useMemo

import React, {Fragment} from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'

const nameList = ['apple', 'peer', 'banana', 'lemon']
const Example = (props) => {
    const [price, setPrice] = useState(0)
    const [name, setName] = useState('apple')
    
    
    function getProductName() {
        console.log('getProductName触发')
        return name
    }
    // 只对name响应
    useEffect(() => {
        console.log('name effect 触发')
        getProductName()
    }, [name])
    
    // 只对price响应
    useEffect(() => {
        console.log('price effect 触发')
    }, [price])
  
    // memo化的getProductName函数   🧬🧬🧬
    const memo_getProductName = useMemo(() => {
        console.log('name memo 触发')
        return () => name  // 返回一个函数
    }, [name])

    return (
        <Fragment>
            <p>{name}</p>
            <p>{price}</p>
            <p>普通的name:{getProductName()}</p>
            <p>memo化的:{memo_getProductName ()}</p>
            <button onClick={() => setPrice(price+1)}>价钱+1</button>
            <button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
        </Fragment>
    )
}
export default Example

点击价钱+1按钮(通过useMemo,多余的memo_getProductName ()没有被触发,只触发price相关的函数)

getProductName触发
price effect 触发

点击修改名字按钮(通过useEffect,只触发name相关)

name memo 触发
getProductName触发
name effect 触发
getProductName触发
总结

useEffect面对一些依赖于某个state的DOM渲染时,会出现一些性能问题,而useMemo可以优化这个问题。
最后,用一句话来概括useMemo的话,那就是:useMemo可以避免一些useEffect搞不定的不必要的重复渲染和重复执行问题。

React除了可以通过props传递数据以外,如何通过context方式传递数据?

假设组件层级较深,props需要一级一级往下传,可以说是props hell问题。
context方式封装的组件,为需要接受数据的组件,提供了一种跨组件层级传递,按需引入上级props的方式。

组件定义context部分

import * as React from 'react'
// myContext.ts
interface IContext {
     foo: string,
     bar?: number,
     baz: string
}
const myContext = React.createContext<IContext>({
     foo: "a",
     baz: "b"
})


interface IProps {
    data: IContext ,
}

const myProvider: React.FC<IProps> = (props) => {
     const {data, children} = props
     return <myContext.Provider value={data}>{children}</myContext.Provider>
}

export default myProvider;

export function useMyContext() {
  return useContext(myContext)
}

使用组件和context部分

<!-- 组件包裹 -->
import myProvider from './myContext.ts'

<myProvider data={{foo: "foo", baz: "baz"}}>
    <div className="root">
        <div className="parent">
            <Component1 />
            <Component2 />
        </div>
     </div>
</myProvider>
// Component1
import  {useMyContext} from './myContext.ts'
const {foo, baz} = useMyContext()

const Compoonent1 = () => {
    return (<div>{foo}{baz}</div>)
}
export Component1

React.createElement(Input, props)中的React.createElement如何理解?

React.createElement()

React.createElement(
    type,
    [props],
    [...children]
)

根据指定类型,返回一个新的React element。

类型这个参数可以是:

  • 一个“标签名字符串”(例如“div”,“span”)
  • 一个React component 类型(一个class或者一个function)
  • 一个React fragment 类型

JSX写法的组件,最终也会被解析为React.createElement()的方式。如果使用JSX的方式的话,不需要显式调用React.createElement()。

React.createElement(Input, props)

基于antd,封装通用表单组件方法。

// generator.js
import React from "react";
import { Input, Select } from "antd";

const components = {
  input: Input,
  select: Select
};

export default function generateComponent(type, props) {
  return React.createElement(components[type], props);
}

简单使用这个通用表单组件方法:

import generateComponent from './generator'

const inputComponent = generateComponent('input', props)
const selectComponent = generateComponent('select', props)

你可能会觉得上面这种方式比较鸡肋,但是如果批量地生成组件,这种方式就很有用了。

// components.js
import React from "react";
import generateComponent from "./generator";

const componentsInfos = [
  {
    type: "input",
    disabled: true,
    defaultValue: "foo"
  },
  {
    type: "select",
    autoClear: true,
    dropdownStyle: { color: "red" }
  }
];

export default class Components extends React.Component {
  render() {
    return componentsInfos.map((item) => {
      const { type, ...props } = item;
      return <>{generateComponent(type, props)}</>;
    });
  }
}

具体的示例可以查看:https://codesandbox.io/s/reac...

基于这种方式,可以封装出可重用的业务组件:表单业务组件,表格业务组件等等,会极大程度的解放生产力!

react中的FC是什么?FC<[interface]>是什么意思?主要用处及最简写法是怎样的?

react中的FC是什么?

type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

FC是FunctionComponent的缩写,FunctionComponent是一个泛型接口。

FC<[interface]>是什么意思?

是为了提供一个函数式组件环境,用于包裹组件。
为什么呢?因为在函数式组件内部可以使用hooks。

函数式组件

const Component = (props) => {
    // 这里可以使用hooks
    return <div />
}
或者
function Component(props) {
  // 这里可以使用hooks
  return <div />;
}

主要用处及最简写法是怎样的?

项目内的公共函数式组件,作为组件容器使用,用于提供hooks上下文环境。

// Container.js
import React, { FC } from 'react'

interface IProps {
     children: any
}

const Container: FC<IProps> = (props) =>  {
  return (
    <div>
      {props.children}
    </div>
  )
}

export default Container
// 使用
<Container>
    <Component1 />
    <Component2 />
</Container>

React中FC的形参的props, context, propTypes, contextTypes, defaultProps, displayName是什么?

type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
}

type PropsWithChildren<P> = P & { children?: ReactNode };

其中props和context都是函数组件的形参。
而propTypes,contextTypes,defaultProps,displayName都是组件的函数组件的属性。

const Foo: FC<{}> = (props, context) => {
    return (
        <div>{props.children}</div>
    )
}
Foo.propTypes = ...
Foo.contextTypes = ...
Foo.defaultProps = ...
Foo.displayName = ...

react函数式组件与纯函数组件有什么区别呢?

1.react函数式组件必须返回ReactElement或者null,纯函数组件返回值没有限定
2.react函数式组件的props限定children的类型为ReactNode,纯函数组件没有限定
3.react函数式组件拥有propTypes,contextTypes,defaultProps,displayName等等类型约束,纯函数组件没有限定

https://stackoverflow.com/que...

import { MouseEvent } from 'react'是什么意思?SyntheticEvent是什么类型?

import { MouseEvent } from 'react'是什么意思?

好文章:https://fettblog.eu/typescrip...

  • 用于事件类型约束
  • 除了MouseEvent,还有AnimationEvent, ChangeEvent, ClipboardEvent, CompositionEvent, DragEvent, FocusEvent, FormEvent, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, TransitionEvent, WheelEvent. As well as SyntheticEvent
  • 可以使用MouseEvent<HTMLButtonElement>约束仅触发HTML button DOM的事件
  • InputEvent较为特殊,因为是一个实验事件,因此可以用SyntheticEvent替代

SyntheticEvent是什么类型?

Synthetic -> 合成的

在React中,几乎所有的事件都继承了SyntheticEvent这个interface。
SyntheticEvent是一个跨浏览器的浏览器事件wrapper,通常用于替代InpuEvent这样的事件类型。

interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
interface BaseSyntheticEvent<E = object, C = any, T = any> {
    nativeEvent: E;
    currentTarget: C;
    target: T;
    bubbles: boolean;
    cancelable: boolean;
    defaultPrevented: boolean;
    eventPhase: number;
    isTrusted: boolean;
    preventDefault(): void;
    isDefaultPrevented(): boolean;
    stopPropagation(): void;
    isPropagationStopped(): boolean;
    persist(): void;
    timeStamp: number;
    type: string;
}

React.forwardRef是什么意思?useImperativeHandle是什么意思?

简而言之,refs转发就是为了获取到组件内部的DOM节点。
React.forwardRef意思是Refs转发,主要用于将ref自动通过组件传递到某一子组件,常见于可重用的组件库中。

在使用forwardRef时,可以让某些组件接收ref,并且将其向下传递给子组件,也可以说是”转发“给子组件。

没有使用refs转发的组件。

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

使用refs转发的组件。

const FancyButton = React.forwardRef((props, ref)=>{
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
})

如何使用?

// 创建一个ref变量
const ref = React.createRef();
// 将ref变量传入FancyButton,FancyButton将ref变量转发给button
<FancyButton ref={ref}></FancyButton>
// ref.current指向button DOM节点

vue中也有refs机制不同,但是vue如果想获取到子组件内部的DOM节点,需要一级一级的去获取,比如this.$refs.parent.$refs.child,这会导致组件层级依赖严重。
相比vue而言,React的refs转发组件层级以来较轻,代码可读性和可维护性更高。

useImperativeHandle是什么意思?

import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    publicFocus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} type="text" />
});

const App = props => {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.publicFocus()}
      >父组件调用子组件的 focus</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

上面这个例子中与直接转发 ref 不同,直接转发 ref 是将 React.forwardRef 中函数上的 ref 参数直接应用在了返回元素的 ref 属性上,其实父、子组件引用的是同一个 ref 的 current 对象,官方不建议使用这样的 ref 透传,而使用 useImperativeHandle 后,可以让父、子组件分别有自己的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 方法来自定义开放给父组件的 current。

期待和大家交流,共同进步:

努力成为优秀前端工程师!
阅读 1.3k

趁你还年轻,做个优秀的前端工程师
努力成为优秀的前端工程师! 文章在微信公众号:大大大前端 同步更新,期待你的关注~
3.8k 声望
4.1k 粉丝
0 条评论
你知道吗?

3.8k 声望
4.1k 粉丝
宣传栏