Vue随堂笔记第二弹

​ 视频地址:https://www.bilibili.com/video/BV15741177Eh

​ 讲师:codewhy(B站搜索)

​ 入门篇:https://www.cnblogs.com/an-shiguang/p/14307317.html

认识VueCLI3

vue-cli 3 与 2 的区别

vue-cli 3 是基于 webpack 4 打造,vue-cli 2 是基于 webapck 3 打造。

vue-cli 3 的设计原则是“0配置”,移除了配置文件根目录下的buildconfig等目录。

vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化。

移除了static文件夹,新增了public文件夹,并且将index.html移动到了public文件夹中。

查看即修改配置

由于vue cli3采用“零配置”原则,一些配置文件都被隐藏了,如果想要修改其中的一些配置,你可以到Service.js中查看。

项目结构如下

如果想要修改默认配置,可以在项目根目录创建一个 vue.config.js 文件,使用 module.exports 导出配置,系统会自动从项目里查找该文件内的配置与原始配置进行合并。

自定义配置:起别名

初始化项目

执行命令创建项目

vue create <project name>

注意:项目名称中不能含有大写字母,否则会报错

执行步骤说明

选择配置方式

Default 默认配置,包含babeleslint

Manually select features 手动选择特性

按上下键跳转至对应的选项,按空格键进行选中或取消。然后按回车

选择Vue.js版本

选择将配置放置到单独的文件中还是存放到package.json

是否将配置保存下来,如果选择yes,需要为保存配置的文件夹设置个名称

下次创建项目时便可以选择已配置好的选项。

还可以到C\Users\你的用户名\.vuerc中更改设置。

选择安装方式

启动服务

npm run serve

vue ui

使用 vue ui 会启动一个图形化界面的服务用于管理Vue项目,可在任意目录使用该命令,默认端口 8000

界面如下图所示

箭头函数的使用和this指向

箭头函数的基本使用

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。

为什么叫Arrow Function?因为它的定义用的就是一个箭头:

x => x * x

上面的箭头函数相当于:

function (x) {
    return x * x;
}
<scripte>
     //箭头函数也是定义函数的一种方式
    //定义函数的方式 
    //1. function
    const aaa = function(){
    //  do something
   }
	
	2.对象字面量中定义函数
    const obj = {
        bbb : function(){
            // do something
        },
        //或者
        ccc(){
            // do something
        }
    }
    
    //3.ES6中的箭头函数
    //cosnt ddd = (参数列表) => {
    //   
    //}
</scripte>

箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }return

x => {
    if (x > 0) {
        return x * x;
    }
    else {
        return - x * x;
    }
}

如果参数不是一个,就需要用括号()括起来:

// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
    var i, sum = x + y;
    for (i=0; i<rest.length; i++) {
        sum += rest[i];
    }
    return sum;
}

如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

// SyntaxError:
x => { foo: x }

因为和函数体的{ ... }有语法冲突,所以要改为:

// ok:
x => ({ foo: x })

this指向

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

<script>
  // 什么时候使用箭头
  // setTimeout(function () {
  //   console.log(this);
  // }, 1000)
  //
  // setTimeout(() => {
  //   console.log(this);
  // }, 1000)

  // 问题: 箭头函数中的this是如何查找的了?
  // 答案: 向外层作用域中, 一层层查找this, 直到有this的定义.
  // const obj = {
  //   aaa() {
  //     setTimeout(function () {
  //       console.log(this); // window
  //     })
  //
  //     setTimeout(() => {
  //       console.log(this); // obj对象
  //     })
  //   }
  // }
  //
  // obj.aaa()


  const obj = {
    aaa() {
      setTimeout(function () {
        setTimeout(function () {
          console.log(this); // window
        })

        setTimeout(() => {
          console.log(this); // window
        })
      })

      setTimeout(() => {
        setTimeout(function () {
          console.log(this); // window
        })

        setTimeout(() => {
          console.log(this); // obj
        })
      })
    }
  }

  obj.aaa()
</script>

路由

路由routing)是一个网络工程里面的术语。就是通过互联的网络把信息从源地址传输到目的地址的活动。

在生活中,也存在路由的概念,比如路由器,那么路由器是做什么的?

路由器提供了两种机制: 路由和转送。

路由是决定数据包从来源目的地的路径。转送将输入端的数据转移到合适的输出端

路由中有一个非常重要的概念叫路由表。路由表本质上就是一个映射表, 决定了数据包的指向。

网页开发的几个阶段

后端路由阶段

早期的网站开发,整个HTML页面是由服务器来渲染的。服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示。但是, 一个网站, 这么多页面服务器如何处理呢?每个页面都有自己对应的网址, 也就是URL,URL会发送到服务器, 服务器通过正则对该URL进行匹配, 最后交给一个Controller进行处理。Controller进行各种处理, 最终生成HTML或者数据, 返回给前端,这就完成了一个IO操作。

上面的这种操作, 就是后端路由。当我们在页面中需要请求不同的路径内容时, 交给服务器进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端。这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO(Search Engine Optimization,搜索引擎优化)。

后端路由的缺点

  • 整个页面的模块由后端人员来编写和维护的,前后端分工不合理。
  • 前端开发人员如果要开发页面, 需要通过PHP或Java等语言来编写页面代码。技术要求高。
  • HTML代码和数据以及对应的逻辑混在一起, 编写和维护困难。

前后端分离阶段

随着Ajax的出现, 有了前后端分离的开发模式,后端只提供API来返回数据, 前端通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中。这样做最大的优点就是前后端责任的清晰, 后端专注于数据上, 前端专注于交互和可视化上。并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可。目前很多的网站依然采用这种模式开发。

单页面富应用阶段(SPA)

SPA最主要的特点就是在前后端分离的基础上加了一层前端路由,也就是前端来维护一套路由规则。

前端路由的核心是改变URL,但是页面不进行整体刷新

前端路由规则

URL的hash

URL的hash也就是锚点(#), 本质上是改变window.location的href属性。我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新。

HTML5的history模式

history接口是HTML5新增的, 它有五种模式改变URL而不刷新页面。

1.history.pushState()

2.history.replaceState()

详细用法可参考:https://www.cnblogs.com/lguow/p/9222272.html

3.history.go()

history.back() 等价于 history.go(-1), history.forward() 等价于 history.go(1)

vue-router

认识vue-router

目前前端流行的三大框架, 都有自己的路由实现:

  • Angular的ngRouter
  • React的ReactRouter
  • Vue的vue-router

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。

官网: https://router.vuejs.org/zh/

vue-router是基于路由和组件的,路由用于设定访问路径, 将路径和组件映射起来。在vue-router的单页面应用中, 页面的路径的改变就是组件的切换。

安装和使用vue-router

步骤一: 安装vue-router

npm install vue-router --save

步骤二: 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)

  1. 导入路由对象,并且调用 Vue.use(VueRouter)
  2. 创建路由实例,并且传入路由映射配置。
  3. 在Vue实例中挂载创建的路由实例。

创建router实例

// src/router/index.js
//配置路由相关的信息
import VueRouter from 'vue-router'
import Vue from 'vue'

//1.通过Vue.use(插件),安装插件
Vue.use(VueRouter)

//2.创建VueRouter对象
const routes = []
const router = new VueRouter({
  //配置路由和组件之间的对应关系
 routes
})

//3.将router对象传入到Vue实例

export default router

挂载到vue实例中

// src/main.js

import Vue from 'vue'
import App from './App'
//import router from './router/index.js'
import router from './router'  //导入文件夹时,默认查找index页面,所以index.js可以省略

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  render: h => h(App)
})

使用vue-router的步骤:

1.创建路由组件

2.配置路由映射: 组件和路径映射关系

3.使用路由: 通过 <router-link> <router-view>

<router-link>: 该标签是一个vue-router中已经内置的组件, 它会被渲染成一个a标签。

该标签会根据当前的路径, 动态渲染出不同的组件。

网页的其他内容, 比如顶部的标题/导航, 或者底部的一些版权信息等会和<router-view>处于同一个等级。

在路由切换时, 切换的是<router-view>挂载的组件, 其他内容不会发生改变。

vue-router细节处理

路由的默认路径

如果我们希望默认情况下进入的时网站的首页,渲染首页的内容。但是实现中, 默认没有显示首页组件, 必须让用户点击才可以实现。如何可以让路径默认跳到到首页, 并且<router-view>渲染首页组件呢?非常简单, 我们只需要配置多配置一个映射就可以了。

方法一:设置缺省值为首页(不推荐)

这样设置可以让缺省值为首页,但有一个缺点,就是并不显示首页的路径

方法二:使用重定向(推荐)

设置重定向更严谨,而且可以显示跳转的路径。

配置解析

path 配置的是根路径: /

redirect 是重定向, 也就是将根路径重定向到/home的路径下。

小贴士:/ 可加可不加,不加时为缺省值,加了为根路径。

HTML5的History模式

使用重定向时页面默认渲染首页内容依然有一点小瑕疵,那就是路径显示的哈希值(路径中含有#),其实可以让其显示为HTML5的History模式。

改变路径的方式有两种: URL的hash 以及 HTML5的history,默认情况下, 路径的改变使用的URL的hash。

如果希望使用HTML5的history模式, 非常简单, 进行如下配置即可。

这样就可以让路径显示为history模式了

router-link补充

在前面的<router-link>中, 我们只使用了一个属性: to, 用于指定跳转的路径。它还有一些其他属性:

  • tag : tag可以指定将<router-link>渲染成什么组件, 比如 <router-link to='/home' tag='li'> 会被渲染成一个<li>元素, 而不会被渲染为<a>元素。

  • replace : replace不会留下history记录, 所以指定replace的情况下, 后退键不能返回到上一个页面中。

  • active-class : 当 <router-link> 对应的路由匹配成功时(被点击后), 会自动给当前元素设置一个router-link-activeclass

    设置 active-class 可以修改默认的名称。在进行高亮显示的导航菜单或者底部tabbar时, 会使用到该类。但是通常不会修改类的属性, 会直接使用默认的 router-link-active即可。

    如果要修改的话,可以使用 active-class 属性,但是这样每一个<router-link> 都需要添加此属性。

    还可以进行统一配置

路由代码跳转

有时候, 页面的跳转需要执行对应的JavaScript代码, 这个时候, 可以使用如下方法

如果不希望页面回退,还可以使用this.$router.replace()方法。

动态路由

在某些情况下,一个页面的 path 路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径:

/user/userId/product/productId 除了有前面的/user之外,后面还跟上了用户的ID。

这种 path Component 的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)。

我们引入一个新的 User 组件,进行如下设置,:userId可以随意定义

可以直接在路径后拼接具体的动态路由

动态路由的具体路径还可以从data 中获取

我们可以用 $router.params 获取到对应的ID

路由懒加载

路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块,只有在这个路由被访问到的时候, 才加载对应的组件。

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。我们可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样做更高效。我们知道路由中通常会定义很多不同的页面,一般情况下,这个页面最终会被打包在一个js文件中,但是, 这么多页面放在一个js文件中, 会导致这个页面非常大,如果一次性从服务器请求这个页面, 可能需要花费一定的时间, 甚至在数据渲染时出现短暂空白的情况。使用路由懒加载可以避免这种情况的发生。

使用懒加载之前的效果

使用懒加载后的效果

懒加载的方式

方式一(不推荐): 结合Vue的异步组件和Webpack的代码分析

const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) })};

方式二(不推荐): AMD写法

const About = resolve => require(['../components/About.vue'], resolve);

方式三(推荐): 在ES6中, 可以有更加简单的写法来组织Vue异步组件和Webpack的代码分割

const Home = () => import('../components/Home.vue')

嵌套路由

嵌套路由是一个很常见的功能,比如在home页面中, 我们希望通过/home/news和/home/message访问一些内容

一个路径映射一个组件, 访问这两个路径也会分别渲染两个组件,路径和组件的关系如下:

实现嵌套路由有两个步骤:

  1. 创建对应的子组件, 并且在路由映射中配置对应的子路由。

  2. 在组件内部使用 <router-link><router-view>标签。

创建子组件

在路由中配置对应的子路由

在组件内部使用 <router-link><router-view> 标签

效果

还可以为子组件设置首页重定向

路由传递参数

为了演示传递参数, 我们这里再创建一个组件, 并且将其配置好。

第一步: 创建新的组件 Profile.vue

第二步: 配置路由映射

第三步: 添加跳转的 <router-link>

效果如下

传递参数主要有两种类型: paramsquery

params 类型:

配置路由格式: /router/:id

传递的方式: 在path后面跟上对应的值

传递后形成的路径: /router/123, /router/abc

query 类型:

配置路由格式: /router, 即普通配置

传递的方式: 对象中使用query的key作为传递方式

传递后形成的路径: /router?id=123, /router?id=abc

使用它们也有两种方式:<router-link> 的方式 和 JavaScript代码方式

方式一:使用 <router-link>

App.vue 中引入并传递参数

Profile.vue 中获取参数

方法二:使用JavaScript代码

<!-- App.vue -->
    
<template>
  <div id="app">
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
<!--    <router-link :to="'/user/'+userId">用户</router-link>-->
<!--    <router-link to="/profile">档案</router-link>-->
<!--    <router-link :to="{path:'/profile',query:{name:'shiguang',age:20}}">档案</router-link>-->
    <button @click="UserClick">用户</button>
    <button @click="ProFileClick">档案</button>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App',
  data(){
    return {
      userId :'zhangsan'
    }
  },
  methods:{
    UserClick(){
      this.$router.push('/user/'+ this.userId)
    },
    ProFileClick(){
      this.$router.push({
        path:'/profile',
        query:{
          name:'時光',
          age:20
        }
      })
    }
  }
}
</script>

<style>
.active {
  color: #f00;
}
</style>

效果如下

$route 和 $router的区别

$route$router是有区别的

$routeVueRouter 实例,想要导航到不同URL,则使用$route.push()方法

$route为当前router跳转对象里面可以获取name、path、query、params等

具体可参考:vue2.0中的$router 和 $route的区别

导航守卫

在一个SPA应用中, 如何改变网页的标题呢?网页标题是通过 <title> 来显示的, 但是SPA只有一个固定的HTML, 切换不同的页面时, 标题并不会改变。但是我们可以通过JavaScript来修改<title>的内容window.doucment.title=’新的标题‘

那么在Vue项目中, 在哪里修改? 什么时候修改比较合适呢?

普通修改方式

我们比较容易想到的修改标题的位置是每一个路由对应的组件.vue文件中。通过mounted生命周期函数, 执行对应的代码进行修改即可。但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码)。

除了这种方式,还有一种更好的方式,那就是导航守卫(Navigation Guard)。

vue-router 提供的导航守卫主要用来监听路由的进入和离开。

vue-router 提供了 beforeEachafterEach 的钩子函数, 它们会在路由即将改变前或后触发。

我们可以利用 beforeEach 来完成标题的修改。

首先, 我们可以在钩子当中利用meta定义定义一些标题

然后 利用导航守卫修改标题

参数解析

to: 即将要进入的目标的路由对象

from: 当前导航即将要离开的路由对象

next: 调用该方法后, 才能进入下一个钩子

注意:next()函数必须调用,否则进程会被中断,导致数据无法渲染。

补充说明

后置钩子 afterEach不需要主动调用next()函数。

beforeEachafterEach 皆属于全局守卫,除此之外还有路由独享守卫组件内守卫

详细介绍可参考官网: 导航守卫

keep-alive

<keep-alive> 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
<router-view > 也是一个组件,如果直接被包在 <keep-alive> 里,所有路径匹配到的视图组件都会被缓存:

 <keep-alive>
    <router-view>
	<!--      所有路径匹配到的视图组件都会被缓存-->
    </router-view>
 </keep-alive>

这里通过createddestroyed 生命周期函数进行验证,首先不使用<keep-alive> ,来回切换首页和其他组件,可以看出控制台不断有信息被打印,说明首页这个组件被不停地创建和销毁。(因为<router-view>内并没有其他内容,所以使用的自闭合标签)

然后使用<keep-alive> 标签对 <router-view> 进行包裹,来回切换首页和其他组件,控制台只在初次创建组件时输出了一次,而且并没有将其销毁。

接下来实现一下组件状态保留,即切换路由后再切换过来任然时刚才的状态而不是被重新渲染。

为了实现首页的状态保存,我们需要将缺省时的重定向注释掉(否则每次切换到首页都会被重定向到news页面)

但是注释掉首页重定向后会带来一个新的问题,就是进到首页必须点击首页内的子组件链接,子组件内的数据才会被渲染出来。我们可以借助 activated 函数 以及组件内守卫解决这个问题。

activated 函数 和 deactivated 函数分别在组件活跃状态和不活跃状态时被调用(所谓的活跃状态即使用当前组件时),并且只有这两个函数使用了 <keep-alive> 标签时才会起作用。

两个重要属性

include : 字符串或正则表达,只有匹配的组件会被缓存

exclude : 字符串或正则表达式,任何匹配的组件都不会被缓存

如上图所示,使用 exclude="User,Profile"User.vueprofile.vue 组件会被频繁创建和销毁。

Promise

作者:王云飞_小四_wyunfei
链接:https://www.jianshu.com/p/1b63a13c2701
来源:简书

promise是什么?

  1. 主要用于异步计算。
  2. 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。
  3. 可以在对象之间传递和操作promise,帮助我们处理队列。

为什么会有promise?

为了避免界面冻结(任务)

  • 同步:假设你去了一家饭店,找个位置,叫来服务员,这个时候服务员对你说,对不起我是“同步”服务员,我要服务完这张桌子才能招呼你。那桌客人明明已经吃上了,你只是想要个菜单,这么小的动作,服务员却要你等到别人的一个大动作完成之后,才能再来招呼你,这个便是同步的问题:也就是“顺序交付的工作1234,必须按照1234的顺序完成”。
  • 异步:则是将耗时很长的A交付的工作交给系统之后,就去继续做B交付的工作,等到系统完成了前面的工作之后,再通过回调或者事件,继续做A剩下的工作。
    AB工作的完成顺序,和交付他们的时间顺序无关,所以叫“异步”。

异步操作的常见语法

  1. 事件监听
document.getElementById('#start').addEventListener('click', start, false);
function start() {
  // 响应事件,进行相应的操作
}
// jquery on 监听
$('#start').on('click', start)
  1. 回调
// 比较常见的有ajax
$.ajax('http://www.wyunfei.com/', {
 success (res) {
   // 这里可以监听res返回的数据做回调逻辑的处理
 }
})

// 或者在页面加载完毕后回调
$(function() {
 // 页面结构加载完成,做回调逻辑处理
})

nodeJS之后,对异步的依赖进一步加剧

大家都知道在nodeJS出来之前PHP、Java、python等后台语言已经很成熟了,nodejs要想能够有自己的一片天,那就得拿出点自己的绝活:
无阻塞高并发,是nodeJS的招牌,要达到无阻塞高并发异步是其基本保障
举例:查询数据从数据库,PHP第一个任务查询数据,后面有了新任务,那么后面任务会被挂起排队;而nodeJS是第一个任务挂起交给数据库去跑,然后去接待第二个任务交给对应的系统组件去处理挂起,接着去接待第三个任务...那这样子的处理必然要依赖于异步操作

异步回调的问题:

  • 之前处理异步是通过纯粹的回调函数的形式进行处理

  • 很容易进入到回调地狱中,剥夺了函数return的能力

  • 问题可以解决,但是难以读懂,维护困难

  • 稍有不慎就会踏入回调地狱 - 嵌套层次深,不好维护

回调地狱

一般情况我们一次性调用API就可以完成请求。
有些情况需要多次调用服务器API,就会形成一个链式调用,比如为了完成一个功能,我们需要调用API1、API2、API3,依次按照顺序进行调用,这个时候就会出现回调地狱的问题

promise

  • promise是一个对象,对象和函数的区别就是对象可以保存状态,函数不可以(闭包除外)
  • 并未剥夺函数return的能力,因此无需层层传递callback,进行回调获取数据
  • 代码风格,容易理解,便于维护
  • 多个异步等待合并便于解决

promise详解

new Promise(
  function (resolve, reject) {
    // 一段耗时的异步操作
    resolve('成功') // 数据处理完成
    // reject('失败') // 数据处理出错
  }
).then(
  (res) => {console.log(res)},  // 成功
  (err) => {console.log(err)} // 失败
)
  • resolve作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
    reject作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  • promise有三个状态:

    1、pending[待定]初始状态
    2、fulfilled[实现]操作成功
    3、rejected[被否决]操作失败
    当promise状态发生改变,就会触发then()里的响应函数处理后续步骤;
    promise状态一经改变,不会再变。
  • Promise对象的状态改变,只有两种可能:
    从pending变为fulfilled
    从pending变为rejected。
    这两种情况只要发生,状态就凝固了,不会再变了。
最简单示例:
new Promise(resolve => {
  setTimeout(() => {
    resolve('hello')
  }, 2000)
}).then(res => {
  console.log(res)
})
分两次,顺序执行
new Promise(resolve => {
    setTimeout(() => {
      resolve('hello')
    }, 2000)
  }).then(val => {
    console.log(val) //  参数val = 'hello'
    return new Promise(resolve => {
      setTimeout(() => {
        resolve('world')
      }, 2000)
    })
  }).then(val => {
    console.log(val) // 参数val = 'world'
  })
promise完成后then()
let pro = new Promise(resolve => {
   setTimeout(() => {
     resolve('hello world')
   }, 2000)
 })
 setTimeout(() => {
   pro.then(value => {
   console.log(value) // hello world
 })
 }, 2000)

结论:promise作为队列最为重要的特性,我们在任何一个地方生成了一个promise队列之后,我们可以把他作为一个变量传递到其他地方。

假如在.then()的函数里面不返回新的promise,会怎样?

.then()

1、接收两个函数作为参数,分别代表fulfilled(成功)和rejected(失败)
2、.then()返回一个新的Promise实例,所以它可以链式调用
3、当前面的Promise状态改变时,.then()根据其最终状态,选择特定的状态响应函数执行
4、状态响应函数可以返回新的promise,或其他值,不返回值也可以我们可以认为它返回了一个null;
5、如果返回新的promise,那么下一级.then()会在新的promise状态改变之后执行
6、如果返回其他任何值,则会立即执行下一级.then()

.then()里面有.then()的情况

1、因为.then()返回的还是Promise实例
2、会等里面的then()执行完,再执行外面的

then嵌套

  • 对于我们来说,此时最好将其展开,也是一样的结果,而且会更好读:

    展开增加可读性

错误处理

Promise会自动捕获内部异常,并交给rejected响应函数处理。

  1. 第一种错误处理

  2. 第二种错误处理

  • 错误处理两种做法:
    第一种:reject('错误信息').then(() => {}, () => {错误处理逻辑})
    第二种:throw new Error('错误信息').catch( () => {错误处理逻辑})
    推荐使用第二种方式,更加清晰好读,并且可以捕获前面所有的错误(可以捕获N个then回调错误)

catch() + then()

  • 第一种情况:

    第一种情况 -结果

    结论:catch也会返回一个promise实例,并且是resolved状态

  • 第二种情况:

    第二种情况结果

结论:抛出错误变为rejected状态,所以绕过两个then直接跑到最下面的catch

Promise.all() 批量执行

Promise.all([p1, p2, p3])用于将多个promise实例,包装成一个新的Promise实例,返回的实例就是普通的promise
它接收一个数组作为参数
数组里可以是Promise对象,也可以是别的值,只有Promise会等待状态改变
当所有的子Promise都完成,该Promise完成,返回值是全部值得数组
有任何一个失败,该Promise失败,返回值是第一个失败的子Promise结果

//切菜
    function cutUp(){
        console.log('开始切菜。');
        var p = new Promise(function(resolve, reject){        //做一些异步操作
            setTimeout(function(){
                console.log('切菜完毕!');
                resolve('切好的菜');
            }, 1000);
        });
        return p;
    }

    //烧水
    function boil(){
        console.log('开始烧水。');
        var p = new Promise(function(resolve, reject){        //做一些异步操作
            setTimeout(function(){
                console.log('烧水完毕!');
                resolve('烧好的水');
            }, 1000);
        });
        return p;
    }

    Promise.all([cutUp(), boil()])
        .then((result) => {
            console.log('准备工作完毕');
            console.log(result);
        })
Promise.race() 类似于Promise.all() ,区别在于它有任意一个完成就算完成
let p1 = new Promise(resolve => {
        setTimeout(() => {
            resolve('I\`m p1 ')
        }, 1000)
    });
    let p2 = new Promise(resolve => {
        setTimeout(() => {
            resolve('I\`m p2 ')
        }, 2000)
    });
    Promise.race([p1, p2])
        .then(value => {
            console.log(value)
        })
  • 常见用法:
    异步操作和定时器放在一起,,如果定时器先触发,就认为超时,告知用户;
    例如我们要从远程的服务家在资源如果5000ms还没有加载过来我们就告知用户加载失败
  • 现实中的用法
    回调包装成Promise,他有两个显而易见的好处:
    1、可读性好
    2、返回 的结果可以加入任何Promise队列

实战示例,回调地狱和promise对比:

/***
   第一步:找到北京的id
   第二步:根据北京的id -> 找到北京公司的id
   第三步:根据北京公司的id -> 找到北京公司的详情
   目的:模拟链式调用、回调地狱
 ***/
 
 // 回调地狱
 // 请求第一个API: 地址在北京的公司的id
 $.ajax({
   url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/city',
   success (resCity) {
     let findCityId = resCity.filter(item => {
       if (item.id == 'c1') {
         return item
       }
     })[0].id
     
     $.ajax({
       //  请求第二个API: 根据上一个返回的在北京公司的id “findCityId”,找到北京公司的第一家公司的id
       url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/position-list',
       success (resPosition) {
         let findPostionId = resPosition.filter(item => {
           if(item.cityId == findCityId) {
             return item
           }
         })[0].id
         // 请求第三个API: 根据上一个API的id(findPostionId)找到具体公司,然后返回公司详情
         $.ajax({
           url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/company',
           success (resCom) {
             let comInfo = resCom.filter(item => {
               if (findPostionId == item.id) {
                 return item
               }
             })[0]
             console.log(comInfo)
           }
         })
       }
     })
   }
 })
// Promise 写法
  // 第一步:获取城市列表
  const cityList = new Promise((resolve, reject) => {
    $.ajax({
      url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/city',
      success (res) {
        resolve(res)
      }
    })
  })

  // 第二步:找到城市是北京的id
    cityList.then(res => {
      let findCityId = res.filter(item => {
        if (item.id == 'c1') {
          return item
        }
      })[0].id
      
      findCompanyId().then(res => {
        // 第三步(2):根据北京的id -> 找到北京公司的id
        let findPostionId = res.filter(item => {
            if(item.cityId == findCityId) {
              return item
            }
        })[0].id

        // 第四步(2):传入公司的id
        companyInfo(findPostionId)

      })

    })

  // 第三步(1):根据北京的id -> 找到北京公司的id
  function findCompanyId () {
    let aaa = new Promise((resolve, reject) => {
      $.ajax({
        url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/position-list',
        success (res) {
          resolve(res)
        }
      })
    })
    return aaa
  }

// 第四步:根据上一个API的id(findPostionId)找到具体公司,然后返回公司详情
function companyInfo (id) {
  let companyList = new Promise((resolve, reject) => {
    $.ajax({
      url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/company',
      success (res) {
        let comInfo = res.filter(item => {
            if (id == item.id) {
               return item
            }
        })[0]
        console.log(comInfo)
      }
    })
  })
}

Promise链式调用

我们在看Promise的流程图时,发现无论是then还是catch都可以返回一个Promise对象。所以,我们的代码其实是可以进行链式调用的。

我们直接通过Promise包装了一下新的数据,将Promise对象返回

Promise.resovle():将数据包装成Promise对象,并且在内部回调resolve()函数

Promise.reject():将数据包装成Promise对象,并且在内部回调reject()函数

简写

如果我们希望数据直接包装成Promise.resolve,那么在then中可以直接返回数据。

注意下面的代码中,将return Promise.resovle(data)改成了return data,结果依然是一样的

推荐参考

JavaScript Promise 对象

廖雪峰教程

ES6 Promise用法小结

Vuex

Vuex是做什么的

官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式

它采用 集中式存储管理应用所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

状态管理是什么

状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。你可以简单地将其看成把需要多个组件共享的变量全部存储在一个对象里面。然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。

那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?是响应式。

如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。

但是,有什么状态时需要我们在多个组件间共享的呢?如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。比如用户的登录状态、用户名称、头像、地理位置信息等等。比如商品的收藏、购物车中的物品等等。

这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的

单界面的状态管理

State:状态(可以当做就是data中的属性)

View:视图层,可以针对State变化显示不同的信息。

Actions:主要是用户的各种操作,点击、输入等等会导致状态的改变。

在这个案例中,counter便是需要管理的状态,counter需要某种方式被记录下来,也就是我们的State。

counter目前的值需要被显示在界面中,也就是我们的View部分。

界面发生某些操作时(这里是用户的点击,也可以是用户的input等),需要更新状态,也就是Actions

多界面状态管理

Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?

多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新),不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)。也就是说对于某些状态(状态1/状态2/状态3)来说只属于某一个视图,但是也有一些状态(状态a/状态b/状态c)属于多个视图共同想要维护的

状态1/状态2/状态3你放在自己的房间中,你自己管理自己用,没问题。但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理。Vuex就是为我们提供这个大管家的工具。

我们现在要做的就是将共享的状态抽取出来,交给大管家进行统一管理。之后,每个视图按照规定好的规定,进行访问和修改等操作。这就是Vuex的基本思想。

Vuex基本使用

先创建一个文件夹store存放Vuex代码,并且在其中创建一个index.js文件

//index.js

import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

const store = Vuex.Store({
    state:{
        count:0
    },
    mutations:{
        increment(state){
            state.count++
        },
        decrement(state){
            state.count--
        }
    }
})

export default store

然后,让所有的Vue组件都可以使用这个store对象

切换到main.js,导入store对象,并且放在new Vue中。这样,在其他Vue组件中就可以通过this.$store的方式,获取到这个store对象了

//main.js

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

new Vue({
    el:'#app',
    store,
    render: h => h(App)
})

这就是使用Vuex最简单的方式,接下来对使用步骤做一个简单的小节

  1. 提取出一个公共的store对象,用于保存在多个组件中共享的状态
  2. 将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
  3. 在其他组件中使用store对象中保存的状态即可
  4. 通过this.$store.state.属性的方式来访问状态
  5. 通过this.$store.commit('mutation中方法')来修改状态

注意事项

通过提交mutation的方式,而非直接改变store.state.count。

因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值。

Vuex核心概念

Vuex有几个比较核心的概念,它们分别是

  1. State
  2. Getters
  3. Mutations
  4. Action
  5. Module

State 单一状态树

Vuex提出使用单一状态树,英文名称是Single Source of Truth,也可以翻译成单一数据源。那么什么是单一状态树呢? 我用一个生活中的例子做一个简单的类比。

我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录,公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等。这些信息被分散在很多地方进行管理,有一天你需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。

这个和我们在应用开发中比较类似

如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难,所以Vuex也使用了单一状态树来管理应用层级的全部状态。单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。

Getters基本使用

有时候,我们需要从store中获取一些state变异后的状态,比如下面的Store中获取学生年龄大于20的个数。

const store = new Vuex.Store({
    state:{
        students:[
            {id:10010,name:'shiguang',age:20},
            {id:10011,name:'zhangsan',age:22},
            {id:10012,name:'lisi',age:21},
            {id:10013,name:'wangwu',age:19},
        ]
    }
})

我们可以在Store中定义getters

computed:{
    getGreaterAgesCount(){
        return this.$store.state.students.filter(age => age >= 20).length
    }
}
getters:{
    greaterAgesCount: state => {
        return state.students.filter(s => s.age >= 20).length
    }
}

如果我们已经有了一个获取所有年龄大于20岁学生列表的getters, 那么代码可以这样来写

getters:{
    greaterAgesStus: state => {
        return state.students.filter(s => s.age >=20)
    },
    greaterAgesCount: (state,getters) => {
        return getters.greaterAgesStus.length
    }
}

getters默认不能传递参数, 如果希望传递参数, 只能让getters本身返回另一个函数。比如根据ID获取用户的信息

getters:{
    stuById: state => {
        return id => {
            return state.students.find(s => s.id === id)
        }
    }
}

Mutation状态更新

Vuex的store状态的唯一更新方式: 提交Mutations

Mutation主要包括两部分:

  • 字符串的 事件类型(type)

  • 一个回调函数(handler),该回调函数的第一个参数就是state。

mutations的定义方式

mutations:{
    increment(state){
        state.count++
    }
}

通过mutation更新

increment:function(){
    this.$store.commit('increment')
}

Mutations传递参数

在通过mutations更新数据的时候, 有可能希望携带一些额外的参数,参数被称为是mutations的载荷(Payload)

Mutations中的代码

decrement(state,n){
    state.count -= n
}
decrement:function(){
    this.$store.commit('decrement',2)
}

但是如果参数不是一个呢?比如我们有很多参数需要传递.这个时候,通常会以对象的形式传递, 也就是payload是一个对象。这个时候可以再从对象中取出相关的信息。

changeCount(state,payload){
    state.count = payload.count
}
changeCount : function(){
    this.$store.commit('changeCount',{count:0})
}

Mutations提交风格

Vue还提供了另外一种风格, 它是一个包含type属性的对象

this.$store.commit({
    type:'changeCount',
    count:100
})

Mutations中的处理方式是将整个commit的对象作为payload使用, 所以代码没有改变, 依然如下

changeCount(state,payload){
    state.count = payload.count
}

Mutations响应规则

Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新。

这就要求我们必须遵守一些Vuex对应的规则

  • 提前在store中初始化好所需的属性

  • 当给state中的对象添加新属性时, 使用下面的方式:

    方式一: 使用Vue.set(obj, 'newProp', 123)

    方式二: 用新对象给旧对象重新赋值

我们来看一个例子,当我们点击更新信息时, 界面并没有发生对应改变。

如何才能让它改变呢?查看下面代码的方式一和方式二,都可以让state中的属性是响应式的。

Mutations常量类型

在mutations中,定义了很多事件类型(也就是其中的方法名称)。当项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutations中的方法越来越多。方法过多, 使用者需要花费大量的精力记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 如果不是复制, 可能还会出现写错的情况。

如何避免上述的问题呢?在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutations事件的类型,可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然。具体怎么做呢?我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量。定义常量时, 可以使用ES2015中的风格, 使用一个常量来作为函数的名称。

Mutation同步函数

通常情况下, Vuex要求我们Mutations中的方法必须是同步方法,主要原因是当使用devtools时, devtools可以帮助我们捕捉mutation的快照。

但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成。比如之前的代码, 当执行更新时, devtools中会有如下信息

但是, 如果Vuex中的代码, 我们使用了异步函数,state中的info数据一直没有被改变, 因为他无法追踪到.

所以通常情况下, 不要在mutations中进行异步的操作

Actions 基本定义

我们强调, 不要在Mutations中进行异步操作,但是某些情况, 确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?

Actions类似于Mutations, 但是是用来代替Mutations进行异步操作的。Action的基本使用代码如下

context是和store对象具有相同方法和属性的对象, 我们可以通过context进行commit相关操作, 也可以获取context.state等。但是需要注意,它们并不是同一个对象。

我们定义了actions, 然后又在actions中去进行commit, 这样的代码是否多此一举呢?事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了。

Actions的分发

在Vue组件中, 如果我们调用actions中的方法, 那么就需要使用dispatch

同样地, 也是支持传递payload

Actions返回Promise

Promise经常用于异步操作,在Actions中, 可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject。

Module

Module是模块的意思,Vue使用单一状态树也意味着很多状态都会交给Vuex来管理。当应用变得非常复杂时,store对象就有可能变得相当臃肿,为了解决这个问题, Vuex允许我们将store分割成模块(Module), 每个模块拥有自己的state、mutations、actions、getters等

Module局部状态

在moduleA中添加state、mutations、getters,mutations和getters接收的第一个参数是局部状态对象

注意:虽然 doubleCount和increment是定义在对象内部,但在调用时, 依然通过this.$store直接调用。

Actions的写法

actions的写法接收一个context参数对象,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

如果getters中也需要使用全局的状态, 可以接受更多的参数。

项目结构

Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰。

posted @ 2021-02-09 10:57  時光心向阳  阅读(195)  评论(0编辑  收藏  举报