React知识点整理

初入React

JSX语法

  • 定义标签时,只允许被一个标签包裹
  • 标签一定要闭合
  • 注释被{}包起来

React组件

  • 无状态组件创建时始终保持了==一个实例==
  • 有状态组件创建几次组件就会创建几次实例

React数据流

state
  • setState是==异步==方法
    props
  • props本身是不可变的(==readonly==)
  • ==defaultProps==静态变量可以定义props默认配置(==默认类型==)

    1
    2
    3
    4
    static defaultProps = {
    classPrefix: 'tabs',
    onChange: () => {},
    };
  • React中有一个内置的prop:children,代表组件的子组件集合

  • JavaScript不是强类型的语言,React对此做了改进,==propTypes==用于规范props的类型与必要状态(==类型检查==)。
    1
    2
    3
    4
    5
    6
    7
    8
    static propTypes = {
    tab: React.PropTypes.oneOfType([
    React.PropTypes.string,
    React.PropTypes.node,
    ]).isRequired,
    order: React.PropTypes.string.isRequired,
    disble: React.PropTypes.bool,
    };

React生命周期

分为挂载、渲染、卸载几个阶段

挂载或卸载

主要做组件状态初始化

挂载
1
2
3
graph LR
componentWillMount-->render
render-->componentDidMount
卸载
1
2
graph LR
render-->componentWillUnmount
数据更新

指父组件向下传递props或组件自身执行setState方法时发生的一系列更新动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {Component, PropTypes} from 'react';

class App extends Component {
componentWillReceiveProps(nextProps){
//this.setState({})
}

shouldComponentUpdate(nextProps, nextState) {

}

componentWillUpdate(nextProps, nextState) {

}

componentDidUpdate(prevProps, prevState) {

}

render() {

}
}

组件自身state更新
1
2
3
4
graph TB
shouldComponentUpdate-->componentWillUpdate
componentWillUpdate-->render
render-->componentDidUpdate

==不能在componentWillUpdate里执行setState==。

父组件更新props而更新
  • 在shouldComponentUpdate之前先执行componentWillReceiveProps。
  • 该方法可作为React在==props传入后,渲染之前==setState。
    1
    2
    3
    componentWillReceiveProps(nextProps) {

    }

React与DOM

refs

refs是React组件中特殊的props,可以附加到任何一个组件上。组件被调用时会新建一个该组件的实例,refs则指向这个实例。

  • refs放在原生DOM组件中可以得到其DOM节点
  • refs放在React组件中则获得组件的实例,可以调用该组件的实例方法

漫谈React

事件系统

在React底层,主要对合成事件做了两件事:事件委派和自动绑定。

在React中使用原生事件

React生命周期方法中,componentDidMount会在组件已经完成安装并且在浏览器中存在真实的DOM后调用,此时可以完成原生事件的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, {Component} from 'react';

class NativeEventDemo extends Component {
componentDidMound() {
this.refs.button.addEventListener('click', e => {
handleClick(e);
});
}

handerClick(e) {
console.log(e);
}

componentWillUnMount() {
this.refs.button.removeEventListener('click');
}

render() {
return <button ref="button">Test</button>
}
}

==注:在React中使用DOM原生事件时,一定要在组件卸载时手动卸除,否则很可能会出现内存泄漏的问题。==

对比React合成事件与JavaScript原生事件
  • 浏览器原生DOM事件的传播可以分为三个阶段:事件捕获阶段、事件处理以及事件冒泡。React的合成事件并没有实现事件捕获,仅仅支持了事件冒泡机制。阻止事件传播e.preventDefault();
  • React合成事件的时间类型是JavaScript原生事件类型的一个子集。

组件间通信

父组件向子组件通信

父组件通过props向子组件传递需要的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {Component} from 'react';

function ListItem({value}) {
return (
<li>
<span>{value}</span>
</li>
);
}

function List({list, title}) {
return(
<div>
<ListTitle title={title} />
<ul>
{this.props.list.map((entry, index) => {
<ListItem key={`list-${index}`} value={entry.text}>

})}
</ul>
</div>
);
}

子组件向父组件通信
  • 利用回调函数
  • 利用自定义事件机制

this.props.function()

跨级组件通信

组件新能优化

影响网页性能最大的因素是浏览器的==重绘==和==重排版==。

纯函数

纯函数由三大原则构成:

  • 给定相同的输入,返回相同的输出
  • 过程没有副作用(在纯函数中不能改变外部状态)
  • 没有额外的状态依赖(方法内的状态都只在方法的生命周期内存活,即不能再方法内使用共享变量)
PureRender

Pure指的是组件满足纯函数的条件,即组件的渲染是被相同的props和state渲染进而得到相同的结果

Immutable
key
  • 如果每一个子组件是一个数组或迭代器,必须有一个唯一的key prop
  • key用来做Virtual DOM dif
  • 当key相同时,只渲染第一个相同key的项,且会报一个警告

解读React源码

Virtual DOM实际上是在浏览器端用JavaScript实现的一套DOM API,它之于React就好似一个虚拟空间,包括一套Virtual DOM模型、生命周期的维护和管理、性能高效的diff算法和将Virtual DOM展示为原生DOM的path方法。

Virtual DOM模型

一个DOM标签所需的基本元素:

  • 标签名
  • 节点属性,包含样式、属性、事件等
  • 子节点
  • 标识id
  • Virtual DOM中的节点称为ReactNode,它分为三种类型ReactElement、ReactFragment和ReactText。其中,ReactElement又分为ReactComponentElement和ReactDOMElement。
创建React元素
1
2
3
4
//createElement只是做了简单的参数修正,返回一个ReactElement实例对象,也就是虚拟元素的实例
ReactElement.createElement = function(type, config, children){

}
DOM标签组件

ReactDOMComponent针对Virtual DOM标签的处理主要分为:

  • 属性的更新,包括更新样式、更新属性、处理事件等。
  • 子节点的更新,包括更新内容、更新子节点,涉及diff算法。
更新属性
  • 如果存在事件,则针对当前的节点添加事件代理
  • 如果存在样式,首先会对样式进行合并操作,然后创建样式
  • 创建属性
  • 创建唯一标识

删除不需要的旧属性,更新新属性。

更新子节点
  • 删除不需要的子节点和内容
  • 更新子节点和内容

生命周期的管理艺术

生命周期在不同状态下的执行顺序:

  • 当首次挂载时,按顺序执行==getDefaultProps==、==getInitialState==、componentWillMount、render、componentDidMount
  • 当卸载组件时,执行componentWillUnmount
  • 当重新挂载组件时,按顺序执行==getInitialState==、componentWillMount、render和componentDidMount,但并不执行getDefaultProps
  • 当再次渲染组件时,组件接受到更新的状态,此时按顺序执行componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render和componentDidUpdate。
first render
1
2
3
4
5
graph TB
getDefaultProps-->getInitialState
getInitialState-->componentWillMount
componentWillMount-->render
render-->componentDidMount
props change
1
2
3
4
5
graph TB
componentWillReceiveProps-->shouldComponentUpdate
shouldComponentUpdate-->componentWillUpdate
componentWillUpdate-->render
render-->componentDidUpdate
state change
1
2
3
4
graph TB
shouldComponentUpdate-->componentWillMount
componentWillMount-->render
render-->componentDidMount

自定义组件的声明周期主要通过3个阶段进行管理:MOUNTING、RECEIVE_PROPS、UNMOUNTING

MOUNTING

mountComponent负责管理生命周期中的getInitialState、componentWillMount、render和componentDidMount。

  • ==getDefault是通过构造函数进行管理==的,所以也是整个生命周期中最先开始执行的,==只执行一次==。
  • 此时在componentWillMount中调用setState方法不会触发re-render,而是会进行==state合并==
  • mountComponent的本质是通过==递归==渲染内容,由于递归的特性,父组件的componentWillMount在其子组件的componentWillMount之前调用,父组件的componentDidMount在子组件的componentDidMount之后调用】
RECEIVE_PROPS

updateComponent负责管理生命周期中的componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render和componentDidUpdate。

  • 首先通过updateComponent更新组件,如果前后元素不一致,说明需要进行组件更新
  • 此时在componentWillReceiveProps中调用setState不会触发re-render,而是会进行==state合并==
  • 只有在render和componentDidUpdate中才能获取到更新后的this.state
  • updateComponent本质上也是通过递归渲染内容,父组件的componentWillUpdate是在其子组件的componentWillUpdate之前调用,父组件的componentDidUpdate子组件的componentDidUpdate之后调用
UNMOUNTING

unmountComponent负责管理生命周期中的componentWilUnmount。

  • 如果存在componentWillUnmount,则执行并重置所有相关参数、更新队列以及更新状态
  • 此时在componentWillUnmount中调用setState不会触发re-render,因为==所有更新队列和更新状态都被重置为null,并清除了公共类,完成了组件卸载操作==。

解密setState机制

  • setState通过一个队列机制实现state更新
  • 当执行setState时,会将需要更新的state合并后放入状态机,而不会立刻更新this.state
  • 如果在shouldComponentUpdate或componentWillUpdate方法中调用setState,会造成循环调用,使得浏览器内存占满后崩溃
setState调用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, {component} from 'react';

class Example extends Component {
constructor() {
super();
this.state = {
val: 0;
}
}

componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val);

this.setState({val: this.state.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);
}, 0);
}

render() {
return null;
}
}

//输出 0 0 2 3
事务
  • 事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行

diff算法

diff帮助计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面

详解diff
diff策略
  • Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,可以通过唯一id进行区分

React分别对tree diff、component diff以及element diff进行算法优化。

tree diff
  • 对树进行分层比较,两棵树只会对==同一层次==的节点进行比较
  • 当发现节点已经不存在时,则改节点及其子节点会被完全删除掉
component diff
  • 如果是同一类型的组件,按照原策略继续比较Virtual DOM树即可
  • 如果不是,则将改组件判断为dirty component,从而替换整个组件下的所有子节点
  • React允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析
element diff
  • INSERT_MARKUP: 新的组件类型不在旧集合里,需对新节点执行插入操作
  • MOVE_EXISTING: 旧集合中有新组件类型,且element是可更新类型,需要做移动操作,可以复用以前的DOM节点
  • REMOVE_NODE: 旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作

==节点在新集合中的索引值大于在旧集合中的索引时,需移动==
==删除操作是移动完成之后遍历旧集合,若有新集合中未出现的节点则删除==

React Patch方法

将tree diff计算出来的DOM差异队列更新到真实的DOM节点上,让浏览器能够渲染出更新的数据

  • 主要通过遍历差异队列实现

认识Flux架构模式

React独立架构

  • 含有抽象数据而没有业务逻辑的组件为容器型组件
  • 没有数据请求逻辑只有业务逻辑的组件为展示型组件

MV*与Flux

MVC/MVVM

主要涉及三种角色:Model、View和Controller

  • Model:负责保存应用数据,和后端交互同步应用数据,或校验数据
  • View:是Model的可视化表示,表示当前状态的视图。前端View负责构建和维护DOM元素。
  • Controller:负责连接View和Model,Model的任何改变会应用到View中,View的操作会通过Controller应用到Model中
Flux

核心思想是==数据和逻辑永远单向流动==。

1
2
3
4
5
6
graph LR
A[Action] --> B[Dispatcher]
B --> C[store]
C --> D[View]
D --> E[Action]
E --> B

在Flux应用中,数据从action到dispatcher再到store,最终到view的路线是单项不可逆的

Flux基本概念

一个Flux应用由3大部分组成:dispatcher、store和view,其中dispatcher负责分发事件;store负责保存数据,同时响应时间并更新数据;view负责订阅store中的数据。

dispatcher

dispatcher是Flux中最核心的方法,也是flux这个npm包中的核心方法。
只需关心.register(callback)和.dispatch(action)这两个API。

  • register方法用来注册一个监听器
  • dispatch方法用来分发一个action
action

action是一个普通的JavaScript对象,一般包含type、payload等字段,用于描述一个事件以及需要改变的相关数据。

store
  • 在Flux中,store负责保存数据,并定义修改数据的逻辑,同时调用dispatcher的register方法将自己注册为一个监听器。
  • 每次使用dispatcher的dispatch方法分发一个action时,store注册的监听器会被调用,同时得到这个action作为参数。
  • 在Flux中,store对外只暴露getter而不暴露setter,即只能读取store中的数据而不能进行任何修改。
controller-view

一般来说,controller-view是整个应用最顶层的view,主要进行store与React组件之间的绑定,定义数据更新以及传递的方式

view

如果界面操作需要修改数据,必须使用dispatcher分发一个action。

actionCreator

深入Redux应用架构

Redux简介

Redux三大原则
单一数据源
  • 在Redux的思想里,一个应用永远只有唯一的数据源。
  • 整个应用状态都保存在一个对象中
状态是只读的

定义一个reducer,其功能是根据当前触发的action对当前应用的状态(state)进行迭代。没有直接修改应用的状态,而是==返回了一份全新的状态==。

  • Reducer提供的createStore方法会根据reducer生成store
  • 用store.dispatch方法来修改状态

==###### ==状态修改均由纯函数完成
在Redux里,通过定义reducer来确定状态的修改,而每一个reducer都是纯函数,即其没有副作用,接受一定的输入,必定会得到一定的输出。

Redux核心API

Redux的核心是一个store,这个store由Redux提供的createStore(reducers[, initialState])方法生成。

  • 在Redux里,负责响应action并修改数据的角色就是reducer
  • reducer在处理action的同时,还需接受一个previousState参数
  • reducer的职责是根据previousState和action计算出新的newState

Redux中最核心的API是createStore,通过createStore方法创建的store是一个对象,包含四个方法:

  • getState():获取store当前状态
  • dispatch(action):分发一个action,并返回这个action,这是==唯一能改变store中数据的方式==
  • subscribe(listener):注册一个监听者,在store发生变化时调用
  • replaceReducer(nextReducer):更新当前store里的reducer,一般只在开发模式中调用该方法

subscribe()和replaceReducer()方法一般会在Redux与某个系统做桥接的时候使用

与React绑定

react-redux提供了一个组件和一个API帮助Redux和React进行绑定。

一个是React组件,一个是connect()

  • 接受一个store作为props,它是整个Redux应用的顶层组件
  • connect()提供了在整个React应用的任意组件中获取store中数据的功能

Redux middleware

middleware的由来
Redux同步数据流动
1
2
3
4
5
graph LR

button -. callback .-> dispatch
dispatch == action ==> reducer
reducer -. state .-> view
应用middleware后Redux处理事件的逻辑
1
2
3
4
5
6
7
8
graph LR

button -. callback .-> mid1
mid1 --> mid2
mid2 == action ==> ...
... --> dispatch
dispatch == action ==> reducer
reducer -. state .-> view

每一个middleware处理一个相对独立的业务需求,通过串联不同的middleware实现变化多样的功能

理解middleware机制

Redux提供了applyMiddleware方法来加载middleware。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import compose from './compose';

export default function applyMiddleware(...middlewares) {
return (next) => (reducer, initialState) {
let store = next(reducer, initialState);
let dispatch = sotre.dispatch;
let chain = [];

var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action),
};

chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

return {
...store,
dispatch,
};
}
}
函数式编程思想设计

middleware是一个层层包裹的匿名函数,即函数式编程中的currying,是一种使用匿名单参数函数来实现多参数的方法。

currying的middleware皆有的好处:

  • 易串联:currying函数具有延迟执行的特性,通过不断currying形成的middleware可以累积参数,再配合组合(compose)的方式,很容易形成pipeline来处理数据流
  • 共享store:在applyMiddleware执行的过程中,store还是旧的,但是因为闭包的存在,applyMiddleware完成后,所有的middleware内部拿到的store是最新且相同的。
给middleware分发store
1
2
//创建普通的store
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
组合串联middleware
1
2
3
4
dispatch = compose(...chain)(store.dispatch);

//假设n=3,dispatch为
dispatch = f1(f2(f3(store.dispatch)));
不能在middleware中调用dispatch

Redux异步流

Redux与路由

在Redux应用中,遇到了一些新的问题,其中最迫切的是,应用程序的所有状态都应该保存在一个单一的store中,而当前的路由状态很明显也属于应用状态的一部分。如果直接使用React Router,就意味着所有路由相关的信息脱离了Redux store的控制,这样就违背了Redux的设计思想。

React Router
路由的基本原理

理由的基本原理即是保证View和URL同步,而View可以看成是资源的一种表现。

1
2
3
4
5
6
7
graph LR


A((Action)) == 获取资源 ==> B((Action))

B == render ==> C((Action))
C == 用户与界面交互 ==> A
React Router特性
  • 在React中,组件就是一个方法,props作为方法的参数,当它们发生变化时触发方法执行,重绘View
  • 在React Router中,可以把Router组件看成一个方法,location作为参数,返回的结果同样是View
声明式路由
  • React是声明式编程,所有的交互逻辑都在render返回的JSX标签中得到体现
  • React Router允许使用JSX表现来书写声明式的路由
1
2
3
4
5
6
7
import {Router, Route, browserHistory} from 'react-router';

const routes = (
<Router history={browserHistory}>
<Route path="/" component={App} />
</Router>
);
嵌套路由及路径匹配
1
2
3
4
5
6
7
8
import {Router, Route, IndexRoute, browserHistory} from 'react-router';

const routes = (
<Router history={browserHistory}>
<IndexRoute component={MailList} />
<Route path="/mail/:mailId" component={Mail}></Route>
</Route>
);
  • 在声明路由时,==path==属性指明了当前路由匹配的路径形式
  • 若某条路由需要参数,只用==加上 :参数名== 即可
支持多种路由切换方式

路由切换可以使用hashChange或history.pushState。

  • hashChange拥有良好的浏览器兼容性,但是url中多了/#/部分
  • history.pushState能提供优雅的url,但需要额外的服务端配置解决任意路径刷新的问题

React Router提供了两种解决方案

==browserHistory即history.pushState的实现==

React Router Redux

职责主要是将应用的路由信息与Redux的store绑定在一起

采用Redux架构时,所有的应用状态都必须放在一个单一的store中管理,路由状态也不例外

将React Router与Redux store绑定

React Router Redux提供了简单直白的API syncHistoryWithStore来完成与Redux store的绑定工作。只需传入React Router中的history,以及Redux中的store,就可以获得一个增强后的history对象。

1
2
3
4
5
6
import { browserHistory } from  'react-router';
import { syncHistoryWithState } from 'react-router-redux';
import reducers from '<project-path>/reducers'

const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory, store);
用Redux的方式改变路由

无论是Flux还是Redux,想要改变数据,必须要分发一个action

  • 在此之前,需要对Redux的store进行一些增强,以便分发的action能被正确识别
1
2
3
4
5
6
7
8
import { browserHistory } from 'react-router';
import { routerMiddleware} from 'react-router-redux';

const middleware = routerMiddleware(browserHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
  • 用store.dispatch来分发一个路由变动的action
    1
    2
    3
    import {push} from 'react-router-redux';

    store.dispatch(push('/home'));

Redux与组件

容器型组件

容器型组件,意为组件是怎么工作的,具体一些就是数据是怎么更新的。不包含任何Virtual DOM的修改或组合,也不会包含组件的样式。

  • 如果映射到Flux上,容器型组件就是与store绑定的组件
  • 如果映射到Redux上,容器型组件就是使用connect的组件
展示型组件

展示型组件,意为组件是怎么渲染的。包含Virtual DOM的修改和组合,也可能包含组件的样式。

Redux中的组件
Layouts
  • 指的是页面布局组件,描述了页面的基本结构,目的是将主框架与页面主题内容分离
  • 常常是无状态函数,传入主题内容的children属性
1
2
3
4
5
6
7
8
9
//一般写法为
const layout = ({ children }) => (
<div className='container'>
<Header />
<div className='content'>
{ children }
</div>
</div>
);
views
  • 指的是子路由入口组件,描述子路由入口的基本结构,包含由此路由下所有的展示型组件
  • 为了保持子组件的纯净,在这一层组件中定义了数据和action的入口,从这里开始将它们分发到子组件中去
Components
  • 末级渲染组件,描述了从路由以下的子组件
  • 包含具体的业务逻辑和交互
  • 所有的数据和action都是由Views传下来的,即其是可以完全脱离数据层而存在的展示型组件

Redux高阶运用

高阶Reducer

在Redux架构中,reducer是一个纯函数,其职责是根据previousState和action计算出新的state

高阶reducer是指将reducer作为一个参数或者返回值的函数