Hook 改变的 React Component 写法思路(1) - useState和useEffect

本文探讨了React Hooks如何改变Component的编写方式,重点讲解了useState如何替代this.setState,以及useEffect如何适应不同的生命周期阶段,尤其是如何在组件挂载、更新和卸载时工作。同时,文章强调了理解Memoize的重要性,它在Hook中的作用相当于缓存,提高性能。

Hook 改变的 React Component 写法思路

React hook 在酝酿近一年后,终于在16.8稳定版中重磅推出。在此前后FB的Dan大神劳心劳力地几乎每天在Twitter上给大家洗脑宣传。

那它究竟跟之前的React应用相比有什么改变?

前提是先要理解Memoize

Memoize是贯穿hook用法的一个非常重要的概念,理解它是正确使用hook的基石。

Memoize基本上就是把一些程序中一些不需要反复计算的值和上下文(context)保存在内存中,起到类似缓存的作用,下次运行计算时发现已经有计算并保存过这个值就直接从内存中读取而不再重新计算。

Javascript中比较常见的做法可参考lodash.memoize源代码,它通过给function设置一个Map的属性,将function的传参作为key,运行结果存为这个key的value值。下次调用这个function时,它就先去查看key是否存在,存在的话就直接将对应的值返回,跳过运行方法里的代码。

这在functional programming中非常的实用。不过也就是说,只有所谓纯粹的function才能适用这种方式,输入和输出是一一对应的关系。

React hooks都是被Memoize的,绑定在使用的component中,只有指定的值发生了变化,这个hook中的代码和代码上下文才会被更新和触发。让我在下文中一步步说明。

用useState替换this.setState

在hook之前,用function写的Component是无法拥有自己的状态值的。想要拥有自己的状态,只能痛苦地将function改成Class Component。

Class Component中使用this.setState来设置一个部件的状态:

class MyComponent extends React.Component{
  constructor(props) {
    super(props);
    // 初始化state值
    this.state= {
      myState: 1
    };
    this.toggleState = this.toggleState.bind(this);
  }

  // 将myState在0和1之间切换
  toggleState() {
    this.setState(prevState => {
     return { myState: 1 - prevState.myState };
    });
  }

  render() {
    return <button onClick={toggleState}>Toggle State</Button>;
  }
}

使用React hook之后这就可以在function component中实现,并且更为简洁:

function MyComponent (props) {
  const [myState, setMyState] = useState(1);

  // 这里应该用useCallback, 会在后面说明
  const toggleState = () => setMyState(1 - myState);

  return <button onClick={toggleState}>Toggle State</button>;
}

代码行数精简为原来的三分之一之外,使用起来也更加地直观。

useState接收一个值作为一个state的初始值,返回一个数组。这个数组由两个成员组成,第一个成员是这个状态的当前值(如上例中的myState),第二个成员是改变这个状态值的方法,即一个专属的setState(如上例中的setMyState)。

During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).

注意这个初始值只在初始渲染中才被赋值给对应变量,也就是只有在component第一次挂载时,渲染之前才做了一次初始化。后来更新引发的重新渲染都不会让初始值对状态产生影响。

useEffect替换Component生命周期函数

useEffect并不能等同或替代原有的component生命周期函数,他们设计的思路完全不同。对于长期习惯使用原有生命周期的人来说,可能需要从“替换”的角度来转换写react代码的思维方式。

简单来说,钩子的设计更符合“react”这个名字。它完全是通过对数据和状态变化的检测,来“反馈”更新。

在前端程序里,我们习惯了一种我称为“事件思维”的方式,就是说发生某件事,就调用某段代码。运用钩子,就是把某些数据的变化作为事情发生的标志。

在此之前,我们回顾一下类component中几个主要的生命周期。

刚挂载:

  1. constructor()
  2. UNSAFE_componentWillMount()
  3. getDerivedStateFromProps()
  4. render()
  5. componentDidMount()

属性或状态更新:

  1. getDerivedStateFromProps()
  2. shouldComponentUpdate()
  3. UNSAFE_componentWillUpdate()
  4. UNSAFE_componentWillReceiveProps()
  5. render()
  6. getSnapshotBeforeUpdate()
  7. componentDidUpdate()

取消挂载:

  1. componentWillUnmount()

componentDidUpdate & shouldComponentUpdate

如上可以看到componentDidUpdate是只在:

  • 属性值或状态值发生变化
  • 并且shouldComponentUpdate()返回为true(默认为true)

时才会被触发的。

useEffect传入的函数,则会在:

  • component 挂载后触发一次
  • 渲染完成后才触发
  • 第二个参数里传入的需要检测比较的数据有变化时才触发
  • 如果没有第二个参数,则每次渲染都会触发

那就很明显useEffect不能简单地替代componentDidUpdate。

但在实际使用过程中,我们通常会:

  1. 在componentDidMount时调用API加载数据
  2. componentDidUpdate里比较一些条件(比如传入的数据id发生变化)后可能再次调用同样API重新加载数据
  3. 并且用shouldComponentUpdate来避免不必要的重新渲染(比如id没变化,redux store里这个id对应的数据也没变化的时候)

如下:

class MyComponent extends React.Component {
    componentDidMount() {
        this.loadData();
    }
    componentDidUpdate(prevProps) {
        if (prevProps.id !== this.props.id) {
            this.loadData();
        }
    }
    shouldComponentUpdate(nextProps) {
        return nextProps.id !== this.props.id ||
            nextProps.data !== this.props.data;
    }
    loadData() {
        this.props.requestAPI(this.props.id);
    }
    render() {
        return <div>{this.props.data}</div>;
    }
}

useEffect就合并并且大大地简化了这一过程:

function MyComponent(props) {
    React.useEffect(() => {
        props.requestAPI(props.id);
    }, [props.id, props.requestAPI);
    return <div>{props.data}</div>;
}
export default React.memo(MyComponent);

上述例子中实际上是将componentDidMount和componentDidUpdate合并了。在shouldComponentUpdate中比较props.data是否变化这一步,借由React.memo来完成。注意它只做每个props值的浅比较。

componentDidMount

那你说componentDidUpdate也许经常做compinentDidMount里会做的事情,那么componentDidMount呢?它是必须的。

虽然react大牛们已经提议了一些concurrent的方法,但react发了那么多版依旧没被加进来。所以在钩子的官方文档里,我们找到这一段:

If you pass an empty array ([]), the props and state as inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually bettersolutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem.

也就是说可以给useEffect第二个参数传一个空数组,来暂代componentDidMount。虽然官方不推荐,但现在没办法只能这么干。。。

so,上面已经说了,第二个参数是用来将某些数据变化作为效果触发的依据。那空数组,首先就防止了第二个参数为空时每次render都会触发的场景,然后每次渲染都没有数据可以比较变化,那就只有component挂载时才能被触发了。

function MyComponent() {
    React.useEffect(() => {
        console.log('MyComponent is mounted!');
    }, []);
    return null;
}

getDerivedStateFromProps

useState与useEffect不同,它不会检测数据的变化,它只接收一个参数 - 它的初始值。初始化过后,所有的状态更新,都需要我们自己调用useState所返回的 ”setState“方法来完成。

这是因为我们已经有useEffect了。getDerivedStateFromProps的效果等同于:

function MyComponent(props) {
    const [intValue, setIntValue] = React.useState(props.value);
    React.useEffect(() => {
        setIntValue(parseInt(props.value, 10));
    }, [props.value, setIntValue]);
    return <div>{intValue}</div>;
}

比起getDerivedStateFromProps,这种方式还有效防止了不必要的多次计算。

componentWillUnmount

componentWillUnmount是又一个非常重要常用的生命周期。我们通常用它来解绑一些DOM事件,清理一些会造成内存泄漏的东西。

这就要说到useEffect里第一个参数的回调函数,是可以返回一个函数用来做这种清洁工作的:

React.useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

以上是官方文档中的一个例子,它等同于:

class MyComponent extends React.Component {
    componentDidMount() {
        this.props.source.subscribe();
    }
    componentDidUpdate(prevProps) {
        if (prevProps.source !== this.props.source) {
            prevProps.source.unsubscribe();
            this.props.source.subscribe();
        }
    }
    componentWillUnmount() {
        this.props.source.unsubscribe();
    }
    render() {
        // ...
    }
}

简单来说,这个返回的清洁函数,会在下一次该效果被触发时首先被调用。

### 三级标题:React 性能优化方法(不包括 Hook 写法) 在 React 应用中,除了使用 Hook 的方式外,还有多种传统且有效的性能优化手段可以提升组件的渲染效率整体应用的响应速度。 #### 使用 `React.PureComponent` 避免不必要的更新 对于类组件而言,继承自 `React.PureComponent` 可以自动对 props state 进行浅层比较,从而避免组件在数据未发生变化时进行无意义的重新渲染。这种机制适用于那些输入稳定、输出一致的组件,能够显著减少不必要的虚拟 DOM 操作[^1]。 ```jsx class MyComponent extends React.PureComponent { render() { return <div>{this.props.value}</div>; } } ``` 需要注意的是,如果组件内部状态或属性值是复杂对象或数组,则应确保其引用保持不变,否则可能导致比较失败并引发不必要的重渲染。 #### 不可变数据结构提升 diff 效率 保持 state 的不可变性有助于 React 更高效地识别出真正发生变化的部分,进而只更新对应的 DOM 节点。这不仅提升了 diff 算法的执行效率,也简化了调试流程,使状态变更更加透明可预测[^1]。 例如,在更新一个列表时,不要直接修改原数组,而是创建一个新的数组副本: ```jsx const newList = [...this.state.list, newItem]; this.setState({ list: newList }); ``` 这种方式确保了每次更新都是基于新的引用,便于 React 快速定位到变化内容。 #### 组件拆分与按需加载 将大型组件拆分为更小、更专注的子组件,有助于隔离变化区域,并允许仅更新受影响的部分。此外,利用 Webpack 的 code splitting 功能实现组件的异步加载,也是提高初始加载性能的重要策略之一。通过 `React.lazy` `<Suspense>`,可以在用户实际访问相关页面时才加载这些组件,从而减少首屏加载时间[^3]。 ```jsx const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <React.Suspense fallback="Loading..."> <LazyComponent /> </React.Suspense> ); } ``` 此方法特别适用于路由级别的组件或者功能模块,能够有效降低首次加载所需下载的 JavaScript 体积。 #### 使用 `shouldComponentUpdate` 控制更新逻辑 对于需要更精细控制更新行为的场景,可以通过覆写 `shouldComponentUpdate` 生命周期方法来自定义是否应该触发重渲染。这种方法提供了比 `PureComponent` 更高的灵活性,但也要求开发者自行管理复杂的比较逻辑。 ```jsx class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { // 自定义判断条件 return nextProps.id !== this.props.id; } render() { return <div>{this.props.id}</div>; } } ``` 合理运用此方法可以帮助跳过那些视觉上无变化的更新过程,从而节省资源消耗。 #### 避免内联函数与对象导致的重复渲染 在 JSX 中频繁使用内联函数或对象字面量作为 props,会导致每次父组件重新渲染时都生成新的引用,进而触发子组件不必要的更新。为了解决这一问题,可以将此类函数提取为类的方法或将对象存储为组件的静态属性[^3]。 错误示例: ```jsx <MyComponent onClick={() => console.log('Clicked')} /> ``` 正确做法: ```jsx class ParentComponent extends React.Component { handleClick = () => { console.log('Clicked'); }; render() { return <MyComponent onClick={this.handleClick} />; } } ``` 这样可以保证函数引用在整个组件生命周期内保持不变,防止因引用变化而导致的非必要重渲染。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值