前端模拟面试练习提升题

一、前言

 

在聊之前,大家要始终记得一句话:一切前端概念,都是纸老虎。

 

不管是Vue,还是 React,都需要管理状态(state),比如组件之间都有共享状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或者一个组件需要改变另一个组件的状态,都是共享状态。

 

如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单理解为会搞得很乱就对了。

 

在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到 filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文件了,我们要找所有的视图,直接从 view 文件夹里找就行。

 

根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。

 

 

 

二、什么是vuex

 

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

 

大白话:对数据(data)统一的管理,如果涉及到了数据的处理,到vuex里面进出吧!就像是超市对商品的统一管理一样。

 

设计思想:Vuex 全局维护着一个对象,使用到了单例设计模式。在这个全局对象中,所有属性都是响应式的,任意属性进行了改变,都会造成使用到该属性的组件进行更新。并且只能通过 commit 的方式改变状态,实现了单向数据流模式。

 

 

 

三、源码解读

 

store 对象中有一个属性叫 state。state 包含了全部的应用层级状态。应用中的各个组件若使用了 state,则会保持与同步最新的状态。state 就好比是 vue 中的 data,但它是整个应用的 data。其实在想vuex是不是和vue的实现双向数据绑定一样,做了 object.defineproperty() 的处理。

 

我们从 store 对象被实例化的代码中开始。

 

import Vue from 'vue';

import Vuex from 'vuex';

 

Vue.use(Vuex);

 

const store = new Vuex.Store({

  state: {

    count: 0

  }

});

 

new Vue({

  el: '#app',

  store,

  // ...

});

store 对象是通过 new Vuex.Store 被实例化出来的,打开 src/index.js 文件,最下面的代码中可以看到 vuex 暴露出的 Store 这个 API:

 

export default {

  Store,

  install,

  mapState,

  mapMutations,

  mapGetters,

  mapActions

}

 

 

3.1 store

 

找到 Store 这一个类,挺长的一大段代码。老办法,全部方法折叠不看,只看构造函数 constructor。过滤掉与 state 无关的代码,最后可以把代码简化成这样:

 

constructor (options = {}) {

  const {

    state = {}

  } = options

 

  // init root module.

  // this also recursively registers all sub-modules

  // and collects all module getters inside this._wrappedGetters

  installModule(this, state, [], options)

 

  // initialize the store vm, which is responsible for the reactivity

  // (also registers _wrappedGetters as computed properties)

  resetStoreVM(this, state)

}

瞬间代码少了好多,接下来只需要关注installModule与resetStoreVM两个方法的实现即可。紧接着,再透露一个好消息,installModule看了一眼,是关于 module 的初始化,等以后研究module再看也不迟,所以只需要研究resetStoreVM即可。

 

 

 

3.2 resetStoreVM

 

定位到 resetStoreVM 方法,再次过滤,其中 Vue.config.silent 去 vue 的官网上搜了一下,是取消 vue 所有的日志与警告的功能,所以也过滤掉,剩下代码如下:

 

function resetStoreVM (store, state) {

  // use a Vue instance to store the state tree

  store._vm = new Vue({

    data: { state }

  })

}

resetStoreVM 函数做的事情就是给 store 添加一个 _vm 属性,并将 state 作为一个 vue 对象的 data,最后将这个 vue 对象赋值给 _vm。所以到这里我们知道了 store 类的构造函数为其添加了一个 _vm 属性。

 

 

 

3.3 set和get

 

构造函数解析完毕,接下来得继续找跟 state 相关的方法,于是找到了 set 和 get 方法:

 

get state () {

  return this._vm.state

}

 

set state (v) {

  assert(false, `Use store.replaceState() to explicit replace store state.`)

}

set 方法没什么好说的,意思就是 store 不给你直接修改 state(但其实是可以修改 state 对象的属性,先不管那么多了)。

 

get方法返回了刚刚构造函数添加的 _vm 属性的一个 data(state)。阅读到这里,这下我们应该知道为什么组件 a 修改了 state.count,组件 b 也会跟着变了吧。因为 state 就是一个新的 vue 对象里 data 的一个属性啊。

 

 

 

 四、常见的API

 

4.1 commit 解析

 

如果需要改变状态的话,一般都会使用 commit 去操作,接下来让我们来看看 commit 是如何实现状态的改变的。

 

commit(_type, _payload, _options) {

  // 检查传入的参数

  const { type, payload, options } = unifyObjectStyle(

    _type,

    _payload,

    _options

  )

 

  const mutation = { type, payload }

  // 找到对应的 mutation 函数

  const entry = this._mutations[type]

  // 判断是否找到

  if (!entry) {

    if (process.env.NODE_ENV !== 'production') {

      console.error(`[vuex] unknown mutation type: ${type}`)

    }

    return

  }

  // _withCommit 函数将 _committing

  // 设置为 TRUE,保证在 strict 模式下

  // 只能 commit 改变状态

  this._withCommit(() => {

    entry.forEach(function commitIterator(handler) {

      // entry.push(function wrappedMutationHandler(payload) {

      //   handler.call(store, local.state, payload)

      // })

      // handle 就是 wrappedMutationHandler 函数

      // wrappedMutationHandler 内部就是调用

      // 对于的 mutation 函数

      handler(payload)

    })

  })

  // 执行订阅函数

  this._subscribers.forEach(sub => sub(mutation, this.state))

}

 

 

4.2 dispatch解析

 

如果需要异步改变状态,就需要通过 dispatch 的方式去实现。在 dispatch 调用的 commit 函数都是重写过的,会找到模块内的 mutation 函数。

 

dispatch(_type, _payload) {

  // 检查传入的参数

  const { type, payload } = unifyObjectStyle(_type, _payload)

 

  const action = { type, payload }

  // 找到对于的 action 函数

  const entry = this._actions[type]

  // 判断是否找到

  if (!entry) {

    if (process.env.NODE_ENV !== 'production') {

      console.error(`[vuex] unknown action type: ${type}`)

    }

    return

  }

  // 触发订阅函数

  this._actionSubscribers.forEach(sub => sub(action, this.state))

 

  // 在注册 action 的时候,会将函数返回值

  // 处理成 promise,当 promise 全部

  // resolve 后,就会执行 Promise.all

  // 里的函数

  return entry.length > 1

    ? Promise.all(entry.map(handler => handler(payload)))

    : entry[0](payload)

}

 

 

4.3 各种语法糖

 

在组件中,如果想正常使用 Vuex 的功能,经常需要这样调用this.$store.state.xxx的方式,引来了很多的不便。为此,Vuex 引入了语法糖的功能,让我们可以通过简单的方式来实现上述的功能。以下以mapState为例,其他的几个 map 都是差不多的原理,就不一一解析了。

 

function normalizeNamespace(fn) {

  return (namespace, map) => {

    // 函数作用很简单

    // 根据参数生成 namespace

    if (typeof namespace !== 'string') {

      map = namespace

      namespace = ''

    } else if (namespace.charAt(namespace.length - 1) !== '/') {

      namespace += '/'

    }

    return fn(namespace, map)

  }

}

// 执行 mapState 就是执行

// normalizeNamespace 返回的函数

export const mapState = normalizeNamespace((namespace, states) => {

  const res = {}

  // normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]

  // normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]

  // function normalizeMap(map) {

  //   return Array.isArray(map)

  //     ? map.map(key => ({ key, val: key }))

  //     : Object.keys(map).map(key => ({ key, val: map[key] }))

  // }

  // states 参数可以参入数组或者对象类型

  normalizeMap(states).forEach(({ key, val }) => {

    res[key] = function mappedState() {

      let state = this.$store.state

      let getters = this.$store.getters

      if (namespace) {

        // 获得对应的模块

        const module = getModuleByNamespace(this.$store, 'mapState', namespace)

        if (!module) {

          return

        }

        state = module.context.state

        getters = module.context.getters

      }

      // 返回 State

      return typeof val === 'function'

        ? val.call(this, state, getters)

        : state[val]

    }

    // mark vuex getter for devtools

    res[key].vuex = true

  })

  return res

})

 

 

五、总结

 

以上是 Vuex 的源码解析,虽然 Vuex 的整体代码并不多,但是却是个值得阅读的项目。了解了 store 的 state,知道它是通过 new 一个新的 vue 对象 _vm 来监听的,而这个 _vm 又是绑在 store 上的。所以通过这一系列的关系,最后我们能在各个组件中使用到被监听的 this.$store.state。

--------------------------------------------------------

智一面|前端面试必备练习题