React 进化进化再进化

前端框架很重要的一点

复用

基于框架开发可复用的组件,代码片段,才能让我们的日常开发提效。

React在复用上的探索

  • Mixin
  • High-Order-Component
  • Render props
  • React hooks

Mixin

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

Mixin (混入)是最早的复用方式,它是一个对象,可以包含自定义的方法和 React 生命周期函数,用来扩充 React 组件的能力,通常把可复用的逻辑放到 mixin 中,在需要使用到的地方引入。

使用Mixin

var createReactClass = require('create-react-class');

var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the mixin
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

通过 mixins 属性引入需要的 mixin,就可以直接在组件中使用混入的方法。

Mixin的缺点

Mixin的缺点主要有以下几点:

1. 数据源来源不清晰。当一个组件使用了多个 mixin 时,很难判断哪个方法/变量来自哪里。

2. 命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率可能会在命名时添加&,_等特殊字符,影响可读性。

3. 复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对 mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。

4. ES6 不支持 mixin 所以使用 ES6 Class 语法的 React 组件无法使用 mixin。

进化-高阶组件

在函数式编程中,如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数

 

如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件。

使用高阶组件

function StoreMixin(...stores) {
    var Mixin = {
        getInitialState() {
            return this.getStateFromStores(this.props);
        },
        componentDidMount() {
            stores.forEach(store =>
                store.addChangeListener(this.handleStoresChanged)
            );
            this.setState(this.getStateFromStores(this.props));
        },
        componentWillUnmount() {
            stores.forEach(store =>
                store.removeChangeListener(this.handleStoresChanged)
            );
        },
        handleStoresChanged() {
            if (this.isMounted()) {
                this.setState(this.getStateFromStores(this.props));
            }
        }
    };
    return Mixin;
}

var UserProfilePage = React.createClass({
    mixins: [StoreMixin(UserStore)],
    propTypes: {
        userId: PropTypes.number.isRequired
    },
    getStateFromStores(props) {
        return {
            user: UserStore.get(props.userId);
        }
    }
    render() {
        var { user } = this.state;
        return <div>{user ? user.name : 'Loading'}</div>;
    }
});

假设有一个 mixin,订阅了特定的 Flux Stores,并且可触发组件状态改变。它可能长这样:

function connectToStores(Component, stores, getStateFromStores) {
    const StoreConnection = React.createClass({
        getInitialState() {
            return getStateFromStores(this.props);
        },
        componentDidMount() {
            stores.forEach(store =>
                store.addChangeListener(this.handleStoresChanged)
            );
        },
        componentWillUnmount() {
            stores.forEach(store =>
                store.removeChangeListener(this.handleStoresChanged)
            );
        },
        handleStoresChanged() {
            if (this.isMounted()) {
                this.setState(getStateFromStores(this.props));
            }
        },
        render() {
            return <Component {...this.props} {...this.state} />;
        }
    });
    return StoreConnection;
};

var ProfilePage = React.createClass({
    propTypes: {
        userId: PropTypes.number.isRequired,
        user: PropTypes.object // note that user is now a prop
    },
    render() {
        var { user } = this.props; // get user from props
        return <div>{user ? user.name : 'Loading'}</div>;
    }
});

// Now wrap ProfilePage using a higher-order component:
ProfilePage = connectToStores(ProfilePage, [UserStore], props => ({
    user: UserStore.get(props.userId)
});

起来和 mixin 非常类似,但是它包装了组件并传递 state 给这个被包装的组件,而不是直接操作被包装组件的 state。通过简单的组件嵌套,包装组件的生命周期,无需任何特殊的合并行为就可以发挥作用

使用高阶组件

HOC的功能和优点

HOC 很棒,它的本质是以函数式的方式将组件中的关注点分离。

 

  • 可以封装复用逻辑
  • 可以在 ES6 Class 中使用
  • 基于 props 操作(一些开发者认为操作 props > 操作 state)
  • 更加的函数式(functional)

mixin的问题解决了吗

  1. 不能在 ES6 Class 中使用。✅

  2. 数据源来源不清晰。当一个组件使用了多个 mixin 时,很难清除哪个方法/变量来自哪里。❎

  3. 命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率肯能会在命名时添加&,_等特殊字符,影响可读性。❎

  4. 复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。❎

过多的组件包裹产生 wrapper hell

新的问题

进化 - render props && Function as Child Components

// Function as Child Components
class Wrap extends React.Component { 
  state = {
     name: 'hello world'
  }
  render() {
    return (
      <div>
        {this.props.children(name)}
      </div>
    );
  }
}

<Wrap>
  {name => (
    <h2>{name}</h2>
  )}
</Wrap>

// render props
class Wrap extends React.Component { 
  render() {
    return (
      <div>
        {this.props.render('hello world')}
      </div>
    );
  }
}

<Wrap render={name => (
    <h2>{name}</h2>
  )}
/>

render props 是一种在 React 组件之间使用一个值为函数的 prop 共享代码的技术

render props 解决的问题

render props

  • 支持ES6 Class。
  • 不用困扰使用的props来自哪里,可以通过render函数的参数很直观地看见。                                     
  • 不会有命名冲突问题。     

mixin && HOC

  • 不能在 ES6 Class 中使用。

  • 数据源来源不清晰。当一个组件使用了多个 mixin 时,很难清除哪个方法/变量来自哪里。

  • 命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率肯能会在命名时添加&,_等特殊字符,影响可读性。

  • 复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。

其他问题

本质是利用了闭包,会引起 callback hell 的问题。

React sucks

  • 逻辑复用方式存在的问题(前面提到的wrapper hell)
  • 当组件变得巨大,被生命周期分割的复用逻辑变得难以追踪
  • 晦涩的 Class 语法(不仅对人,对编译器来说也是)

React Conf 2018 上,React 团队 leader 指出 React 仍然存在的问题:

Class Component 现状

  • 必须调用 super(props),很烦

  • 方法要手动 bind this

  • 通过生命周期组织代码,强制把相关的代码穿插在不同的生命周期之间

  • 难以优化,Class 语法在 Babel 编译后代码量增加很多

 

告别 Class

React 的方向从 Class Component 转向 Function Component

// 一个简单的函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

不支持state

不支持生命周期

React 缺少一种方便的,在函数组件中处理 state,生命周期 的机制。

Function Component 的问题

再进化 - React Hooks

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

State Hook

顾名思义,useState 让函数组件可以使用 state。入参是 initialState,返回一个数组,第一值是 state,第二个值是改变 state 的函数。

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

如果 initialState 的提高需要消耗大量的计算力,同时不期望这些计算阻塞后面要干的事情的话。initialState 可以是个函数,会在 render 前调用达到 Lazy Calc 的效果。

同时为了避免不必要的性能开销,在设置 State 的时候如果两个值是相等的,则也不会触发 rerender。(判断两个值相等使用的是 Object.is

Effect Hook

Effect hooks 用来处理函数组件中的副作用。useEffect 相当于 componentDidMount, componentDidUpdate, componentWillUnmount 的组合。在组件挂载、更新、卸载的时候都会执行 effect 里面的函数。可以认为 Effect hooks 的执行时机是 “after render”

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Class Component with Cleanup

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

Class Component 一般在 componentWillUnmount 中做取消订阅的操作。

Effects with Cleanup

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);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

需要注意的是这里的 subscribe 和 unsubscribe每次after render都会触发,可以减少bug(比如prosps.friend.id突然变化)

useEffect 的返回值会作为 cleanup 函数,在组件更新后或卸载前执行。

跳过 effects 优化性能

如果 effects 确实不需要每次 after render 都执行,可以向useEffect传入第二个参数跳过不必要的effects。第二个数组中的变量表示是这个 effect 的依赖,仅当依赖发生变化时才重新渲染。如果传入空数组,effect 只执行一次(didMount阶段)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

// 类似于
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

Custom hooks

我们可以基于 useState,useEffect 封装自定义 hooks

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);
  return isOnline === null ? 'Loading...' : isOnline ? 'Online' : 'Offline';
}

Rules of hooks

  • 不能在条件判断或嵌套函数中使用 hooks,只能在顶级使用 hooks,以确保组件每次 render,hooks 都是按照相同的顺序执行,保证 useState 和 useEffect 执行结果正确。
  • 不能在普通 JS 函数中使用 hooks,只能在 React 函数组件,自定义 hook 中调用hook。

function component with hooks

对比 Class Component

  • 必须调用 super(props),很烦
  • 方法要手动bind this
  • 通过生命周期组织代码,强制把相关的代码穿插在不同的生命周期之间
  • 难以优化

hooks

  • hooks 让复用更优雅,我们复用的不是方法,不是类,只是一个函数
  • 复用逻辑不再被生命周期分割,而是以职责划分
  • 都是函数,方便编译器优化

END

React Hooks

By showonne

React Hooks

  • 340