阅读视图

发现新文章,点击刷新页面。

canvas中常见问题的解决方法及分析,踩坑填坑经历

一、canvas中画线条,线条效果比预期宽1像素且模糊问题分析及解决。

【出现条件】:这种情况一般是垂直或者水平的线,且坐标为整数,宽度不是偶数。 【解决方法】:坐标偏移0.5像素。


1.1、canvas画线的原理:以指定坐标为中心向两侧画线(两侧各画宽的一半)。

下面我们看个例子

var dom = document.querySelector("#canvas1");
var ctx = dom.getContext('2d');

ctx.strokeStyle = '#000';

// 正常画线(坐标为整数,线宽为1px),1像素画出的效果像2像素。
ctx.lineWidth = 1;
ctx.moveTo(30, 50);
ctx.lineTo(30, 200);
ctx.stroke();

// 处理之后(坐标偏移0.5像素),线条宽度正常。
ctx.lineWidth = 1;
ctx.moveTo(50.5, 50);
ctx.lineTo(50.5, 200);
ctx.stroke();

效果如下图(在PS中放大后效果) canvas画线的原理

【实例解析】

  1. 指定坐标为30px时,实际是以30px为中心向两边各画一半(0.5px),会画在30px前后的两个像素格子中。又因为像素是最小单位,所以30px前后的两个像素都被画了1px的线,但是颜色要比实际的谈一些。

  2. 而指定坐标为50.5px时,线是以50.5为中心向两边各画一半(0.5px),这样子刚好只占用了一个像素的宽,就实现了1px的宽了。


1.2、当线的宽度为非整数时,同样会出现“宽度大1px”的情况

canvas画非整数宽的线

如上图所示,从左到右宽分别是1.3px、0.8px、0.5px、0.1px。上面4条以整数为坐标的线宽度其实是2px,下面4条X坐标都偏移了0.5px。效果更接近预期的宽度。


canvas画线问题总结

以上所说的偏移0.5px,其实并不准确。因为上面例子中,坐标都是整数。 更准确的说法应该是: 当线宽为偶数时,坐标应指定为整数。否则坐标应指定为整数+0.5px。


1.3、下面奉上我总结的最终解决方案

这里以竖线为例,横线同理


// 封装一个画线的方法
function drawLine (ctx, x, y1, y2, width) {
  // 当线宽为偶数时,坐标应指定为整数。否则坐标应指定为整数+0.5px。
  let newx = width % 2 === 0 ? Math.floor(x) : Math.floor(x) + 0.5;

  ctx.lineWidth = width;
  ctx.moveTo(newx, y1);
  ctx.lineTo(newx, y2);
}

ctx.beginPath();
ctx.strokeStyle = '#000';
drawLine (ctx, 350, 250, 380, 1);
ctx.stroke();

ctx.beginPath();
ctx.strokeStyle = '#000';
drawLine (ctx, 360, 250, 380, 2);
ctx.stroke();

ctx.beginPath();
ctx.strokeStyle = '#000';
drawLine (ctx, 370.4, 250, 380, 1.3);
ctx.stroke();

具体效果请看canvas画线条源码中,右下角的三根线。

想了解更多canvas画线问题的解析,请稳步到 canvas中画线条,线条效果比预期宽1像素且模糊问题分析及解决方案


二、canvas中没有画圆角矩形的API,我们自己写一个方法实现圆角矩形

2.1、canvas已有的api,并且我们可能会用到用来画圆角矩形的有如下几个:

2.1.1、创建矩形:

语法:context.rect(x,y,width,height)

参数 描述
x 矩形左上角的 x 坐标。
y 矩形左上角的 y 坐标。
width 矩形的宽度,以像素计。
height 矩形的高度,以像素计。

2.1.2、创建弧/曲线/圆:

语法:context.arc(x,y,r,sAngle,eAngle,counterclockwise)

参数 描述
x 圆的中心的 x 坐标。
y 圆的中心的 y 坐标。
r 圆的半径。
sAngle 起始角,以弧度计(弧的圆形的三点钟位置是 0 度)。
eAngle 结束角,以弧度计。
counterclockwise 可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。

2.1.3、把路径移动到画布中的指定点,不创建线条:

语法:context.moveTo(x,y)

参数 描述
x 路径的目标位置的 x 坐标。
y 路径的目标位置的 y 坐标。

2.1.4、添加一个新点,然后在画布中创建从该点到最后指定点的线条

语法:context.lineTo(x,y)

参数 描述
x 路径的目标位置的 x 坐标。
y 路径的目标位置的 y 坐标。

2.1.5、创建介于两个切线之间的弧/曲线:

image.png

语法:context.arcTo(x1,y1,x2,y2,r)

参数 描述
x1 两切线交点的横坐标。
y1 两切线交点的纵坐标。
x2 第二条切线上一点的横坐标。
y2 第二条切线上一点的纵坐标。
r 弧的半径。

2.2、这些API要么是画矩形,要么是画弧,但是没有真正画圆角矩形的。但是我们可以结合它们画出圆角矩形。

【分析方案】

方案1:圆弧(arc)+ 线(moveTo+lineTo)画矩形。 分析:可以实现画圆角矩形,不过需要反复多次调用以上API(要画8条线),性能略差。

方案2:使用两个切线之间的弧(arcTo)结合moveTo画矩形。 分析:可以实现画圆角矩形,并且调用API较少(只画4条线)。【推荐】

更详细的分析说明请移步到 在Canvas中绘制圆角矩形及逐步分析过程


2.3、最终实现画圆角矩形代码如下:

// 入参说明:上下文、左上角X坐标,左上角Y坐标,宽,高,圆角的半径
function arcRect (ctx, x, y, w, h, r) { 
  // 右上角弧线
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + r, r);

  // 右下角弧线
  ctx.moveTo(x + w, y + r);
  ctx.arcTo(x + w, y + h, x + w - r, y + h, r);

  // 左下角弧线
  ctx.moveTo(x + w - r, y + h);
  ctx.arcTo(x, y + h, x, y + h - r, r);

  // 左上角弧线
  ctx.moveTo(x, y + h - r);
  ctx.arcTo(x, y, x + r, y, r);
}

该代码实现的效果如下图

矩形

圆角矩形画好后,具体是要路径(外框),还是要填充。只需要分别使用stroke()fill()方法实现即可。


三、canvas中有阴影API,但是没有内阴影API。如果在Canvas中实现内阴影效果呢?

3.1、首先我们先看一下canvas中和阴影有关的API。

1.1、阴影相关属性如下:

属性 描述
shadowColor 设置或返回用于阴影的颜色。
shadowBlur 设置或返回用于阴影的模糊级别。
shadowOffsetX 设置或返回阴影与形状的水平距离。
shadowOffsetY 设置或返回阴影与形状的垂直距离

1.2、clip()方法

clip()方法从原始画布中剪切任意形状和尺寸。

提示:一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。您也可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。

3.2、实现内阴影的原理:

给闭合线(如:矩形、圆等)设置阴影,然后把线以及线外部的阴影裁切掉,只留线内部的阴影。从而达到内阴影效果。

3.3、影响内阴影效果的因素:

1、shadowBlur值越大,范围变大,但阴影也更模糊。 2、线越宽,阴影越清晰。 3、所有这两个属性相结合一起控制【内阴影】的大小。

用线框内部的阴影来实现内阴影

3.4、矩形内阴影效果的实现:

ctx.strokeStyle = '#f00';
ctx.lineWidth = 7;

// 单独内阴影
rectInnerShadow(ctx, 80, 40, 200, 120);

// 矩形框和内阴影一起时,要先画内阴影
rectInnerShadow(ctx, 20, 230, 150, 120);
ctx.strokeRect(20, 230, 150, 120);

// 否则会有重叠(因为线是向两侧画的)
ctx.strokeRect(220, 230, 150, 120);
rectInnerShadow(ctx, 220, 230, 150, 120);

// 【矩形内阴影(边框+阴影,再把边框和外阴影裁剪掉)】
// 参数说明:ctx上下文内容,x,y,w,h同rect的入参,shadowColor阴影颜色,shadowBlur和lineWidth一同控制阴影大小。
function rectInnerShadow (ctx, x, y, w, h, shadowColor, shadowBlur, lineWidth) {
  var shadowColor = shadowColor || '#00f'; // 阴影颜色
  var lineWidth = lineWidth || 20; // 边框越大,阴影越清晰
  var shadowBlur = shadowBlur || 30; // 模糊级别,越大越模糊,阴影范围也越大。

  ctx.save();
  ctx.beginPath();

  // 裁剪区(只保留内部阴影部分)
  ctx.rect(x, y, w, h);
  ctx.clip();

  // 边框+阴影
  ctx.beginPath();
  ctx.lineWidth = lineWidth;
  ctx.shadowColor = shadowColor;
  ctx.shadowBlur = shadowBlur;
  // 因线是由坐标位置向两则画的,所以要移动起点坐标位置,和加大矩形。
  ctx.strokeRect(x - lineWidth/2, y - lineWidth/2 , w + lineWidth, h + lineWidth);
  
  // 取消阴影
  ctx.shadowBlur = 0;

  ctx.restore();
}

用线框内部的阴影来实现内阴影

五、Redux进阶:UI组件、容器组件、无状态组件、异步请求、Redux中间件:Redux-thunk、redux-saga,React-redux

一、UI组件和容器组件

  1. UI组件负责页面的渲染(傻瓜组件)
  2. 容器组件负责页面的逻辑(聪明组件)

当一个组件内容比较多,同时有逻辑处理和UI数据渲染时,维护起来比较困难。这个时候可以拆分成“UI组件”和"容器组件"。 拆分的时候,容器组件把数据和方法传值给子组件,子组件用props接收。

需要注意的是: 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。

拆分实例

未拆分前原组件

import React, {Component} from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';
// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />
          <Button type="primary" onClick={this.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.state.list}
          renderItem={(item, index) => (<List.Item onClick={this.handleDelete.bind(this, index)}>{item}</List.Item>)}
        />
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }
  
  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-容器组件

import React, {Component} from 'react';

// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';
import TodoListUI from './TodoListUI';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleClick={this.handleClick}
        handleDelete={this.handleDelete}
      />
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }

  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-UI组件

import React, { Component } from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';

class TodoListUI extends Component {
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.props.inputValue}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" onClick={this.props.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.props.list}
          // renderItem={(item, index) => (<List.Item onClick={(index) => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
          renderItem={(item, index) => (<List.Item onClick={() => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
        />
        {/* 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。 */}
      </div>
    )
  }
}

export default TodoListUI;

二、无状态组件

当一个组件只有render函数时,可以用无状态组件代替。

  1. 无状态组件比普通组件性能高; 因为无状态组件只是函数,普通组件是class声明的类要执行很多生命周期函数和render函数。
  2. 无状态组件中的函数接收一个参数作为父级传过来的props。

例如下面这个例子 普通组件:

class TodoList extends Component {
  render() {
    return <div> {this.props.item} </div>
  }
}

无状态组件:

const TodoList = (props) => {
  return(
    <div> {props.item} </div>
  )}

三、Redux 中发送异步请求获取数据

1、引入axios,使用axios发送数据请求

import axios from 'axios';

2、在componentDidMount中调用接口

componentDidMount() {
  axios.get('/list.json').then(res => {
    const data = res.data;
    // 在actionCreators.js中定义好initListAction,并在reducer.js中作处理(此处省略这部分)
    const action = initListAction(data);
    store.dispatch(action);
  })
}

四、使用Redux-thunk 中间件实现ajax数据请求

1、安装和配置Redux-thunk

1.1、安装Redux-thunk

npm install redux-thunk --save

1.2、正常使用redux-thunk中间件在store中的写法

// 引用applyMiddleware
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

// 创建store时,第二个参数传入中间件
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

export default store;

redux-thunk使用说明

1.3、redux-thunk中间件 和 redux-devtools-extension 一起使用的写法

// 引入compose
import { createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(thunk),
);

const store = createStore(reducer, enhancer);

export default store;

Redux DevTools插件配置说明

2、redux-thunk 的作用和优点

  1. 不使用redux-thunk中间件,store接收的action只能是对象;有了redux-thunk中间件,action也可以是一个函数。这样子就可以在action中做异步操作等。
  2. store接收到action之后发现action是函数而不是对象,则会执行调用这个action函数。
  3. 可以把复杂的异步数据处理从组件的生命周期里摘除出来(放到action中),避免组件过于庞大,方便后期维护、自动化测试。

3、使用redux-thunk的流程

  1. 在创建store时,使用redux-thunk。详见以上配置说明。

  2. 在actionCreators.js中创建返回一个方法的action,并导出。在这个方法中执行http请求。

import types from './actionTypes';
import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// 当使用redux-thunk后,action不仅可以是对象,还可以是函数
// 返回的如果是方法会自动执行
// 返回的方法可以接收到dispatch方法,去派发其它action
export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/initList').then(res => {
      const action = initItemAction(res.data);
      dispatch(action);
    })
  }
}

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-thunk派发/执行一个action函数)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把处理异步请求的action函数派发给store;
  2. 因使用了redux-thunk中间件,所以可以接收一个action函数(正常只能接收action对象)并执行该方法;
  3. 在这个方法中执行http异步请求,拿到结果后再次派发一个正常的action对象给store;
  4. store发现是action对象,则根据拿来的值修改store中的状态。

五、什么是Redux的中间件

  1. 中间件指的是action 和 store 中间。
  2. 中间件实现是对store的dispatch方法的升级。

Redux数据流

几个常见中间件的作用(对dispatch方法的升级)

  1. redux-thunk:使store不但可以接收action对象,还可以接收action函数。当action是函数时,直接执行该函数。
  2. redux-log:每次dispatch时,在控制台输出内容。
  3. redux-saga:也是处理异步逻辑,把异步逻辑单独放在一个文件中管理。

六、redux-saga中间件入门

1、安装和配置redux-saga

1.1、安装redux-saga

npm install --save redux-saga

yarn add redux-saga

1.2、正常使用redux-saga中间件在store中的写法

import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);

export default store;

redux-saga使用说明

1.3、redux-saga中间件 和 redux-devtools-extension 一起使用的写法

import { createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware),
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(mySaga);

export default store;

Redux DevTools插件配置说明

2、redux-saga 的作用和与redux-thunk的比较

  1. redux-saga也是解决异步请求的。但是redux-thunk的异步处理还是在aciton中,而redux-saga的异步处理是在一个单独的文件(sagas.js)中处理。
  2. redux-saga同样是作异步代码拆分的中间件,可以使用redux-saga完全代替redux-thunk。(redux-saga使用起来更复杂,更适合大型项目)
  3. redux-thunk只是把异步请求放到action中,并没有多余的API。而redux-saga是单独放在一个文件中处理,并且有很多PAI。
  4. 使用流程上的区别; 4.1. 使用redux-thunk时,从组件中派发action(action函数)时,监测到是函数,会在action中接收并处理,然后拿到结果后再派发一个普通action交给store的reducer处理,更新store的状态。 4.2. 使用redux-saga时,从组件中派发action(普通action对象)时,会先交给sagas.js匹配处理异步请求。拿到结果后再使用put方法派发一个普通action交给store的reducer处理,更新store的状态。

3、使用redux-saga的流程

  1. 在创建store时,使用redux-saga。详见以上配置说明。

  2. 在actionCreators.js中创建一个普通的action,并导出。

import types from './actionTypes';
// import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// redux-thunk的写法,异步请求依然在这个文件中
// export const getTodoList = () => {
//   return (dispatch) => {
//     axios.get('/initList').then(res => {
//       const action = initItemAction(res.data);
//       dispatch(action);
//     })
//   }
// }

// redux-saga的写法,这里返回一个普通action对象;
// sagas.js中会用takeEvery监听这个type类型,然后执行对应的异步请求
export const getTodoList = () => ({
  type: types.GET_INIT_ACTION,
})

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在store文件夹中,创建一个文件sagas.js,使用redux-saga的takeEvery方法监听刚才派发的type类型,然后执行对应的函数,执行异步请求代码。拿到结果后再使用redux-saga的put方法派发一个普通的action对象,交给store的reducer处理。
import { takeEvery, put } from 'redux-saga/effects';
import types from './actionTypes';
import axios from 'axios';
import { initItemAction } from './actionCreators';

function* getInitList() {
  try {
    const res = yield axios.get('/initList');
    const action = initItemAction(res.data);
    yield put(action);
  } catch(e) {
    console.log('接口请求失败');
  }
}

// generator 函数
function* mySaga() {
  yield takeEvery(types.GET_INIT_ACTION, getInitList);
}

export default mySaga;
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-saga派发一个普通action对象,经由sagas.js的generator 函数匹配处理后,再交由store的reducer处理)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把一个普通的action对象派发给store;
  2. 因使用了redux-saga中间件,所以会被sagas.js中的generator函数匹配到,并交给对应的函数(一般也是generator函数)处理;
  3. sagas.js的函数拿到结果后,使用redux-saga的put方法再次派发一个普通action对象给store;
  4. sagas.js中没有匹配到对应的类型,则store交由reducer处理并更新store的状态。

七、如何使用React-redux完成TodoList功能

安装React-redux

npm install react-redux --save

1、把redux写法改成React-redux写法

1.1、 入口文件(src/index.js)的修改

  • 使用react-redux的Provider组件(提供器)包裹所有组件,把 store 作为 props 传递到每一个被 connect() 包装的组件。
  • 使组件层级中的 connect() 方法都能够获得 Redux store,这样子内部所有组件就都有能力获取store的内容(通过connect链接store)。

原代码

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './todoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));

修改后代码 ```jsx import React from 'react'; import ReactDOM from 'react-dom'; import TodoList from './TodoList'; import { Provider } from 'react-redux'; import store from './store';

// Provider向内部所有组件提供store,内部组件都可以获得store const App = ( )

ReactDOM.render(App, document.getElementById('root'));

<br/>
#### 1.2、组件(TodoList.js)代码的修改

Provider的子组件通过react-redux中的connect连接store,写法:
```jsx
connect(mapStateToProps, mapDispatchToProps)(Component)
  • mapStateToProps:store中的数据映射到组件的props中;
  • mapDispatchToProps:把store.dispatch方法挂载到props上;
  • Component:Provider中的子组件本身;

导出的不是单纯的组件,而是导出由connect处理后的组件(connect处理前是一个UI组件,connect处理后是一个容器组件)。


原代码
import React, { Component } from 'react';
import store from './store';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleChange = this.handleChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }

  render() {
    return(
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange} />
          <button onClick={this.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map((item, index) => {
              return <li onClick={() => {this.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }
  
  handleChange(e) {
    const action = {
      type: 'change-input-value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  handleClick() {
    const action = {
      type: 'add-item'
    }
    store.dispatch(action)
  }

  handleDelete(index) {
    const action = {
      type: 'delete-item',
      value: index
    }
    store.dispatch(action);
  }
}

export default TodoList;

修改后代码

省去了订阅store使用store.getState()更新状态的操作。组件会自动更新数据。

import React, { Component } from 'react';
import { connect } from 'react-redux';

class TodoList extends Component {
  render() {
    // const { inputValue, handleChange, handleClick, list, handleDelete} = this.props;

    return(
      <div>
        <div>
          <input value={this.props.inputValue} onChange={this.props.handleChange} />
          <button onClick={this.props.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.props.list.map((item, index) => {
              return <li onClick={() => {this.props.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }
}

// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

#### 1.3、store/index.js 代码不需要修改 ```jsx import { createStore } from 'redux'; import reducer from './reducer'

const store = createStore(reducer);

export default store;

<br/>
#### 1.4、store/reducer.js 代码也不需要修改
```jsx
const defaultState = {
  inputValue: '',
  list: []
}
export default (state = defaultState, action) => {
  const { type, value } = action;
  let newState = JSON.parse(JSON.stringify(state));

  switch(type) {
    case 'change-input-value':
      newState.inputValue = value;
      break;
    case 'add-item':
      newState.list.push(newState.inputValue);
      newState.inputValue = '';
      break;
    case 'delete-item':
      newState.list.splice(value, 1);
      break;
    default:
      return state;
  }

  return newState;
}

2、代码精简及性能优化

  • 因现在组件(TodoList.js)中代码只是用来渲染,是UI组件。并且没有状态(state),是个无状态组件。所以可以改成无状态组件,提高性能。
  • 但connect函数返回的是一个容器组件。
import React from 'react';
import { connect } from 'react-redux';

const TodoList = (props) => {
  const { inputValue, handleChange, handleClick, list, handleDelete} = props;

  return(
    <div>
      <div>
        <input value={inputValue} onChange={handleChange} />
        <button onClick={handleClick}>提交</button>
      </div>
      <ul>
        {
          list.map((item, index) => {
            return <li onClick={() => {handleDelete(index)}} key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
}


// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

二、React基础精讲:编写TodoList、事件绑定、JSX语法、组件之间传值

一、使用React编写TodoList功能

JSX语法:render返回元素最外层必须由一个元素包裹。 Fragment 可以作为React的最外层元素占位符。

import React, {Component, Fragment} from 'react';

class TodoList extends Component {
  render() {
    return (
      <Fragment>
        <div>
          <input/>
          <button>提交</button>
        </div>
        <ul>
          <li>1111111</li>
          <li>2222222</li>
          <li>3333333</li>
        </ul>
      </Fragment>
    )
  }
}

export default TodoList;

二、React 中的响应式设计思想和事件绑定

  1. React在创建实例的时候, constructor(){} 是最先执行的;
  2. this.state 负责存储数据;
  3. 如果修改state中的内容,不能直接改,需要通过setState向里面传入对象的形式进行修改;
  4. JSX中js表达式用{}包裹;
  5. 事件绑定需要通过bind.(this)对函数的作用域进行变更;
import React, {Component, Fragment} from 'react';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: []
    }
  }
  
  render() {
    return (
      <Fragment>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button>提交</button>
        </div>
        <ul>
          <li>1111111</li>
          <li>2222222</li>
          <li>3333333</li>
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }
}

export default TodoList;

三、实现 TodoList 新增删除功能

import React, {Component, Fragment} from 'react';

lass TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
  }
  
  render() {
    return (
      <Fragment>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {this.state.list.map((item, index) => {
            return <li key={index} onClick={this.handleItemDelete.bind(this, index)}>{item}</li>
          })}
        </ul>
      </Fragment>
    )
  }
  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    this.setState({
      list: [...this.state.list, this.state.inputValue],
      inputValue: ''
    })
  }

  // 删除
  handleItemDelete(index) {
    let list = [...this.state.list]; // 拷贝一份原数组,因为是对象,所以不能直接赋值,会有引用问题
    list.splice(index, 1);

    this.setState({
      list: list
    })

    // 以下方法可以生效,但是不建议使用。
    // React中immutable的概念:  state 不允许直接操作改变,否则会影响性能优化部分。
    
    // this.state.list.splice(index, 1);
    // this.setState({
    //   list: this.state.list
    // })
  }
}

export default TodoList;

四、JSX语法细节补充

1、在jsx语法内部添加注释:

  {/*这里是注释*/}

或者:

{
  //这里是注释
}

2、JSX语法中的属性不能和js中自带的属性和方法名冲突

元素属性class 替换成 className lable标签中的for 替换成 htmlFor

3、解析html内容

如果需要在JSX里面解析html的话,可以在标签上加上属性dangerouslySetInnerHTML属性(标签中不需要再输出item):如dangerouslySetInnerHTML={{__html: item}}

...

render() {
  return (
    <Fragment>
      {/* 这是一个注释 */}
      {
        // class 换成 className
        // for 换成 htmlFor
      }
      <div className="input">
        <lable htmlFor={"insertArea"}>请输入内容</lable>
        <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
        <button onClick={this.handleBtnClick.bind(this)}>提交</button>
      </div>
      <ul>
        {this.state.list.map((item, index) => {
          return (
            <li
              key={index}
              onClick={this.handleItemDelete.bind(this, index)}
              dangerouslySetInnerHTML={{__html: item}}
            >
            </li>

            // <li
            //   key={index}
            //   onClick={this.handleItemDelete.bind(this, index)}
            // >
            //   {item}
            // </li>
          )
        })}
      </ul>
    </Fragment>
  )
}

...

五、拆分组件与组件之间的传值

父子组件之间通讯:

①父=>子

父组件通过属性向子组件传递数据,子组件通过this.props.属性名 获取父组件传来的数据。

②子=>父

子组件调用父组件的方法来改变父组件的数据。也是父组件通过属性把父组件对应的方法传递给子组件(在父组件向子组件传入方法时,就要绑定this,不然在子组件找不到方法),然后在子组件中通过this.props.方法名(属性名) 调用对应的父组件的方法并传递对应的参数。通过触发父组件方法改变数据,数据改变从而重新渲染页面。

父组件(todoList.js)
import React, {Component, Fragment} from 'react';
import TodoItem from './todoItem';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
  }
  
  render() {
    return (
      <Fragment>
        <div className="input">
          <label htmlFor={"insertArea"}>请输入内容</label>
          <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {this.state.list.map((item, index) => {
            return (
              <TodoItem
                key={index}
                index={index}
                item={item}
                deleteItem={this.handleItemDelete.bind(this)}
              />
            )
          })}
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    this.setState({
      list: [...this.state.list, this.state.inputValue],
      inputValue: ''
    })
  }

  // 删除
  handleItemDelete(index) {
    let list = [...this.state.list];
    list.splice(index, 1);

    this.setState({
      list: list
    })
  }
}

export default TodoList;
子组件(todoItem.js)
import React, { Component } from 'react';

class TodoItem extends Component {
  constructor(props) {
    super(props);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);
  }

  render() {
    return (
      <li onClick={this.handleDeleteItem}>
        {this.props.item}
      </li>
    )
  }

  handleDeleteItem() {
    this.props.deleteItem(this.props.index);
  }
}

export default TodoItem;

六、TodoList 代码优化

  1. 事件方法的this指向要在constructor里面统一进行绑定,这样可以优化性能,如:this.fn = this.fn.bind(this)
  2. setState在新版的react中写成:this.setState(()=>{retuen {}}) 或 this.setState(()=>({}))。第一中写法可以在return前写js逻辑,新版的写法有一个参数prevState,可以代替修改前的this.state,同样是可以提高性能,也能避免不小心修改state导致的bug。
  3. JSX中也可以把某一块代码提出来,直接定义一个方法把内容return出来,再在JSX中引用这个方法。以达到拆分代码的目的。
import React, {Component, Fragment} from 'react';
import TodoItem from './todoItem';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
    
    // 统一在constructor中绑定this,提交性能
    this.handleChange = this.handleChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    this.getTodoItem = this.getTodoItem.bind(this);
  }
  
  render() {
    return (
      <Fragment>
        <div className="input">
          <label htmlFor={"insertArea"}>请输入内容</label>
          <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange}/>
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
          {this.getTodoItem()}
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    // this.setState({
    //   inputValue: e.target.value
    // })

    // 因这种写法setState是异步的,有时e.target获取不到,所以先赋值给一个变量再使用。
    const value = e.target.value;
    // 新版写法,setState不但可以接受一个对象,也可以接受一个方法
    // this.setState(() => {
    //   return {
    //     inputValue: value
    //   }
    // })

    // this.setState(()=>{retuen {}}) 简写成 this.setState(()=>({}))
    // 还可以再简写成
    this.setState(() => (
      {
        inputValue: value
      }
    ))
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    // this.setState({
    //   list: [...this.state.list, this.state.inputValue],
    //   inputValue: ''
    // })

    // 新版写法,可以使用prevState代替修改前的this.state,不但可以提高性能,也能避免不小心修改state导致的bug。
    this.setState((prevState) => {
      return {
        list: [...prevState.list, prevState.inputValue],
        inputValue: ''
      }
    })
  }

  // 删除
  handleItemDelete(index) {
    // let list = [...this.state.list];
    // list.splice(index, 1);

    // this.setState({
    //   list: list
    // })

    // 新版写法,可以在return前写js逻辑
    this.setState(() => {
      let list = [...this.state.list];
      list.splice(index, 1);
      return {list: list}
    })
  }

  // 把循环提取出来,放在一个方法中
  getTodoItem () {
    return this.state.list.map((item, index) => {
      return (
        <TodoItem key={index} index={index} item={item} deleteItem={this.handleItemDelete}/>
      )
    })
  }
}

export default TodoList;

七、围绕 React 衍生出的思考

1、声明式开发 可减少大量的dom操作; 对应的是命令式开发,比如jquery,操作DOM。

2、可以与其它框架并存 React可以与jquery、angular、vue等框架并存,在index.html页面,React只渲染指定id的div(如:root),只有这个div跟react有关系。

3、组件化 继承Component,组件名称第一个字母大写。

4、单向数据流 父组件可以向子组件传递数据,但子组件绝对不能改变该数据(应该调用父级传入的方法修改该数据)。

5、视图层框架 在大型项目中,只用react远远不够,一般用它来搭建视图,在作组件传值时要引入一些框架(Fux、Redux等数据层框架);

6、函数式编程 用react做出来的项目更容易作前端的自动化测试。

❌