初入React
JSX语法
- 定义标签时,只允许被一个标签包裹
- 标签一定要闭合
- 注释被{}包起来
React组件
- 无状态组件创建时始终保持了一个实例
- 有状态组件创建几次组件就会创建几次实例
React数据流
state
- setState是异步方法
props
- props本身是不可变的(readonly)
- defaultProps静态变量可以定义props默认配置(默认类型)
static defaultProps = {
classPrefix: 'tabs',
onChange: () => {},
};
- React中有一个内置的prop:children,代表组件的子组件集合
- JavaScript不是强类型的语言,React对此做了改进,propTypes用于规范props的类型与必要状态(类型检查)。
static propTypes = {
tab: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.node,
]).isRequired,
order: React.PropTypes.string.isRequired,
disble: React.PropTypes.bool,
};
React生命周期
分为挂载、渲染、卸载几个阶段
挂载或卸载
主要做组件状态初始化
挂载
卸载
数据更新
指父组件向下传递props或组件自身执行setState方法时发生的一系列更新动作
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更新
不能在componentWillUpdate里执行setState。
父组件更新props而更新
- 在shouldComponentUpdate之前先执行componentWillReceiveProps。
- 该方法可作为React在props传入后,渲染之前setState。
componentWillReceiveProps(nextProps) {
}
React与DOM
refs
refs是React组件中特殊的props,可以附加到任何一个组件上。组件被调用时会新建一个该组件的实例,refs则指向这个实例。
- refs放在原生DOM组件中可以得到其DOM节点
- refs放在React组件中则获得组件的实例,可以调用该组件的实例方法
漫谈React
事件系统
在React底层,主要对合成事件做了两件事:事件委派和自动绑定。
在React中使用原生事件
React生命周期方法中,componentDidMount会在组件已经完成安装并且在浏览器中存在真实的DOM后调用,此时可以完成原生事件的绑定。
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向子组件传递需要的信息
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元素
//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
props change
state change
自定义组件的声明周期主要通过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调用栈
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
核心思想是<mark style="box-sizing: border-box;">数据和逻辑永远单向流动</mark>。
在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,这是<mark style="box-sizing: border-box;">唯一能改变store中数据的方式</mark>
- subscribe(listener):注册一个监听者,在store发生变化时调用
- replaceReducer(nextReducer):更新当前store里的reducer,一般只在开发模式中调用该方法
subscribe()和replaceReducer()方法一般会在Redux与某个系统做桥接的时候使用
与React绑定
react-redux提供了一个组件和一个API帮助Redux和React进行绑定。
一个是React组件<Provider />,一个是connect()
- <Provider />接受一个store作为props,它是整个Redux应用的顶层组件
- connect()提供了在整个React应用的任意组件中获取store中数据的功能
Redux middleware
middleware的由来
Redux同步数据流动
应用middleware后Redux处理事件的逻辑
每一个middleware处理一个相对独立的业务需求,通过串联不同的middleware实现变化多样的功能
理解middleware机制
Redux提供了applyMiddleware方法来加载middleware。
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
//创建普通的store
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
组合串联middleware
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可以看成是资源的一种表现。
React Router特性
- 在React中,组件就是一个方法,props作为方法的参数,当它们发生变化时触发方法执行,重绘View
- 在React Router中,可以把Router组件看成一个方法,location作为参数,返回的结果同样是View
声明式路由
- React是声明式编程,所有的交互逻辑都在render返回的JSX标签中得到体现
- React Router允许使用JSX表现来书写声明式的路由
import {Router, Route, browserHistory} from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path="/" component={App} />
</Router>
);
嵌套路由及路径匹配
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提供了两种解决方案
<mark style="box-sizing: border-box;">browserHistory即history.pushState的实现</mark>
React Router Redux
职责主要是将应用的路由信息与Redux的store绑定在一起
采用Redux架构时,所有的应用状态都必须放在一个单一的store中管理,路由状态也不例外
将React Router与Redux store绑定
React Router Redux提供了简单直白的API syncHistoryWithStore来完成与Redux store的绑定工作。只需传入React Router中的history,以及Redux中的store,就可以获得一个增强后的history对象。
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能被正确识别
import { browserHistory } from 'react-router';
import { routerMiddleware} from 'react-router-redux';
const middleware = routerMiddleware(browserHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
- 用store.dispatch来分发一个路由变动的action
import {push} from 'react-router-redux';
store.dispatch(push('/home'));
Redux与组件
容器型组件
容器型组件,意为组件是怎么工作的,具体一些就是数据是怎么更新的。不包含任何Virtual DOM的修改或组合,也不会包含组件的样式。
- 如果映射到Flux上,容器型组件就是与store绑定的组件
- 如果映射到Redux上,容器型组件就是使用connect的组件
展示型组件
展示型组件,意为组件是怎么渲染的。包含Virtual DOM的修改和组合,也可能包含组件的样式。
Redux中的组件
Layouts
- 指的是页面布局组件,描述了页面的基本结构,目的是将主框架与页面主题内容分离
- 常常是无状态函数,传入主题内容的children属性
//一般写法为
const layout = ({ children }) => (
<div className='container'>
<Header />
<div className='content'>
{ children }
</div>
</div>
);
views
- 指的是子路由入口组件,描述子路由入口的基本结构,包含由此路由下所有的展示型组件
- 为了保持子组件的纯净,在这一层组件中定义了数据和action的入口,从这里开始将它们分发到子组件中去
Components
- 末级渲染组件,描述了从路由以下的子组件
- 包含具体的业务逻辑和交互
- 所有的数据和action都是由Views传下来的,即其是可以完全脱离数据层而存在的展示型组件