React沉思录

#1

原文链接:https://github.com/jnotnull/dva-generator/issues/4 后续还有更多文章 欢迎star

1. 什么是React

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES 这是React官方的定义。从这里面我们能得到两层含义:1. 它首先是一个库,不是框架。2. 用来构建用户界面的,也就是说是专注于view层的。
因为是库,所以可以很方便的和前端生态系统中的其他库融合到一起,比如Redux等。通过引入Redux,它就不再简单的处理view层了,Controller层和Model层也收入旗下了。

它有两个重要的特征:一个就是Virtual DOM,另一个就是JSX。当然了Virtual DOM和JSX也可以用在其他框架。

2. React组件的生命周期

使用React开发时候用到最多的就是React的组件了,通过继承React.Component,加入constructor构造函数,实现Render方法即可。这当中React组件为我们提供了很多钩子,以便更加方便精细的控制程序。

钩子包括:componentWillMount/componentDidMount,同时在调用setState时候会触发:componentWillReceiveProps/shouldComponentUpdate/ComponentWillUpdate/render/ComponentDidUpdate。另外在最新发布的16版本中添加了componentDidCatch(error, info) 钩子来为组件做异常边界。

值得注意的是,不能在ComponentWillUpdate使用setState方法,否则会造成循环调用,这是因为componentWillMount是在render前触发的,因此设置state不会触发再次渲染。

3. 什么是JSX

const element = <h1>Hello, world!</h1>; 这就是JSX。通过名字我们就能看出来它是JavaScript的扩展。它就是使用JavaScript的强大功能来供你写UI。因为使用了JavaScript,所以你不用记任何标签语法,唯一你需要记住的可能就是className替代了class熟悉,htmlFor替代了for熟悉,另外绑定事件名称要采用骆驼命名法,比如onClick。对了,因为是JavaScript,所以添加行样式时候采用对象的形式,比如style={{width: ‘calc(100% - 20px)’, marginLeft: “20px”}},仅此而已。

对于外界一直吐槽的只能返回一个节点的问题,最新发布的16版本中也增加了返回数组元素了。

4. React Components VS. Web Components

按照官方说法,他们两者是互补的,不冲突的,你可以在React中使用Web Components,也可以在Web Components中使用React。但是呢,有兴趣的人终究想要比个一二。具体可以看看这里的帖子 Pros and Cons of Facebook’s React vs. Web Components (Polymer) 。事实是,react完全封装了自己一套玩法,和Web Components标准相差太远,React官方也在说它们是解决不同问题的,React侧重于数据同步层面。

5. state数据类别 UI state/Domain state, Local state/Global state

编写好的组件,第一个解决的就是数据问题。一个组件中包含的数据有多种,一种分类就是UI state和Domain state。

先来说说Domain state,它就是我们正常的业务数据,比如说消息数据,我们会通过一些转换然后在界面上显示,当然你也可以去修改。而UI state则是增对组件本身的一些数据,比如说某个按钮的显示,某个checkbox的选择,这些数据一般不会和其他组件共享。

另一种分类是Local state/Global state。

Local state和我们上面说的UI state有一个共同点,就是不会和其他组件共享,但是还有一类数据是属于Local state的,那就是表单的输入数据。这些数据在提交之前只要所属组件知道就可以了。另外一种是Global state,这类数据是从父组件中传递下来的,修改之后还可能需要调用回调方法通知其他组件。

这里为什么要说说state的类别呢,这个其实是和下面说的redux有些关系的,redux管理的state应该是Domain state或者Global state,对于UI state和Local state就不要放进去了,自己setState就可以了。因为毕竟使用redux还是有开销的。

6. 你所不知道的setState

既然前面提到了setState,那我们就来终点说说这个方法。React本身提供给开发者调用的方法很少,这也是React好用的一个重要原因,你不用记着太多的方法签名。而setState就是React提供的很少方法之一。细心的你可能已经知道setState不是同步的,但是你可能还不知道哪些情况却可能是同步的。我们首先来看下setState的方法签名:

setState(updater, [callback])

首先看下第一个参数updater,它可以是一个对象,也可以是函数,函数的签名是

(prevState, props) => stateChange

再来看第二个参数,它的作用就是在执行update成功后执行的回调。

我们重点看下setState执行后到底发生了什么。setState后首先加入到pending队列中,然后判断当前环境是否是Batch Updates,如果是就执行更新,否则就加入到dirtyComponents。那什么时候会处于batch updates状态呢,答案就是在渲染组件的时候。那什么时候不会处于batch updates状态呢,没错,那当然就是脱离React组件能管理的生命周期外的情况了,比如在setTimeout()、addEventListener()等回调中调用setState。

7. SyntheticEvent

SyntheticEvent即合成事件。在JSX中绑定一个事件很简单:

onClick={this.xx.bind(this)}

最终JSX是要转换为VDOM的,而VDOM是在内存中存在的,那对于绑定的事件,React不会直接绑定到某个元素上,而是绑定到元素的最外层,这就是我们所需的事件代理,它的好处我们也是知道的,能极大的提升性能。这就是React封装的SyntheticEvent的原理。

另外按照官方说法,为了性能考虑,SyntheticEvent可以被重复使用的,也就是说使用完后就会被清掉,所以我们只能同步的获取事件。 可以看下官方提供的例子:

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

8. Stateful and Stateless Components 以及Stateless Components的功能增强recompose

无状态组件(Stateless Components)因为其书写简单,被众人推崇,看下代码;

import React from ‘react’;

const HelloWorld = ({name}) => (
 <div>{`Hi ${name}`}</div>
);

export default HelloWorld;

看起来非常简单,但是因为没有React组件的生命周期概念,也就不能使用setState/refs/shouldComponentUpdate等属性和方法了,也就有了很大的局限性。除非特别简单的展示组件,否则不推荐使用这种无法交互的组件的。

Stateful Components组件就是我们熟悉的集成React.Component的组件了。

那如果我想在Stateless Components中修改属性怎么做呢,对了,可以使用recompose:

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

9. Functional and Class Components

在官方说法中,还有一种组件叫做Functional Components,看下代码:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

接收参数为props,实现起来非常简单。

另一种就是Class Components,它其实和Stateful Components是一个东东,都是继承React.Comonent得到的组件。

10. Smart and Dumb Components / Presentational and Container Components

Smart Components(智能组件)和Dumb Components(木偶组件)也是我们区分组件最常用的方式。

Dumb Components一般也叫做Presentational Components,一般只做展示数据使用,组件内部很少有数据变化,它的数据要依赖外部传入,所以Dumb Components可以使用Stateless Components来定义,一般也是没问题的。

而Smart Components也叫Container Components,我们从名字也能看出来,它是有数据处理能力的,不完全依赖外部数据传入,处理完成之后还可能通知其他组件。

11. Pure components

Pure Components默认会调用 shouldComponentUpdate做数据检查,如果props或者state没有变化的话,就不会触发重新渲染。

Recompose 提供了pure方法,另外React也在v15.3.0提供了React.PureComponent。

import { pure } from 'recompose';
export default pure((props, context) => {
  return <SomeComponent someProp={props.someProp}/>
})
import { PureComponent } from 'react';

export default class Example extends PureComponent {
  render() {
    return <SomeComponent someProp={props.someProp}/>
  }
}
})

12. Height Order Components

Height Order Components即高阶组件,简称HOC。HOC的原理其实很简单,输入一个组件,输出另一个组件。但是它的用途却很大。它其实类似于Decorator,可以增强组件功能。下面来看下它的用法:

var enhanceComponent = (Component) =>
  class Enhance extends React.Component {
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
        />
      )
    }
  };

export default enhanceComponent;
var OriginalComponent = () => <p>Hello world.</p>;
var EnhancedComponent = enhanceComponent(OriginalComponent);

class App extends React.Component {
  render() {
    return <EnhancedComponent />;
  }
};

这样的写法可以避免我们手工传递一个一个参数,同时增加一个属性也变得非常简单。Dan Abramov说过,我们最好在顶层做HOC,不要在另一个组件中去做HOC,那样因为每次都要重新生成会影响到性能。关于避免深层次传递参数,还有一个有意思的文章推荐: Avoiding deeply nested component trees

13. Redux解决哪一类问题

回答这个问题之前我们先看下什么是Redux。Redux本身使用的思想其实我们很熟悉的,就是不要直接操作数据本身。这个我们很熟悉啊,之前的bean的getter和setter不就是这样的。没错,但是它做了更高层的抽象,首先是动作(Action)的抽象。Action定义了可以做的事情,比如add(计数器加1),注意,它只定义一个名称,具体的实现就交给Reducer。所以从这里我们就知道Reducer要做什么事情了把,对的,就是具体事件的定义,比如就给某个属性+1。那我们Action不可能一个吧,多个Action我们就可以抽取出Action Creater。Reducer也不可能一个吧,多个Reducer就形成了reducers集合。那调用reducers也不能说你想用就用,必须有个统一入口,这个就是store,所有的调用都要走store.disptach方法。

这样看下来,似乎没有引入啥新的库,你可能平时或多或少也进行过类似的封装,只可能你封装的抽象程度没有那么高而已。事实也是如此,Redux库本身的源码也不多,除了createStore外,其他大部分是辅助的函数:

  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose

了解了Redux是什么,我们就能知道它是解决哪一类问题了,对了,除了解决数据流问题,其实它还封装了对数据的操作,类似于Controller的概念了。

14. Redux和react-redux的分工是什么,为什么要引入react-router-redux

既然有了Redux,为啥还要有react-redux呢,那是因为Redux不仅仅提供给React,Vue或者Angular也可以使用的。针对React,react-redux主要提供了两个东东,一个是<Provider/>组件 ,一个是 connect()方法。

为啥需要Provider组件呢,那是因为我们在子组件中也可能用到Redux相关东东,我们不能一个组件一个组件把store向下传吧,那样太累了,于是我们在应用的最顶层封装了<Provider/>组件,它直接使用getChildContext 方法将属性传递给子组件,简直就是八神庵的大招啊。connect()方法则是沟通Redux和组件的桥梁,它的方法签名如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

mapStateToProps顾名思义,定义store 中的数据作为 props 绑定到组件上;
mapDispatchToProps同样,它会将store中的action作为props绑定到组件上;
mergeProps和options默认不用传的。

那为什么要引入react-router-redux呢。我们开发React应用时候,最常用的路由是react-router。Redux是管理应用的状态的,而路由的状态也算是一种状态,自然也要收入到Redux门下了,通过引入react-router-redux,我们只要做下路由和redux的同步就可以了:

this._history = syncHistoryWithStore(history, this._store);

15. Redux middleware

要想扩展Redux的功能,最好的方法就是使用Redux的middleware机制。Redux的middleware类似于KOA的middleware。在项目中用到最多的当然是异步数据请求的中间件saga,看下DVA的源码:

const sagaMiddleware = createSagaMiddleware();
      let middlewares = [
        sagaMiddleware,
        ...flatten(extraMiddlewares),
      ];
      if (routerMiddleware) {
        middlewares = [routerMiddleware(history), ...middlewares];
      }
      let devtools = () => noop => noop;
      if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) {
        devtools = window.__REDUX_DEVTOOLS_EXTENSION__;
      }
      const enhancers = [
        applyMiddleware(...middlewares),
        devtools(),
        ...extraEnhancers,
      ];
      const store = this._store = createStore(
        createReducer(),
        initialState,
        compose(...enhancers),
      );

从中我们可以看出,多个中间件可以compose的,关于这一段的详细逻辑可以参考我之前的文章:DVA源码阅读-初始化篇

1 Like