MrDream

MrDream 查看完整档案

杭州编辑燕山大学  |  生物医学工程 编辑杭州攻壳科技  |  前端工程师 编辑 mrdream.xyz 编辑
编辑

爱恋不过是一场高烧,思念是紧跟着的好不了的咳。

个人动态

MrDream 赞了文章 · 2019-03-08

30分钟精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?
——拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。

你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?
——拥有了Hooks,生命周期钩子函数可以先丢一边了。

你在还在为组件中的this指向而晕头转向吗?
——既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this。

这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。如果你也对react感兴趣,或者正在使用react进行项目开发,答应我,请一定抽出至少30分钟的时间来阅读本文好吗?所有你需要了解的React Hooks的知识点,本文都涉及到了,相信完整读完后你一定会有所收获。

一个最简单的Hooks

首先让我们看一下一个简单的有状态组件:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们再来看一下使用hooks后的版本:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

是不是简单多了!可以看到,Example变成了一个函数,但这个函数却有自己的状态(count),同时它还可以更新自己的状态(setCount)。这个函数之所以这么了不得,就是因为它注入了一个hook--useState,就是这个hook让我们的函数变成了一个有状态的函数。

除了useState这个hook外,还有很多别的hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。

Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。咦?这听起来有点像被诟病的Mixins啊?难道是Mixins要在react中死灰复燃了吗?当然不会了,等会我们再来谈两者的区别。总而言之,这些hooks的目标就是让你不再写class,让function一统江湖。

React为什么要搞一个Hooks?

想要复用一个有状态的组件太麻烦了!

我们都知道react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但假如你在大型的工作项目中用react,你会发现你的项目中实际上很多react组件冗长且难以复用。尤其是那些写成class的组件,它们本身包含了状态(state),所以复用这类组件就变得很麻烦。

那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)高阶组件(Higher-Order Components)。我们可以稍微跑下题简单看一下这两种模式。

渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider组件包含了所有跟状态相关的代码,而Cat组件则可以是一个单纯的展示型组件,这样一来DataProvider就可以单独复用了。

import Cat from 'components/cat'
class DataProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { target: 'Zac' };
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    )
  }
}

<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

虽然这个模式叫Render Props,但不是说非用一个叫render的props不可,习惯上大家更常写成下面这种:

...
<DataProvider>
  {data => (
    <Cat target={data.target} />
  )}
</DataProvider>

高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。

const withUser = WrappedComponent => {
  const user = sessionStorage.getItem("user");
  return props => <WrappedComponent user={user} {...props} />;
};

const UserPage = props => (
  <div class="user-container">
    <p>My name is {props.user}!</p>
  </div>
);

export default withUser(UserPage);

以上这两种模式看上去都挺不错的,很多库也运用了这种模式,比如我们常用的React Router。但我们仔细看这两种模式,会发现它们会增加我们代码的层级关系。最直观的体现,打开devtool看看你的组件层级嵌套是不是很夸张吧。这时候再回过头看我们上一节给出的hooks例子,是不是简洁多了,没有多余的层级嵌套。把各种想要的功能写成一个一个可复用的自定义hook,当你的组件想用什么功能时,直接在组件里调用这个hook即可。

图片描述

生命周期钩子函数里的逻辑太乱了吧!

我们通常希望一个函数只做一件事情,但我们的生命周期钩子函数里通常同时做了很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听等等。同时,有时候我们还需要在componentDidUpdate做一遍同样的事情。当项目变复杂后,这一块的代码也变得不那么直观。

classes真的太让人困惑了!

我们用class来创建react组件时,还有一件很麻烦的事情,就是this的指向问题。为了保证this的指向正确,我们要经常写这样的代码:this.handleClick = this.handleClick.bind(this),或者是这样的代码:<button onClick={() => this.handleClick(e)}>。一旦我们不小心忘了绑定this,各种bug就随之而来,很麻烦。

还有一件让我很苦恼的事情。我在之前的react系列文章当中曾经说过,尽可能把你的组件写成无状态组件的形式,因为它们更方便复用,可独立测试。然而很多时候,我们用function写了一个简洁完美的无状态组件,后来因为需求变动这个组件必须得有自己的state,我们又得很麻烦的把function改成class。

在这样的背景下,Hooks便横空出世了!

什么是State Hooks?

回到一开始我们用的例子,我们分解来看到底state hooks做了什么:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

声明一个状态变量

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

useState是react自带的一个hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。

所以我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。

上面这种表达形式,是借用了es6的数组解构(array destructuring),它可以让我们的代码看起来更简洁。不清楚这种用法的可以先去看下我的这篇文章30分钟掌握ES6/ES2015核心内容(上)

如果不用数组解构的话,可以写成下面这样。实际上数组解构是一件开销很大的事情,用下面这种写法,或者改用对象解构,性能会有很大的提升。具体可以去这篇文章的分析Array destructuring for multi-value returns (in light of React hooks),这里不详细展开,我们就按照官方推荐使用数组解构就好。

let _useState = useState(0);
let count = _useState[0];
let setCount = _useState[1];

读取状态值

<p>You clicked {count} times</p>

是不是超简单?因为我们的状态count就是一个单纯的变量而已,我们再也不需要写成{this.state.count}这样了。

更新状态

  <button onClick={() => setCount(count + 1)}>
    Click me
  </button>

当用户点击按钮时,我们调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react了,react将会重新渲染我们的Example组件,并且使用的是更新后的新的状态,即count=1。这里我们要停下来思考一下,Example本质上也是一个普通的函数,为什么它可以记住之前的状态?

一个至关重要的问题

这里我们就发现了问题,通常来说我们在一个函数中声明的变量,当函数运行完成后,这个变量也就销毁了(这里我们先不考虑闭包等情况),比如考虑下面的例子:

function add(n) {
    const result = 0;
    return result + 1;
}

add(1); //1
add(1); //1

不管我们反复调用add函数多少次,结果都是1。因为每一次我们调用add时,result变量都是从初始值0开始的。那为什么上面的Example函数每次执行的时候,都是拿的上一次执行完的状态值作为初始值?答案是:是react帮我们记住的。至于react是用什么机制记住的,我们可以再思考一下。

假如一个组件有多个状态值怎么办?

首先,useState是可以多次调用的,所以我们完全可以这样写:

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。唯一需要注意的点是,之前我们的this.setState做的是合并状态后返回一个新状态,而useState是直接替换老状态后返回新状态。最后,react也给我们提供了一个useReducer的hook,如果你更喜欢redux式的状态管理方案的话。

从ExampleWithManyStates函数我们可以看到,useState无论调用多少次,相互之间是独立的。这一点至关重要。为什么这么说呢?

其实我们看hook的“形态”,有点类似之前被官方否定掉的Mixins这种方案,都是提供一种“插拔式的功能注入”的能力。而mixins之所以被否定,是因为Mixins机制是让多个Mixins共享一个对象的数据空间,这样就很难确保不同Mixins依赖的状态不发生冲突。

而现在我们的hook,一方面它是直接用在function当中,而不是class;另一方面每一个hook都是相互独立的,不同组件调用同一个hook也能保证各自状态的独立性。这就是两者的本质区别了。

react是怎么保证多个useState的相互独立的?

还是看上面给出的ExampleWithManyStates例子,我们调用了三次useState,每次我们传的参数只是一个值(如42,‘banana’),我们根本没有告诉react这些值对应的key是哪个,那react是怎么保证这三个useState找到它对应的state呢?

答案是,react是根据useState出现的顺序来定的。我们具体来看一下:

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  useState('banana');  //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
  useState([{ text: 'Learn Hooks' }]); //...

假如我们改一下代码:

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [fruit, setFruit] = useState('banana');
    showFruit = false;
  }
 
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

这样一来,

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  // useState('banana');  
  useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错

鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。

什么是Effect Hooks?

我们在上一节的例子中增加一个新功能:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 更新文档的标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们对比着看一下,如果没有hooks,我们会怎么写?

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。

同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。

useEffect做了什么?

我们再梳理一遍下面代码的逻辑:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

首先,我们声明了一个状态变量count,将它的初始值设为0。然后我们告诉react,我们的这个组件有一个副作用。我们给useEffecthook传了一个匿名函数,这个匿名函数就是我们的副作用。在这个例子里,我们的副作用是调用browser API来修改文档标题。当react要渲染我们的组件时,它会先记住我们用到的副作用。等react更新了DOM之后,它再依次执行我们定义的副作用函数。

这里要注意几点:
第一,react首次渲染和之后的每次渲染都会调用一遍传给useEffect的函数。而之前我们要用两个声明周期函数来分别表示首次渲染(componentDidMount),和之后的更新导致的重新渲染(componentDidUpdate)。

第二,useEffect中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount或componentDidUpdate中的代码则是同步执行的。这种安排对大多数副作用说都是合理的,但有的情况除外,比如我们有时候需要先根据DOM计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生。

useEffect怎么解绑一些副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。

怎么清除呢?让我们传给useEffect的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。这种模式在一些pubsub模式的实现中很常见。看下面的例子:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

这里有一个点需要重视!这种解绑的模式跟componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次而已,而useEffect里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。所以我们一起来看一下下面这个问题。

为什么要让副作用函数每次组件更新都执行一遍?

我们先看以前的模式:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

很清除,我们在componentDidMount注册,再在componentWillUnmount清除注册。但假如这时候props.friend.id变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:

...
  componentDidUpdate(prevProps) {
    // 先把上一个friend.id解绑
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 再重新注册新但friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
...

看到了吗?很繁琐,而我们但useEffect则没这个问题,因为它在每次组件更新后都会重新执行一遍。所以代码的执行顺序是这样的:

1.页面首次渲染
2.替friend.id=1的朋友注册

3.突然friend.id变成了2
4.页面重新渲染
5.清除friend.id=1的绑定
6.替friend.id=2的朋友注册
...

怎么跳过一些不必要的副作用函数

按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句

当我们第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount加componentWillUnmount的模式。不过这种用法可能带来bug,少用。

还有哪些自带的Effect Hooks?

除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks:

useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect

我不再一一介绍,大家自行去查阅官方文档。

怎么写自定义的Effect Hooks?

为什么要自己去写一个Effect Hooks? 这样我们才能把可以复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”,哪个组件要用来,我就插进哪个组件里,so easy!看一个完整的例子,你就明白了。

比如我们可以把上面写的FriendStatus组件中判断朋友是否在线的功能抽出来,新建一个useFriendStatus的hook专门用来判断某个id是否在线。

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

这时候FriendStatus组件就可以简写为:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

简直Perfect!假如这个时候我们又有一个朋友列表也需要显示是否在线的信息:

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

简直Fabulous!

结尾

不知道你阅读完整篇文章的感受如何,或者对hooks有任何角度的看法和思考都欢迎在评论区一起讨论。另外如果你有换工作的打算,我们部门真的很缺人,欢迎私信勾搭~(阿里巴巴,base在深圳的部门)

查看原文

赞 189 收藏 109 评论 10

MrDream 关注了用户 · 2017-08-14

王德福 @wonderful611

关注 1

MrDream 赞了文章 · 2017-07-17

你想要的——vuex源码分析

大家好,今天给大家带来的是vuex(2.3.1)源码分析,希望能够能跟大家进行交流,欢迎提意见,写的不好的地方欢迎拍砖

[github源码地址][1]

首先我们先来看看vuex是如何跟vue项目一起结合使用的,以下是官方demo中的一个简单例子

(1)我们必须先创建一个store

import Vue from 'vue'
import Vuex from 'vuex'
import { state, mutations } from './mutations'
import plugins from './plugins'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  plugins
})

(2)将这个store传给vue的实例,这样我们就能够在vue中获取到这个store并且使用它的功能了

import 'babel-polyfill'
import Vue from 'vue'
import store from './store'
import App from './components/App.vue'

new Vue({
  store, // inject store to all children
  el: '#app',
  render: h => h(App)
})

以上就是vuex的简单使用方法,然后接下来我们就开始来分析vuex的源码吧

  • 目录结构

    clipboard.png

从目录结构可以看出,vuex是一个代码比较简洁的框架

  • index.js——入口文件

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

入口文件只做了一件事,就是导入了其他相关的文件,并且将vuex的功能export出去,相当于定义vuex对外使用的API

  • store.js——vuex的仓库,也是vuex中比较重要的一环
    这个文件比较长,我们可以一点一点来分析:
    总体来说,这个文件做了几件事,定义并导出了Store这个类和install方法,并执行了install这个方法,我们都知道,vue的所有插件都是通过install这个方法来安装的

import applyMixin from './mixin'
import devtoolPlugin from './plugins/devtool'
import ModuleCollection from './module/module-collection'
import { forEachValue, isObject, isPromise, assert } from './util'

一开始导入相关的方法,后面会解释这些方法的用处

let Vue // 定义了变量Vue,为的是引用外部的vue构造函数,这样vuex框架就可以不用导入vue这个库了

----------------------------------------------------------这是分割线----------------------------------------------------------------------------------------

接下来是定义Store这个类,从图中可以看出这个vuex中的外store对外提供的能力,包括常用的commit,dispatch,watch等

clipboard.png

先看看构造函数吧:

constructor (options = {}) {
    // 这个是在开发过程中的一些环节判断,vuex要求在创建vuex store实例之前必须先使用这个方法Vue.use(Vuex)来安装vuex,项目必须也得支持promise,store也必须通过new来创建实例
    
    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }
    
    // 从参数options中结构出相关变量
    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    
    // 这个简单的,不解释
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    // 初始化store内部状态,Object.create(null)可以创建一个干净的空对象
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    // vuex支持模块,即将state通过key-value的形式拆分为多个模块
    // 模块的具体内容可以查看这里 :https://vuex.vuejs.org/en/mutations.html
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    // 监听队列,当执行commit时会执行队列中的函数
    this._subscribers = []
    // 创建一个vue实例,利用vue的watch的能力,可以监控state的改变,具体后续watch方法会介绍
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    
    const store = this
    // 缓存dispatch和commit方法
    const { dispatch, commit } = this
    // 定义dispatch方法
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    // 定义commit方法
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    // 定义严格模式,不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。
    // 具体后续enableStrictMode方法会提到
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    // 这个作者的注释已经写得挺明白,就是初始化根模块,递归注册子模块,收集getter
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    // 初始化store中的state,使得state变成响应式的,原理就是将state作为一个vue实例的data属性传入,具体在分析这个函数的时候会介绍
    resetStoreVM(this, state)

    // apply plugins
    // 执行插件,这个是一个数组,所以遍历他,然后执行每个插件的函数
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }

呼呼呼~ 至此,终于把store类全部读完了,休息五分钟,然后继续往下看哈。

接下来关于state的获取和设置

  // 获取state,  直接返回内部data的$$state
  get state () {
    return this._vm._data.$$state
  }

  set state (v) {
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `Use store.replaceState() to explicit replace store state.`)
    }
  }

commit是vuex中一个比较重要的操作,因为它可以触发mutation修改对state的修改,并且是同步执行的

commit (_type, _payload, _options) {
    // check object-style commit
    // 首先统一传入参数的格式,主要是针对当type是个对象的情况,需要把这个对象解析出来
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)
    
    // 缓存本次commit操作的类型和负荷,以供后续监听队列(this._subscribers)使用
    const mutation = { type, payload }
    // 获取相关的type的mutation函数,我们都知道,在vuex中都是通过commit一个类型然后触发相关的mutation函数来操作state的,所以在此必须获取相关的函数
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // 在_withCommit中触发上面获取的mutation函数,简单粗暴使用数组的forEach执行哈哈,之所以要在外面包一层_withCommit,是表明操作的同步性
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 这个就是之前说的监听队列,在每次执行commit函数时都会遍历执行一下这个队列
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

dispatch是跟commit有点相似的函数,但是commit必须是同步的,而dispatch是异步的,内部还是必须通过commit来操作state

dispatch (_type, _payload) {
    // check object-style dispatch
    // 同上面commit,不解释
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)
    
    // 因为dispatch触发的是actions中的函数,所以这里获取actions相关函数,过程类似commit
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // 因为dispatch支持异步,所以这里作者使用Promise.all来执行异步函数并且判断所有异步函数是否都已经执行完成,所以在文件最开始判断了当前环境必须支持promise就是这个原因
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }

subscribe函数,这是pub/sub模式在vuex中的一个运用,用户可以通过subscribe函数来监听state的变化,函数返回一个取消监听的函数,便于用户在合适的时机取消订阅

subscribe (fn) {
    const subs = this._subscribers
    if (subs.indexOf(fn) < 0) {
      subs.push(fn)
    }
    // 返回取消订阅的函数,通过函数额splice方法,来清除函数中不需要的项
    return () => {
      const i = subs.indexOf(fn)
      if (i > -1) {
        subs.splice(i, 1)
      }
    }
  }

watch函数,响应式地监测一个 getter 方法的返回值,当值改变时调用回调函数,原理其实就是利用vue中的watch方法

watch (getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    // 在上面构造函数中,我们看到this._watcherVM就是一个vue的实例,所以可以利用它的watch来实现vuex的watch,原理都一样,当监听的值或者函数的返回值发送改变的时候,就触发相应的回调函数,也就是我们传入的cb参数,options则可以来让监听立即执行&深度监听对象
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

replaceState,根据名字就可知道,是替换当前的state

replaceState (state) {
    this._withCommit(() => {
      this._vm._data.$$state = state
    })
  }

registerModule函数,可以使用 store.registerModule 方法注册模块

registerModule (path, rawModule) {
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }
    //其实内部时候通过,register方法,递归寻找路径,然后将新的模块注册root模块上,具体后续介绍到module的时候会详细分析
    this._modules.register(path, rawModule)
    //安装模块,因为每个模块都有他自身的getters,actions, modules等,所以,每次注册模块都必须把这些都注册上,后续介绍installModule的时候,会详细介绍到
    installModule(this, this.state, path, this._modules.get(path))
    // reset store to update getters...
    // 重置VM
    resetStoreVM(this, this.state)
  }

unregisterModule函数,上述registerModule函数的相反操作,具体在module的时候会介绍到,在此了解个大概,先不纠结细节

unregisterModule (path) {
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    this._modules.unregister(path)
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      // 利用vue.delete方法,确保模块在被删除的时候,视图能监听到变化
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }

hotUpdate函数,Vuex 支持在开发过程中热重载 mutation、modules、actions、和getters

hotUpdate (newOptions) {
    this._modules.update(newOptions)
    resetStore(this, true)
  }

_withCommit函数,从函数名可以看出这是一个内部方法,作用就是保证commit过程中执行的方法都是同步的

_withCommit (fn) {
    // 保存原来的committing的状态
    const committing = this._committing
    //将想在的committing状态设置为true
    this._committing = true
    //执行函数
    fn()
    //将committing状态设置为原来的状态
    this._committing = committing
  }

到目前为止,我们已经看完了Store这个类的所有代码~慢慢消化,然后继续往下

----------------------------------------------------------这又是分割线----------------------------------------------------------------------------------------

接下来,我们分析一下,一些其他的辅助方法,跟上面store的一些内容会有相关。ready? Go

resetStore函数,用于重置整个vuex中的store,从代码中可以看出,这个函数主要的功能,就是将传入的store实例的_actions,_mutations,_wrappedGetters,_modulesNamespaceMap置为空,然后重新安装模块和重置VM,此方法在上述热更新和注销模块的时候会使用到

function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

resetStoreVM函数,这个用于重置store中的vm,所谓vm,指的就是视图模型,也就是常见mvvm中的vm,在此指的是将state作为data中$$state属性的一个vue实例

function resetStoreVM (store, state, hot) {
   // 保存原有store的_vm
  const oldVm = store._vm
    
  // bind store public getters
  store.getters = {}
  // store的_wrappedGetters缓存了当前store中所有的getter
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  //遍历这个对象,获取每个getter的key和对应的方法
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // 将getter以key-value的形式缓存在变量computed中,其实后面就是将getter作为vue实例中的计算属性
    computed[key] = () => fn(store)
    // 当用户获取getter时,相当于获取vue实例中的计算属性,使用es5的这个Object.defineProperty方法做一层代理
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  // silent设置为true,则取消了所有的警告和日志,眼不见为净
  Vue.config.silent = true
  
  // 将传入的state,作为vue实例中的data的$$state属性,将刚刚使用computed变量搜集的getter,作为实例的计算属性,所以当state和getter都变成了响应式的了
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
    
  if (store.strict) {
    //如果设置了严格模式则,不允许用户在使用mutation以外的方式去修改state
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        // 将原有的vm中的state设置为空,所以原有的getter都会重新计算一遍,利用的就是vue中的响应式,getter作为computed属性,只有他的依赖改变了,才会重新计算,而现在把state设置为null,所以计算属性重新计算
        oldVm._data.$$state = null
      })
    }
    // 在下一次周期销毁实例
    Vue.nextTick(() => oldVm.$destroy())
  }
}

installModule函数,用于安装模块,注册相应的mutation,action,getter和子模块等

function installModule (store, rootState, path, module, hot) {
   //判断是否为根模块
  const isRoot = !path.length
   //根据路径生成相应的命名空间
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    // 将模块的state设置为响应式
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  //设置本地上下文,主要是针对模块的命名空间,对dispatch,commit,getters和state进行修改,后面讲到makeLocalContext的时候会详细分析,现在只需要知道,这个操作让用户能够直接获取到对象子模块下的对象就可以了
  const local = module.context = makeLocalContext(store, namespace, path)
 
  //将mutation注册到模块上
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  //将action注册到模块上  
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })
  //将getter注册到模块上
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  //递归安装子模块  
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

makeLocalContext函数,就是installModule中设置本地上下文的具体实现

function makeLocalContext (store, namespace, path) {
   //如果没有命名空间,则是使用全局store上的属性,否则对store上的属性进行本地化处理
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      //dispatch的本地化处理,就是修改type
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
         //在type前面加上命名空间
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }
        //调用store上的dispatch方法
      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
    // commit的本地化修改跟dispatch相似,也是只是修改了type,然后调用store上面的commit
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
   //gettters和state的修改,则依赖于makeLocalGetters函数和getNestedState函数,后面会分析
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

makeLocalGetters函数,则是对getter进行本地化处理

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    //这里获取的每个type都是一个有命名空间+本地type的字符串,例如: type的值可能为 “m1/m2/”+"typeName"
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    //相当于做了一层代理,将子模块的localType映射到store上的type
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

registerMutation函数,就是注册mutation的过程,将相应type的mutation推到store._mutations[type]的队列中,当commit这个type的时候就触发执行队列中的函数

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(local.state, payload)
  })
}

registerAction函数,注册action的过程,原理类似于registerMutation,不同点在于action支持异步,所以必须用promise进行包装

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler({
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

registerGetters函数,根据type,将getter方法挂载在store._wrappedGetters[type]下面

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 为子模块的getter提供了这个四个参数,方便用户获取,如果是根模块,则local跟store取出来的state和getters相同
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

enableStrictMode函数则是在严格模式下,不允许state被除mutation之外的其他操作修改,代码比较简单,利用vue的$watch方法实现的

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

getNestedState函数,获取对应路径下的state

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

unifyObjectStyle函数,作用是调整参数,主要是当type是一个对象的时候,对参数进行调整

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

以上是相关辅助函数的全部内容,你看明白了么~

----------------------------------------------------------这依然是分割线------------------------------------------------------------------------------------

文件的最后,就是定义了install函数,然后自动执行了这个函数,让vuex能够在项目中运作起来

export function install (_Vue) {
  if (Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  //在vue的生命周期中初始化vuex,具体实现后面讲到mixin.js这个文件时会说明
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

以上就是store.js的所有内容啦~

----------------------------------------------------------严肃分割线------------------------------------------------------------------------------------

接下来讲解有关module目录下的内容,该目录有两个文件分别是module-collection.js和module.js,这两个文件主要是有关于vuex中模块的内容;
首先我们看看module-collection.js,这个文件主要导出一个ModuleCollection类:
构造函数

constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    //主要是注册根模块,我们在之前store的构造函数中曾经使用到 this._modules = new ModuleCollection(options),注册一个根模块然后缓存在this._module中
    this.register([], rawRootModule, false)
  }

紧接着看看下面register函数,它用于注册模块

register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    // 创建一个新模块,具体会在后面讲到Module的时候分析
    const newModule = new Module(rawModule, runtime)
    // 判读是否为根模块
    if (path.length === 0) {
      this.root = newModule
    } else {
      //根据path路径,利用get方法获取父模块  
      const parent = this.get(path.slice(0, -1))
      //为父模块添加子模块
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    // 如果当前模块里面有子模块,则递归的去注册子模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

相反,unregister函数则是移除一个模块

unregister (path) {
    // 通过get方法获取父模块
    const parent = this.get(path.slice(0, -1))
    //获取需要删除的模块的名称,即他的key
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return
    //利用module中removeChild方法删除该模块,其实就是delete了对象上的一个key
    parent.removeChild(key)
  }

get函数,其实就是利用es5中数组reduce方法,从根模块开始根据传入的path来获取相应的子模块

get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

getNamespace函数,利用传入的参数path,生成相应的命名空间,实现的原理跟上述的get方法类似

getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

upate方法,就是更新模块,具体看下面update方法的实现

update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

以上就是整个ModuleCollection类的实现

接下来讲解一下function update的实现

function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  //目标模块更新为新模块,具体实现是将原有模块的namespaced,actions,mutations,getters替换为新模块的namespaced,actions,mutations,getters
  // 具体会在Module类中update方法讲解
  targetModule.update(newModule)

  // update nested modules
  // 如果新的模块有子模块,则递归更新子模块
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

至于assertRawModule方法和makeAssertionMessage方法,就是一些简单的校验和提示,不影响主流程&代码比较简单,这里不做赘述

以上就是整个module-collection.js文件的所有内容

接下来就应该分析目录中的另一个文件module.js,这个文件主要导出一个Module类,这个类主要描述了vuex中模块的功能

构造函数,主要做了一些模块初始化的事情

//构造函数,主要做了一些模块初始化的事情
  constructor (rawModule, runtime) {
    //缓存运行时的标志
    this.runtime = runtime
    //创建一个空对象来保存子模块
    this._children = Object.create(null)
    //缓存传入的模块
    this._rawModule = rawModule
    //缓存传入模块的state,如果state是一个函数,则执行这个函数
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

namespaced函数是主要就是获取当前模块是否是命名模块,vuex支持命名模块和匿名模块

get namespaced () {
    return !!this._rawModule.namespaced
  }

addChild,removeChild,getChild这三个函数就分别是添加,删除,获取子模块,内容比较简单,不赘述

update方法,将原有缓存模块的namespaced,actions,mutations,getters替换成新传入模块的namespaced,actions,mutations,getters

update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

forEachChild函数,利用util中forEachValue方法,变量每个子模块,将每个子模块作为传入的回调函数参数,然后执行回调函数

forEachChild (fn) {
    forEachValue(this._children, fn)
  }

forEachGetter,forEachAction,forEachMutation代码逻辑跟上述forEachChild十分类似,不在赘述

以上就是module.js文件的所有内容,至此我们也已经全部分析完module目录下的所有代码了

---------------------------------------------------一本正经分割线--------------------------------------------------------------------------------
接下来,我们再看看help.js这个文件,这个文件主要是提供了一些帮助性的方法,使得用户在使用vuex的过程中体验更好,更加方便

首先我们先看看文件后面三个函数:normalizeMap,normalizeNamespace,getModuleByNamespace

normalizeMap函数,这个方法的作用是格式化传入的对象

function normalizeMap (map) {
  // 如果传入的对象是数组,则放回一个每一项都是key-val对象的数组,其中key和val的值相同
  // 如果出入的是一个对象,则变量这个对象,放回一个每一项都是key-val数组,其中key对应对象的key,val对应相应key的值
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

normalizeNamespace函数,调整参数,格式化命名空间

function normalizeNamespace (fn) {
  return (namespace, map) => {
    //如果没传入命名空间,或者传入的命名空间不是一个字符串,则丢弃该参数
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      //否则判断命名空间后面是否有加上‘/’,如果没有则加上
      namespace += '/'
    }
    //最后执行传入的回调函数
    return fn(namespace, map)
  }
}

getModuleByNamespace函数,通过命名空间来获取模块

function getModuleByNamespace (store, helper, namespace) {
  // 返回store._modulesNamespaceMap缓存的模块
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

mapState函数,我们可以通过这个方法将state解构到vue项目中去,使其变成vue实例中的计算属性

export const mapState = normalizeNamespace((namespace, states) => {
  //定义一个空对象
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    //收集states的所有key,对应key的值,改变成一个mappedState方法,符合计算属性的特点
    res[key] = function mappedState () {
      //获取store的state和getters
      let state = this.$store.state
      let getters = this.$store.getters
      //如果存在命名空间,则将命名空间下子模块的state和getters覆盖原来store的state和getters
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      //如果对应的val是函数则执行,否则返回state下的值
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  //返回这个包装过state的对象,这个对象可以结构成vue中的计算属性
  return res
})

mapMutations函数,则是将mutation解构到vue实例中的methods中,使得用户可以直接调用methods中的方法来执行store.commit

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  //定义一个空对象
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedMutation (...args) {
      if (namespace && !getModuleByNamespace(this.$store, 'mapMutations', namespace)) {
        return
      }
      //调用了store中的commit方法,触发相应的mutation函数的执行
      return this.$store.commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

mapGetters的逻辑跟mapState类似,mapActions的逻辑跟mapMutations类似,这里不再赘述

自此,我们把help.js的内容也分析完了

---------------------------------------------------一本正经分割线--------------------------------------------------------------------------------
接下来我们看看mixin.js文件
还记得之前store.js里面有个install方法么,这个方法就用到了mixin.js文件提供的内容

// 这个文件其实就导出了一个方法,供vuex在被引入的时候,能够顺利安装到项目中
export default function (Vue) {
  // 首先,判断vue版本,不同的vue版本,生命周期不同,所以需要做差异处理
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    // 如果版本是2.0以上的,则在vue的beforeCreate生命周期中,触发vuex的初始化
    // 利用的是vue中全局混入的形式
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // 如果是1.x版本的话,就改写原有Vue原型上的_init方法
    // 先将原来的函数保存在常量_init中
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
        // 将初始化方法作为原有init的参数传入,所以在vue初始化的时候就会执行vuexInit方法来初始化vuex
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  // vuex的初始化钩子 
  function vuexInit () {
    const options = this.$options
    // store injection
    // 注入store
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

plugins文件夹中,主要是关于插件相关的内容

devtool.js,是关于是当用户开启vue-devtools时,触发了一些操作

// 通过全局变量__VUE_DEVTOOLS_GLOBAL_HOOK__,判断是否开启vue-devtools
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook
  // vue-devtool自身实现了一套事件机制,有兴趣可以看看其中的实现
  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state', targetState => {
    //用targetState替换当前的state
    store.replaceState(targetState)
  })
  // 当触发commit的时候执行这个方法
  store.subscribe((mutation, state) => {
    // devtoolHook会emit一个vuex:mutation事件
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

logger.js是在开发过程中记录日志的插件

// Credits: borrowed code from fcomb/redux-logger
// 引入深拷贝方法
import { deepCopy } from '../util'

export default function createLogger ({
  collapsed = true,
  filter = (mutation, stateBefore, stateAfter) => true,
  transformer = state => state,
  mutationTransformer = mut => mut
} = {}) {
  return store => {
    // 保存原有的state
    let prevState = deepCopy(store.state)
    // 监听state的变化
    store.subscribe((mutation, state) => {
      if (typeof console === 'undefined') {
        return
      }
      //深拷贝并且获取新的state
      const nextState = deepCopy(state)

      if (filter(mutation, prevState, nextState)) {
        // 获取当前时间
        const time = new Date()
        // 格式化时间
        const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
        // 格式化mutation
        const formattedMutation = mutationTransformer(mutation)
        // 获取输出的信息
        const message = `mutation ${mutation.type}${formattedTime}`
        // 在 Web控制台上创建一个新的分组.随后输出到控制台上的内容都会被添加一个缩进
        const startMessage = collapsed
          ? console.groupCollapsed
          : console.group

        // render
        try {
          // 输出日志
          startMessage.call(console, message)
        } catch (e) {
          console.log(message)
        }

        console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
        console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
        console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))

        try {
          console.groupEnd()
        } catch (e) {
          console.log('—— log end ——')
        }
      }
      // 替换state
      prevState = nextState
    })
  }
}

至于util.js,内部提供一些简单的工具方法,不再赘述啦~可自行研究
---------------------------------------------------最后的分割线--------------------------------------------------------------------------------

以上,便是vuex源码的所有内容。。不管写的怎么样,你都看到这里啦,对此深表感谢~

查看原文

赞 9 收藏 25 评论 2

MrDream 发布了文章 · 2017-03-29

改造你的网站,变身 PWA

pwa

最近有很多关于 Progressive Web Apps(PWAs)的消息,很多人都在问这是不是(移动)web 的未来。我不想陷入native app 和 PWA 的纷争,但是有一件事是确定的 --- PWA极大的提升了移动端表现,改善了用户体验。

好消息是开发一个 PWA 并不难。事实上,我们可以将现存的网站进行改进,使之成为PWA。这也是我这篇文章要讲的 -- 当你读完这篇文章,你可以将你的网站改进,让他看起来就像是一个 native web app。他可以离线工作并且拥有自己的主屏图标。

Progressive Web Apps 是什么?

Progressive Web Apps (下文以“PWAs”代指) 是一个令人兴奋的前端技术的革新。PWAs综合了一系列技术使你的 web app表现得就像是 native mobile app。相比于纯 web 解决方案和纯 native 解决方案,PWAs对于开发者和用户有以下优点:

  1. 你只需要基于开放的 W3C 标准的 web 开发技术来开发一个app。不需要多客户端开发。

  2. 用户可以在安装前就体验你的 app。

  3. 不需要通过 AppStore 下载 app。app 会自动升级不需要用户升级。

  4. 用户会受到‘安装’的提示,点击安装会增加一个图标到用户首屏。

  5. 被打开时,PWA 会展示一个有吸引力的闪屏。

  6. chrome 提供了可选选项,可以使 PWA 得到全屏体验。

  7. 必要的文件会被本地缓存,因此会比标准的web app 响应更快(也许也会比native app响应快)

  8. 安装及其轻量 -- 或许会有几百 kb 的缓存数据。

  9. 网站的数据传输必须是 https 连接。

  10. PWAs 可以离线工作,并且在网络恢复时可以同步最新数据。

现在还处在 PWA 的早期,但已经有 很多成功案例

PWA 技术目前被 Firefox,Chrome 和其他基于Blink内核的浏览器支持。微软正在努力在Edge浏览器上实现。Apple没有动作 although there are promising comments in the WebKit five-year plan。幸运的是,浏览器支持对于 PWA 似乎不太重要...

PWAs 是渐进增强的

你的app仍然可以运行在不支持 PWA 技术的浏览器里。用户不能离线访问,不过其他功能都像原来一样没有影响。综合利弊得失,没有理由不把你的 app 改进为 PWA。

不只是 Apps

Google 引领了 PWA 的一系列动作,所以大多数教程都在说如何从零开始构建一个基于 Chrome,native-looking mobile app。然而并不是只有特殊的单页应用可以PWA化,也不需要一定遵循 material interface design guidelines。大多数网站都可以在数小时内实现 PWA 化。这包括你的 WordPress站点或者静态站点。

示例代码

示例代码可以在https://github.com/sitepoint-editors/pwa-retrofit找到。

代码提供了一个简单的四个页面的网站。其中包含一些图片,一个样式表和一个main javascript 文件。这个网站可以运行在所有现代浏览器上(IE10+)。如果浏览器支持 PWA 技术,当离线时用户可以浏览他们之前看过的页面。

运行代码前,确保 Node.js 已经安装,然后再命令行里启动服务:

node ./server.js [port]

[port]是可配置的,默认为 8888。打开 Chrome 或者其他基于Blink内核的浏览器,比如 Opera 或者 Vivaldi,然后输入链接 http://localhost:8888/(或者你指定的某个端口)。你也可以打开开发者工具看一下各个console信息。

浏览主页,或者其他页面,然后用以下任一方法使页面离线:

  1. 按下 Cmd/Ctrl + C ,停止 node 服务器,或者

  2. 在开发者工具的 Network 或者 Application - Service Workers 栏里点击 offline 选项。

重新浏览任意之前浏览过的页面,它们仍然可以浏览到。浏览一个之前没有看过的页面,你会看到一个专门的离线页面,标识“you’re offline”,还有一个你可以浏览的页面列表:

连接手机

你也可以通过 USB 连接你的安卓手机来预览示例网页。在开发者工具中打开 Remote devices 菜单。

在左边选择 Settings ,点击 Add Rule 输入 8888 端口。你可以在你的手机上打开Chrome,打开 http://localhost:8888/

你可以点击浏览器菜单里的 “Add to Home screen”。浏览几个页面,浏览器会提醒你去安装。这两种方式都可以创建一个新的图标在你的主屏上。浏览几个页面后关掉Chrome,断开设备连接。你依然可以打开 PWA Website app -- 你会看到一个启动页,并且可以离线访问之前你访问过的页面。

将你的网站改进为一个 Progressive Web App 总共有三个必要步骤:

第一步:开启 HTTPS

由于一些显而易见的原因,PWAs 需要 HTTPS 连接。

HTTPS 在示例代码中并不是必须的,因为 Chrome 允许使用 localhost 或者任何 127.x.x.x 的地址来测试。你也可以在 HTTP 连接下测试你的 PWA,你需要使用 Chrome ,并且输入以下命令行参数:

  • --user-data-dir

  • --unsafety-treat-insecure-origin-as-secure

第二步:创建一个 Web App Manifest

manifest 文件提供了一些我们网站的信息,例如 name,description 和需要在主屏使用的图标的图片,启动屏的图片等。

manifest文件是一个 JSON 格式的文件,位于你项目的根目录。它必须用Content-Type: application/manifest+json 或者 Content-Type: application/json 这样的 HTTP 头来请求。这个文件可以被命名为任何名字,在示例代码中他被命名为 /manifest.json:

{
  "name"              : "PWA Website",
  "short_name"        : "PWA",
  "description"       : "An example PWA website",
  "start_url"         : "/",
  "display"           : "standalone",
  "orientation"       : "any",
  "background_color"  : "#ACE",
  "theme_color"       : "#ACE",
  "icons": [
    {
      "src"           : "/images/logo/logo072.png",
      "sizes"         : "72x72",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo152.png",
      "sizes"         : "152x152",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo192.png",
      "sizes"         : "192x192",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo256.png",
      "sizes"         : "256x256",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo512.png",
      "sizes"         : "512x512",
      "type"          : "image/png"
    }
  ]
}

在页面的<head>中引入:

<link rel="manifest" href="/manifest.json">

manifest 中主要属性有:

  • name —— 网页显示给用户的完整名称

  • short_name —— 当空间不足以显示全名时的网站缩写名称

  • description —— 关于网站的详细描述

  • start_url —— 网页的初始 相对 URL(比如 /

  • scope —— 导航范围。比如,/app/的scope就限制 app 在这个文件夹里。

  • background-color —— 启动屏和浏览器的背景颜色

  • theme_color —— 网站的主题颜色,一般都与背景颜色相同,它可以影响网站的显示

  • orientation —— 首选的显示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary

  • display —— 首选的显示方式:fullscreen, standalone(看起来像是native app),minimal-ui(有简化的浏览器控制选项) 和 browser(常规的浏览器 tab)

  • icons —— 定义了 src URL, sizestype的图片对象数组。

MDN提供了完整的manifest属性列表:Web App Manifest properties

在开发者工具中的 Application tab 左边有 Manifest 选项,你可以验证你的 manifest JSON 文件,并提供了 “Add to homescreen”。

第三步:创建一个 Service Worker

Service Worker 是拦截和响应你的网络请求的编程接口。这是一个位于你根目录的一个单独的 javascript 文件。

你的 js 文件(在示例代码中是 /js/main.js)可以检查是否支持 Service Worker,并且注册:

if ('serviceWorker' in navigator) {

  // register service worker
  navigator.serviceWorker.register('/service-worker.js');

}

如果你不需要离线功能,可以简单的创建一个空的 /service-worker.js文件 —— 用户会被提示安装你的 app。

Service Worker 很复杂,你可以修改示例代码来达到自己的目的。这是一个标准的 web worker,浏览器用一个单独的线程来下载和执行它。它没有调用 DOM 和其他页面 api 的能力,但他可以拦截网络请求,包括页面切换,静态资源下载,ajax请求所引起的网络请求。

这就是需要 HTTPS 的最主要的原因。想象一下第三方代码可以拦截来自其他网站的 service worker, 将是一个灾难。

service worker 主要有三个事件: installactivatefetch

Install 事件

这个事件在app被安装时触发。它经常用来缓存必要的文件。缓存通过 Cache API来实现。

首先,我们来构造几个变量:

  1. 缓存名称(CACHE)和版本号(version)。你的应用可以有多个缓存但是只能引用一个。我们设置了版本号,这样当我们有重大更新时,我们可以更新缓存,而忽略旧的缓存。

  2. 一个离线页面的URL(offlineURL)。当离线时用户试图访问之前未缓存的页面时,这个页面会呈现给用户。

  3. 一个拥有离线功能的页面必要文件的数组(installFilesEssential)。这个数组应该包含静态资源,比如 CSS 和 JavaScript 文件,但我也把主页面(/)和图标文件写进去了。如果主页面可以多个URL访问,你应该把他们都写进去,比如//index.html。注意,offlineURL也要被写入这个数组。

  4. 可选的,描述文件数组(installFilesDesirable)。这些文件都很会被下载,但如果下载失败不会中止安装。

// configuration
const
  version = '1.0.0',
  CACHE = version + '::PWAsite',
  offlineURL = '/offline/',
  installFilesEssential = [
    '/',
    '/manifest.json',
    '/css/styles.css',
    '/js/main.js',
    '/js/offlinepage.js',
    '/images/logo/logo152.png'
  ].concat(offlineURL),
  installFilesDesirable = [
    '/favicon.ico',
    '/images/logo/logo016.png',
    '/images/hero/power-pv.jpg',
    '/images/hero/power-lo.jpg',
    '/images/hero/power-hi.jpg'
  ];

installStaticFiles()方法添加文件到缓存,这个方法用到了基于 promise的 Cache API。当必要的文件都被缓存后才会生成返回值。

// install static assets
function installStaticFiles() {

  return caches.open(CACHE)
    .then(cache => {

      // cache desirable files
      cache.addAll(installFilesDesirable);

      // cache essential files
      return cache.addAll(installFilesEssential);

    });

}

最后,我们添加install的事件监听函数。 waitUntil方法确保所有代码执行完毕后,service worker 才会执行 install。执行 installStaticFiles()方法,然后执行 self.skipWaiting()方法使service worker进入 active状态。

// application installation
self.addEventListener('install', event => {

  console.log('service worker: install');

  // cache core files
  event.waitUntil(
    installStaticFiles()
    .then(() => self.skipWaiting())
  );

});

Activate 事件

当 install完成后, service worker 进入active状态,这个事件立刻执行。你可能不需要实现这个事件监听,但是示例代码在这里删除老旧的无用缓存文件:

// clear old caches
function clearOldCaches() {

  return caches.keys()
    .then(keylist => {

      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );

    });

}

// application activated
self.addEventListener('activate', event => {

  console.log('service worker: activate');

    // delete old caches
  event.waitUntil(
    clearOldCaches()
    .then(() => self.clients.claim())
    );

});

注意,最后的self.clients.claim()方法设置本身为active的service worker。

Fetch 事件

当有网络请求时这个事件被触发。它调用respondWith()方法来劫持 GET 请求并返回:

  1. 缓存中的一个静态资源。

  2. 如果 #1 失败了,就用 Fetch API(这与 service worker 的fetch 事件没关系)去网络请求这个资源。然后将这个资源加入缓存。

  3. 如果 #1 和 #2 都失败了,那就返回一个适当的值。

// application fetch network data
self.addEventListener('fetch', event => {

  // abandon non-GET requests
  if (event.request.method !== 'GET') return;

  let url = event.request.url;

  event.respondWith(

    caches.open(CACHE)
      .then(cache => {

        return cache.match(event.request)
          .then(response => {

            if (response) {
              // return cached file
              console.log('cache fetch: ' + url);
              return response;
            }

            // make network request
            return fetch(event.request)
              .then(newreq => {

                console.log('network fetch: ' + url);
                if (newreq.ok) cache.put(event.request, newreq.clone());
                return newreq;

              })
              // app is offline
              .catch(() => offlineAsset(url));

          });

      })

  );

});

最后这个offlineAsset(url)方法通过几个辅助函数返回一个适当的值:

// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {

  return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);

}


// return offline asset
function offlineAsset(url) {

  if (isImage(url)) {

    // return image
    return new Response(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers: {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'no-store'
      }}
    );

  }
  else {

    // return page
    return caches.match(offlineURL);

  }

}

offlineAsset()方法检查是否是一个图片请求,如果是,那么返回一个带有 “offline” 字样的 SVG。如果不是,返回 offlineURL 页面。

开发者工具提供了查看 Service Worker 相关信息的选项:

在开发者工具的 Cache Storage 选项列出了所有当前域内的缓存和所包含的静态文件。当缓存更新的时候,你可以点击左下角的刷新按钮来更新缓存:

不出意料, Clear storage 选项可以删除你的 service worker 和缓存:

再来一步 - 第四步:创建一个可用的离线页面

离线页面可以是一个静态页面,来说明当前用户请求不可用。然而,我们也可以在这个页面上列出可以访问的页面链接。

main.js中我们可以使用 Cache API 。然而API 使用promises,在不支持的浏览器中会引起所有javascript运行阻塞。为了避免这种情况,我们在加载另一个 /js/offlinepage.js 文件之前必须检查离线文件列表和是否支持 Cache API 。

// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
  var scr = document.createElement('script');
  scr.src = '/js/offlinepage.js';
  scr.async = 1;
  document.head.appendChild(scr);
}

/js/offlinepage.js locates the most recent cache by version name, 取到所有 URL的key的列表,移除所有无用 URL,排序所有的列表并且把他们加到 ID 为cachedpagelist的 DOM 节点中:

// cache name
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');

// fetch all caches
window.caches.keys()
  .then(cacheList => {

    // find caches by and order by most recent
    cacheList = cacheList
      .filter(cName => cName.includes(CACHE))
      .sort((a, b) => a - b);

    // open first cache
    caches.open(cacheList[0])
      .then(cache => {

        // fetch cached pages
        cache.keys()
          .then(reqList => {

            let frag = document.createDocumentFragment();

            reqList
              .map(req => req.url)
              .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
              .sort()
              .forEach(req => {
                let
                  li = document.createElement('li'),
                  a = li.appendChild(document.createElement('a'));
                  a.setAttribute('href', req);
                  a.textContent = a.pathname;
                  frag.appendChild(li);
              });

            if (list) list.appendChild(frag);

          });

      })

  });

开发工具

如果你觉得 javascript 调试困难,那么 service worker 也不会很好。Chrome的开发者工具的 Application 提供了一系列调试工具。

你应该打开 隐身窗口 来测试你的 app,这样在你关闭这个窗口之后缓存文件就不会保存下来。

最后,Lighthouse extension for Chrome 提供了很多改进 PWA 的有用信息。

PWA 陷阱

有几点需要注意:

URL 隐藏

我们的示例代码隐藏了 URL 栏,我不推荐这种做法,除非你有一个单 url 应用,比如一个游戏。对于多数网站,manifest 选项 display: minimal-ui 或者 display: browser是最好的选择。

缓存太多

你可以缓存你网站的所有页面和所有静态文件。这对于一个小网站是可行的,但这对于上千个页面的大型网站实际吗?没有人会对你网站的所有内容都感兴趣,而设备的内存容量将是一个限制。即使你像示例代码一样只缓存访问过的页面和文件,缓存大小也会增长的很快。

也许你需要注意:

  • 只缓存重要的页面,类似主页,和最近的文章。

  • 不要缓存图片,视频和其他大型文件

  • 经常删除旧的缓存文件

  • 提供一个缓存按钮给用户,让用户决定是否缓存

缓存刷新

在示例代码中,用户在请求网络前先检查该文件是否缓存。如果缓存,就使用缓存文件。这在离线情况下很棒,但也意味着在联网情况下,用户得到的可能不是最新数据。

静态文件,类似于图片和视频等,不会经常改变的资源,做长时间缓存没有很大的问题。你可以在HTTP 头里设置 Cache-Control 来缓存文件使其缓存时间为一年(31,536,000 seconds):

Cache-Control: max-age=31536000

页面,CSS和 script 文件会经常变化,所以你应该改设置一个很短的缓存时间比如 24 小时,并在联网时与服务端文件进行验证:

Cache-Control: must-revalidate, max-age=86400

译自 Retrofit Your Website as a Progressive Web App

查看原文

赞 50 收藏 149 评论 3

MrDream 赞了文章 · 2017-03-20

你真的了解javascript吗?(一)

原题出处:JavaScript Puzzlers!
当初以为不过是一些小题目,结果做到怀疑人生,都要怀疑可能我javascript白学了。读者可以去试试。

不多说,直接上题:

第一题

["1", "2", "3"].map(parseInt)

不要被套路,这题没看上去那么简单,首先我们来看看Array.map这个函数:

array.map(function(currentValue,index,arr), thisValue)

map() 方法按照原始数组元素顺序依次处理元素,返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

map接收两个参数,一个回调函数,一个是可选的回调函数的this值,默认为undefined

其中回调函数接收三个参数,依次是当前元素的值,可选的当前元素的索引值和当期元素属于的数组对象

题目中只传入了parseInt函数,让我们再看看parseInt这个函数:

parseInt(string, radix)

parseInt() 函数可解析一个字符串,并返回一个整数。
string: 必需。要被解析的字符串。
radix: 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。

当忽略参数 radix , JavaScript 默认数字的基数如下:

  • 如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数。

  • 如果 string 以 0 开头,那么 ECMAScript v3 允许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。

  • 如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。

把这题分开来看就是:

// parseInt(currenValue, index)
parseInt('1', 0)    // 1
parseInt('2', 1)    // NaN
parseInt('3', 2)    // NaN

答案就是: [1, NaN, NaN]

第二题

[typeof null, null instanceof Object]

这题还好一点,typeof 返回一个表示类型的字符串. instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上.

我们都知道在javascript一切皆是对象,所以第一部分不用说,返回的肯定是Object,但是null并不存在于参数 object 的原型链上.

所以答案就是: [Object, false]

第三题

[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ]

先说说Array.reduce这个方法吧:

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

reduce接收两个参数,一个回调函数,一个可选的将用作累积的初始值。

如果提供了 initialValue,则 reduce 方法会对数组中的每个元素调用一次 callbackfn 函数(按升序索引顺序)。如果未提供 initialValue,则 reduce 方法会对从第二个元素开始的每个元素调用 callbackfn 函数。

回调函数接收四个参数,依次是

  • 通过上一次调用回调函数获得的值。如果向 reduce 方法提供 initialValue,则在首次调用函数时,total 为 initialValue。

  • 当前数组元素的值。

  • 当前数组元素的数字索引。

  • 包含该元素的数组对象。

Math.pow(x,y)

pow() 方法返回 x 的 y 次幂。

[3, 2, 1].reduce(Math.pow) 拆分开来就是:

Math.pow(3, 2)    // 9
Math.pow(9, 1)    // 9

但是reduce在两个情况下会抛出异常:

  • 当满足下列任一条件时,将引发 TypeError 异常: callbackfn 参数不是函数对象。

  • 数组不包含元素,且未提供 initialValue。

所以 [].reduce(Math.pow) 会抛出异常

答案为:an error

第四题

var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

这题看上去很简单对不对?答案不就是 Value is Something,如果你真的这么想,你会哭的,是的,笔者已经亲自试毒了。

简单来说就是+的优先级大于?

所以原题等价于:

console.log('Value is true' ? 'Something' : 'Nothing')

答案应该是 Something

第五题

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

我还是先说答案吧,可能你们会不相信,答案就是 Goodbye, Jack

为什么会这样呢,因为在javascript里,声明变量或函数会被提升,就是说,
变量提升是JavaScript将声明移至作用域 scope (全局域或者当前函数作用域) 顶部的行为。
你可以先使用一个函数或变量,再声明它:

show('Wiess')    // Hello Wiess
function (name) {
    console.log('Hello ' + name)
}

但是javascript只提升声明,而不是初始化,如果使用一个在已经使用后才声明和初始化的值,这个值将是undefined

所以这题就相当于:

var name = 'World!';
(function () {
    var name;
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

到这里是不是有点肝疼?习惯就好,习惯就好

第六题

var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
    count++;
}
console.log(count);

Math.pow(2, 53) = 9007199254740992(可以表示的最大值) 最大值加1还是9007199254740992,所以这个循环会一直下去

答案是: other

第七题

var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});

这里先科普一下Array.filter():

var newArray = array.filter(function(currentValue,index,arr), thisValue)

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

filter接收两个参数, 一个回调函数,一个是可选的回调函数的this值,默认为undefined

回调函数依次接收三个参数:

  • 必须。当前元素的值

  • 可选。当期元素的索引值

  • 可选。当期元素属于的数组对象

再说稀疏矩阵,当你取数组中某个没有定义的数时:

arr[4]    // undefined

但是当你遍历它时,你会发现,它并没有元素。
JavaScript会跳过这些缝隙.

所以答案为: []

第八题

var two   = 0.2
var one   = 0.1
var eight = 0.8
var six   = 0.6
[two - one == one, eight - six == two]

这题笔者认为应该选[false, false],但是答案却是[true, false]

就不说javascript里没有精确的浮点数了,这个大家应该都知道,但是为什么不能一视同仁呢?!希望有个大神来解答一下

第九题

function showCase(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(new String('A'));

这题主要考察的是switch,在switch里,比较用的是 ===,所以new String('A') 与 字面声明的 'A'是不一样的,所以答案是 Do not know!

第十题

function showCase2(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase2(String('A'));

String(x)不创建对象,但返回一个字符串,即typeof String(1)===“string”

所以答案为 Case A

查看原文

赞 2 收藏 20 评论 3

MrDream 关注了用户 · 2017-03-09

Aresn @aresn

正直 进取 合作 创新

关注 1745

MrDream 赞了文章 · 2017-02-22

Vue原理解析之observer模块

本文是针对Vue@2.1.8进行分析

observer是Vue核心中最重要的一个模块(个人认为),能够实现视图与数据的响应式更新,底层全凭observer的支持。

observer模块在Vue项目中的代码位置是src/core/observer,模块共分为这几个部分:

  • Observer: 数据的观察者,让数据对象的读写操作都处于自己的监管之下

  • Watcher: 数据的订阅者,数据的变化会通知到Watcher,然后由Watcher进行相应的操作,例如更新视图

  • Dep: ObserverWatcher的纽带,当数据变化时,会被Observer观察到,然后由Dep通知到Watcher

示意图如下:

clipboard.png

Observer

Observer类定义在src/core/observer/index.js中,先来看一下Observer的构造函数

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
      const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

value是需要被观察的数据对象,在构造函数中,会给value增加__ob__属性,作为数据已经被Observer观察的标志。如果value是数组,就使用observeArray遍历value,对value中每一个元素调用observe分别进行观察。如果value是对象,则使用walk遍历value上每个key,对每个key调用defineReactive来获得该key的set/get控制权。

解释下上面用到的几个函数的功能:

  • observeArray: 遍历数组,对数组的每个元素调用observe

  • observe: 检查对象上是否有__ob__属性,如果存在,则表明该对象已经处于Observer的观察中,如果不存在,则new Observer来观察对象(其实还有一些判断逻辑,为了便于理解就不赘述了)

  • walk: 遍历对象的每个key,对对象上每个key的数据调用defineReactive

  • defineReactive: 通过Object.defineProperty设置对象的key属性,使得能够捕获到该属性值的set/get动作。一般是由Watcher的实例对象进行get操作,此时Watcher的实例对象将被自动添加到Dep实例的依赖数组中,在外部操作触发了set时,将通过Dep实例的notify来通知所有依赖的watcher进行更新。

如果不太理解上面的文字描述可以看一下图:

clipboard.png

Dep

DepObserverWatcher之间的纽带,也可以认为Dep是服务于Observer的订阅系统。Watcher订阅某个ObserverDep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher

Dep提供了几个接口:

  • addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中

  • removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除

  • depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍

  • notify: 通知依赖数组中所有的watcher进行更新操作

Watcher

Watcher是用来订阅数据的变化的并执行相应操作(例如更新视图)的。Watcher的构造器函数定义如下:

constructor (vm, expOrFn, cb, options) {
  this.vm = vm
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = function () {}
      process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      )
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get()
}

参数中,vm表示组件实例,expOrFn表示要订阅的数据字段(字符串表示,例如a.b.c)或是一个要执行的函数,cb表示watcher运行后的回调函数,options是选项对象,包含deepuserlazy等配置。

watcher实例上有这些方法:

  • get: 将Dep.target设置为当前watcher实例,在内部调用this.getter,如果此时某个被Observer观察的数据对象被取值了,那么当前watcher实例将会自动订阅数据对象的Dep实例

  • addDep: 接收参数dep(Dep实例),让当前watcher订阅dep

  • cleanupDeps: 清除newDepIdsnewDep上记录的对dep的订阅信息

  • update: 立刻运行watcher或者将watcher加入队列中等待统一flush

  • run: 运行watcher,调用this.get()求值,然后触发回调

  • evaluate: 调用this.get()求值

  • depend: 遍历this.deps,让当前watcher实例订阅所有dep

  • teardown: 去除当前watcher实例所有的订阅

Array methods

src/core/observer/array.js中,Vue框架对数组的pushpopshiftunshiftsortsplicereverse方法进行了改造,在调用数组的这些方法时,自动触发dep.notify(),解决了调用这些函数改变数组后无法触发更新的问题。在Vue的官方文档中对这个也有说明:http://cn.vuejs.org/v2/guide/list.html#变异方法

查看原文

赞 33 收藏 40 评论 4

MrDream 关注了用户 · 2017-02-22

JoeRay61 @joeray61

前百度前端,泛陆家嘴地区非知名前端,公众号:前端boy

关注 94

MrDream 关注了专栏 · 2017-02-14

Weex

Weex 相关文章。(个人原创,不代表官方)

关注 137

MrDream 关注了用户 · 2017-02-14

Hanks10100 @hanks10100

关注 226

认证与成就

  • 获得 110 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-03-20
个人主页被 518 人浏览