React 拾遗

443 阅读13分钟

源码地址

gitee.com/Actoress/re…

备忘

build 之后调试

在项目目录下

npm install -g serve
serve -s build

关于PWA

PWA: progressive web application
通过写网页的的方式来写手机APP应用

React 中的 immutable

React 中不允许直接对 state 直接改变
操作数据时,可以拷贝一个副本出来操作

React中的虚拟DOM

React简单逻辑:

1. state 数据
2. JSX 模板
3. 数据 + 模板 结合,生成真实的DOM,来显示
4. state 发生改变
5. 数据 + 模板 结合,生成真实的DOM,替换原有的DOM
......

缺陷:每一次都重新生成了完整的DOM片段,非常消耗性能

第一次优化之后的逻辑:

1. state 数据
2. JSX 模板
3. 数据 + 模板 结合,生成真实的 DOM ,来显示
4. state 发生改变
5. 数据 + 模板 结合,生成真实的 DOM ,不替换DOM
6. 拿新的 DOM 和旧的 DOM 做比对,找差异
7. 找出发生变化的 DOM ,替换老的 DOM 中改变的部分
......

缺陷:比对新旧的 DOM 又消耗了新的性能,性能的提升不够显著

第二次优化之后的逻辑:

1. state 数据
2. JSX 模板
3. 数据 + 模板 结合,生成真实的 DOM ,来显示
   真实DOM: <div id="abc"><span>hello world</span></div>
4. 数据 + 模板 结合生成虚拟DOM(虚拟 DOM 就是一个JS对象,用来描述真实的DOM)
   虚拟DOM: ['div', {id: 'abc'}, ['span', {}, 'hello world']]
5. 数据发生变化时,生成新的虚拟DOM
6. 比较新旧虚拟DOM
7. 根据虚拟DOM的不同,改变真实DOM
......

优点1:比较的性能消耗大幅减少
优点2:用JS生成JS对象的资源是极少的,但是生成真实的DOM,消耗的资源很大
优点3:比较虚拟DOM,就是比较JS对象,是极其节省资源的

React中的优化

1. state 数据
2. JSX 模板
3. 数据 + 模板 结合生成虚拟DOM(虚拟 DOM 就是一个JS对象,用来描述真实的DOM)
   虚拟DOM: ['div', {id: 'abc'}, ['span', {}, 'hello world']]
4. 根据虚拟DOM生成真实DOM
   真实DOM: <div id="abc"><span>hello world</span></div>
5. 数据发生变化时,生成新的虚拟DOM
6. 比较新旧虚拟DOM
7. 根据虚拟DOM的不同,改变真实DOM
......

优点:先生成虚拟DOM,大幅提升性能

虚拟DOM如何变成真实DOM

流程:JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
React 底层会通过 React.createElement() 这个方法,将 JSX 语法转成JS对象,React.createElement()可以接收三个参数,第一个为标签名称,第二参数为属性,第三个参数为内容,例如:

    React.createElement('div', {'id': 'div1'}, 'hello world')`
    React.createElement('div', {'id': 'div1'}, React.createElement('span', {}, 'hello'))`

虚拟DOM优点:

  1. 性能提升,DOM的比对变换成JS对象的比对;
  2. 跨端应用得以实现;

为什么React的setState()要被设计成一个异步的方法?

假如,这一个页面中,现在需要设置3次state,且三次设置间隔时间极短,如果每次都去都去修改state,极其浪费性能,React的底层会将3次setState合并成一个setState,只需做一次虚拟DOM的比对,然后去更新一次DOM,这样就可以省去另外两次setState产生的性能浪费,所以设计成异步方法的初衷,是为了提升性能。

虚拟DOM的Diff算法

Diff算法:difference算法,主要用于找差异,用于找新旧虚拟DOM间的差异

Diff算法有一个很重要的概念是同级比较,例如:在生成新的虚拟DOM后,Diff算法开始比较,先从第一级开始比较,如果第一级一样,就会比较第二级,以此类推。如果第一级不同,将不会继续比较将会把这一级下所有的旧虚拟DOM替换,然后替换真实的DOM。

虽然这么做可能会造成DOM重新渲染的浪费,但是会大幅提升比对的速度和性能。

Key值会大幅提升比对的性能。同时,根据下图比对的方法,不建议循环数组时使用 index 做 key 值,因为当数组长度发生变化时,index值可能会改变,那key值就发生改变,就会影响性能,所以尽量选用稳定的值做 key 值

React基础

渲染逻辑

当组件的state或者props发生改变的时候,render函数就会重新执行
当父组件的render函数被执行时,它的子组件的render都将被重新执行

特性

  • 声明试开发:减少DOM操作的代码量;
  • 可以与其他框架共存;
  • 组件化,UI组件(负责数据展示)和容器组件(负责业务逻辑);
  • 单向数据流(one-way stream of data),子组件只能使用值,不能去直接改变值;
  • 视图层框架;
  • 函数式编程:业务复杂的时候可以对函数进行拆分,各司其职,方便前端自动化测试,给值就能测试;

创建项目

  • 安装脚手架 npm install -g create-react-app
  • create-react-app (project name) 项目名不能用大写
  • npm start

setState

# 如果直接 return 的函数,可以这样写
this.setState(() => ({
    inputValue: inputValue
}), () => {
    console.log("异步的回调函数,非必填");
});

父子组件传值

  • 父组件给子组件传值通过属性
  • 子组件修改父组件的数据,通过调用方父组件法,间接的操作父组件的数据

PropTypes对传递的参数进行强校验

官方文档:react.docschina.org/docs/typech…
引入PropTypes,import PropTypes from 'prop-types';

// 对传递的参数强校验
TodoItem.propTypes = {
  content: PropTypes.string.isRequired, // 限制为字符串且必传
  deleteItem: PropTypes.func, // 限制为函数
  index: PropTypes.number // 限制为数字
};

export default TodoItem;

传递参数的默认值

// 参数默认值,父组件未传值时的默认值
TodoItem.defaultProps = {
  test: 'hello world'
}

使用 ref 操作 DOM

<div ref={(box) => {this.refBox = box}}></div>

React 的生命周期

在某一时刻组件会自动调用执行的函数

Initialization 初始化

  • constructor() : class 的构造函数,并非React独有

Mounting 挂载

  • componentWillMount() : 在组件即将被挂载到页面的时刻自动执行;
  • render() : 页面挂载;
  • componentDidMount() : 组件被挂载到页面之后自动执行;

componentWillMount()componentDidMount(),只会在页面第一次挂载的时候执行,state变化时,不会重新执行

Updation 组件更新

  • shouldComponentUpdate() : 该生命周期要求返回一个bool类型的结果,如果返回true组件正常更新,如果返回false组件将不会更新;
  • componentWillUpdate() : 组件被更新之前执行,如果shouldComponentUpdate()返回false,将不会被被执行;
  • componentDidUpdate() : 组件更新完成之后执行;

componentWillReceiveProps() : props独有的生命周期,执行条件如下:

  1. 组件要从父组件接收参数;
  2. 只要父组件的render()被执行了,子组件的该生命周期就会执行;
  3. 如果这个组件第一次存在于父组件中,不会执行;
  4. 如果这个组件之前已经存在于父组件中,才会执行;

Unmounting 组件卸载

  • componentWillUnmount() : 当组件即将被从页面中剔除的时候,会被执行;

生命周期简单使用场景

  1. 使用shouldComponentUpdate()防止页面进行不必要的渲染
# 用生命周期进行性能优化
shouldComponentUpdate () {
    if (nextProps.content !== this.props.content) {
      return true;
    }
    return false;
}
  1. Ajax 请求页面初始数据componentDidMount()

不能写在render()之中,因为会重复调用,也不能写在componentWillMount()之中,会与RN等其它框架冲突,不然也可以写在这里面,同样是只执行一次。

同样也可以写在构造函数constructor()之中,但是不建议这样做。

import axios from 'axios'

componentDidMount () {
    axios.get('/api/todolist').then((res) => {
      console.log(res.data);
      this.setState(() => ({
        list: [...res.data]
      }));
    }).catch((err) => {
      console.log(err);
    });
}

React 的过渡动画

CSS过渡动画(transition & keyframes)

通过切换className,改变css效果,实现动画

<div className={this.state.show ? 'show' : 'hide'}>CssAnimation</div>

react-transition-group

官方文档:reactcommunity.org/react-trans… 当前版本react-transition-group包含4个组件:Transition, CSSTransition, SwitchTransition, TransitionGroup

CSSTransition

CSSTransition属性:

  • in : 判断进出场;
  • timeout : 动画时间;
  • unmountOnExit : 动画结束后移除元素;
  • classNames : 动画的class;
  • appear: 页面初始化时,是否需要进入效果;对应的class为.my-node-appear{}.my-node-appear-active{}
  • 多种钩子函数 : onEnteronEnteringonEnteredonExit等;
npm install react-transition-group --save
import { CSSTransition } from 'react-transition-group';

------ JSX ------
<CSSTransition
  in={this.state.show}
  timeout={1000}
  unmountOnExit
  classNames="my-node"
  onEntered={(element) => {element.style.color = 'red'}}
>
  <div>react-transition-group CSSTransition</div>
</CSSTransition>

------ style.css ------
.my-node-enter, .my-node-appear {
  opacity: 0;
}
.my-node-enter-active, .my-node-appear-active {
  opacity: 1;
  transition: opacity 1000ms;
}
.my-node-enter-done {
  opacity: 1;
}
.my-node-exit {
  opacity: 1;
}
.my-node-exit-active {
  opacity: 0;
  transition: opacity 1000ms;
}
.my-node-exit-done {
  opacity: 0;
}

TransitionGroup

import { CSSTransition, TransitionGroup } from 'react-transition-group';

<TransitionGroup>
{
  this.state.list.map((item, index) => {
    return (
      <CSSTransition
        in={this.state.show}
        timeout={1000}
        unmountOnExit
        appear={true}
        classNames="my-node"
        key={index}
      >
        <div>{item}</div>
      </CSSTransition>
    )
  })
}
</TransitionGroup>

Redux

Redux = Reducer + Flux

Redux基本原则

  1. Store必须是唯一,整个项目只能一个Store;
  2. 只有Store能够改变自己的内容,Store接收了action之后,将数据传给ReducerReducer将新的state传给StoreStore再更新自己的数据;
  3. Reducer必须是个传函数,即给定固定的输入,就一定会有固定的输出,而且不会有任何其它影响。例如函数里有Ajax请求、new Date() 这种日期相关的内容之后,就不再是一个纯函数。

Redux常用API

  1. createStore() : 创建 Store 对像;
  2. store.dispatch() : 派发 action;
  3. store.getState() : 获取Store当中的数据;
  4. store.subscribe() : 订阅 Store 的改变;

Redux 工作流程

React 中想要去改变 Reduxstore 的数据,首先需要去开发一个 actionaction 会通过store.dispatch(action)将数据传递给storestore收到dispatch之后,将action旧的state转发给ReducerReducer是一个函数,在接收到了stateaction之后,会进行一系列的处理,然后返回新的state给到storestore新的state替换旧的state,在store数据发生改变之后,React的组件会感知到数据发生变化,这个时候组件会从store中重新取数据,更新组件的内容,页面即发生改变。

使用 Redux

安装 Redux

npm install redux --save

在项目中简单使用

1.在src下创建一个文件夹/src/store
2.在store目录下,创建文件index.js

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer); // 创建存储仓库
export store;

3.在store目录下,创建文件reducer.js

const defaultState = {
    inputValue: '',
    list: []
};
export default (state = defaultState, action) => {
    return state;
}

4.组件中调用

import store from './store/index';

// 构造函数
constructor (props) {
    super(props); // 固定写法,调用父级的构造函数
    this.state = store.getState();
}

使用 action 修改数据

页面上调用

// 构造函数
  constructor (props) {
    super(props); // 固定写法,调用父级的构造函数
    this.state = store.getState();
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    
    // 订阅 store 的改变,只要store发生改变,该方法就会执行一次
    store.subscribe(this.handleStoreChange); 
  }

// 调用
handleInputChange (e) {
    const action = {
        type: 'change_input_value',
        value: e.target.value
    }
    store.dispatch(action);
}

// store 变化时更新 state
handleStoreChange () {
    this.setState(store.getState());
}

reducer.js中接收并获取

reducer 中很重要的一点,reducer可以接收state,但是觉得不能修改state,所以需要深拷贝

export default (state = defaultState, action) => {
    // 根据不同的type,执行不一样的代码
    if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state)); // 深拷贝
    newState.inputValue = action.value;
    return newState
    }
    return state;
}

ActionTypes 和 ActionCreator

/src/store 下创建 actionTypes.js

export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_LIST_ITEM = 'add_list_item';
export const DELETE_LIST_ITEM = 'delete_list_item';

/src/store 下创建 actionCreator.js

import { CHANGE_INPUT_VALUE, ADD_LIST_ITEM, DELETE_LIST_ITEM } from './actionTypes'
export const getInputValueChangeAction = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
});
export const getAddListItemAction = () => ({
  type: ADD_LIST_ITEM
});
export const getDeleteListItem = (itemIndex) => ({
  type: DELETE_LIST_ITEM,
  itemIndex
});

在组件中使用

import { handleInputChange } from './store/actionCreator'

handleInputChange (e) {
    const action = getInputValueChangeAction(e.target.value);
    store.dispatch(action);
}

Redux中发送http请求获取数据

// actionTypes.js
export const INIT_STORE_DATA = 'init_store_data';

// actionCreator.js
import { INIT_STORE_DATA } from './actionTypes';
export const getInitStoreData = (data) => ({
  type: INIT_STORE_DATA,
  data
});

// reducer.js
import { INIT_STORE_DATA } from './actionTypes';
export default (state = defaultState, action) => {
  if (action.type === INIT_STORE_DATA) {
    const newState = JSON.parse(JSON.stringify(state)); // 深拷贝
    newState.list = action.data;
    return newState;
  }
  return state;
}
// 组件中
import store from './store/index';
import { getInitStoreData } from './store/actionCreator';
componentDidMount () {
    axios.get('/api/todolist').then((res) => {
        const data = res.data;
        const action = getInitStoreData(data);
        store.dispatch(action);
    });
}

Redux 中间件

Redux中间件Middleware,是对store.dispatch的封装、升级。

Redux-thunk 中间件进行异步请求

安装

npm install redux-thunk --save

Store中的配置

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
// https://github.com/zalmoxisus/redux-devtools-extension
const middlewareList = [thunk]
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(applyMiddleware(...middlewareList));
const store = createStore(reducer, enhancer);
// 不需要使用 devtools 时的配置,devtools也是一个中间件
// const store = createStore(
//   reducer,
//   applyMiddleware(thunk),
// );
export default store;

在不使用redux-thunk的时候,action返回的是一个对象,在使用redux-thunk之后,action可以返回一个函数
actionCreator.js中配置

import { INIT_STORE_DATA } from './actionTypes';
export const getInitStoreData = (data) => ({
  type: INIT_STORE_DATA,
  data
});
// 当使用 redux-thunk 返回一个函数时,函数可以接收到 store 的 dispatch 方法作为参数
export const getTodoList = () => {
    return (dispatch) => {
        axios.get('/api/todolist').then((res) => {
            const data = res.data;
            const action = getInitStoreData(data);
            dispatch(action);
        });
    }
}

在组件中使用

import { getTodoList } from './store/actionCreator';
componentDidMount () {
    const action = getTodoList();
    store.dispatch(action);
}

Redux-saga 中间件进行异步请求

安装

npm install --save redux-saga

Store中的配置

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';
import ToDoSagas from './sagas';

const sagaMiddleware = createSagaMiddleware();
const middlewareList = [sagaMiddleware];
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(applyMiddleware(...middlewareList));

const store = createStore(reducer, enhancer);
sagaMiddleware.run(ToDoSagas);

export default store;

actionCreator.js中的配置

import { INIT_STORE_DATA } from './actionTypes';
export const getInitStoreData = (data) => ({
  type: INIT_STORE_DATA,
  data
});

sagas.js中配置

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { INIT_STORE_DATA } from './actionTypes';
import { getInitStoreData } from './actionCreator';
import axios from 'axios';
// Generator 函数
function* getInitList () {
  try {
      // Generator 函数处理异步请求可以加 yield 等待执行完成,类似 async/await
      const res = yield axios.get('/api/todolist');
      const action = getInitStoreData(res.data);
      yield put(action); // 执行 action,类似 store.dispatch
  } catch (e) {
      console.log('网络请求异常');
  }
}
// Generator 函数
function* ToDoSagas () {
  // takeEvery 第一个参数为:捕获的Action的类型
  // takeEvery 第二个参数为:捕获成功后,需要执行的方法
  yield takeEvery(INIT_STORE_DATA, getInitList);
}
export default ToDoSagas;

actionCreator.js中配置

import { INIT_STORE_DATA } from './actionTypes'
export const getInitStoreData = () => ({
   type: INIT_STORE_DATA
});

在组件中使用

import { getInitStoreData } from './store/actionCreator';
componentDidMount () {
    const action = getInitStoreData();
    store.dispatch(action);
}

Redux-thunk 和 Redux-saga 取舍

  • 共同点:两者同样是都是Redux的中间件,同样是对store.dispatch的封装,同样可以在封装之后使得提交action的时候可以使用一些异步方法及数据处理。
  • 不同点:Redux-thunk没有提供丰富的API,主要功能是使得action能够返回自动执行的函数
  • 不同点:Redux-saga提供了丰富的API,在dispatch之前可以先进入自己写的sagas.js执行异步方法及数据处理。
  • 在超大型项目中,Redux-saga适用于大型及超大型项目,因为其提供了丰富的API及单独拆分的sagas.js文件,可以方便运营维护,但是也使得项目变得更加复杂。其它情况下使用Redux-thunk都更加方便快捷。

React-redux

React-redux可以使,在 React 项目中使用 Redux 更加方便。

安装

npm install react-redux --save

Provider 组件

通过 Provider 将 Redux 的 store 提供给 Provider 下的所有组件。

import { Provider } from 'react-redux';
import store from './store';
import ReactRedux from './ReactRedux';
// 通过 Provider 将 Redux 的 store 提供给 Provider 下的所有组件
const App = (
  <Provider store={store}>
    <ReactRedux />,
  </Provider>
)
ReactDOM.render(App, document.getElementById('root'));

connect()

使业务组件和store进行连接,该业务组件必须在Provider组件下

import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
class ReactRedux extends Component {
  constructor (props) {
    super(props)
  }
  render () {
    return (
      <Fragment>
        <div>
          <input placeholder="你接下来想做什么" value={this.props.inputValue} onChange={this.props.changeInputValue}/>
          <button onClick={this.props.handleClick}>提交</button>
        </div>
        <ul>
          {this.props.list.map((item, index) => (<li key={index} onClick={() => {
            return this.props.handleItemDelete(index)
          }}>{item}</li>))}
        </ul>
      </Fragment>
    )
  }
}
// 将 Redux 的 store 挂载到 props
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}
// 将 Redux 的 dispatch 挂载到 props
const mapDispatchToProps = (dispatch) => {
  return {
    changeInputValue (e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    },
    handleClick () {
      const action = {
        type: 'add_list_item'
      }
      dispatch(action)
    },
    handleItemDelete (itemIndex) {
      const action = {
        type: 'delete_list_item',
        itemIndex
      }
      dispatch(action)
    }
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(ReactRedux);

组件化

  1. UI组件:只负责数据展示;
  2. 容器组件:负责业务逻辑;

无状态组件

当一个组件只有一个render()函数时,我们就可将这个组件定义为无状态组件,无状态组件只有一个函数。
无状态组件的性能比较高,因为它仅是一个函数,而普通组件是一个class.

import React, { Component, Fragment } from 'react';
import { Input, Button, Typography, List } from 'antd';
// 无状态组件
const TodoListUI = (props) => {
  const { Title } = Typography;
  return (
    <div className="wrapper">
      <Title>To Do List By Actoress</Title>
      <div className="box">
        <Input
          placeholder="你接下来想做什么..."
          value={props.inputValue}
          style={{ width: '320px' }}
          size="large"
          onChange={props.handleInputChange}
        />
        <Button type="primary" size="large" onClick={props.handleButtonClick}>提交</Button>
      </div>
      <List
        size="large"
        dataSource={props.list}
        renderItem={(item, index) => <List.Item onClick={(index) => {
          props.handleItemDelete(index);
        }}>{item}</List.Item>}
        style={{ width: '390px', margin: '18px auto 0' }}
      />
    </div>
  )
}
export default TodoListUI;