React组件设计理论
React以一种全新的编程范式定义了前端开发约束,它为视图开发带来了一种全新的心智模型:
- React认为,UI视图是数据的一种视觉映射,即UI = F(DATA),这里的F需要负责对输入数据进行加工、并对数据的变更做出响应
 - 公式里的F在React里抽象成组件,React是以组件(Component-Based)为粒度编排应用的,组件是代码复用的最小单元
 - 在设计上,React采用props属性来接收外部的数据,使用state属性来管理组件自身产生的数据(状态),而为了实现(运行时)对数据变更做出响应需要,React采用基于类(Class)的组件设计!
 
除此之外,React认为组件是有生命周期的,因此开创性地将生命周期的概念引入到了组件设计,从组件的create到destory提供了一系列的API供开发者使用
这就是React组件设计的理论基础,我们最熟悉的React组件一般长这样:
// React基于Class设计组件
class MyConponent extends React.Component {
  // 组件自身产生的数据
  state = {
    counts: 0
  }
  // 响应数据变更
  clickHandle = () => {
    this.setState({ counts: this.state.counts++ });
    if (this.props.onClick) this.props.onClick();
  }
  // lifecycle API
  componentWillUnmount() {
    console.log('Will mouned!');
  }
    // lifecycle API
  componentDidMount() {
    console.log('Did mouned!');
  }
  // 接收外来数据(或加工处理),并编排数据在视觉上的呈现
  render(props) {
    return (
      <>
        <div>Input content: {props.content}, btn click counts: {this.state.counts}</div>
        <button onClick={this.clickHandle}>Add</button>
      </>
    );
  }
}
Class Component的问题
组件复用困局
组件并不是单纯的信息孤岛,组件之间是可能会产生联系的,一方面是数据的共享,另一个是功能的复用:
- 对于组件之间的数据共享问题,React官方采用单向数据流(Flux)来解决
 - 对于(有状态)组件的复用,React团队给出过许多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass之后又设计了Render Props和Higher Order Component,直到再后来的Function Component+ Hooks设计,React团队对于组件复用的探索一直没有停止
 
HOC使用(老生常谈)的问题:
- 嵌套地狱,每一次HOC调用都会产生一个组件实例
 - 可以使用类装饰器缓解组件嵌套带来的可维护性问题,但装饰器本质上还是HOC
 - 包裹太多层级之后,可能会带来props属性的覆盖问题
 
Render Props:
- 数据流向更直观了,子孙组件可以很明确地看到数据来源
 - 但本质上Render Props是基于闭包实现的,大量地用于组件的复用将不可避免地引入了callback hell问题
 - 丢失了组件的上下文,因此没有this.props属性,不能像HOC那样访问this.props.children
 
Javascript Class的缺陷
this的指向(语言缺陷)
class People extends Component {
  state = {
    name: 'dm',
    age: 18,
  }
  handleClick(e) {
    // 报错!
    console.log(this.state);
  }
  render() {
    const { name, age } = this.state;
    return (<div onClick={this.handleClick}>My name is {name}, i am {age} years old.</div>);
  }
}
createClass不需要处理this的指向,到了Class Component稍微不慎就会出现因this的指向报错。
编译大小(还有性能)问题
// Class Component
class App extends Component {
  state = {
    count: 0
  }
  componentDidMount() {
    console.log('Did mount!');
  }
  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  }
  decreaseCount = () => {
    this.setState({ count: this.state.count - 1 });
  }
  render() {
    return (
      <>
        <h1>Counter</h1>
        <div>Current count: {this.state.count}</div>
        <p>
          <button onClick={this.increaseCount}>Increase</button>
          <button onClick={this.decreaseCount}>Decrease</button>
        </p>
      </>
    );
  }
}
// Function Component
function App() {
  const [ count, setCount ] = useState(0);
  const increaseCount = () => setCount(count + 1);
  const decreaseCount = () => setCount(count - 1);
  useEffect(() => {
    console.log('Did mount!');
  }, []);
  return (
    <>
      <h1>Counter</h1>
      <div>Current count: {count}</div>
      <p>
        <button onClick={increaseCount}>Increase</button>
        <button onClick={decreaseCount}>Decrease</button>
      </p>
    </>
  );
}
Class Component编译结果(Webpack):
var App_App = function (_Component) {
  Object(inherits["a"])(App, _Component);
  function App() {
    var _getPrototypeOf2;
    var _this;
    Object(classCallCheck["a"])(this, App);
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    _this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
    _this.state = {
      count: 0
    };
    _this.increaseCount = function () {
      _this.setState({
        count: _this.state.count + 1
      });
    };
    _this.decreaseCount = function () {
      _this.setState({
        count: _this.state.count - 1
      });
    };
    return _this;
  }
  Object(createClass["a"])(App, [{
    key: "componentDidMount",
    value: function componentDidMount() {
      console.log('Did mount!');
    }
  }, {
    key: "render",
    value: function render() {
      return react_default.a.createElement(/*...*/);
    }
  }]);
  return App;
}(react["Component"]);
Function Component编译结果(Webpack):
function App() {
  var _useState = Object(react["useState"])(0),
    _useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var increaseCount = function increaseCount() {
    return setCount(count + 1);
  };
  var decreaseCount = function decreaseCount() {
    return setCount(count - 1);
  };
  Object(react["useEffect"])(function () {
    console.log('Did mount!');
  }, []);
  return react_default.a.createElement();
}
- Javascript实现的类本身比较鸡肋,没有类似Java/C++多继承的概念,类的逻辑复用是个问题
 - Class Component在React内部是当做Javascript Function类来处理的
 - Function Component编译后就是一个普通的function,function对js引擎是友好的
 
Function Component缺失的功能
不是所有组件都需要处理生命周期,在React发布之初Function Component被设计了出来,用于简化只有render时Class Component的写法。
Function Component是纯函数,利于组件复用和测试
Function Component的问题是只是单纯地接收props、绑定事件、返回jsx,本身是无状态的组件,依赖props传入的handle来响应数据(状态)的变更,所以Function Component不能脱离Class Comnent来存在!
function Child(props) {
  const handleClick = () => {
    this.props.setCounts(this.props.counts);
  };
  // UI的变更只能通过Parent Component更新props来做到!!
  return (
    <>
      <div>{this.props.counts}</div>
      <button onClick={handleClick}>increase counts</button>
    </>
  );
}
class Parent extends Component() {
  // 状态管理还是得依赖Class Component
  counts = 0
  render () {
    const counts = this.state.counts;
    return (
      <>
        <div>sth...</div>
        <Child counts={counts} setCounts={(x) => this.setState({counts: counts++})} />
      </>
    );
  }
}
所以,Function Comonent是否能脱离Class Component独立存在,关键在于让Function Comonent自身具备状态处理能力,即在组件首次render之后,“组件自身能够通过某种机制再触发状态的变更并且引起re-render”,而这种“机制”就是Hooks!
Hooks的出现弥补了Function Component相对于Class Component的不足,让Function Component取代Class Component成为可能。
Function Component + Hooks组合
1、功能相对独立、和render无关的部分,可以直接抽离到hook实现,比如请求库、登录态、用户核身、埋点等等,理论上装饰器都可以改用hook实现(如react-use,提供了大量从UI、动画、事件等常用功能的hook实现)。
case:Popup组件依赖视窗宽度适配自身显示宽度、相册组件依赖视窗宽度做单/多栏布局适配
function useWinSize() {
  const html = document.documentElement;
  const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });
  useEffect(() => {
    const onSize = e => {
      setSize({ width: html.clientWidth, height: html.clientHeight });
    };
    window.addEventListener('resize', onSize);
    return () => {
      window.removeEventListener('resize', onSize);
    };
  }, [ html ]);
  return size;
}
// 依赖win宽度,适配图片布局
function Article(props) {
  const { width } = useWinSize();
  const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;
  return (
    <>
      <article>{props.content}<article>
      <div className={cls}>recommended thumb list</div>
    </>
  );
}
// 弹层宽度根据win宽高做适配
function Popup(props) {
  const { width, height } = useWinSize();
  const style = {
    width: width - 200,
    height: height - 300,
  };
  return (<div style={style}>{props.content}</div>);
}
2、有render相关的也可以对UI和功能(状态)做分离,将功能放到hook实现,将状态和UI分离
case:表单验证
function App() {
  const { waiting, errText, name, onChange } = useName();
  const handleSubmit = e => {
    console.log(`current name: ${name}`);
  };
  return (
    <form onSubmit={handleSubmit}>
      <>
        Name: <input onChange={onChange} />
        <span>{waiting ? "waiting..." : errText || ""}</span>
      </>
      <p>
        <button>submit</button>
      </p>
    </form>
  );
}
React Hooks 的本质
稍微复杂点的项目肯定是充斥着大量的 React 生命周期函数(注意,即使你使用了状态管理库也避免不了这个),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。

而 Hooks 的出现本质是把这种面向生命周期编程变成了面向业务逻辑编程,你不用再去关心本不该关心的生命周期。

一个 Hooks 演变
我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:
- 因为业务简单,没有引入额外状态管理库
 - 因为业务强相关,并不想把数据跟组件分开放
 - API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
 - 为了后期优化,不可以有额外的组件创建和销毁
我们可能的实现如下: 
class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }
  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }
  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }
  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }
  render() {
    const user = this.state.user;
    return (
      <ReactModal
        isOpen={this.props.visible}
      >
        <button onClick={this.props.handleCloseModal}>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }
      </ReactModal>
    )
  }
}
我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visible 和 handleCloseModal(用于 Modal 关闭自己)。
为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMount 和 componentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。
其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用。
下面我们看看采用 Hooks 改造后会是什么样:
function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分几乎与上面相同
  );
}
很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDM 和 cDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了(如果有读取或修改 DOM 的需求可以看 这里)。做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。
Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:
// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}
function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 与上面相同
  );
}
这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。
业务逻辑复用
这里说的业务逻辑复用主要是需要跨生命周期的业务逻辑。单单按照组件堆积的形式组织代码虽然也可以达到各种复用的目的,但是会导致组件非常复杂,数据流也会很乱。组件堆积适合 UI 布局,但是不适合逻辑组织。为了解决这些问题,在 React 发展过程中,产生了很多解决方案,我认知里常见的有以下几种:
Mixins
坏处远远大于带来的好处,因为现在已经不再支持,不多说,可以看看这篇文章:Mixins Considered Harmful。
Class Inheritance
官方 很不推荐此做法,实际上我也没真的看到有人这么做。
High-Order Components (HOC)
React 高阶组件 在封装业务组件上简直是屡试不爽,它的实现是把自己作为一个函数,接受一个组件,再返回一个组件,这样它可以统一处理掉一些业务逻辑并达到复用目的。
比较常见的一个就是 react-redux 里的 connect 函数:

但是它也被很多人吐槽嵌套问题:

Render Props
Render Props 其实很常见,比如 React Context API:
class App extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <ThemeContext.Consumer>
          {val => <div>{val}</div>}
        </ThemeContext.Consumer>
      </ThemeProvider>
    )
  }
}
它的实现思路很简单,把原来该放「组件」的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。
但是,同样这会产生 Wrapper Hell 问题:

Hooks
Hooks 本质上面说了,是把面向生命周期编程变成了面向业务逻辑编程,写法上带来的优化只是顺带的。
这里,做一个类比,await/async 本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的 Callback Hell 被打平了。
总结对比:
- 
await/async把 Callback Hell 干掉了,异步编程思维变成了同步编程思维 - Hooks 把 Wrapper Hell 干掉了,面向生命周期编程变成了面向业务逻辑编程
 
这里不得不客观地说,HOC 和 Render Props 还是有存在的必要,一方面是支持 React Class,另一方面,它们不光适用于纯逻辑封装,很多时候也适合逻辑 + 组件的封装场景,虽然此时使用 Hooks 也可以,但是会显得啰嗦点。另外,上面诟病的最大的问题 Wrapper Hell,我个人觉得使用 Fragment 也可以基本解决。
状态盒子
首先,React Hooks 的设计是反直觉的,为什么这样说呢?可以先试着问自己:为什么 Hooks 只能在其它 Hooks 的函数或者 React Function 组件里?
在我们的认知里,React 社区一直推崇函数式、纯函数等思想,引入 Hooks 概念后的 Functional Component 变的不再纯了,useXxx 与其说是一条执行语句,不如说是一个声明。声明这里放了一个「状态盒子」,盒子有输入和输出,剩下的内部实现就一无所知,重要的是,盒子是有记忆的,下次执行到此位置时,它有之前上下文信息。
类比「代码」和「程序」的区别,前者是死的,后者是活的。表达式 c = a + b 表示把 a 和 b 累加后的值赋值给 c,但是如果写成 c := a + b 就表示 c 的值由 a 和 b 相加得到。看起来表述差不多,但实际上,后者隐藏着一个时间的维度,它表示的是一种联系,而不单单是个运算。这在 RxJS 等库中被大量使用。

这种声明目前是通过很弱的 use 前缀标识的(但是设计上会简洁很多),为了不弄错每个盒子和状态的对应关系,书写的时候 Hooks 需要 use 开头且放在顶层作用域,即不可以包裹 if/switch/when/try 等。如果你按文章开头引入了那个 ESLint Plugin 就不用担心会弄错了。
总结
这篇文章可能并没有一个很条理的目录结构,大多是一些个人理解和相关思考。因此,这不能替代你去看真正的文档了解更多。如果你看完后还是觉得废话太多,不知所云,那我希望你至少可以在下面几点上跟作者达成共鸣:
- Hooks 本质是把面向生命周期编程变成了面向业务逻辑编程;
 - Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
 - Hooks 是 React 的未来,但还是无法完全替代原始的 Class。
 
参考:
https://zhuanlan.zhihu.com/p/92211533
https://segmentfault.com/a/1190000017182184









