Vue-过渡效果

进入/离开 & 列表过渡

概述

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。
包括以下工具:

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

单元素/组件的过渡

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

这里是一个典型的例子:

<div id="demo">
    <button v-on:click="show = !show">
        Toggle
    </button>
    <transition name="fade">
        <p v-if="show">hello</p>
    </transition>
</div>
new Vue({
  el: '#demo',
  data: {
    show: true
  }
})
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
  2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)

过渡的类名

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
  4. v-leave: 定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

Transition Diagram

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 ,则 `v-` 是这些类名的默认前缀。如果你使用了 ,那么 v-enter 会替换为 my-transition-enter

v-enter-activev-leave-active 可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。

初始化渲染页面时过渡

需要利用apper属性

<transition name='retr0' appear>
    <div v-if='isshow' style='background:red;'>
        show
    </div>
</transition>

自定义过渡类名

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)
<transition enter-active-class='animated bounceInRight' leave-active-class='animated bounceOutRight' appear>
    <div v-if='isshow'>动画</div>
</transition>

过渡模式

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。
<transition enter-active-class='animated bounceInRight' leave-active-class='animated bounceOutRight' appear mode-'out-in'>
    <div v-if='isshow'>动画一</div>
    <div v-else>动画一</div>
</transition>

多个组件的过渡

多个组件的过渡简单很多 - 我们不需要使用 key attribute。相反,我们只需要使用动态组件

<transition name="component-fade" mode="out-in">
  <component v-bind:is="view"></component>
</transition>
.component-fade-enter-active, .component-fade-leave-active {
  transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active for below version 2.1.8 */ {
  opacity: 0;
}

new Vue({
  el: '#transition-components-demo',
  data: {
    view: 'v-a'
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>'
    },
    'v-b': {
      template: '<div>Component B</div>'
    }
  }
})

列表过渡

目前为止,关于过渡我们已经讲到:

  • 单个节点
  • 同一时间渲染多个节点中的一个

那么怎么同时渲染整个列表,比如使用 v-for ?在这种场景中,使用 `` 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 不同于 ,它会以一个真实元素呈现:默认为一个 。你也可以通过 tag attribute 更换为其他元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素 总是需要 提供唯一的 key 属性值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

列表的进入/离开过渡

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。

<div id="list-demo" class="demo">
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list" tag="p">
        <span v-for="item in items" v-bind:key="item" class="list-item">
            {{ item }}
        </span>
    </transition-group>
</div>
new Vue({
    el: '#list-demo',
    data: {
        items: [1,2,3,4,5,6,7,8,9],
        nextNum: 10
    },
    methods: {
        randomIndex: function () {
            return Math.floor(Math.random() * this.items.length)
        },
        add: function () {
            this.items.splice(this.randomIndex(), 0, this.nextNum++)
        },
        remove: function () {
            this.items.splice(this.randomIndex(), 1)
        },
    }
})

可复用的过渡

过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 或者 作为根组件,然后将任何子组件放置在其中就可以了。

使用 template 的简单例子:

Vue.component('my-special-transition', {
  template: '\
    <transition\
      name="very-special-transition"\
      mode="out-in"\
      v-on:before-enter="beforeEnter"\
      v-on:after-enter="afterEnter"\
    >\
      <slot></slot>\
    </transition>\
  ',
  methods: {
    beforeEnter: function (el) {
      // ...
    },
    afterEnter: function (el) {
      // ...
    }
  }
})

函数式组件更适合完成这个任务:

Vue.component('my-special-transition', {
  functional: true,
  render: function (createElement, context) {
    var data = {
      props: {
        name: 'very-special-transition',
        mode: 'out-in'
      },
      on: {
        beforeEnter: function (el) {
          // ...
        },
        afterEnter: function (el) {
          // ...
        }
      }
    }
    return createElement('transition', data, context.children)
  }
})

第三方过渡动画库

npm istall animate.css --save 下载依赖包
import 'animate.css'; 引入

https://animate.style/

React- React Hooks

React Hooks

使用hooks的理由

  1. 高阶组件为了复用,导致代码层级复杂
  2. 生命周期的复杂
  3. 写成functional组件,无状态组件 ,因为需要状态,又改成了class,成本高

useState

import React , {useState} from 'react'
export default function App () {
    const [name,setname] = useState('xiaoming')
    const changeName = ()=>{
        setname('retr0')
    }
    return (
        <div>
            {name}
            <button onClick={changeName}></button>
        </div>
    )
}

useRef

import React, {useRef} from 'react'
export default function App () {
    const ref = useRef(null)
    return (
        <div>
            <input type='text' ref={ref}/>
             <button onClick={()=>{
                       console.log(ref.current)
                }}></button>
        </div>
    )
}

useEffect

Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座。

useEffect(处理函数,[依赖]) 副作用方法:每次依赖改变都会执行一次,如果没有依赖只有一个空数组,那么只会执行一次(有点像componentDidmount)

参数

  1. 回调函数
  2. 数组
    • 不传数组:无法起到只渲染一次的作用
    • 空数组:一次生命周期中只调用一次回调函数
    • [text,name]:如果text状态或者name状态变化,就会调用该回调函数
import React , {useState,useEffect} from 'react'
export default function App () {
    const [name,setname] = useState('xiaoming')
    useEffect(()=>{
        let index =1
        let t = setInterval(()=>{
            console.log(++index)
        },500)
        return ()=>{
            console.log('销毁')
            clearInterval(t)
        }
    },[])
    return (
        <div></div>
    )
}

不要对 Dependencies 撒谎, 如果你明明使用了某个变量,却没有申明在依赖中,你等于向 React撒了谎,后果就是,当依赖的变量改变时,useEffect 也不会再次执行, eslint会报警告

Preview页面改造成函数式组件,在路径上从id=1切换到id=2也会自动重新加载,比class组件方便,class组件需要通过componentWillMout和

useEffect(()=>{
    axios.get(`/articles/${props.match.params.id}`)
    .then(res => {
        settitle(res.data.title)
        setcontent(res.data.content)
        setcategory(res.data.category)
    })},[props])

useCallback

防止因为组件重新渲染,导致方法被重新创建,起到缓存作用; 只有第二个参数变化了,才重新声明一次

import React , {useState,useCallback} from 'react'
export default function App () {
    const [text,settext] = useState('xxx')
    useCallback(()=>{
        console.log(text)
    },[text])
    return (
        <div>
            {text}
            <button onClick={()=>{
                    settext('retr0')
                }}></button>
        </div>
    )
}

useReducer和useContext

redux-react-hook 无缝使用原来的 redux,和中间件 promise,thunk,saga

import React, {useReducer,useContext} from 'react'
import reducer from './reducer'
const GlobalContext = React.createContext()

reducer.js

// 纯函数设计 reudx中的reducer一个概念
const reducer = (prevState,action)=>{
  let {type,payload} = action
  switch(type){
    case "isShow":
      //深复制老状态,返回新状态
    return {
      ...prevState,
      isShow:payload
    } // immutable
    case "list":
      //深复制老状态,返回新状态
    return {
      ...prevState,
      list:payload
    }
  }
}
export default reducer

App.js

const Child1 = ()=>{
    const {state,dispatch} = useContext(GlobalContext)
       return (
        <div>
            <button onClick={()=>{
                    dispatch({
                        type:'isShow',
                        payload:!state.isShow
                    })
                }}></button>
        </div>
    )
}
const Child2 = ()=>{
    
}
export default function App () {
    let [state,dispatch] = useReducer(reducer,{
        isShow:false,
        list:[]
    })
    return (
        <GlobalContext.Provider value={{
                state,
                dispatch
            }}>
            <Child1/>
            {
                state.isShow?<Child2/>:null
            }
        </GlobalContext.Provider>
    )
}

自定义hooks

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。必须以“use”开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则

import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { PageHeader } from 'antd';
import store from '../../mobx/store'
//为preview提供数据
const usePreviewData = (props)=>{
  const [title, setTitle] = useState('')
  const [category, setCategory] = useState('')
  const [content, setContent] = useState('')
  useEffect(() => {
    store.set('isShow', false)
    axios.get(`http://localhost:12138/articles/${props.match.params.myid}`).then(res => {
      let { title, category, content } = res.data
      setTitle(title)
      setCategory(category)
      setContent(content)
    })
  }, [props.match.params.myid])
  return {
    title,
    category,
    content
  }
}
export default function HookPreview(props) {
   // console.log(this.props);//函数式组件从函数的形参中就能拿到属性
  let {title,category,content} = usePreviewData(props)
  return (
    <div>
      <PageHeader
        className="site-page-header"
        onBack={() => {
          props.history.goBack()
        }}
        title={title}
        subTitle={category ? category.join('/') : null}
      />
      <div style={{ padding: '24px' }} dangerouslySetInnerHTML={{
        __html: content
      }}>
      </div>
    </div>
  )
}

React- Redux最主要是用作应用状态的管理

Redux

Redux最主要是用作应用状态的管理。简言之,Redux用一个单独的常量状态树(state对象)保存这一整个应用的状态,这个对象不能直接被改变。当一些数据变化了,一个新的对象就会被创建(使用actions和reducers),这样就可以进行数据追踪,实现时光旅行。

redux介绍及设计和使用的三大原则

  • state以单一对象存储在store对象中
  • state只读(每次都返回一个新对象)
  • 使用纯函数reducer执行state更新

redux工作流

img

我个人粗浅的理解是:
Store的角色是整个应用的数据存储中心,集中大部分页面需要的状态数据;
ActionCreators ,view 层与data层的介质;
Reduce ,接收action并更新Store。
所以流程是 用户通过界面组件 触发ActionCreator,携带Store中的旧State与Action 流向Reducer,Reducer返回新的state,并更新界面。

Action

首先,让我们来给 action 下个定义。

Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

添加新 todo 任务的 action 是这样的:

const ADD_TODO = 'ADD_TODO'
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
样板文件使用提醒

使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。参照 减少样板代码 获取更多保持代码简洁的实践经验。

除了 type 字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议。

这时,我们还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 index 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。

{
  type: TOGGLE_TODO,
  index: 5
}

我们应该尽量减少在 action 中传递的数据。比如上面的例子,传递 index 就比把整个任务对象传过去要好。

最后,再添加一个 action type 来表示当前的任务展示选项。

{
  type: SET_VISIBILITY_FILTER,
  filter: SHOW_COMPLETED
}

Action 创建函数

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。

在 Redux 中的 action 创建函数只是简单的返回一个 action:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

这样做将使 action 创建函数更容易被移植和测试。

传统的 Flux 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样:

function addTodoWithDispatch(text) {
  const action = {
    type: ADD_TODO,
    text
  }
  dispatch(action)
}

不同的是,Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。

dispatch(addTodo(text))
dispatch(completeTodo(index))

或者创建一个 被绑定的 action 创建函数 来自动 dispatch:

const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))

然后直接调用它们:

boundAddTodo(text);
boundCompleteTodo(index);

store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。

Action 创建函数也可以是异步非纯函数。你可以通过阅读 高级教程 中的 异步 action章节,学习如何处理 AJAX 响应和如何把 action 创建函数组合进异步控制流。因为基础教程中包含了阅读高级教程和异步 action 章节所需要的一些重要基础概念, 所以请在移步异步 action 之前, 务必先完成基础教程。

源码

actions.js

/*
 * action 类型
 */

export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * 其它的常量
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action 创建函数
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

设计 State 结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?

以 todo 应用为例,需要保存两种不同的数据:

  • 当前选中的任务过滤条件;
  • 完整的任务列表。

通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
处理 Reducer 关系时的注意事项

开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById: { id -> todo }todos: array 是比较好的方式,本文中为了保持示例简单没有这样处理。

Action 处理

现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

高级篇里会介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 action

我们将以指定 state 的初始状态作为开始。Redux 首次执行时,state 为 undefined,此时我们可借机设置并返回应用的初始 state。

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
};

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState
  }

  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state
}

这里一个技巧是使用 ES6 参数默认值语法 来精简代码。

function todoApp(state = initialState, action) {
  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state
}

现在可以处理 SET_VISIBILITY_FILTER。需要做的只是改变 state 中的 visibilityFilter

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意:

  1. 不要修改 state 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。
  2. default 情况下返回旧的 state遇到未知的 action 时,一定要返回旧的 state
Object.assign 须知

Object.assign() 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它库如 _.assign() 提供的帮助方法。

switch 和样板代码须知

switch 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。

很不幸到现在为止,还有很多人存在一个误区:根据文档中是否使用 switch 来决定是否使用它。如果你不喜欢 switch,完全可以自定义一个 createReducer 函数来接收一个事件处理函数列表,参照“减少样板代码”

处理多个 action

还有两个 action 需要处理。就像我们处理 SET_VISIBILITY_FILTER 一样,我们引入 ADD_TODOTOGGLE_TODO 两个actions 并且扩展我们的 reducer 去处理 ADD_TODO.

import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

如上,不直接修改 state 中的字段,而是返回新对象。新的 todos 对象就相当于旧的 todos 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。

最后,TOGGLE_TODO 的实现也很好理解:

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。(译者注:Javascript中的对象存储时均是由值和指向值的引用两个部分构成。此处突变指直接修改引用所指向的值, 而引用本身保持不变。) 如果经常需要这类的操作,可以选择使用帮助类 React-addons-updateupdeep,或者使用原生支持深度更新的库 Immutable。最后,时刻谨记永远不要在克隆 state 前修改它。

拆分 Reducer

目前的代码看起来有些冗长:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

上面代码能否变得更通俗易懂?这里的 todosvisibilityFilter 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 todos 更新的业务逻辑拆分到一个单独的函数里:

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 *reducer 合成*,它是开发 Redux 应用最基础的模式。

下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 visibilityFilter?当然可以:

首先引用, 让我们使用 ES6 对象结构 去声明 SHOW_ALL:

const { SHOW_ALL } = VisibilityFilters

接下来:

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

现在看起来好多了!随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。

最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

注意上面的写法和下面完全等价:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

ES6 用户使用注意

combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object:

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

由于 import * 还是比较新的语法,为了避免困惑,我们不会在本文档中使用它。但在一些社区示例中你可能会遇到它们。

源码

reducers.js

import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

React- mobx

mobx

https://cn.mobx.js.org/

介绍

  1. Mobx是一个功能强大,上手非常容易的状态管理工具。
  2. Mobx背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。
  3. Mobx利用getter和setter来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件需要重绘,在界面的规模变大的时候,往往会有很多细粒度更 新。

Mobx与redux的区别

  • Mobx写法上更偏向于OOP
  • 对一份数据直接进行修改操作,不需要始终返回一个新的数据
  • 并非单一store,可以多store
  • Redux默认以JavaScript原生对象形式存储数据,而Mobx使 用可观察对象

优点

  • 学习成本小
  • 面向对象编程

缺点

  • 过于自由:Mobx提供的约定及模版代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一
  • 相关的中间件很少,逻辑层业务整合是问题。

observable

import {observable} from 'mobx'

.box

const store = observable.box(true) //生成一个可以观察的布尔值,box只能观察简单数据类型

.map

const store = observable.map({
    isShow:true,
    age:12,
    roleList:[],
    rightList:[]
})
//如果要观察复杂数据类型,需要用map方法

autorun

import {autorun} from 'mobx'
import store from '../../mobx/store'
autorun(()=>{
    console.log(store.get())
})

实例

import { observable, autorun } from 'mobx';
const value = observable.box(0);
const number = observable.box(100);
autorun(() => {
    console.log(value.get());
});
value.set(1);
value.set(2);
number.set(101);
//0,1,2。 // autorun 使用到才能被执行
//只能是同步,异步需要处理
//观察对象,通过map
const map = observable.map({ key: "value"});
//map.set("key", "new value");
//map.get("key")
//观察对象,不通过map
const map = observable({ key: "value"});
// map.key map.key="xiaoming"
//观察数组
const list = observable([1, 2, 4]);
list[2] = 3;

mobx取消绑定

componentWillMount () {
    this.cancel = autorun(()=>{
        this.setState({
            isShow:store.get('isShow')
        })
    })
}
componentWillUnMount () {
    this.cancel()
    //或者直接更改setState函数,这样就不会触发上一个组件销毁,但上一个组件中定义的autorun没销毁,导致进行setState操作,但上个组件已经销毁,因此会报一个warning:无法设置一个状态到unmount的组件
}

React- redux-persist实现redux持久化,存到localStorage

redux-persist

实现redux持久化,存到localStorage

安装

npm install redux-persist

基础使用

// configureStore.js

import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web

import rootReducer from './reducers'

const persistConfig = {
  key: 'root',
  storage,
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

export default () => {
  let store = createStore(persistedReducer)
  let persistor = persistStore(store)
  return { store, persistor }
}

store.js

import {createStore,applyMiddleware,combineReducers,compose } from 'redux' //createStore 方法创建一个store对象
//创建一个reducer,“修改状态”,(老状态,修改的值)深复制之后,再返回一个新的状态
import reduxThunk from 'redux-thunk'
import reduxPromise from 'redux-promise'
import roleListReducer from './reducess/role'
import rightListReducer from './reducess/right'
import sliderReducer from './reducess/slider'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
const persistConfig = {
  key: 'root',
  storage,
}

const reducer = combineReducers({
  isCollapsed : sliderReducer,
  rightList:rightListReducer,
  roleList:roleListReducer
})
const persistedReducer = persistReducer(persistConfig, reducer) //把reducer进行包裹


//store,只能接受一个reducer
//app开发,只能有一个store
//轮船运货车(reducer拆开一个一个,每个reducer一个文件,combineReducer:合并所有的reducer)
//redux devtools的配置:Advanced store setup
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(persistedReducer,/* preloadedState, */ composeEnhancers(applyMiddleware(reduxThunk,reduxPromise)))
const persistor = persistStore(store)
//默认action是普通对象
//创建store,顺便应用中间件thunk,如果action是函数,我来处理
//创建store,顺便应用中间件promise,那么action就变成了promise对象
export  {
  store,
  persistor
}

React-immutable

immutable

https://github.com/immutable-js/immutable-js

介绍

每次修改一个 Immutable 对象时都会创建一个新的不可变的对象,在新对象上操作并不会影响到原对象的数据。这个库的实现是深拷贝还是浅拷贝?

深拷贝和浅拷贝的关系

  1. var arr = {};
    var arr2 = arr
    //浅拷贝,引用地址相同
    
  2. Object.assign() 只是一级属性复制,比浅拷贝多深拷贝了一层而已。

  3. const obj1 = JSON.parse(JSON.stringify(obj))
    //数组,对象都好用(缺点: 不能有undefined)
    

Immutable优化性能的方式

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构), 也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

https://upload-images.jianshu.io/upload_images/2165169-cebb05bca02f1772

import {Map} from 'immutable'
let a = Map({
    select:'users',
    filter:Map({name:'Cam'})
})
let b = a.set('select','people');
a===b; //false
a.get('filter') === b.get('filter'); // true

延伸:如果上述select 属性 给一个组件用,因为此值改变了,shouldComponentUpdate 应该返回true, 而filter 属性给另一个组件用,通过判断并无变化,shouldComponentUpdate 应该返回false,此组件就避免了重复进行diff对比

Immutable中常用类型(Map,List)

Map

const {Map} = require("immutable")
const preState = {
  a:1,
  b:2
}
let map1 = Map(preState)
let map2 = map1.set('b','3')
console.log(map1.toJS(),map2.toJS());
//{ a: 1, b: 2 } { a: 1, b: '3' }

List

const {List} = require("immutable")
const preState = ['111','222','333']
let arr1 = List(preState)
//List内置绝大部分的数组方法
let arr2 = arr1.push(444)
console.log(arr1.toJS(),arr2.toJS());
//['111','222','333']   ['111','222','333',444]

merge

const {Map} = require("immutable")
const preState = {
  a:1,
  b:2
}
let map1 = Map(preState)
let map2 = map1.merge({name:'admin'})
console.log(map1.toJS(),map2.toJS());
//{ a: 1, b: 2 } { a: 1, b: 2, name: 'admin' }

concat

const {List} = require("immutable")
const preState = ['111','222','333']
let arr1 = List(preState)
//List内置绝大部分的数组方法
let arr2 = arr1.concat([444,555,666])
console.log(arr1.toJS(),arr2.toJS());
//['111','222','333'] ['111','222','333',444,555,666]

fromJS

const {fromJS} = require("immutable")
const prevState = {
  name:'retr0',
  age:1,
  location:{
    city:1,
    sss:2,
    ddd:{
      aaa:34
    }
  }
} 
let map1 = fromJS(prevState)
let map2 = map1.setIn(['location','city'],231231)
console.log(map2.toJS());
/*
    {
      name: 'retr0',
      age: 1,
      location: { city: 231231, sss: 2, ddd: { aaa: 34 } }
      }
*/

Immutable+Redux的开发方式

import {fromJS} from 'immutable'
const righteducer = (prevState=[],action)=>{
  let {type,payload} = action;
  switch(type){
    case "right":
      let newState = fromJS(prevState).concat(payload).toJS()
      return newState
    default:return prevState
  }
  
}
export default righteducer

缺点

  1. 容易跟原生混淆
  2. 文档与调试不方便

React-redux-thunk

redux-thunk

redux-promise

安装

npm i --save redux-thunk

引入

import ReduxThunk from 'redux-promise'

store.js

import {createStore,applyMiddleware} from 'redux'
const reducer = ()=>{
    ....
}
const store = createStore(reducer,applyMiddleware(ReduxThunk))

组件.js

import React,{Component} from 'react'
import store from '.....'

class App extends Component {
    actionCreator = ()=>{
        return dispatch =>{  //返回一个函数,并传了一个dispath函数到回调函数的形参中
            axios....then(res=>{
                dispatch(...) //就可以在任意自己想return的地方运行dispatch函数
            })
        }
    }
    componentWillMount(){
        if(store.getState().xxxx.length===0){
            //  store.dispath只能接受一个普通对象
            store.dispatch(this.actionCreator())
        }else{
               this.setState({
                xxx:store.getState().xxxx
            })
        }
        //要抓第一次redux改变的的时候,因此要订阅一下
        this.unsub = store.subscribe({
            this.setState({
                ...:store.getState().xxxx
            })
        })
    }
    render () {
        return (
            <div></div>
        )
    }
}

React-redux-promise

redux-promise

安装

npm i --save redux-promise

引入

import ReduxPromise from 'redux-promise'

store.js

import {createStore,applyMiddleware} from 'redux'
const reducer = ()=>{
    ....
}
const store = createStore(reducer,applyMiddleware(ReduxPromise))

组件.js

import React,{Component} from 'react'
import store from '.....'

class App extends Component {
    actionCreator = ()=>{
        return ( //返回一个promise对象
            axios.....
        )
    }
    componentWillMount(){
        if(store.getState().xxxx.length===0){
            //  store.dispath只能接受一个普通对象
            store.dispatch(this.actionCreator())
            .then(res=>{
                this.setState({
                     ...:res.payload
                })
            })
        }else{
               this.setState({
                xxx:store.getState().xxxx
            })
        }
    }
    render () {
        return (
            <div></div>
        )
    }
}

React-React-Radux

React-Radux

实际项目中,需要权衡是直接使用Redux还是用React-Redux。
React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)

UI组件

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API

容器组件

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API

UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

connect()

import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

上面VisibleTodoList 便是通过UI组件TodoList,通过connect方法自动生成的容器组件。
但需要定义业务逻辑,组件才有意义。

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

mapStateToProps()

它是一个函数,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。
mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数,后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。

mapDispatchToProps()

mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

  • 是函数则会得到dispatch和ownProps(容器组件的props对象)两个参数。
const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

  • 是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。
const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

组件

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。
React-Redux 提供Provider组件,可以让容器组件拿到state。

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

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

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。

简单计数器

import React, { Component } from 'react'
import PropTypes from 'prop-types'   //类型检查
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'

// 定义counter组件
class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    // const value = this.props.value
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}> +1</button>
      </div>
    )
  }
}
//对Counter组件接受的props进行类型检查
Counter.propTypes = {
  value: PropTypes.number.isRequired,   //要求数字类型,没有提供会警告
  onIncreaseClick: PropTypes.func.isRequired //要求函数类型
}

// Action  
const increaseAction = { type: 'increase' }

// Reducer   基于原有state根据action得到新的state
function counter(state = { count: 0 }, action) {
  const count = state.count
  switch (action.type) {
    case 'increase':
      return { count: count + 1 }
    default:
      return state
  }
}

// 根据reducer函数通过createStore()创建store
const store = createStore(counter)

//  将state映射到Counter组件的props
function mapStateToProps(state) {
  return {
    value: state.count
  }
}

//  将action映射到Counter组件的props
function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}

//  传入上面两个函数参数,将Counter组件变为App组件
const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

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