25

logo

完全是不可能滴, 这辈子都不可能完全!        -- 来自某非著名码农

本文总结 React 实用的特性,部分实验性和不实用的功能将不会纳入进来,或许未来可期~

1、setState

  • 面试题

    class App extends Component {
      state = {
        val: 0
      }
      // 震惊!隔壁老王也有失手的时候~
      componentDidMount() {
        this.setState({val: this.state.val + 1});
        console.log(this.state.val); // ?
        
        this.setState((prevState) => ({val: prevState.val + 1}));
        console.log(this.state.val); // ?
          
        setTimeout(() => {
          this.setState({val: this.state.val + 1});
          console.log(this.state.val); // ?
          
          this.setState({val: this.state.val + 1});
          console.log(this.state.val); // ?
        }, 1000)
      }
      
      render() {
        return <h2>App组件</h2>;
      }
    }
  • 总结

    • setState 只在 React 合成事件和钩子函数中是“异步”的,在原生DOM事件和定时器中都是同步的。
    • 如果需要获取“异步”场景的 setState 的值 --> this.setState(partial, callback) 在 callback 中拿到最新的值
    • 如果要在“异步”场景保证同步更新多次 setState --> this.setState((prevState, props) => {return newState})

      • 能保证同步更新, 但是外面获取的值还是之前的值

2、Fragment

  • before

    • 代码

      export default class App extends Component {
        render() {
          return (
            <div>
              <h2>App组件</h2>
              <p>这是App组件的内容</p>
            </div>
          );
        }
      }
    • 效果
      img
  • after

    • 代码

      export default class App extends Component {
        render() {
          return (
            <Fragment>
              <h2>App组件</h2>
              <p>这是App组件的内容</p>
            </Fragment>
          );
        }
      }
    • 效果
      img
  • 总结:使用 Fragment ,可以不用添加额外的DOM节点

3、React性能优化

  • shouldComponentUpdate

    // 举个栗子:
    shouldComponentUpdate(nextProps, nextState) {
      if (nextProps !== this.props) {
        return true;  // 允许更新
      }
      if (nextState !== this.state) {
        return true;
      }
      return false;  // 不允许更新
    }
  • PureComponent 组件

    • 使用

      // 实现了对 state 和 props 的浅比较
      // 相等就不更新,不相等才更新
      class App extends PureComponent {}
    • 浅比较源码

      // 实现 Object.is() 方法, 判断x y是否完全相等
      function is(x, y) {
        // (x !== 0 || 1 / x === 1 / y) 用于判断 0 和 -0 不相等
        // x !== x && y !== y     用于判断 NaN 等于 NaN
        return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y
        ;
      }
      
      // 提取了hasOwnProperty方法,缓存
      var hasOwnProperty$1 = Object.prototype.hasOwnProperty;
      
      // 返回false为更新,true为不更新
      function shallowEqual(objA, objB) {
        // 如果A和B完全相等,返回true
        if (is(objA, objB)) {
          return true;
        }
        // 如果A和B不相等,并且不是对象,说明就是普通值,返回false
        if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
          return false;
        }
        // 提取A和B的所有属性
        var keysA = Object.keys(objA);
        var keysB = Object.keys(objB);
        // 如果长度不相等,返回false
        if (keysA.length !== keysB.length) {
          return false;
        }
        
        // 检测 A 的属性 和 B 的属性是否一样
        for (var i = 0; i < keysA.length; i++) {
          if (!hasOwnProperty$1.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
            return false;
          }
        }
        
        return true;
      }
  • 问题:如果使用 pureComponent 只能进行浅比较,如果修改了原数据再更新,就会导致地址值一样从而不会更新。但实际需要更新。
  • 解决:

    • 手动保证每次都是新的值
    • 使用 immutable-js 库,这个库保证生成的值都是唯一的

      var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
      // 设置值
      var map2 = map1.set('a', 66);
      // 读取值
      map1.get('a'); // 1
      map2.get('a'); // 66
  • 总结:使用以上方式,可以减少不必要的重复渲染。

4、React 高阶组件

  • 基本使用

    // WrappedComponent 就是传入的包装组件
    function withHoc(WrappedComponent) {
      return class extends Component {
        render () {
          return <WrappedComponent />;
        }
      }
    }
    
    // 使用
    withHoc(App)
  • 向其中传参

    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          render () {
            return <WrappedComponent />;
          }
        }
      }
    }
    
    // 使用
    withHoc('hello hoc')(App)
  • 接受props

    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          render () {
            // 将接受的 props 传递给包装组件使用
            return <WrappedComponent {...this.props}/>;
          }
        }
      }
    }
  • 定义组件名称

    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          // 定义静态方法,修改组件在调试工具中显示的名称
          static displayName = `Form(${getDisplayName(WrappedComponent)})`
          render () {
            return <WrappedComponent {...this.props}/>;
          }
        }
      }
    }
    // 封装获取包装组件的 displayName 的方法
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
原文链接

5、render props

原文太长,直接上 链接

官网真香, 建议大家将 React 官网过一遍~

6、React 懒加载

  • react-loadable

    import Loadable from 'react-loadable';
    import Loading from './components/loading'
    
    const LoadableComponent = Loadable({
      loader: () => import('./components/home'),
      loading: Loading,
    });
    
    export default class App extends Component {
      render() {
        return (
          <div>
            <h2>App组件</h2>
            <LoadableComponent />
          </div>
        );
      }
    }
  • Suspenselazy

    import React, {Component, Suspense, lazy} from 'react';
    import Loading from './components/loading';
    
    const LazyComponent = lazy(() => import('./components/home'));
    
    export default class App extends Component {
      render() {
        return (
          <div>
            <h2>App组件</h2>
            <Suspense fallback={<Loading />}>
              <LazyComponent />
            </Suspense>
          </div>
        );
      }
    }
  • 区别

    • react-loadable 是民间 --> 需要额外下载引入
    • Suspenselazy 是官方 --> 只需引入
    • react-loadable 支持服务器渲染
    • Suspenselazy 不支持服务器渲染
  • 总结:使用 create-react-app 会将其单独提取成一个bundle输出,从而资源可以懒加载和重复利用。

7、虚拟DOM diff算法

  • 虚拟DOM diff算法主要就是对以下三种场景进行优化:
  • tree diff

    • 对树进行分层比较,两棵树只会对同一层次的节点进行比较。(因为 DOM 节点跨层级的移动操作少到可以忽略不计)
    • 如果父节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
    • 注意:

      • React 官方建议不要进行 DOM 节点跨层级的操作,非常影响 React 性能。
      • 在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
  • component diff

    • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree(tree diff)。

      • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
    • 如果不是,直接替换整个组件下的所有子节点。
  • element diff

    • 对处于同一层级的节点进行对比。
    • 这时 React 建议:添加唯一 key 进行区分。虽然只是小小的改动,性能上却发生了翻天覆地的变化!

      • 如: A B C D --> B A D C
      • 添加 key 之前: 发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
      • 添加 key 之后: B、D 不做任何操作,A、C 进行移动操作,即可。
    • 建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • 总结

    • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
    • React 通过分层求异的策略,对 tree diff 进行算法优化;
    • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
    • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
    • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
    • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
原文链接

8、Fiber

  • Fiber 是为了解决 React 项目的性能问题和之前的一些痛点而诞生的。
  • Fiber 的核心流程可以分为两个部分:

    • 可中断的 render/reconciliation 通过构造 workInProgress tree 得出 change。
    • 不可中断的 commit 应用这些 DOM change。
  • 异步实现不同优先级任务的协调执行:

    • requestIdleCallback: 在线程空闲时期调度执行低优先级函数;
    • requestAnimationFrame: 在下一个动画帧调度执行高优先级函数;
  • 总结

    • 可切分,可中断任务。
    • 可重用各分阶段任务,且可以设置优先级。
    • 可以在父子组件任务间前进/后退切换任务。
    • render方法可以返回多元素(即可以返回数组)。
    • 支持异常边界处理异常。
原文链接:

https://mp.weixin.qq.com/s/uD...
https://juejin.im/post/5a2276...

9、Redux

  • 作用: 集中管理多个组件共享的状态
  • 特点: 单一数据源、纯函数、只读state
  • redux 核心模块定义:

    • store.js

      import { createStore, applyMiddleware } from 'redux';
      // 异步actions使用的中间件
      import thunk from 'redux-thunk';
      // redux开发chrome调试插件
      import { composeWithDevTools } from 'redux-devtools-extension';
      
      import reducers from './reducers';
      
      export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk)));
    • reducers.js

      import { combineReducers } from 'redux';
      import { TEST1, TEST2 } from './action-types';
      
      function a(prevState = 0, action) {
        switch (action.type) {
          case TEST1 :
            return action.data + 1;
          default :
            return prevState;
        }
      }
      
      function b(prevState = 0, action) {
        switch (action.type) {
          case TEST2 :
            return action.data + 1;
          default :
            return prevState;
        }
      }
      // 组合两个reducer函数并暴露出去
      export default combineReducers({a, b});   
    • actions.js

      import { TEST1, TEST2 } from './action-types';
      // 同步action creator,返回值为action对象
      export const test1 = (data) => ({type: TEST1, data});
      export const test2 = (data) => ({type: TEST2, data});
      // 异步action creator,返回值为函数
      export const test2Async = (data) => {
        return (dispatch) => {
          setTimeout(() => {
            dispatch(test2(data));
          }, 1000)
        }
      };
    • action-types.js

      export const TEST1 = 'test1';
      export const TEST2 = 'test2';
  • 组件内使用:

    • App.jsx

      import React, { Component } from 'react';
      import PropTypes from 'prop-types';
      import { connect } from 'react-redux';
      import {test1, test2Async} from './redux/actions';
      
      class App extends Component {
        static propTypes = {
          a: PropTypes.number.isRequired,
          b: PropTypes.number.isRequired,
          test1: PropTypes.func.isRequired,
          test2Async: PropTypes.func.isRequired,
        }
        
        componentDidMount() {
          const { a, b, test1, test2Async } = this.props;
          // 测试
          test1(a + 1);
          test2Async(b + 1);
        }
        
        render() {
          return (
            <div>App组件</div>
          );
        }
      }
      
      /* =============== redux相关代码 ================== */
      // 将状态数据映射为属性以props方式传入组件
      const mapStateToProps = (state) => ({a: state.a, b: state.b});
      // 将操作状态数据的方法映射为属性以props方式传入组件
      const mapDispatchToProps = (dispatch) => {
        return {
          test1(data) {
            dispatch(test1(data));
          },
          test2Async(data) {
            dispatch(test2Async(data));
          }
        }
      }
      // connect就是一个典型的HOC
      export default connect(mapStateToProps, mapDispatchToProps)(App);
      
      /*
      // 上面写的太复杂了,但是好理解。而以下就是上面的简写方式
      export default connect(
        (state) => ({...state}),
        { test1, test2Async }
      )(App);
      */
    • index.js

      // 入口文件的配置
      import React from 'react';
      import ReactDOM from 'react-dom';
      import { Provider } from 'react-redux';
      import App from './App';
      import store from './redux/store';
      
      ReactDOM.render(
        <Provider store={store}>
          <App />
        </Provider>, document.getElementById('root'));
  • 总结:

    • 我们会发现使用 Redux 会变得更加复杂,以及多了很多模板代码(例如: action creators)
    • 但是,这是 Redux 能帮助我们更好操作状态,追踪和调试错误等。
    • 并且 Redux 有着一整套丰富的生态圈,这些你都能在 官方文档 找到答案
    • 总之,目前比起世面上 mobx 等库,更适用于大型项目开发~

10、未来可期

其实还有很多技术没有说,像 contextReact Hooks 等,但受限于笔者的眼界,目前没有发现大规模使用的场景(如果有,请小伙伴们指正),所以就不谈了~有兴趣的小伙伴去找找看吧~


xpromise
464 声望32 粉丝

非知名人士