react

数据绑定

react 没有数据绑定,而是通过对比更新前后的数据状态来进行更新,参考下文的 diffing 算法

事件绑定与原生的区别

react 中的事件为合成事件,通过初始化使它们在不同的浏览器中拥有一致的属性,使用 naviteEvent 属性可获取对应的原生事件(e.naviteEvent)

  1. 无法通过 return false 阻止默认行为,必须显示使用 preventDefault
  2. react 的事件处理函数都在冒泡阶段被触发,如需注册捕获阶段的函数,需为事件名添加 Capture,如 onClickCapture
  3. react 没有将事件真正的绑定到 DOM 上,而是在 document 处监听所有事件,当事件触发时,使用统一的分发函数执行指定事件函数

props 与 state 的区别

一个重要的不同点:props 是传递给组件的,类似于函数的形参,而 state 是在组件内部,被组件自己管理的,类似于函数内部声明的变量

propsstate
从父组件获取值
由父组件更改
在组件内部设置默认值
在组件内部更改
为子组件设置初始值
更改子组件

生命周期

Mounting、Updating、Unmounting

Mounting(挂载)

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下

constructor

避免在构造函数中引入任何副作用或订阅,如果不初始化 state 或不进行方法绑定,则不需要为组件实现构造函数,构造函数仅用于以下两种情况

  • 通过给 this.state 赋值对象来初始化内部 state
  • 为事件处理函数绑定实例

static getDerivedStateFromProps()(不常用)

每次渲染都会被调用,该函数返回一个对象来更新 state,如果返回 null 则不更新任何内容

此方法适用于罕见用例,即 state 的值在任何时候都取决于 props。如实现 <Transition> 组件

由于此方法会产生派生状态,从而导致代码冗余,且组件难以维护,因此尽量使用如下方法替代

  • 如果需要执行副作用,以响应 props 中的更改,请改用 componentDidUpdate
  • 如果只想在 prop 更改时重新计算某些数据,请使用 memoization helper 代替
  • 如果你想在 prop 更改时重置某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控代替

render()

class 组件中唯一必须实现的方法,应该为纯函数,当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

  • React 元素:通常通过 JSX 创建
  • 数组或 fragments:使得 render 方法可以返回多个元素。fragments,即 <Fragment />
  • Portals:可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型:会被渲染为文本节点
  • 布尔类型或 null:什么都不渲染

componentDidMount()

在组件挂载后立即调用,依赖于 DOM 节点的初始化放在此处

Updating(更新)

当组件的 props 或 state 发生变化时会出发更新,调用顺序如下

static getDerivedStateFromProps()

shouldComponentUpdate()(不常用)

此方法仅作为性能优化的方法而存在

根据该函数的返回值,默认为 true,判断组件的输出是否受当前 state 或 props 更改的影响,默认行为是 state 每次变化都会重新渲染

当该函数返回 false,则不会调用 render(),所以也不会调用 componentDidUpdate()

render()

getSnapshotBeforeUpdate() (不常用)

在最近一次更新渲染输出之前调用,它使得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)

componentDidUpdate()

当组件更新后立即调用,首次渲染不会执行此方法,如果组件实现了 getSnapshotBeforeUpdate(),则它的返回值将作为 componentDidUpdate() 的第三个参数 snapshot 参数传递

Unmounting(卸载)

当组件从 DOM 中移除时会调用

componentWillUnmount()

在组件卸载及销毁之前调用

错误处理

Error boundaries 是 react 组件,它会在其子组件树中的任何位置捕获 js 错误,并记录这些错误,展示降级 UI 而不是崩溃的组件树

当渲染过程、生命周期、子组件的构造函数中抛出错误时,会调用,当组件定义了以下两个方法时(任何一个或者两个),它就成为了 Error boundaries

仅将该组件用于处理从意外异常中恢复的情况,不要将他们用于流程控制,仅捕获组件中的错误,自身的错误无法捕获

static getDerivedStateFromError()

该方法在渲染阶段调用,因此不允许出现副作用

componentDidCatch()

该方法在提交阶段被调用,因此可以出现副作用

该方法在组件抛出错误后被调用,接收两个参数:

  1. error:抛出的错误
  2. info:带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

其他两个 API

这两个方法是在组件中手动调用的

setState()

forceUpdate()

调用该方法将强制更新组件,会跳过 shouldComponentUpdate(),尽量避免使用该方法,应该按照正常流程

react hook 对应的生命周期

class 组件hooks 组件
constructoruseState
getDerivedStateFromPropsuseState 里面的 update 函数
shouldComponentUpdateuseMemo
render函数本身
componentDidMountuseEffect
componentDidUpdateuseEffect
componentWillUnmountuseEffect 里面的返回函数
componentDidCatch暂无
getDerivedStateFromError暂无

常用 hook 介绍

useEffect

第一个参数为一个函数,第二个参数为一个变化参数数组

当第二个参数传入空数组 [] 时,将只执行一次,此时对应 componentDidMount

当第二个参数不传时,则对应 componentDidUpdate,将会在每次更新之后都调用,当第二个参数传入一个带值的数组,则只在该值变化时才更新

第一个参数的返回函数对应 componentWillUnMount,该清除机制实际上是清除上一个 effect 的结果,在清除之后才执行更新

useEffect 执行两次

react 18 默认情况下,useEffect 会执行两次

  • 开发模式下会出现
  • strict mode 下会出现

使用自定义 hook 解决

function useEffectOnce(effect: () => void | (() => void)) {
    const destroyFunc = useRef<void | (() => void)>();
    const effectCalled = useRef(false);
    const renderAfterCalled = useRef(false);
    const [val, setVal] = useState<number>(0);

    if (effectCalled.current) {
        renderAfterCalled.current = true;
    }

    useEffect(() => {
        // only execute the effect first time around
        if (!effectCalled.current) {
            destroyFunc.current = effect();
            effectCalled.current = true;
        }

        // this forces one render after the effect is run
        setVal(val + 1);

        return () => {
            // if the comp didn't render since the useEffect was called,
            // we know it's the dummy React cycle
            if (!renderAfterCalled.current) {
                return;
            }
            if (destroyFunc.current) {
                destroyFunc.current();
            }
        };
    }, []);
}

或者简单更改

const firstRenderRef = useRef(true);

useEffect(() => {
    // React 18 开发环境下默认会执行两次,使用该方法限制第二次执行
    if (firstRenderRef.current) {
        firstRenderRef.current = false;
        return;
    }
    const getData = async () => {
        const res = await API.getBingPictures({ n: 8, mkt: 'zh-CN' });
        setPictures(res.images);
    };
    getData();
}, []);

useState

该钩子主要用来初始化 state,对应了 constructor,函数式组件不需要构造函数,此处只是简单的对应

同时也可以实现 getDerivedStateFromProps,记住 getDerivedStateFromProps 的作用:在每次渲染的时候都会调用,但仅在返回 true 时才更新,所以可以用以下代码模拟实现

function scrollview({ row }) {
    const [isScrollDown, setIsScrollDown] = useState(false);
    const [prevRow, setPrevRow] = useState(null);
    if (row !== prevRow) {
        setIsScrollDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
    }
    return `scrolling down: ${isScrollDown}`;
}

useContext

用来获取 React.createContext 的值,此时可以不通过 Context.Consumer 来获取值

// 跨文件组件使用时,需要使用公共 context 文件
// 公共文件 context.js
import { createContext } from 'react';
const Context = React.createContext(null);
export default Context;
// 父组件
import Context from './context.js';
<Context.Provider value={value}>
    <Child />
</Context.Provider>;
// 子组件
import Context from './context.js';
const count = useContext(Context);
console.log(count);
// 若未使用 useContext 则需要通过以下写法来获取值
<Context.Consumer>{(value) => value}</Context.Consumer>;

useReducer

语法:const [state, dispatch] = useReducer(reducer, initialState, init)

  1. useState 的替代方案,接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 及配套的 dispatch(类似 redux)

dispatch 函数是稳定的,不会在组件重新渲染时改变,所以可以安全的从 useEffect 和 useCallback 依赖列表中省略 disptach

普通初始化

const initialState = { count: 0 };
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'reset':
            return { count: 0 };
        default:
            throw new Error();
    }
}
const [state, dispatch] = useReducer(reducer, initialState);
<button onClick={() => dispatch({ type: 'increment' })}>增加count</button>;

惰性初始化

传入第三个参数,此时初始值会被设置为 init(initialState)

function init(initialState = 0) {
    return { count: initialState };
}
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();
    }
}
const [state, dispatch] = useReducer(reducer, initialState, init);
<button onClick={() => dispatch({ type: 'reset', payload: initialState })}>重置count</button>
<button onClick={() => dispatch({ type: 'increment' })}>增加count</button>
  1. 可以用来模拟 forceUpdate,但尽量避免使用
const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);
function handleClick() {
    forceUpdate();
}

useCallback

返回一个记忆化的回调函数

语法

// 仅当第二个参数的依赖项(a,b)不改变时才返回同一个函数
const memoizedCallback = useCallback(() => {
    doSomeTime(a, b);
}, [a, b]);

// 如果要记住一个变化的值,需要 useRef 配合
const [text, setText] = useState('');
const ref = useRef();
useEffect(() => {
    // 写入 ref
    ref.current = text;
});
const memoizedCallback = useCallback(() => {
    // 从 ref 读取
    const currentText = ref.current;
}, [ref]);

useCallback(fn, inputs),全等于 useMemo(() => fn, inputs)

useCallback 返回函数,useMemo 返回值

useMemo

返回一个记忆化的值,相当于 vue 的计算属性(computed),仅当某个属性变化时才重新计算

语法 const func = useMemo(()=>fn, inputs)

useRef

返回一个可变的 ref 对象,它的 current 属性被初始化为传入的参数,ref 对象在组件的整个生命周期内保持不变

语法 const ref = useRef(initialValue)

useImperativeHandle

让你在使用 ref 时自定义暴露给父组件的实例值,在大多数情况下,应当避免使用 ref 这样的命令式代码,该函数应当与 forwardRef 一起使用,因为 ref 无法直接挂载到函数式组件上,被挂载的组件需要使用 forwardRef 包裹

语法 useImperativeHandle(ref, createHandle, [inputs])

说明:

  • ref:一个 ref 对象,通常由 useRef() 生成
  • createHandle:回调函数,必须有返回值,返回值通常为对象
  • [inputs]:依赖列表

useLayoutEffect

与 useEffect 相同,但仅在所有 DOM 变更之后才同步调用 effect,当使用 useEffect 出现问题后,才考虑使用 useLayoutEffect

执行时机

  • useLayoutEffect 执行在类组件生命周期前
  • useEffect 执行在类组件生命周期后

渲染

  • useLayoutEffect 同步渲染,会阻塞 DOM
  • useEffect 异步渲染,不会阻塞 DOM

实现 shouldComponentUpdate

const Button = React.memo((props) => {
    // 组件
});

虚拟 DOM 及内核

一种编程概念,UI 以一种虚拟的形式被保存在内存中。通俗的讲:通过某种算法,将真实的 DOM,转换成某个变量,然后将其存放在内存中,当需要时,再转换成真实的 DOM

react 中,react 元素与 fibers 内部对象被认为是虚拟 DOM 实现的一部分

  • react 元素:最小单元,是个普通对象,如 const element = <h1>hello, world</h1>;
  • fibers 对象:react 内部对象,用于存放组件树的附加信息

Shadow DOM:一种浏览器技术,在 web 组件中封装变量和 css Virtual DOM:由 js 类库基于浏览器 API 实现的概念

diffing 算法

两个假设的基础:

  1. 两个不同类型的元素会产生出不同的树
  2. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定

当对比两颗树时,首先比较两棵树的根节点,不同类型的根节点元素会有不同的形态

  • 当根节点为不同类型的元素时,拆卸原有的树,并建立新树,其中的 DOM 节点也将被销毁
  • 当根节点为相同类型的元素时,保留 DOM 节点,仅对比有改变的属性,然后递归子节点

对比同类型的组件元素

当一个组件更新时,组件实例保持不变,react 将更新该组件实例的 props 以跟最新的元素保持一致

优化

  • 保持 DOM 结构的稳定,尽可能的减少动态操作 DOM 结构
  • 尽量选择 css 隐藏或显示,而不是真的移除或添加节点
  • 酌情使用 shouldComponentUpdate() 来减少不必要的更新
  • 结构类似的尽量封装成组件
  • 对于列表结构,尽量减少节点移动操作

通信

父向子通信

父组件通过 props 向子组件传递

子向父通信

父组件向子组件传递函数,子组件调用该函数并返回值,父组件接收该返回值

兄弟通信

通过相同的父组件来实现

跨级通信

使用 Context,具体见下文

示例

// 创建 context
const PriceContext = React.createContext('price');
// A 组件
class ClassA extends Component {
    constructor() {
        super();
        this.state = { price: 0 };
    }
    // 点击事件
    clickGoods(e) {
        this.setState({
            price: e,
        });
    }
    render() {
        const { price } = this.state;
        return (
            // 传递 price
            <PriceContext.Provider value={price}>
                <button onClick={this.clickGoods.bind(this, 100)}>
                    goods1
                </button>
                <button onClick={this.clickGoods.bind(this, 1000)}>
                    goods2
                </button>
            </PriceContext.Provider>
        );
    }
}
// B 组件
class ClassB extends Component {
    render() {
        return (
            <div>
                <span>price:</span>
                <span>
                    <ClassC />
                </span>
            </div>
        );
    }
}
// C 组件
class ClassC extends Component {
    render() {
        return (
            // Consumer 需要包含一个函数
            <PriceContext.Consumer>
                {(price) => <span>{price}</span>}
            </PriceContext.Consumer>
        );
    }
}

Context

Context 提供了一种无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

何时使用

对于某个组件树而言,属于全局的数据,它的所有子组件都要访问同一批数据,并且能访问到后续的更新

// 创建一个 context,默认值 light
const ThemeContext = React.createContext('light');
class App extends React.Component {
    render() {
        // 使用 Provider 将当前的 theme 传递给以下的组件树,无论多深,都可以读取这个值
        return (
            <ThemeContext.Provider value="dark">
                <Toolbar />
            </ThemeContext.Provider>
        );
    }
}
// 中间组件无需再次传递 theme
function Toolbar() {
    return (
        <div>
            <ThemeButton />
        </div>
    );
}
class ThemeButton extends React.Component {
    // 指定 contextType 读取当前的 theme context
    // react 会往上找到最近的 theme Provider,然后使用他们的值
    static contextType = ThemeContext;
    render() {
        return <Button theme={this.contextType} />;
    }
}

主要 API

  • React.createContext

    创建 context:const MyContext = React.createContext('value')

  • Context.Provider

    每个 context 都会包含一个 Provider 组件,通过该组件传递值:

    <MyContext.Provider value='value'>
        // 组件
    </MyContext.Provider>
    

    子组件通过 MyContext 获取值,react 会自动寻找最近的 Provider 作为值

  • Context.Consumer

    订阅 context 的改变,需要一个函数式组件作为子元素,该函数接收 context 的当前值,并返回一个 react 节点

Last Updated:
Contributors: af, zhangfei