Vue:vuex

简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式(插件)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

嗯,就是把一些全局性的代码放到Vuex中进行保管。每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation 这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

为什么要使用Vuex

假如有一个场景:整站(若干个组件构成)有很多数据和状态,组件和组件之间又会共享一些数据和状态,比如用户的登录状态、购物车数据同步等,那么在其中一个组件中改变它们的状态时,其他组件要利用watch或者computed来进行数据的修改,这样做的缺点是当状态复杂的时候,调用的组件非常多,那要挨个去通知组件进行数据同步,过程十分繁琐和麻烦。这时候Vuex就出来了。

这是Vuex的理念图:

Vue Components 通过调用 Actions来控制Mutations(动作),Mutations控制数据中心的State(状态),在状态改变之后,再次渲染到Vue Components中,整个过程就完成了。

在整个过程中,Actions用来执行异步操作,比如调用后台的API,根据响应结果再决定什么时候调用同步操作MutationsMutationsActions的区别就是前者属于同步,Actions属于异步。并且这个过程是单向的。

如果是一个简单的项目,就无需使用Vuex。

安装

在 Vue 之后引入 vuex 会进行自动安装:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

npm:

npm install vuex --save-dev

整个过程和vue-router一样

开始

第一步:

import Vuex from "vuex";

第二步:

Vue.use(Vuex);

第三步:

new Vuex.Store({

});

在进行了Vue插件三步曲之后,就正式开始Vuex的踏坑之路

注意

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation

每个应用将仅仅包含一个 store 实例,因为state作为数据源必须是唯一的。

一个简单的案例

/vuex/index.js:

import Vuex from "vuex"; // 1.引入
import Vue from "vue";

Vue.use(Vuex); // 2.注册

export default new Vuex.Store({ // 3.实例化
  // 1.数据中心
  state: {
    price: 0
  },
  // 2.更新方法
  mutations: {
    // 接收两个参数,一个是数据中心,一个是修改的值
    purchase (state, price) {
      state.price += price;
    },
    sold (state, price) {
      state.price -= price
      if (state.price < 0) {
        state.price = 0;
      }
    }
  }
});

main.js:

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './vuex'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store, // 4.注入,所有的子组件都能够通过this.$store拿到此对象
  components: { App },
  template: '<App/>'
})

App.vue:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    
    <p>总价:{{price}}</p>
    <myvuex></myvuex>
  </div>
</template>

<script>
  import zeng from './components/hello.vue'
  import test from './components/test.vue'
  import myvuex from './components/MyVuex.vue'
  import { mapState } from 'vuex' // 辅助函数

  export default {
    name: 'App',
    components: {
      zeng /* 简写形式 ->> zeng: zeng */,
      test,
      myvuex
    },
    computed: mapState(['price']), // 6.消费
    methods: {}
  }
</script>

<style>
  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

MyVuex.vue:

<template>
  <div>
    <!--<p>总价:{{this.$store.state.price}}</p>-->
    <button @click="purchase">购买</button>
    <button @click="sold">出售</button>
  </div>
</template>

<script>
  export default {
    methods: {
      purchase () {
        this.$store.commit('purchase', 5) // 通过commit方法控制mutations中的方法
      },
      sold () {
        this.$store.commit('sold', 2);
      }
    }
  }
</script>

state和mapState

state就是一个数据源。

mapState就是为了在子组件中更加方便的取出state中的值

之前每次通过计算属性动态更新state里面的内容,那样都是创建一个个方法进行更新,如果属性多了,就会很麻烦,所以有了辅助函数mapState,还有其他的map...,用到了再说。

对象形式:

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex' // 这一步是必须要的

export default {
  // ...
  data () {
    localCount: 2
  },
  computed: mapState({
    // 箭头函数可使代码更简练,接收一个数据中心的参数
    count: state => state.price,

    // 传字符串参数 'price' 等同于 `state => state.price`
    countAlias: 'price',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数,因为箭头函数是没有上下文信息的
    countPlusLocalState (state) {
      return state.price + this.localCount
    }
  })
}

数组形式:

computed: mapState([
  // 映射 this.price 为 store.state.price
  'count'
])

mapState中使用的字符串都是可以映射到state里面的同名属性,就像上面那个案例一样,在state里面注册了一个属性price之后,可以通过mapState直接写price就能把stateprice的值原样搬过来:

computed: mapState([
  // 映射 this.price 为 store.state.price
  'price'
])

// ↓↓↓↓↓ 等同于 ↓↓↓↓↓↓↓

computed: function() {
    price: function () {
        return this.$store.state.price;
    }
}

上面这么写,computed只能写一个计算属性,如果还想把computed换成对象执行则需这么写:

computed: {
  ...mapState(['price']), // 如果不加扩展运算符的话,computed 是不能写成对象形式的,
  name : () => this.name + '.'
}
注意

即使state能够保存所有数据,但是那样会使代码显得冗长和不直观,所以还是需要将一些变量属性作为组件局部属性存放。

getters和mapGetters

getters可以认为是store中的计算属性,相信有点基础的Vue看官都很熟悉计算属性了。但是不同于之前的计算属性,它只能是function形式,所以不能给计算属性设置set

特别要注意这句话:

有时候我们需要从 store 中的 state 中派生出一些状态

这是引用官网的一句话,意思很明显,虽然getters接收了一个state参数,但是我们不能直接在getters中去把state的值给修改了,如果忘记了这件事,你可以看看文章开头

store中定义getters

new Vuex.Store({
    state: {
        price: 0
    },
    getters: {
        updatedPrice (state, getters) { <!-- 接收两个参数 1.数据中心 2.getters本身 -->
            return state.price + 1;
        }
    }
})

在组件中使用,第一种形式:

this.$store.getters.updatedPrice

第二种形式就是靠它的辅助函数mapGetters

// 1.computed: mapGetters(['updatedPrice']) ->> {{updatedPrice}}
// 2.computed: mapGetters({ uPrice: 'updatedPrice' }) ->> {{uPrice}}
// 3.computed: { ...mapGetters(['updatedPrice']) } ->> {{updatedPrice}}

mutations

mutations相当于是事件,并且再次说一次,它里面的回调函数都是同步!同步!同步的!!

每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,第二个参数是一些额外的值,一般来说是一个对象:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state, obj) { // 事件注册
      // 变更状态
      state.count += obj.age
    }
  }
})

必须用commit触发mutations事件:

// 1.store.commit('increment', { age: 21 }) // 事件使用
// 2.store.commit({ 'increment', age: 21, name: '曾小晨' })
注意
  1. 最好提前在你的 store 中初始化好所有所需属性。
  2. 当需要在对象上添加新属性时,你应该:使用 Vue.set(obj, 'newProp', 123), 或者以新对象替换老对象。
// obj => { name: 'zeng', age: 21 }
state.obj = { ...state.obj, newProp: 123 }
// ↓↓↓↓↓ 等同于 ↓↓↓↓↓
state.obj = { name: 'zeng', age: 21, newProp: 123 }

使用常量代替事件类型

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'

// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

辅助函数mapMutations

mapStatemapGetters是一样的,只不过mapMutations是在methods属性里。

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

actions

actionsmutations可以说是孪生兄弟,不同的是:

  • actions提交的是mutationsactions担任着中转站的角色),真正的状态改变还是由mutations去完成。
  • actions可以包含任意的异步操作,mutations是则是同步。

注册actions

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

首先actions中的每个函数都可以接收一个context参数,这个对象跟store拥有着一样的功能,但是context != store,至于为什么,到时候就知道了。

actions: {
  increment ({ commit }) { // 对象解构语法
    commit('increment')
  }
}

使用actions

this.$store.dispatch('increment');

看着会不会想:这样不是多此一举吗?就不能干掉中介者,然后让客户和厂家面对面沟通吗?

答案是不能,原因就是说的都快烦了的 mutations的同步机制。如果没有actions就无法完成异步操作。

辅助函数mapActions

mapStatemapGetters是一样的,只不过mapActions是在methods属性里。

export default {
  // ...
  methods: {
    // mutations
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    }),
    
    // actions
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

组合Actions

有时候可能会因为一个action的执行而触发另外一个action,还有,如何知道action什么时候结束呢?

首先,要知道的是store.dispatch可以处理被出发的action的处理函数返回的Promise,并且store.dispatch同样也返回Promise对象:

export default new Vuex.Store({
    // ...
    actions: {
        actionA ({commit}) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    commit('someMutation');
                    resolve()
                }, 1000);
            })
        }
    }
})

现在你就可以:

this.$store.dispatch('actionA')
    // 成功
    .then()
    // 失败
    .catch();

在另外一个action里也可以:

export default new Vuex.Store({
    // ...
    actions: {
        actionA ({commit}) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    commit('someMutation');
                    resolve()
                }, 1000);
            })
        },
        actionB ({commit}) {
            dispatch('actionA').then(() => {
                // 执行成功之后调用Mutations的方法
                commit('someOtherMutation');
            });
        }
    }
})

modules

引用官网的一句话:

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决这个问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

console.log(store.commit('purchase', 100))
console.log(store.dispatch('user2'))
console.log(store.getters.updatePrice)
console.log(store.state.b.age)

// 同时用它们辅助函数的地方也需要一些改变
// ...mapState('username') ->> {{usernmae}} 换成 ...mapState('a') ->> {{a.username}},更多方法下面有讲到

模块局部状态

对于局部内部的mutationsgetter,接收的第一个参数是局部状态对象:

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

对于局部的action,局部状态通过context.state暴露出来,根节点的状态为:context.rootState

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于局部的getters,局部状态会在第三个参数中暴露:

getters: {
    sumWithRootCount (state, getters, rootState) {
        return state.count + rootState.count
    }
}

命名空间

在这个知识点的一开始的例子中写到:

console.log(store.commit('purchase', 100))
console.log(store.dispatch('user2'))
console.log(store.getters.updatePrice)
console.log(store.state.b.age)

这么写的话,actionsgettersmutations都是类似全局的,万一模块和模块之间出现了相同的名字,那么两个都会执行,这根本不是我们想要的,所以有了namespeced的出现:

const USER = "user2";

export default {
  namespaced: true,
  state: {
    age: 21
  },
  actions: {
    [USER] (context) {
      console.log('moduleB -> actions, age = ' + context.state.age, context.rootState.num)
    }
  },
  mutations: {
    testMutations (state, newValue) {
      console.log('moduleB -> mutations, newValue = ' + newValue)
    }
  },
  getters: {
    testGetter (state, getters, rootState) {
      console.log('moduleB -> getters')
    }
  }
}

那么访问也不能再像第一个例子那样了:

console.log(store.commit('b/testMutations', 100)) // 前缀b
console.log(store.dispatch('b/user2'))
console.log(store.getters['b/testGetter'])
console.log(store.state.b.age)

再来一个例子,多多益善:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

在命名空间内访问全局内容

如果你想访问全局的stategettersrootStaterootGetter会作为第三、第四参数传给getters,同时actions接收的context对象也包含了这两个属性,通过context.rootState进行访问

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

上面这是官网的,我觉得这么说更容易理解一些:

如果需要在命名空间内操作全局的action或者mutations的话,需要配置第三个参数,如果不需要传第二个参数,配置为null就行:

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

带命名空间的辅助函数

将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  // 在全局modules注册了一个模块叫a ->> a: moduleA
  ...mapActions('a', [
    'foo',
    'bar'
  ]) // ->> a/foo, a/bar
}

而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex' // 引入此方法

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module') // 传入命名空间的路径,返回此命名空间对应的模块下的所有辅助函数,包括 mapState, mapGetters, mapActions, mapMutations

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({ // 这里使用的mapState就是上面createNamespacedHelpers创建的对象的mapState
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

async/await

  • async/await 是一种编写异步代码的新方法。之前异步代码的方案是回调和 Promise
  • async/await 是建立在 Promise 的基础上,同时也有非阻塞的特点。

async标明函数是异步函数,并且函数会把返回值通过Promise.resolve()包装成一个Promise对象

async function() {
    let hel = await "hello world";
    return hel;  // ->> return new Promise.resolve(hel);
}

await表示要“等待”异步操作返回值,它等待的对象也可以是一个Promise,但是可以不必写.then().catch(),异常用try/catch捕捉(await对于等待的返回值没有特殊要求,可以是Promise,也可以是其他对象)

var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            // 模拟出错了,返回 ‘error’
            reject('error');
        }, time);
    })
};

var start = async function () {
    try {
        console.log('start');
        await sleep(3000); // 这里得到了一个返回错误
        
        // 所以以下代码不会被执行了
        console.log('end');
    } catch (err) {
        console.log(err); // 这里捕捉到错误 `error`
    }
};

await只能用在被async修饰的函数里:

// ..省略以上代码

let one2ten = [1,2,3,4,5,6,7,8,9,10];

// 错误示范
one2ten.forEach(function (v) {
    console.log(`当前是第${v}次等待..`);
    await sleep(1000); // 错误!! await只能在async函数中运行
});

// 正确示范
for(var v of one2ten) {
    console.log(`当前是第${v}次等待..`);
    await sleep(1000); // 正确, for循环的上下文还在async函数中
}
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

执行顺序

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面JS栈(后面会详述)的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await

后面表达式的返回值,如果返回值为非Promise则继续执行async函数后面的代码,否则将返回的Promise放入Promise队列(PromiseJob Queue

function testSometing() {
    console.log("执行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("执行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing(); //关键点1
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve) => {
  console.log("promise start..."); // 5
  setTimeout(() => console.log('promise -> setTimeout'), 1000)// 最后执行 15
  console.log("promise end..."); // 6
  resolve("promise");
}); // 关键点2
promise.then((val)=> console.log(val));

console.log("test end...")

当程序执行到const v1 = await testSometing();时,先执行testSometing()后跳出async函数,并执行之后的JS代码(执行下一个任务),当执行完后面的JS任务,又会回到await上继续往下执行(一直按照这个规矩执行)。

这个参考async/await 执行顺序详解和下文的事件队列进行学习。

好处

优化了Promise调用链

它的出现解决了.then()调用链,单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

错误处理

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  }
  catch (err) {
    console.log(err)
  }
}

条件判断

想象一下这样的业务需求:我们需要先拉取数据,然后根据响应的数据结果判断是否输出此数据,或者根据数据内容拉取更多的信息。如下:

const makeRequest = () => {
    return getJSON()
        .then(data => {
            if (data.needsAnotherRequest) {
                return makeAnotherRequest(data)
                        .then(moreData => {
                            console.log(moreData)
                            return moreData
                        })
            } 
            else {
                console.log(data)
                return data
            }
        })
}

async

const makeRequest = async () => {
    const data = await getJSON()
    if (data.needsAnotherRequest) {
        const moreData = await makeAnotherRequest(data);
        console.log(moreData)
        return moreData
    } 
    else {
        console.log(data)
        return data    
    }
}

中间值Intermediate values

一个经常出现的场景是,我们先调起promise1,然后根据返回值,调用promise2,之后再根据这两个Promises得值,调取promise3。使用Promise,我们不难实现:

const makeRequest = () => {
    return promise1()
        .then(value1 => {
            // do something
            return promise2(value1)
                .then(value2 => {
                    // do something          
                    return promise3(value1, value2)
                })
        })
}

又或者用Promise.all()实现:

const makeRequest = () => {
    return promise1()
        .then(value1 => {
            // do something
            return Promise.all([value1, promise2(value1)])
        })
        .then(([value1, value2]) => {
            // do something          
            return promise3(value1, value2)
        })
}

但最终不如这样:

const makeRequest = async () => {
    const value1 = await promise1()
    const value2 = await promise2(value1)
    return promise3(value1, value2)
}

错误堆栈信息Error stacks

想象一下我们链式调用了很多promises,一级接一级。紧接着,这条promises链中某处出错,如下:

const makeRequest = () => {
    return callAPromise()
        .then(() => callAPromise())
        .then(() => callAPromise())
        .then(() => callAPromise())
        .then(() => callAPromise())
        .then(() => {
            throw new Error("oops");
        })
}

makeRequest()
    .catch(err => {
        console.log(err);
        // output
        // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
    })

此链条的错误堆栈信息并没用线索指示错误到底出现在哪里。更糟糕的事,他还会误导开发者:错误信息中唯一出现的函数名称其实根本就是无辜的。
我们再看一下async/await的展现:

const makeRequest = async () => {
    await callAPromise()
    await callAPromise()
    await callAPromise()
    await callAPromise()
    await callAPromise()
    throw new Error("oops");
}

makeRequest()
    .catch(err => {
        console.log(err);
        // output
        // Error: oops at makeRequest (index.js:7:9)
    })

也许这样的对比,对于在本地开发阶段区别不是很大。但是想象一下在服务器端,线上代码的错误日志情况下,将会变得非常有意义。你一定会觉得上面这样的错误信息,比“错误出自一个then的then的then。。。”有用的多。

结合这几个好处来说,就是优化了代码,让代码的可读性更高~

事件队列

先看第一个案例的运行结果:

console.log("script start");

setTimeout(function () {
    console.log("setTimeout");
}, 1000);

console.log("script end");

// script start
// script end
// ->> 延迟1秒
// setTimeout

在上面这个例子中,当我们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束之后事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务过后便将任务压入执行栈(stack)当中,执行栈执行任务 ,输出 setTimeout

事件队列: 是一个存储着待执行的任务的队列,任务的执行时严格按照顺序执行的,头先出尾后出。

执行栈: 类似于函数调用栈的运行容器,当执行栈等于空时,JS引擎就会检查事件队列,如果事件队列不为空的话,事件队列中的第一个任务便会压入执行栈中执行

把上面的案例进行改造:

console.log("script start");

setTimeout(function () {
    console.log("setTimeout");
}, 0); // 1000 ->> 0

console.log("script end");

// script start
// script end
// setTimeout

打印顺序还是没有变,是因为每个浏览器都有可能存在一个最小延迟时间,有的是 15ms,有的是 10ms,这个在很多书当中都有提到,这可能会给同学们造成一种错觉:由于程序运行速度很快,并且有最小延迟时间,所以 setTimeout 会在 script end 之后输出。

还需要知道的是:

setTimeout的回调函数只是会被添加到事件队列中,但是不会立即进入执行栈执行,由于当前的任务没有执行结束,执行栈不为空,所有下一个任务(setTimeout)不会执行,直到输出了script end(当前任务结束)之后才会执行。

有个问题,那么在执行函数主体的时间超过了函数内部setTimeout设置的延迟时间,那会怎么样?

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 6000);

// (相当于给当前函数主体的任务延迟了n秒) > (6秒)
for (var i = 0; i < 800000000; i++) {
  //do something
}

console.log("script end")

当控制台输出 script start 之后延迟了n秒,随后输出了 script end ,紧接着输出了 setTimeout,也就是说延迟了6秒把回调函数添加到了事件队列,等到函数主体执行完毕之后执行的setTimeout任务。

事件队列有什么作用

通过以上的 demo 相信同学们都会对事件队列和执行栈有了一个基本的认识,那么事件队列有何作用?最简单易懂的一点就是之前我们所提到的异步问题。由于 JS 是单线程的,同步执行任务会造成浏览器的阻塞,所以我们将 JS 分成一个又一个的任务,通过不停的循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。所以事件循环的运行机制大致分为以下步骤:

  1. JS引擎对事件队列进行检查,如果为空,则继续检查;如果不为空,则执行步骤 2。
  2. 取出事件队列的首个任务,压入执行栈。
  3. 执行任务。
  4. 检查执行栈,如果执行栈为空,则调回第一步;如果不为空,则继续检查往下执行

事件队列注意点

  • 事件队列是严格按照时间的先后顺序将任务压入执行栈执行。
  • 当执行栈不为空的时候,取出第一个任务执行;如果为空,则一直检查事件队列。
  • 每一个任务完成之后,都会对页面进行重新的渲染

推荐文章(async/await)

async

await

理解 JavaScript 的 async/await

深入理解 JavaScript 事件循环(一)— event loop

Last modification:February 23rd, 2018 at 01:26 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment