来自:今日头条技术团队(微信号:toutiaotechblog)
众所周知,react的单向数据流模式导致状态只能一级一级的由父组件传递到子组件,在大中型应用中较为繁琐不好管理,通常我们需要使用Redux来帮助我们进行管理,然而随着React 16.3的发布,新context api成为了新的选择。
一、Redux的简介以及缺陷
Redux来源于Flux并借鉴了Elm的思想,主要原理如下图所示:
可以看到,Redux的数据流其实非常简单,外部事件通过actionCreator函数调用dipsatch发布action到reducers中,然后各自的reducer根据action的类型(action.type) 来按需更新整个应用的state。
1. state是单例模式且不可变的,单例模式避免了不同store之间的数据交换的复杂性,而不可变数据提供了十分快捷的撤销重做、“时光旅行”等功能。
2. state只能通过reducer来更新,不可以直接修改。
3. reducer必须是纯函数,形如(state,action) => newState
redux本身是个非常纯粹的状态管理库,需要通过react-redux这个库的帮助来管理react的状态。react-redux主要包含两个部分。
1. Provider组件:可以将store注入到子组件的cotext中,所以一般放在应用的最顶层。
2. connect函数: 返回一个高阶函数,把context中由Provider注入的store取出来然后通过props传递到子组件中,这样子组件就能顺利获取到store了。
虽然redux在React项目中得到了普遍的认可与使用率,然而在现实项目中redux还是存在着很多缺点:
1.样板代码过多:增加一个action往往需要同时定义相应的actionType然后再写N个相关的reducer。例如当添加一个异步加载事件时,需要同时定义加载中、加载失败以及加载完成三个actionType,需要一个相对应的reducer通过switch分支来处理对应的actionType,冗余代码过多。
2.更新效率问题:由于使用不可变数据模式,每次更新state都需要拷贝一份完整的state造成了内存的浪费以及性能的损耗。
3.数据传递效率问题:由于react-redux采用的旧版context API,context的传递存在着效率问题。
其中,第一个问题目前已经存在着非常多的解决方案,诸如dva、rematch以及mirror等等,笔者也造过一个类似的轮子restated这里不做过多阐述。
第二个问题首先redux以及react-redux中已经做了非常详尽的优化了,其次擅用shouldComponentUpdate方法也可以避免很多不必要的更新,最后,也可以使用一些不可变数据结构如immutable、Immr等来从根本上解决拷贝开销问题。
第三个问题属于React自身API的局限,从第三方库的角度上来说,能做的很有限。
二、Context API
context API主要用来解决跨组件传参泛滥的问题(prop drilling),旧的context API的语法形式如下:
// 传递者,生成数据并放入context中class DeliverComponent extends Component {
getChildContext() { return { color: "purple" };
}
render() { return <MidComponent />
}
}
DeliverComponent.childContextTypes = {
color: PropTypes.string
};// 中间与context无关的组件
const MidComponent = (props) => <ReceiverComponent />;// 接收者,需要用到context中的数据
const ReceiverComponent = (props, context) =>
<div style={{ color: context.color }}> Hello, this is receiver. </div>;
ReceiverComponent.contextTypes = {
color: PropTypes.string
};
Reactdom.render(
<DeliverComponent>
<MidComponent>
<ReceiverComponent />
</MidComponent>
</DeliverComponent>, document.getElementById('root'));
可以看到,使用context api可以把DeliverComponent中的参数color直接跨越MidComponent传递到ReceiverComponent中,不需要冗余的使用props参数传递,特别是ReceiverComponent层级特别深的时候,使用context api能够很大程度上节省重复代码避免bug。
旧Context API的缺陷
1.代码冗余:提供context的组件要定义`childContextTypes`与`getChildContext`才能把context传下去。同时接收context的也要先定义contextTypes才能正确拿到数据。
2.传递效率:虽然功能上context可以跨层级传递,但是本质上context也是同props一样一层一层的往下传递的,当层级过深的时候还是会出现效率问题。
3.shouldComponentUpdate:由于context的传递也是一层一层传递,因此它也会受到shouldComponent的阻断。换句话说,当传递组件的context变化时,如果其下面某一个中间组件的shouldComponentUpdate方法返回false,那么之后的接收组件将不会受到任何context变化。
为了解决旧版本的shouldComponentUpdate问题,保证所有的组件都能收到store的变化,react-redux只能传递一个getState方法给各个组件用于获取最新的state(直接传递state可能会被阻断,后面的组件将接收不到state的变化),然后每个connect组件都需要直接或间接监听state的变化,当state发生改变时,通过内部notifyNestedSubs方法从上往下依次触发各个子组件通过getState方法获取最新的state更新视图。这种方式效率较低而且比较hack。
三、新Context API
React自16.3开始提供了一个新的context api,彻底解决了旧Context API存在的种种问题。
下面是新context api(右)与使用旧context api的react-redux(左)数据流的比较:
可以看到,新的context api可以直接将context数据传递到传递到子组件中而不需要像旧context api那样级联传递。因此也可以突破shouldComponentUpdate的限制。新版的context api的定义如下:
type Context<T> = {
Provider: Provider<T>,
Consumer: Consumer<T>,
};
interface React {
createContext<T>(defaultValue: T): Context<T>;
}
type Provider<T> = React.Component<{
value: T, children?: React.Node,
}>;
type Consumer<T> = React.Component<{
children: (value: T) => React.Node,
}>;
下面是一个比较简单的应用示例:
import React, { Component, createContext } from 'react';const DEFAULT_STATE = {color: 'red'}; const { Provider, Consumer } = createContext(DEFAULT_STATE);// 传递者,生成数据并放入context中class DeliverComponent extends Component {
state = { color: "purple" };
render() { return ( <Provider value={this.state}>
<MidComponent />
</Provider>
)
}
}// 中间与context无关的组件const MidComponent = (props) => <ReceiverComponent />;
// 接收者,需要用到context中的数据
const ReceiverComponent = (props) => (
<Consumer>
{context => (
<div style={{ color: context.color }}> Hello, this is receiver. </div>
)}
</Consumer>
);
ReactDOM.render(
<DeliverComponent>
<MidComponent>
<ReceiverComponent />
</MidComponent>
</DeliverComponent>, document.getElementById('root'))
可以看到新的context api主要包含一个Provider和Consumer对,在Provider输入的数据可以在Consumer中获得。 新context api的要点如下:
1. Provider和 Consumer必须来自同一次 React.createContext调用。也就是说 NameContext.Provider和 AgeContext.Consumer是无法搭配使用的。
2. React.createContext方法接收一个默认值作为参数。当 Consumer外层没有对应的 Provider时就会使用该默认值。
3. Provider 组件的 valueprop 值发生变更时,其内部组件树中对应的 Consumer组件会接收到新值并重新执行 children函数。此过程不受 shouldComponentUpdete 方法的影响。
4. Provider组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is和 === 的行为不完全相同。
5. Consumer组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。
四、新Context API的应用
新的Context API大大简化了react状态传递的问题,也出现了一些基于它的状态管理库,诸如:unstated、react-waterfall等等。下面我们主要尝试使用新context api来造一个react-redux的轮子。
由于新的context api传递过程中不会被shouldComponentUpdate阻断,所以我们只需要在Provider里面监听store变化即可:
import React, { PureComponent, Children } from 'react'; import { IContext, IStore } from '../helpers/types'; import { Provider } from '../context';
interface IProviderProps {
store: IStore;
}export default class EnhancedProvider extends PureComponent<IProviderProps, IContext> {
constructor(props: IProviderProps) { super(props); const { store } = props; if (store == null) { throw new Error(`Store should not omit in <Provider />`);
} this.state = { // 得到当前的state
state: store.getState(),
dispatch: store.dispatch,
}
store.subscribe(() => { // 单纯的store.getState函数是不变的,需要得到其结果state才能触发组件更新。
this.setState({ state: store.getState() });
})
}
render() { return <Provider value={this.state}>{Children.only(this.props.children)}</Provider>;
}
};
相比较于react-redux,connect中的高阶组件逻辑就简单的多,不需要监听store变化,直接获得Provider传入的state然后再传递给子组件即可:
import React, { Component, PureComponent } from 'react'; import { IState, Dispatch, IContext } from './helpers/types'; import { isFunction } from './helpers/common'; import { Consumer } from './context';export default (mapStateToProps: (state: IState) => any, mapDispatchToProps: (dispatch: Dispatch) => any) =>
(WrappedComponent: React.ComponentClass) => class ConnectedComponent extends Component<any>{
render() { return <Consumer>
{(context: IContext) => {
const { dispatch, state } = context;
const filterProps = {};
if (isFunction(mapStateToProps)) {
Object.assign(filterProps, mapStateToProps(state));
}
if (isFunction(mapDispatchToProps)) {
Object.assign(filterProps, mapDispatchToProps(dispatch));
}
return <WrappedComponent
{...this.props}
{...filterProps}
/>
}} </Consumer>
}
};
好了,至此整个React-redux的接口和功能都已经基本cover了,下面继续介绍一些比较重要的性能优化。
性能优化最大的一部分就是要减少无意义的重复渲染,当WrappedComponent的参数值没有变化时我们应该阻止其重新渲染。可以通过手写shouldComponentUpdate方法实现,也可以直接通过PureComponent组件来达到我们的目标:
// ...render() {
return <Consumer>
{(context: IContext) => { const { dispatch, state } = context; const filterProps = {}; if (isFunction(mapStateToProps)) {
Object.assign(filterProps, mapStateToProps(state));
} if (isFunction(mapDispatchToProps)) { // mapDispatchToProps 返回值始终不变,可以memory
this.dpMemory = this.dpMemory || mapDispatchToProps(dispatch);
Object.assign(filterProps, this.dpMemory);
} return <Prevent
combinedProps={{ ...this.props, ...filterProps }}
WrappedComponent={WrappedComponent} />
}}
</Consumer>
}//...// PureComponent内部自动实现了前后参数的浅比较class Prevent extends PureComponent<any> {
render() { const { combinedProps, WrappedComponent } = this.props; return <WrappedComponent {...combinedProps} />;
}
}
这里需要注意的是,本示例的mapDispatchToProps未支持ownProps参数,因此可以把它的返回值看成是不变的,否则每次调用它返回的action函数都是新创建的,从而导致Prevent接收到的参数始终是不同的,达不到预期效果。更为复杂的情况请参考react-redux源码中selector相关的部分。
性能优化另一个要点就是减少组件的层级嵌套,新context api在获取context值的时候需要嵌套一层Consumer组件,这也是其比旧context api劣势的地方。除此之外,我们应该尽量减少层级的嵌套。因此在前一个性能优化中我们不应该再次嵌套一个PureComponent,取而代之的是,我们可以直接在Cunsumer中实现一个memory机制,实现代码如下:
// ...private shallowEqual(prev: any, next: any) {
const nextKeys = Object.keys(next); const prevKeys = Object.keys(prev); if (nextKeys.length !== prevKeys.length) return false; for (const key of nextKeys) { if (next[key] !== prev[key]) { return false;
}
} return true;
}
render() {
return <Consumer>
{(context: IContext) => { const { dispatch, state } = context; const filterProps = {}; if (isFunction(mapStateToProps)) {
Object.assign(filterProps, mapStateToProps(state));
} if (isFunction(mapDispatchToProps)) { // mapDispatchToProps 返回值始终不变
this.dpMemory = this.dpMemory || mapDispatchToProps(dispatch);
Object.assign(filterProps, this.dpMemory);
} const combinedProps = { ...this.props, ...filterProps }; if (this.prevProps && this.shallowEqual(this.prevProps, combinedProps)) { // 如果props一致,那么直接返回缓存之前的结果
return this.prevComponent;
} else { this.prevProps = combinedProps; // 对当前的子节点进行缓存
this.prevComponent = <WrappedComponent {...combinedProps} />; return this.prevComponent;
}
}}
</Consumer>
}
下面是前后chrome开发人员工具中组件层级的对比,可以看到嵌套层级成功减少了一层,两层嵌套是新context api的局限,如果要保持react-redux的接口模式则无法再精简了。
如今的 Web 前端已被 React、Vue 和 Angular 三分天下,尽管现在的 jQuery 已不再那么流行,但 jQuery 的设计思想还是非常值得致敬和学习的,特别是 jQuery 的插件化。
受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意。这恰恰显示React的威力,满足不同规模大小的工程需求。
一般在传统模式下,我们构建前端项目很简单。就是下载各种js文件,如JQuery、Echart等,直接放置在html静态文件。Webpack则是JavaScript中比较知名的打包工具。这两个构建工具构成了React应用快速搭建的基础。
Gatsby能快速的使用 React 生态系统来生成静态网站,可以结合React Component、Markdown 和服务端渲染来完成静态网站生成让他更强大。
React推出后,出于不同的原因先后出现三种定义react组件的方式,殊途同归;具体的三种方式:函数式定义的无状态组件、es5原生方式React.createClass定义的组件、es6形式的extends React.Component定义的组件
React主要思想是通过构建可复用组件来构建用户界面,每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段改变和执行。所谓组件就是有限状态机,,表示有限个状态以及在这些状态之间的转移和动作行为的模型。
React 相关的优化:使用 babel-react-optimize 对 React 代码进行优化,检查没有使用的库,去除 import 引用,按需打包所用的类库,比如 lodash 、echarts 等.Webpack 构建打包存在的问题两个方面:构建速度慢,打包后的文件体积过大
这篇文章主要介绍React Router定义路由之后如何传值,有关React和React Router 。react router中页面传值的三种方法:props.params、query、state
react 高阶组件简单的理解是:一个包装了另一个基础组件的组件。高阶组件的两种形式:属性代理(Props Proxy)、反向继承 (Inheritance Inversion)
React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance )。这样就可以确保在任何时间总是拿到正确的实例
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!