Git Product home page Git Product logo

old-blog's People

Contributors

zzzmj avatar

old-blog's Issues

Redux基础

1. Redux

Redux 的工作流程图

2. Redux基本原则

Redux三个基本原则

  • 单一数据源
  • 保持状态只读
  • 数据改变只能通过纯函数完成

2.1 单一数据源

单一数据源的意思是应用的数据状态只存储在一个唯一的Store中

2.2 保持状态只读

保持状态只读,意思就是不要去修改状态,要修改Store的状态,只能通过派发一个action对象完成

思考:驱动用户界面更改的是状态,状态只读,那怎么能引起用户界面的改变呢?

答:当然要改,只是我们不去修改状态值,而是创建一个新的状态对象给Redux,由Redux完成新状态的组装

2.3 数据改变只能通过纯函数

这里说的纯函数是Reducer

reducer(state, action)

第一个参数state是当前的状态
第二个参数action是接收到的action对象,而reducer函数要做的事情,就是根据state和action的值产生一个新的对象返回,注意reducer必须是纯函数,也就是说函数的返回结果必须完全由参数state和action决定,而且不产生任何副作用,也不能修改参数state和action对象。

3. Redux使用教程

例子在src目录下,与上节相同的例子加减组件,用Redux改写。

通过三张图了解Redux中的重要概念
先用因为react-redux帮我们省去了很多代码,不利于理解,所以先从redux开始

给出概念图
)

3.1 Action

Action是一个对象,用来代表所有会引起状态(state)变化的行为

action.js如下所示。

// ActionTypes.js
export const INCREMENT = 'increment'

export const DECREMENT = 'decrement'

// Action.js
import * as ActionTypes from './ActionTypes'

export const increment = (counterCaption) => {
    return {
        type: ActionTypes.INCREMENT,
        counterCaption: counterCaption
    }
}

export const decrement = (counterCaption) => {
    return {
        type: ActionTypes.DECREMENT,
        counterCaption: counterCaption
    }
}

3.2 Store

Store是Redux中数据的统一存储,维护着state的所有内容

import { createStore } from 'redux'
import reducer from './Reducer.js'

const initValue = {
    'First': 0,
    'Second': 10,
    'Third': 20
}

const store = createStore(reducer, initValue)

export default store

3.3 Reducer

Reducer决定着如何更新state

``(previousState, action) => newState```

该函数接收两个参数,一个旧的状态previousState和一个Action对象
然后返回一个新的状态newState,去重新渲染View

3.4 View

  1. 初始化状态
    在counter组件中,不在由自己决定状态,而是去store中获取状态
class Counter extends Component {
    constructor(props) {
        super(props)
        this.state = this.getOwnState()
        //...
    }

    getOwnState() {
        const { caption } = this.props
        return {
            value: store.getState()[caption]
        }
    }
}
  1. 监听状态是否发生改变
    肯定要监听组件的状态是否更新, 这里用到了发布订阅模式
import store from '../Store'

//...
componentDidMount() {
   // 通过store.subscribe()监听组件变化,只要组件的状态变化,就会调用onChange方法
   store.subscribe(this.onChange)
}

componentWillUnmount() {
   // 组件删除后,注销监听
   store.unsubscribe(this.onChange)
}

onChange() {
  const newState = this.getOwnState()
  this.setState(newState)
}
  1. 组件更新的时候,想改变状态唯一的办法是派发action
onClickIncrementButton() {
  const { caption } = this.props
  store.dispatch(Actions.increment(caption))
}

onClickDecrementButton() {
  const { caption } = this.props
  store.dispatch(Actions.decrement(caption))
}

大功告成。 看似复杂了很多,增加了很多约束,其实对开发项目有好处,利于提高软件质量

注意一个地方,我们在每个组件里都需要引入store,这样才能更新变化。
但这样做很麻烦,那有啥好的办法呢? 后面会讲到使用Context组件

ps: 其实Redux的核心功能实现并不难,自己实现了一下核心的功能,导入环境能正常使用

// 发布订阅模式
function createStore(reducer, initState) {
    let state = initState
    let listeners = []

    function subscribe(listener) {
        listeners.push(listener)
    }

    function dispatch(action) {
        // 按照 reducer 修改 state
        state = reducer(state, action)
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i]
            listener()
        }
    }

    function getState() {
        return state
    }

    return {
        subscribe,
        dispatch,
        getState,
    }
}

export default createStore

React-Redux基础

想学会react-redux,得先清楚几个概念

  1. 容器组件和傻瓜组件
  2. Context组件

1. 容器组件和傻瓜组件的概念

在Redux框架下,一个React组件基本上就是要承担两个任务

  1. 和Redux打交道,读取Store的状态,用于初始化组件状态。
    同时还要监听Store的状态改变
    当需要更新Store状态时,就要派发Action对象
    Store状态发生改变时,就需要更新组件状态

  2. 根据当前props和state,渲染出用户界面

如果每个React组件都要包办这两个任务,似乎事情稍微多了一些。
所以考虑拆分成两个组件,分别承担一个任务

承担第一个任务的叫做容器组件,负责与Redux打交道(也叫聪明组件)
承担第二个任务的叫做傻瓜组件,负责渲染界面(也叫展示组件)

1.1 傻瓜组件

傻瓜组件是一个纯函数,根据容器组件传进来的props产生结果,因此它也是一个无状态组件

状态全交给容器组件打理

改写Counter

class Counter extends Component {
    render() {
        const { caption, onClickDecrementButton, onClickIncrementButton, value } = this.props
        return (
            <div>
                <button style={style} onClick={onClickIncrementButton} >+</button>
                <button style={style} onClick={onClickDecrementButton} >-</button>
                <span>{caption} count: {value}</span>
            </div>
        )
    }
}

可以把傻瓜组件写成无状态组件,获取props就不能用this.props,而是通过函数的
参数props获得

const Counter = (props) => {
    const { caption, onClickDecrementButton, onClickIncrementButton, value } = props
    return (
        <div>
            <button style={style} onClick={onClickIncrementButton} >+</button>
            <button style={style} onClick={onClickDecrementButton} >-</button>
            <span>{caption} count: {value}</span>
        </div>
    )
}

1.2 容器组件

容器组件承担了所有的和Store关联的工作,它的Render函数只负责渲染傻瓜组件Counter,传递必要的prop

import store from '../Store.js';

class CounterContainer extends Component {
    constructor(props) {
        super(props)
        this.state = this.getOwnState()
        this.onClickIncrementButton = this.onClickIncrementButton.bind(this)
        this.onClickDecrementButton = this.onClickDecrementButton.bind(this)
        this.onChange = this.onChange.bind(this)
    }

    shouldComponentUpdate(nextProps, nextState) {
        // 避免不必要的渲染
        return (nextProps.caption !== this.props.caption) || 
            (nextState.value !== this.state.value);
    }

    componentDidMount() {
        // 通过store.subscribe()监听组件变化,只要组件的状态变化,就会调用onChange方法
        store.subscribe(this.onChange)
    }

    componentWillUnmount() {
        // 组件删除后,注销监听
       store.unsubscribe(this.onChange)
    }

    getOwnState() {
        return {
            value: store.getState()[this.props.caption]
        }
    }

    onChange() {
        const newState = this.getOwnState()
        this.setState(newState)
    }

    onClickIncrementButton() {
        store.dispatch(Actions.increment(this.props.caption))
    }

    onClickDecrementButton() {
        store.dispatch(Actions.decrement(this.props.caption))
    }

    render() {
        return (
            <Counter caption={this.props.caption}
                onClickDecrementButton={this.onClickDecrementButton} 
                onClickIncrementButton={this.onClickIncrementButton}
                value={this.state.value} />
        )
    }
}

2. Context组件

2.1 遇到的麻烦

前面我们注意到,我们必须在每个需要监听组件状态的组件中引入store

这样做不仅在组件很多的时候很麻烦,而且在团队开发的时候很不利于复用。

所以在一个应用中,最好只有一个地方直接导入Store,这个地方当然是在最顶层的React组件中。

2.2 设计方法

那使用什么来向下传递我们的Store呢?

用prop显然是不行的,因为prop需要一级一级往下传,假设最底层的组件需要用store,中间的组件都得帮忙传递,这样无疑非常麻烦

React提供了一个叫Context的功能,实现了跨层级的组件数据传递,不需要通过组件树逐层传递prop

首先上级组件宣称自己支持Context,并提供一个函数来返回Context对象

然后这个上级组件的所有子孙组件都可以通过this.context访问到这个共同的环境对象

2.3 使用Context

使用Context需要用到两种组件

  • 一个是Context生产者(Provider),通常是一个父节点
  • 一个Context的消费者(Consumer),通常是一个或者多个子节点。
    所以Context的使用基于生产者消费者模式。

2.4 Provider

现在我们先实现Context的生产者Provider,作为最顶级的父节点

对于Provider,要做两件事

  1. 实现getChildContext,返回代表context的对象
  2. 通过childContextTypes静态属性来声明提供给子组件的属性

下面是代码示例

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Provider extends Component {
    // 1. 实现getChildContext,返回代表context的对象
    getChildContext() {
        return {
            store: this.props.store
        }
    }

    render() {
        // 简单的将子组件渲染出来
        return this.props.children
    }
}

// 2. 通过childContextTypes静态属性来声明提供给子组件的属性
Provider.childContextTypes = {
    store: PropTypes.object
}

export default Provider

其中render函数中返回了this.props.children

每个React组件的props中都可以一个特殊属性children,代表的是子组

例如下面的代码,this.props.children代表的就是ControlPanel

<Provider>  
    <ControlPanel />
</Provider>

然后我们使用<Provider>来包裹顶层组件中

ReactDOM.render(
    <Provider store={store}>
        <ControlPanel />
    </Provider>,
    document.getElementById('root')
)

2.5 Consumer

写好了Provider, 我们看子孙组件如何使用context对象

class CounterContainer extends Component {
    constructor(props, context) {
        // 得带上参数context, 
        super(props, context)
    }

    // ...
}

// 子组件需要通过一个静态属性contextTypes声明后,才能访问父组件Context对象的属性
CounterContainer.contextTypes = {  
    store: PropTypes.object
}

3. react-redux

在上面两节中,引入了容器组件,傻瓜组件Context组件来优化Redux

有一个这样的库已经帮我们做好了这些工作,也就是react-redux

react-redux提供了两个最主要的api

  • connect: 连接容器组件和傻瓜组件
  • Provider: 提供包含store的context

3.1 connect

connect用法

// 完整写法
const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter)
export default CounterContainer;

connect的作用就是根据我们传入的UI组件和业务逻辑,自动生成容器组件

它具体做了什么工作呢?

  1. 把Store上的状态,转化为props传给傻瓜组件
  2. 将内层傻瓜组件的用户动作转化为派发给容器组件的动作

换句话说这两个工作一个就是傻瓜组件的输入,一个就是傻瓜组件的输出

3.2 mapStateToProps

mapStateToProps是connect函数的第一个参数

看名字就知道意思是建立state和props的映射,也就是把Store上的状态,转化为props传给傻瓜组件

例子

// 第二个是可选参数,这个参数代表着容器组件的props
function mapStateToProps(state, ownProps) {
    const { caption } = ownProps
    return {
        value: state[caption]
    }
}

3.3 mapDispatchToProps

mapDispatchToProps是connect函数的第二个参数

建立dispathch和props的映射,将内层傻瓜组件的用户动作转化为派发给容器组件的动作

// 第二个是可选参数,这个参数代表着容器组件的props
function mapDispatchToProps(dispatch, ownProps) {
    const {caption} = ownProps
    return {
        onIncrement: () => {
            dispatch(Actions.increment(caption))
        },
        onDecrement: () => {
            dispatch(Actions.decrement(caption))
        }
    }
}

3.4 connect总结

connect函数还是有一些复杂的,脑海中要时刻存在容器组件和傻瓜组件的两个概念

connect函数帮我们做的事情就是创建容器组件,然后连接容器组件和傻瓜组件

记住导出的是connect帮我们创造的容器组件,而并不是我们自己写的傻瓜组件

3.5 Provider

Provider的用法比较简单,也就是提供包含store的context。

之前也实现过一个完整的Provider,只是没那么严谨

用法没有差别,在APP的顶层包裹,将store传入即可

<Provider store={store}>  
    <ControlPanel />
</Provider>

3.6 流程图

最后通过react-redux的流程图,再复习一遍流程

Redux的中间件和Store Enhancer

1. 前言

Redux自身提供了很强大的数据流管理功能,当Redux不满足我们要求的时候,开发者可以通过扩展增强Redux的功能

一般有两种方式扩展Redux

  • 中间件
  • Store Enhancer

2. Redux中间件

Redux中间件原理图解

在派发一些特殊action的时候,中间件可以拦截,中间件先将这个action进行处理后,再交给下一个中间件,依次最后交付给原生的dispatch处理

中间件的特定

  • 中间件是独立的函数
  • 中间件可以组合使用
  • 中间件有一个统一的接口

来看一个最简单的中间件例子

function doNothingMiddleware({dispatch, getState}) {
    return function(next) {
        return function(action) {
            return next(action)
        }
    }
}

看起来很复杂吧,其实不然,现在分析一下这个中间件

接收一个对象为参数,对象参数有两个函数dispatchgetState
这代表Redux上两个同名函数

  • 函数doNothingMiddleware返回的函数接收一个next类型的参数
    • 这个next是一个回调函数,如果调用它,代表这个中间件已经完成自己的任何,则将控制器交给下一个中间件
  • 以action为参数的函数,对传入的action对象进行处理,在这个函数内,能访问上两层的函数,因为js的闭包机制。
    1. 调用dispatch,发出一个新的action对象(不建议使用)
    2. 调用getState,获得当前Store的状态
    3. 调用next告诉Redux当前中间件工作完毕,让Redux调用下一个中间件;
    4. 访问action对象action上的所有数据。

2.1 redux-thunk中间件

redux-thunk源码非常的简单,只有十行不到

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

代码很容易看懂,redux-thunk执行时,发现action是函数,于是执行这个action,否则就直接交给下一个中间件处理

2.2 自己写一个中间件

写一个Logger中间件

// logger.js
const logger = ({dispatch, getState}) => next => action => {
    console.group(action.type)
    console.log('dispathching:', action)
    const result = next(action)
    console.log('next state:', getState())
    console.groupEnd()
    return result
}

export default logger
// store.js
const store = createStore(rootReducer, applyMiddleware(logger))

2.3 数据流动

再看一遍图,理解中间件的在Redux数据流动中的位置

3. Store Enhancer

中间件可以用来增强Redux Store的dispatch方法,但是也仅限于dispatch方法

Store Enhancer可以对Redux Store进行更深度的定制,比中间件更强大(事实上中间件applyMiddleware也是作为一个store enhancer

3.1 原理

怎么用呢?
createStore(reducer, [preloadedstate], [enhancer])
createStore的第三个参数就是我们要传入的Enhancer

看一个什么都不做的Enhancer的例子

const doNothingEnhancer = (createStore) => (reducerpreloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    return store
}

最里层的函数可以拿到createStore函数,然后最里层的函数接收三个参数,通过这三个参数,创造出一个store对象,然后定制store对象,最后返回store对象就可以了

3.2 logEnhancer

自己写一个logEnhancer

const logger = createStore => (...args) => {
    const store = createStore(...args)
    const dispatch = (action) => {
        console.group(action.type)
        console.log('dispathching:', action)
        const result = store.dispatch(action)
        console.log('next state:', getState())
        console.groupEnd()
        return result
    }
    return {...store, dispatch}
}

export default logger

enhancer一般都是这样的套路

这个例子中,增强了dispatch方法的功能

3.3 使用compose组合多个Store Enhancer

使用compose可以组合多个Store Enhancer

import { createStore, applyMiddleware, compose } from 'redux'
import rootReducer from './reducers'
import logger from './middleare/logger';
import logEnhancer from './enhancers/logger'

const store = createStore(rootReducer, compose(applyMiddleware(logger), logEnhancer))

export default store

Redux中的selector和reselect

1. selector

回顾todoList项目中的FooterContainer(这是一个todoList的过滤器容器)

import { connect } from 'react-redux'
import * as actions from '../actions'
import Footer from '../components/Footer'

const mapStateToProps = state => {
    return {
        filter: state.filter
    }
}

const mapDispatchToProps = dispatch => {
    return {
        setVisibleTodos: (filter) => dispatch(actions.setFilter(filter))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Footer)

mapStateToProps函数中,我们发现FooterContainer作为视图层的一个容器组件,直接引用了Redux层的数据,这是一种强耦合的关系。

我们可以通过selector来对这两者进行解耦

改写后

// FooterContainer
import { connect } from 'react-redux'
import * as actions from '../actions'
import Footer from '../components/Footer'
import { getFilter } from '../seletors'

const mapStateToProps = state => {
    return {
        filter: getFilter(state)
    }
}

const mapDispatchToProps = dispatch => {
    return {
        setVisibleTodos: (filter) => dispatch(actions.setFilter(filter))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Footer)

// selectors
export const getFilter = (state) => state.filter

这样我们就通过selector,实现了两者的解耦

2. reselect

我们来看todoList中的getVisibleTodos方法

export const getVisibleTodos = (state) => {
    const {todos, filter} = state
    switch (filter) {
        case 'all':
            return todos
        case 'completed':
            return todos.filter(todo => todo.completed)
        case 'active':
            return todos.filter(todo => !todo.completed)
        default:
            return new Error('Unknown filter:' + filter)
    }
}

我们发现,我们的这个selector函数,需要经过一些计算。

每当state发生改变的时候,即使是一个无关的state,getVisibleTodos函数都会被重新执行一遍

我们发现只要我们的statetodosfilter字段没有发生改变,我们就不需要再重复进行计算

可以选择引用上次计算好的结果,就没必要再重复计算了,reselect就帮我们做了这件事情,帮我们缓存好结果,避免重新的计算

使用方法:

import { createSelector } from 'reselect'

export const getFilter = (state) => state.filter
const getTodos = (state) => state.todos

export const getVisibleTodos = createSelector(
    [ getFilter, getTodos ],
    (filter, todos) => {
        switch (filter) {
            case 'all':
                return todos
            case 'completed':
                return todos.filter(todo => todo.completed)
            case 'active':
                return todos.filter(todo => !todo.completed)
            default:
                return new Error('Unknown filter:' + filter)
        }
    }
)

React基础

1. 前言

这一章是介绍构建高质量组件的原则和方法

  • 划分组件边界的原则。
  • React组件的数据种类。
  • React组件的生命周期。

2. React组件的数据

React组件的数据只分为两种:propstate

无论prop或者state发生改变,都可能引发组件的重新渲染

使用这两者的原则是:
对外使用prop,对内使用state

2.1 React中的prop

在React中, prop是从外部传递给组件的数据。

每个React组件都是独立存在的模块,组件之外都是外部世界,外部世界就是通过prop跟组件进行对话的

class ControlPanel extends Component {
    render() {    
        return (      
            <div>        
                <Counter caption="First" initValue={0} />       
                <Counter caption="Second" initValue={10} />       <Counter caption="Third" initValue={20} />      
            </div>    
        );  
    }
}


class Counter extends Component {
    constructor(props) {
        // 如果要用this,必须调用super()
        super(props)
        this.state = {
            count: props.initValue || 0
        }
        this.onClickIncrementButton = this.onClickIncrementButton.bind(this)
        this.onClickDecrementButton = this.onClickDecrementButton.bind(this)    
    }

    onClickIncrementButton() {
        var count = this.state.count + 1
        this.setState({ 
            count
        })
    }

    onClickDecrementButton() {
        var count = this.state.count - 1
        this.setState({
            count
        })
    }

    render() {
        const { caption } = this.props
        return (
            <div>
                <button style={style} onClick={this.onClickIncrementButton} >+</button>
                <button style={style} onClick={this.onClickDecrementButton} >-</button>
                <span>{ caption } count: {this.state.count}</span>
            </div>
        )
    }
}

ControlPanel组件给Counter组件传递了两个prop属性captioninitValue

propTypes

因为prop是组件的对外接口,以防传进来的东西混杂,应该制定良好的接口规范

通过propTypes即可实现约束我们的prop

Counter.propTypes = {
    caption: PropTypes.string.isRequired, // caption必须是字符串并且必须要填
    initValue: PropTypes.number // initValue必须是数字,可不填
}

2.2 React中的state

state代表组件的内部状态,是对内的。

React不能修改传入的prop,记录自身数据变化就需要用到state

上面的例子也说明了

要更新state必须使用setState方法,不能直接修改

2.3 React组件的生命周期

React的生命周期分为三个阶段

  1. 装载过程(Mount),组件第一次在DOM树中渲染的过程
  2. 更新过程(Update),组件重新被渲染的过程
  3. 卸载过程(Unmount),组件从DOM中删除

如图所示

讲解

2.3.1 constructor()

constructor()也就是ES6中的构造函数。

注意:并不是每个组件都需要定义构造函数,无状态组件就不需要定义构造函数

一个React组件需要定义构造函数的原因一般有几点:

  1. 初始化state
  2. 绑定成员函数的this环境

2.3.2 static getDerivedStateFromProps()

尽量不要去动它

2.3.3 render()

render函数无疑是React组件中最重要的函数,一个React组件可以忽略

其他所有函数都不实现,但是一定要实现render函数,因为所有React组件的
父类React.Component类对除render之外的生命周期函数都有默认实现.

render()函数并不会做实际上的渲染动作,它只是返回JSX描述的结构,最终由React完成渲染。

需要注意,render函数应该是一个纯函数,完全根据this.state和this.props
来决定返回的结果,而且不要产生任何副作用

2.3.4 componentDidMount()

componentDidMount在render函数之后被调用

组件渲染完成后调用。

2.3.5 shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState)

render和shouldComponentUpdate函数,也是React生命周期函数中唯二两
个要求有返回结果的函数

render函数的返回结果将用于构造DOM对象,而
shouldComponent-Update函数返回一个布尔值,告诉React库这个组件在这次
更新过程中是否要继续。

值得一提的是,通过this.setState函数引发更新过程,并不是立刻更新组件的state值,在执行到到函数shouldComponentUpdate的时候,this.state依然是this.setState函数执行之前的值,所以我们要做的实际上就是在nextProps、nextState、this.props和this.state中互相比对。

React-Router 4 基础

1. 实现路由原理

react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。

基本实现方式有两种

  • 通过Hash实现前端路由
  • 通过history实现前端路由

1.1 通过Hash实现前端路由

改变url的hash值是不会刷新页面的

使用方法:

// 使用前页面http://localhost:3000
window.location.hash='/discover'

// 使用后页面http://localhost:3000/#/3000

url中多了一个#号的hash值,然后我们可以通过监听url中的hash值变化,来显示不同的页面,从而实现前端路由

优点是兼容性比较好,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

缺点是不太美观

1.2 通过history实现前端路由

history是HTML5提供的一个对象

提供了两个方法history.pushStatehistory.replaceState

这两者的区别,前者是push进去当前地址状态,后者是修改当前地址状态,也不会引起浏览器的刷新

它像一个栈,可以让我们将浏览器地址历史记录push到栈中

// 初始值:http://localhost:3000

// http://localhost:3000/test
window.history.pushState(null, null, "test"); 

// http://localhost:3000/test/book
window.history.pushState(null, null, "test/book");

// http://localhost:3000/test/book#/hello
window.history.pushState(null, null, "#/hello");

// http://localhost:3000/test/book?name=
window.history.pushState(null, null, "?name=");

// http://localhost:3000/test/book#/hello
window.history.back()

// http://localhost:3000/test/book?name=
window.history.forward()

然后我们push进去的状态,可以通过前进,后退来控制

优点是比较美观,不带#

缺点是需要后端配合,不然刷新就会发送http请求,导致404错误

2. React-Router

react-router中有三种组件

  • 路由器组件 (Routers)
  • 路由匹配组件 (Route Matching)
  • 导航组件 (Navigation)

2.1 路由器组件Routers

对于每个react-router项目来说, 核心就是路由器组件.
react-router-dom提供了两个路由器

  1. : 使用了HTML5的history API来记录你的路由历史。
  2. : 使用URL(window.location.hash)的hash部分来记录。

两者的区别

// <BrowserRouter>
http://example.com/about

// <HashRouter>
http://example.com/#/about

如果你想兼容老式浏览器,你应该使用。
一般来说使用第一个.

2.2 路由匹配组件Route Matching

有两种

2.2.1 Route

Route是react-router中最重要的组件, 负责路由的匹配, 一般和Link结合使用

Route的用法.

// 如果路径是/about, 则显示组件About
<Route path="/about" component={About}></Route>

有一点需要注意, 如果有两个路由匹配组件

<Route path="/about" component={About}></Route>
<Route path="/about/resume" component={Resume}></Route>

如路径是/about, 则两个组件均会被渲染.

如果要完全匹配, 需要加上exact关键字

<Route exact path="/about" component={About}></Route>

Link的用法

Link类似于HTML中的a标签, 使用它可以跳转到指定的URL

// 点击即可跳到about界面
<Link to="/about">about</Link>
<Route exact path="/about" component={About}></Route>

2.2.2 Switch

Switch有点类似我们js中的switch语法, 它与Route组合使用

<Switch>
   <Route exact path="/"  component={Main}></Route>
   <Route path="/about" component={About}></Route>
   <Route path="/topics" component={Topics}></Route>
</Switch>

Switch只渲染一个与之匹配的地址, 找到一个与之匹配的地址后, 就会结束.

2.2.3 match

当路由路径和当前路径成功匹配,会生成一个对象,我们叫它match.
match对象包含了很多url和path的信息.

  • match.url 返回URL匹配的字符串, 对于创建嵌套的Link很有用
  • match.path 返回路由路径字符串, 对于创建嵌套的Route很有用
  • match.params 返回一个对象, 包含URL解析的键值对

第三个属性需要例子来理解=.=
举个栗子

<Route path="/Home/:name" component={HomePage} />

const HomePage = ({ match }) => (
  <div>
    <h1> parameter => {match.params.name}
  </div>
);

在这个例子中match.params.name 就是 从Route中传来的:name属性

2.3 Route渲染组件的方式

Route组件特别重要,因此来详细了解一下Route组件

Route组件有三个可以来定义渲染内容的props

  • component. 一个React组件。当带有component参数的route匹配成功后,route会返回一个新的元素,其为component参数所对应的React组件(使用React.createElement创建)。
  • render. 一个无状态组件。当匹配成功后调用该函数。该过程与传入component参数类似
  • children. 一个返回React element的函数。与上述两个参数不同,无论route是否匹配当前location,其都会被渲染。
<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
  <Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

component参数与render参数的组件是用很大的区别的。使用component参数的组件会使用React.createElement来创建元素,使用render参数的组件则会调用render函数。如果我们定义一个内联函数并将其传给component参数,这将会比使用render参数慢很多。

参考:
React Router 4 官方文档
React Router 4 簡易入門

React组件的性能优化

1. 单个组件的性能优化

在之前做的Todo应用中其实是用性能问题的

const todoList = props => {
    const { todos, onToggleTodo, onRemoveTodo } = props
    return (
        <div>
            <ul>
                {todos.map(todo => (
                    <TodoItem
                        key={todo.id}
                        text={todo.text}
                        completed={todo.completed}
                        onToggleTodo={() => onToggleTodo(todo.id)}
                        onRemoveTodo={() => onRemoveTodo(todo.id)}
                    />
                ))}
            </ul>
        </div>
    )
}

// ...
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(todoList)


// TodoItem
const TodoItem = (props) => {
    const { text, completed, onToggleTodo, onRemoveTodo } = props
    const checked = completed ? 'checked' : ''
    return (
        <li>
            <input type="checkbox" onClick={onToggleTodo} checked={checked} readOnly />
            <span>{text}</span>
            <button onClick={onRemoveTodo}>删除</button>
        </li>
    )
}

我们发现我们的TodoItem是一个无状态组件

假设我们列表有一千条数据,只有一条数据发生了变化,但是整个TodoList都被重新渲染了一遍

因为TodoItem是一个无状态函
数,所以使用的是React默认的shouldComponentUpdate函数实现,也就是永
远返回true的实现。

所以整个TodoList都被重新渲染了一遍

所以我们可以改写TodoItem

class TodoItem extends Component {
    shouldComponentUpdate(nextProps, nextState) {
        // completed或者text发生变化时,才重新渲染
        return (nextProps.completed !== this.props.completed) || 
        (nextProps.text !== this.props.text)
    }
    render() {
        const { text, completed, onToggleTodo, onRemoveTodo } = this.props
        const checked = completed ? 'checked' : ''
        return (
            <li>
                <input type="checkbox" onClick={onToggleTodo} checked={checked} readOnly />
                <span>{text}</span>
                <button onClick={onRemoveTodo}>删除</button>
            </li>
        )
    }
}

2. 多个组件的性能优化

这里要介绍到React的原理了。

首先在装载阶段,没有太多优化的过程,所有的React的组件都要经历一遍装载

至于卸载阶段,只有一个生命周期函数componentWillUnmount,这个函
数做的事情只是清理componentDidMount添加的事件处理监听等收尾工作,
做的事情比装载过程要少很多,所以也没有什么可优化的空间

关键在更新阶段

2.1 React的调和(Reconciliation)过程

在装载阶段的时候,React通过render()函数在内存中产生了一个树形结构,树上每一个节点就代表React组件或者原生DOM元素,这个树形结构就是所谓的虚拟DOM(Virtual DOM)

用户操作触发了页面更新,React重新生成虚拟DOM,然后比较两个虚拟DOM的不同,来修改真实的DOM树

这个找不同的过程就叫做调和,React采用了巧妙的diffing算法进行实现

1. 节点类型不同的情况

如果根节点类型不相同,就不用费心考虑了,就直接销毁旧树,重新建树。

原有的树会经历componentWillUnmount的生命周期,取而代之的组件componentWillMount、render和componentDidMount方法依次被调用

举个例子

// 原有的结构
<div>
    <Todos />
</div>

// 更新
<span>
    <Todos />
</span>

我们只是将根节点div改成了span,但是这个算法会废掉div节点以及所有子节点,一切推倒重来

很明显,这是一个巨大的浪费,顶层的元素实际上不做什么实质的功能,但是仅仅因为类
型不同就把本可以重用的Todos组件卸载掉,然后重新再把这个组件装载一
遍。

所以作为开发者一定要尽量避免这种情况

2. 节点类型相同的情况

如果两个树形结构的根节点类型相同,React就认为原来的根节点只需
要更新过程,不会将其卸载,也不会引发根节点的重新装载

  • 对于DOM元素类型,React会保留节点对应的DOM元素,只对树形结构
    根节点上的属性和内容做一下比对,然后只更新修改的部分。
  • 对于React组件类型,React能做的只是根据新节点的props去更
    新原来根节点的组件实例,引发这个组件实例的更新过程(调用生命周期函数)

这里考虑一个情况

// 原状态
<ul>
    <TodoItem text="First" completed={false}>  
    <TodoItem text="Second" completed={false}>
</ul>

// 更新
<ul>
    <TodoItem text="Zero" completed={false}>
    <TodoItem text="First" completed={false}>  
    <TodoItem text="Second" completed={false}>
</ul>

按道理说只需要渲染 text=Zero 的组件

但React没有那么聪明,它不会仔细判断两个组件是否相同,只是单纯的按位置比较

于是三个组件都被重新渲染了一遍

React就出了一个key的功能,在渲染列表的时候,指定组件的唯一key值(必须稳定不变)

React就会根据key去比较组件,避免重复渲染

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.