dva-generator
dva-generator copied to clipboard
React沉思录
使用React也有一年多时间了,也是时候拿起笔杆子整理下了。以下算是自己对React的一份理解。当然每一点如果放开写的话都可能写上好几页,时间有限,先奉上这么多,有错误欢迎指正。
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源码阅读-初始化篇
拜读!!
@xudihui 何不star下 😄