vue的实践使用

# MVC/MVVM

# MVC

  • 模型(Model):数据保存
  • 视图(View):用户界面
  • 控制器(Controller):业务逻辑

分析

  • (1)View 传送指令到 Controller
  • (2)Controller 完成业务逻辑后,要求 Model 改变状态
  • (3)Model 将新的数据发送到 View ,用户得到反馈所有通信都是单向的。

# MVVM

MVVM(Model-View-ViewModel)是对 MVC(Model-View-Control)和 MVP(Model-View-Presenter)的进一步改进。

  • 模型(Model): Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
  • 视图(View): View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
  • 视图模型(ViewModel)MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用

分析

  • 各部分间都是双向通信
  • View 与 Model 不发生联系,都通过 ViewModel 传递
  • View 非常薄,不部署任何业务逻辑,称为“被动视图”(Passive View),即没有任何主动性;而 ViewModel 非常厚,所有逻辑都部署在那里
  • 采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel ,反之亦然。

# 图示比较

18-29-43 18-30-14

# 区别

mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到View

AngularEmber 都采用这种模式。

# VUE示例

通过一个 Vue 实例来说明 MVVM 的具体实现

(1) Model 层

{
    "url": "/your/server/data/api",
    "res": {
        "success": true,
        "name": "samy",
        "domain": "www.baidu.com"
    }
}
1
2
3
4
5
6
7
8

(2)View 层

<div id="app">
    <p>{{message}}</p>
    <button v-on:click="showMessage()">Click me</button>
</div>
1
2
3
4

(3)ViewModel 层

var app = new Vue({
    el: '#app',
    data: {  // 用于描述视图状态   
        message: 'Hello Vue!', 
    },
    methods: {  // 用于描述视图行为  
        showMessage(){
            let vm = this;
            alert(vm.message);
        }
    },
    created(){
        let vm = this;
        ajax({// Ajax 获取 Model 层的数据
            url: '/your/server/data/api',
            success(res){
                vm.message = res;
            }
        });
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# SPA

# 简介

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

# 优缺点

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小; (内容的改变不需要重新加载整个页面)
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
  • 页面效果会比较炫酷(比如切换页面内容时的专场动画)

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);
  • 页面复杂度提高很多

# 如何在单页 Vue 应用(SPA)中实现路由

可以通过官方的 vue-router 库在用 Vue 构建的 SPA 中进行路由。该库提供了全面的功能集,其中包括嵌套路线、路线参数和通配符、过渡、HTML5 历史与哈希模式和自定义滚动行为等功能。 Vue 还支持某些第三方路由器包。

# 单页面应用和多页面应用区别及优缺点

单页面应用(SPA),通俗一点说就是指只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多应用于pc端。

多页面(MPA),就是指一个应用中有多个页面,页面跳转时是整页刷新

# 渐进式框架

使用渐进式框架的代价很小,从而使现有项目(使用其他技术构建的项目)更容易采用并迁移到新框架Vue.js 是一个渐进式框架因为你可以逐步将其引入现有应用,而不必从头开始重写整个程序。

Vue 的最基本和核心的部分涉及“视图”层,因此可以通过逐步将 Vue 引入程序并替换“视图”实现来开始你的旅程

由于其不断发展的性质,Vue 与其他库配合使用非常好,并且非常容易上手。这与 Angular.js 之类的框架相反,后者要求将现有程序完全重构并在该框架中实现。

# 虚拟 DOM 的理解

分析

首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.

其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.

虚拟 DOM 的实现原理主要包括以下 3 部分

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树

# $nextTick

Vue 是异步修改 DOM 的并且不鼓励开发者直接接触 DOM,但有时候业务需要必须对数据更改--刷新后的 DOM 做相应的处理,这时候就可以使用 Vue.nextTick(callback)这个 api 。

$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

# Vue基础

# 简介

Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。 Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

DOMListeners和DataBindings是实现双向绑定的关键。

  • DOMListeners监听页面所有View层DOM元素的变化,当发生变化,Model层的数据随之变化;

  • DataBindings监听Model层的数据,当数据发生变化,View层的DOM元素随之变化。

# 优/缺点

  • 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

  • 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

  • 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码。

  • 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写

  • Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。不支持ie8及以下,部分兼容ie9 ,完全兼容10以上 // 缺点

# vue.js的核心【要点】

# 设计理念
  • 数据驱动(响应式):data中的数据变了,视图才会变; -----》 响应的数据绑定系统;MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
  • 组件系統(组件化):拆组装,目的在于重用,方便,脏活累活一次干完

得益于 Vue 的 响应式系统虚拟 DOM 系统 ,Vue 在渲染组件的过程中能自动追踪数据的依赖,并精确知晓数据更新的时候哪个组件需要重新渲染,渲染之后也会经过虚拟 DOM diff 之后才会真正更新到 DOM 上,Vue 应用的开发者一般不需要做额外的优化工作。

data

# VUE与REACT的区别

react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在react中,是单向数据流

vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。

# 和其它框架(jquery)的区别是什么?哪些场景适合?

区别:vue数据驱动,通过数据来显示视图层而不是节点操作。

场景:数据操作比较多的场景,更加便捷

# 响应式系统简述

4个点:跟数据的双向绑定的原理做比较;

  • 任何一个 Vue Component 都有一个与之对应的 Watcher 实例
  • Vue 的 data 上的属性会被添加 getter 和 setter 属性
  • Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被读, getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这一过程被称为依赖收集)
  • data 被改动时(主要是用户操作), 即被写, setter 方法会被调用, 此时 Vue 会去通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新

# 实例

虽然没有完全遵循 MVVM 模型 (opens new window),但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。

# 数据与方法

var data = { a: 1 }
var vm = new Vue({
  data: data
})
// 获得这个实例上的属性; 返回源数据中对应的字段
vm.a == data.a // => true

// 设置属性也会影响到原始数据
vm.a = 2
data.a // => 2
// ……反之亦然
data.a = 3
vm.a // => 3

//这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。
var obj = {
  foo: 'bar'
}
Object.freeze(obj)
new Vue({
  el: '#app',
  data: obj
})
<div id="app">
  <p>{{ foo }}</p><!-- 这里的 `foo` 不会更新! -->
  <button v-on:click="foo = 'baz'">Change it</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀 $,以便与用户定义的属性区分开来。

var data = { a: 1 }
var vm = new Vue({
  el: '#example',
  data: data
})

vm.$data === data // => true
vm.$el === document.getElementById('example') // => true

// $watch 是一个实例方法
vm.$watch('a', function (newValue, oldValue) {
  // 这个回调将在 `vm.a` 改变后调用
})
1
2
3
4
5
6
7
8
9
10
11
12
13

实例属性 (opens new window)

# 生命周期

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

# 生命周期的作用

它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑

# 各个生命周期及描述 (8+2)
生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前;vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化
created 组件实例已经完全创建,属性也绑定,完成了 data数据初始化;但真实 dom 还没有生成,$el 还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用;vue实例的$el和data都初始化了
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子;在 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activited keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
beforeDestory 组件销毁前调用
destoryed 组件销毁后调用

官方版本

# 生命周期总括【要点】

8 个生命周期,创建前/创建后、挂载前/挂载后、更新前/更新后、销毁前/销毁后。Vue 生命周期的作用方便我们通过它的生命周期,在业务代码中更好地操作数据,实现相关功能。这些函数称为生命周期 hook

8个生命周期过程:Vue 实例(组件)从其初始化到销毁和删除都经历生命周期;

  1. 创建前/后
    • beforeCreated 阶段,Vue 实例的挂载元素 $el 和数据对象 data 以及事件还未初始化。
    • created 阶段,Vue 实例的数据对象 data 以及方法的运算有了,$el 还没有。
  2. 载入前/后
    • beforeMount 阶段,render 函数首次被调用,Vue 实例的 $el 和 data 都初始化了,但还是挂载在虚拟的 DOM 节点上
    • mounted 阶段,Vue 实例挂载到实际的 DOM 操作完成,一般在该过程进行 Ajax 交互
  3. 更新前/后
    • 在数据更新之前调用,即发生在虚拟 DOM 重新渲染和打补丁之前,调用 beforeUpdate
    • 在虚拟 DOM 重新渲染和打补丁之后,会触发 updated 方法。
  4. 销毁前/后
    • 在执行实例销毁之前调用 beforeDestory,此时实例仍然可以调用
    • 在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 Vue 实例已经解除了事件监听以及和 DOM 的绑定,但是 DOM 结构依然存在
# keep-alive的生命周期

使用keep-alive标签后,会有两个生命周期函数分别是:activated、deactivated;

activated:页面渲染的时候被执行。deactivated:页面被隐藏或者页面即将被替换成新的页面时被执行。

keep-alive是一个抽象的组件,缓存的组件不会被mounted,为此提供activateddeactivated钩子函数

在2.1.0 版本后keep-alive新加入了两个属性: include(包含的组件缓存生效) 与 exclude(排除的组件不缓存,优先级大于include) 。

当引入keep-alive的时候,页面第一次进入,钩子的触发顺序 created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated

事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去都执行的方法放在 activated 中, activated 中的方法不管是否需要缓存都会执行

# 简单说一下:在生命周期内做事情;

  • beforecreate : 可以在这加个loading事件,在加载实例时触发
  • created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
  • mounted : 挂载元素,获取到DOM节点
  • updated : 如果对数据统一处理,在这里写上相应函数
  • beforeDestroy : 可以做一个确认停止事件的确认框
  • nextTick : 更新数据后立即操作dom

# 第一次页面加载会触发哪几个生命周期

第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个生命周期。

# DOM 渲染在哪个周期就已经完成

挂载前后阶段;在 beforeMounted 时它执行了 render 函数,对 $el 和 data 进行了初始化,但此时还是挂载到虚拟的 DOM 节点,然后它mounted 时就完成了 DOM 渲染,这时候我们一般还进行 Ajax 交互。Vue 在 rendercreateElement 的时候,并不是产生真实的 DOM 元素;

dom是在 mounted 中完成渲染;

# 生命周期内调用异步请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性

# 列举出3个Vue中常用的生命周期钩子函数

  1. created: 实例已经创建完成之后调用,在这一步,实例已经完成数据观测, 属性和方法的运算, watch/event事件回调. 然而, 挂载阶段还没有开始, $el属性目前还不可见
  2. mounted: el被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。
  3. activated::keep-alive组件激活时调用
  • created
  • mounted
  • updated
  • destroyed
# this 指向问题

生命周期钩子的 this 上下文指向调用它的 Vue 实例。

new Vue({
  data: {
    a: 1
  },
  created: function () {
    console.log('a is: ' + this.a)// `this` 指向 vm 实例
  }
}) // => "a is: 1"

var data = { a: 1 }
var vm = new Vue({
  el: '#example',
  data: data
})
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

不要在选项属性或回调上使用箭头函数 (opens new window),比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

# 开发人员经常使用字母 “vm” 作为变量名来声明根 Vue 实例。例如 const vm = new Vue()。在这种情况下,“vm”指的是什么?

虽然这不是约定,但是开发人员经常使用变量名称 'vm' 来命名根 Vue 实例,该变量名称代表 'ViewModel',因为 Vue 本质上负责视图层,并且部分受到了 MVVM 模式的启发(Model-View-View-Model)。但是,根本没有必要将根实例命名为 “vm”

# 模板语法

# 插值

<!--文本-->
<span>Message: {{ msg }}</span>
<span v-once>这个将不会改变: {{ msg }}</span>

<!--原始 HTML: 请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。-->
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

<!--attribute-->
<div v-bind:id="dynamicId"></div>

<!-- 使用 JavaScript 表达式 -->
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

因为 mustache 模板标签 将传入的数据视为字符串,而不将其解析为可执行代码。**这也有助于缓解把恶意代码注入到页面的 XSS 相关的问题 。**这类似于在 JavaScript 中使用 elementSelector.innerText = text 语句。

# 指令

指令是一系列特殊属性,你可以通过将其添加到模板 HTML 标记中来赋予它们特殊的响应功能。指令允许模板中的元素使用数据属性、方法、计算或监视的属性和内联表达式根据定义的逻辑对更改做出反应。

指令以 v- 开头来指示 Vue 特定的属性。

<p v-if="seen">现在你看到我了</p>

<a v-bind:href="url">...</a>
<a v-on:click="doSomething">...</a>

<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
<!--当 eventName 的值为 "focus" 时,v-on:[eventName] 将等价于 v-on:focus。-->
<a v-on:enlarge-text="postFontSize += 0.1"></a>  <!--自定义事件监听-->

<!--
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>
<!-- .prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault() -->
<form v-on:submit.prevent="onSubmit">...</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# v-html

更新元素的 innerHTML注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译 。如果试图使用 v-html 组合模板,可以重新考虑是否通过使用组件来替代。在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击 (opens new window)只在可信内容上使用 v-html,永不用在用户提交的内容上

单文件组件 (opens new window)里,scoped 的样式不会应用在 v-html 内部,因为那部分 HTML 没有被 Vue 的模板编译器处理如果你希望针对 v-html 的内容设置带作用域的 CSS,你可以替换为 CSS Modules (opens new window) 或用一个额外的全局 <style> 元素手动设置类似 BEM 的作用域策略

确保在单文件组件中定义的 CSS 样式仅应用于该组件,而不被用于其他组件; 可以通过样式标签上的 scoped 属性来实现。在内部 Vue 使用 PostCSS 插件为所有样式元素分配唯一的数据属性,然后使样式针对这些唯一的元素。

<div v-html="html"></div>

<template>
    <div class=”title”>This is a title</div>
</template>
<style scoped>
    .title {
        font-family: sans-serif;
        font-size: 20px;
</style>
1
2
3
4
5
6
7
8
9
10

# 缩写

Vue 为 v-bindv-on 这两个最常用的指令,提供了特定简写:

<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>

<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
1
2
3
4
5
6
7
8
9

# 计算属性(computed)和侦听器(watch)

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    reversedMessage: function () { // 计算属性的 getter
      return this.message.split('').reverse().join('')// `this` 指向 vm 实例
    }
  }
})
//因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。
//意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 计算属性缓存vs方法

不同的是计算属性是基于它们的响应式依赖进行缓存

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A 。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

computed: {
  now: function () {//相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
    return Date.now()
  }
}
1
2
3
4
5

# 计算属性vs侦听属性

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性

当有一些数据需要随着其它数据变动而变动时; 然而通常更好的做法是使用计算属性而不是命令式的 watch 回调;

一个自定义的侦听器 watch ;当需要在数据变化时执行异步或开销较大的操作时watch 这个方式是最有用的。

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

//上面代码是命令式且重复的。将它与计算属性的版本进行比较:
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    },
    fullName: {//计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter :
      get: function () {
        return this.firstName + ' ' + this.lastName
      },
      set: function (newValue) {
        var names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

computed: 是计算属性,依赖其它属性值(A),并且 computed 的值有缓存(B),只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

watch的使用示例单页面加载 vue 及axios的处理示例

使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

除了 watch 选项之外,您还可以使用命令式的 vm.$watch

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
<script>
  var watchExampleVM = new Vue({
    el: '#watch-example',
    data: {
      question: '',
      answer: 'I cannot give you an answer until you ask a question!'
    },
    watch: {
      question: function(newQuestion, oldQuestion) {// 如果question发生改变,这个函数就会运行
        this.answer = 'Waiting for you to stop typing...'
        this.debouncedGetAnswer()
      }
    },
    created: function () {// 初始化方法并调用一次;限制操作频率的函数。
      this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
    },
    methods: {
      getAnswer: function () {
        if (this.question.indexOf('?') === -1) {
          this.answer = 'Questions usually contain a question mark. ;-)'
          return
        }
        this.answer = 'Thinking...'
        var vm = this
        axios.get('https://yesno.wtf/api')
          .then(function (response) {
            vm.answer = _.capitalize(response.data.answer)//转换字符串首字母为大写,剩下为小写。
          })
          .catch(function (error) {
            vm.answer = 'Error! Could not reach the API. ' + error
          })
      }
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# watch、computed与methods的联系和区别【要点】

watch 就是监听的意思,其专门用来观察和响应Vue实例上的数据的变动

能使用watch属性的场景基本上都可以使用computed属性,而且computed属性开销小,性能高,因此能使用computed就尽量使用computed属性

想要执行异步或昂贵的操作以响应不断变化的数据时,这时watch就派上了大用场。

其应用场景一般都是搜索框之类的,需要不断的响应数据的变化;如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。

computed 就是计算属性,其可以当成一个data数据来使用。直接使用,不用像方法那样调用();

使用计算属性要注意几点:

  • computed是在HTML DOM加载后马上执行的,如赋值操作;
  • 计算属性计算时所依赖的属性一定是响应式依赖,否则计算属性不会执行。
  • 计算属性是基于依赖进行缓存的,就是说在依赖没有更新的情况,调用计算属性并不会重新计算,可以减少开销。所以可以说computed的值在getter执行后是会进行缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算出对应的新值。

其一般应用于比较复杂的渲染数据计算或者不必重新计算数值的情况;当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed。

methods 就是方法。没有缓存,也不像computed在DOM加载后可以自动执行,他必须有一定的触发条件才能执行,如点击事件等;

我们可以将同一函数定义为一个methods或者一个computed。对于最终的结果,两种方式确实是相同的。

但是最大的区别在于:计算属性 computed 和事件 methods 有什么区别

  • computed计算属性是基于它们的依赖进行缓存的。如果你进行多次访问的时候(在不改变值的情况下),计算属性会立即返回数据,而不必再次执行函数。并且他还可以自动执行。
  • methods只要发生重新渲染,就必定执行该函数,他必须有一定的触发条件才能执行。

# 观察者

观察者允许我们观察更改的特定属性,并执行定义为函数的自定义操作。尽管它们的用例与计算的属性相交叉,但是当某些数据属性发生改变时,有时需要观察者执行自定义操作或运行代价昂贵的操作。

# Class与Style绑定

Class可以通过对象语法和数组语法进行动态绑定:可简写 :class="{}"

<!-- 对象语法:-->
<div :class=”{ divStyle : showDiv }”></div>
//只要数据属性 showDiv 为 true,类名 divStyle 将应用于 div
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: {
  isActive: true,
  hasError: false
}
<div v-bind:class="classObject"></div>
data: {
  classObject: {
    active: true,
    'text-danger': false
  }
}
data: {
  isActive: true,
  error: null
},
computed: {
  classObject: function () {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}

<!-- 数组语法:-->
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}
<div v-bind:class="[activeClass, errorClass]"></div>
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
<Button :class=”[‘btn’, ‘btnRed’, { btnActive : isActive }]”>Process</button>
//将串联各个类的数组,并基于 isActive 数据属性的值对对象中的表达式进行响应式评估

<!-- 用在组件上:-->
Vue.component('my-component', {
  template: '<p class="foo bar">Hi</p>'
})
<my-component class="baz boo"></my-component>
<!--HTML 将被渲染为:<p class="foo bar baz boo">Hi</p>-->
<my-component v-bind:class="{ active: isActive }"></my-component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

Style 也可以通过对象语法和数组语法进行动态绑定:可简写 :style="{}"

自动添加前缀:当 v-bind:style 使用需要添加浏览器引擎前缀 (opens new window)的 CSS 属性时,如 transformVue.js 会自动侦测并添加相应的前缀

<!-- 对象语法:-->
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
  activeColor: 'red',
  fontSize: 30
}
<div v-bind:style="styleObject"></div>

<!-- 数组语法:-->
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}

<!-- 多重值:常用于提供多个带前缀的值-->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div
<!--这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 条件渲染

# v-if

<div v-if="type === 'A'"> A </div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else> Not A/B/C</div>
1
2
3
4
# 用key管理可复用的元素
<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="email-input">
</template>
1
2
3
4
5
6
7
8

# v-show

带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display。使用 v-show 指令时,可使用 CSS 的 display 属性切换元素的可见性

注意,v-show 不支持 <template> 元素,也不支持 v-else

# v-ifv-show指令

  • v-if指令是直接销毁和重建DOM达到让元素显示和隐藏的效果; 每次显示状态更改时,代价通常会更大。

  • v-show指令是通过修改元素的display CSS属性(display:none)让其显示或者隐藏; 但是display:none, 把元素隐藏起来,并且会改变页面布局

  • 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,

    • 如果需要非常频繁地切换,则使用 v-show 较好
    • 如果在运行时条件很少改变,则使用 v-if 较好

    总括

    另一方面,v-show 成本较低,因为它仅切换元素的CSS显示属性。所以如果必须经常切换元素,则 v-show 会提供比 v-if 更好,更优化的结果。

    就加载元素的初始渲染成本而言,v-if 不会渲染最初隐藏的元素的节点,而 v-show 会渲染其 CSS display 属性被设置为 none 的元素

# v-ifv-for指令

不推荐同时使用 v-ifv-for; 当 v-ifv-for 一起使用时,v-for 具有比 v-if 更高的优先级。这意味着 v-if 将分别重复运行于每个 v-for 循环中。

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>
<!-- 上面的代码将只渲染未完成的 todo -->

<!-- 优化后 -->
<!--而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>)上。-->
<ul v-if="todos.length">
  <li v-for="todo in todos">
    {{ todo }}
  </li>
</ul>
<p v-else>No todos left!</p>
1
2
3
4
5
6
7
8
9
10
11
12
13

v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
	 return user.isActive
    })
  }
}
//不推荐:
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# vue中常用的命令

  • v-if/v-show区别 判断是否隐藏
  • v-for 数据循环出来
  • v-model 实现双向绑定
  • v-bind v-bind:class:绑定一个属性;
  • v-on

# 列表渲染

# v-for

要遍历对象或数组,可以使用 v-for 指令

使用数组

<ul id="example-2">
 <!-- <li v-for="item in items"> -->
  <!-- 也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法 -->
  <!-- <li v-for="item of items"> -->
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>
var example2 = new Vue({
  el: '#example-2',
  data: {
    parentMessage: 'Parent',
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用对象: 在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。

<ul id="v-for-object" class="demo">
  <li v-for="value in object">
    {{ value }}
  </li>
  <div v-for="(value, key) in object">
    {{ key }}: {{ value }}
  </div>
</ul>
new Vue({
  el: '#v-for-object',
  data: {
    object: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。 <span v-for="n in 10"> </span>

#<template>及组件上使用

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>
1
2
3
4
5
6

任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。我们要使用 prop:不自动将 item 注入到组件里的原因是,这会使得组件与 v-for 的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。

<my-component v-for="item in items" :key="item.id"></my-component>

<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>
1
2
3
4
5
6
7
8

完整示例: 注意这里的 is="todo-item" 属性。**这种做法在使用 DOM 模板时是十分必要的,因为在 <ul> 元素内只有 <li>元素会被看作有效内容。**这样做实现的效果与 <todo-item> 相同,但是可以避开一些潜在的浏览器解析错误。

<div id="todo-list-example">
  <form v-on:submit.prevent="addNewTodo">
    <label for="new-todo">Add a todo</label>
    <input
      v-model="newTodoText"
      id="new-todo"
      placeholder="E.g. Feed the cat"
    >
    <button>Add</button>
  </form>
  <ul>
    <li
      is="todo-item"
      v-for="(todo, index) in todos"
      v-bind:key="todo.id"
      v-bind:title="todo.title"
      v-on:remove="todos.splice(index, 1)"
    ></li>
  </ul>
</div>

Vue.component('todo-item', {
  template: '\
    <li>\
      {{ title }}\
      <button v-on:click="$emit(\'remove\')">Remove</button>\
    </li>\
  ',
  props: ['title']
})

new Vue({
  el: '#todo-list-example',
  data: {
    newTodoText: '',
    todos: [
      {
        id: 1,
        title: 'Do the dishes',
      }
    ],
    nextTodoId: 2
  },
  methods: {
    addNewTodo: function () {
      this.todos.push({
        id: this.nextTodoId++,
        title: this.newTodoText
      })
      this.newTodoText = ''
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 维护状态

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出v1.0版本的缺点

<div v-for="item in items" v-bind:key="item.id">
  <!-- 内容 -->
</div>
1
2
3

不要使用对象或数组之类非基本类型值作为 v-forkey。请用字符串或数值类型的值。

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:

  • 完整地触发组件的生命周期钩子
  • 触发过渡
<transition>
  <span :key="text">{{ text }}</span>
</transition>
<!-- 当 text 发生改变时,这个元素的key属性就发生了改变,在渲染更新时,Vue会认为这里新产生了一个元素,而老的元素由于key不存在了,所以会被删除,从而触发了过渡。 -->
1
2
3
4

现在,每次切换时,输入框都将被重新渲染。注意,<label> 元素仍然会被高效地复用,因为它们没有添加 key 属性。

# key 的作用【要点】
  1. 让vue精准的追踪到每一个元素,高效的更新虚拟DOM
  2. 触发过渡

key 的作用主要是为了高效的更新虚拟DOM在更新组件时判断两个节点是否相同相同就复用,不相同就删除旧的创建新的。【高效的更新虚拟DOM

**key是给每一个vnode的唯一id,**可以依靠key,更准确, 更的拿到oldVnode中对应的vnode节点。我们的 diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。【要点】

  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,【要点】

源码如下

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}
1
2
3
4
5
6
7
8
9

# 过滤/排序结果

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。

<li v-for="n in evenNumbers">{{ n }}</li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

<!-- 在计算属性不适用的情况下 (例如,在嵌套 v-for 循环中) 你可以使用一个方法: -->
<li v-for="n in even(numbers)">{{ n }}</li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
  even: function (numbers) {
    return numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 数组更新检测 vm.$set

由于 JavaScript 的限制,Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

解决办法

解决上面1问题: Vue 提供了以下操作方法:

Vue.set(vm.items, indexOfItem, newValue)// Vue.set
vm.$set(vm.items, indexOfItem, newValue)// vm.$set,Vue.set的一个别名
vm.items.splice(indexOfItem, 1, newValue)// Array.prototype.splice; 底层实现方式;
1
2
3

解决上面2问题:Vue 提供了以下操作方法:

vm.items.splice(newLength)// Array.prototype.splice
1

示例:通过socket监听设备在线状态

sockets: {
    sDevices: function (value) {
        const index = this.data.findIndex(item => value.id === item.id)
        if (index >= 0) {
            this.data.splice(index, 1, value)
        }
    }
},
1
2
3
4
5
6
7
8

# 对象更新检测

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性

var vm = new Vue({
  data: {
    a: 1
  }
})// `vm.a` 现在是响应式的
vm.b = 2// `vm.b` 不是响应式的

var vm = new Vue({
  data: {
    userProfile: {
      name: 'Anika'
    }
  }
})
//你还可以使用 vm.$set 实例方法,它只是全局 Vue.set 的别名:
Vue.set(vm.userProfile, 'age', 27)
vm.$set(vm.userProfile, 'age', 27)
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject,'b',2)

//有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:
vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })


export default {
    data(){
        return {
            obj: {
                name: 'samy'
            }
        }
    },
    mounted(){
        this.$set(this.obj, 'sex', 'man')
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# vm.$set()实现原理【要点】

受现代 JavaScript 的限制(而且 Object.observe 也已经被废弃) ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性;

我们查看对应的 Vue 源码:vue/src/core/instance/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  // target 为数组  
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式  
    target.splice(key, 1, val)
    return val
  }
  // key 已经存在,直接修改属性值  
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 对属性进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

我们阅读以上源码可知,vm.$set 的实现原理是

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式
  • 如果目标是对象,会先判断属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

# vue 中怎么重置 data

使用Object.assign()vm.$data可以获取当前状态下的datavm.$options.data可以获取到组件初始化状态下的dataObject.assign(this.$data, this.$options.data())

# vue组件 data 为什么必须是函数?

因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,使得所有组件实例共用了一份data,造成了数据污染,会影响到所有Vue实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响 用在子组件中多;

# 为什么会出现vue修改数据后页面没有刷新?

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的

# vue不能检测数组或对象变动问题的解决方法有哪些?

  • vm.$set(vm.items, indexOfItem, newValue)
  • vm.items.splice
  • 使用this.$forceupdate强制刷新
  • 使用Proxy
  • 使用立即执行函数

数组监听源码实现:

const arrProto = Array.prototype
const arrayMethods = Object.create(arrProto)
const m = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
m.forEach(function (method) {
    const original = arrProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function v(...args) {
            return original.apply(this, args)
        }
    })
})
function defineReactive(data) {
    if (data && typeof data !== 'object') return
    if (Array.isArray(data)) {
        data.__proto__ = arrayMethods
    } else {
        Object.keys(data).forEach(function (val) {
            observer(data, val, data[val])
        })
    }
}
function observer(data, key, value) {
    defineReactive(data)
    Object.defineProperty(data, key, {
        get() {return value},
        set(newVal) {
            if (newVal === value) return
            value = newVal
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 事件处理(v-on)

所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处

  1. 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
  2. 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
  3. 一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。

this 在方法里指向当前 Vue 实例;event 是原生 DOM 事件; 也可以用 JavaScript 直接调用方法;

<div id="example-2">
  <button v-on:click="greet">Greet</button>
  <button v-on:click="count++">You clicked me {{ count }} times.</button>
</div>
var example2 = new Vue({
  el: '#example-2',
  data: {
    name: 'Vue.js',
    count: 0
  },
  methods: {
    greet: function (event) {
      alert('Hello ' + this.name + '!')// `this` 在方法里指向当前 Vue 实例
      if (event) { // `event` 是原生 DOM 事件
        alert(event.target.tagName)//Button
      }
    }
  }
})
// 也可以用 JavaScript 直接调用方法
example2.greet() // => 'Hello Vue.js!'

<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>
<button v-on:click="warn('Form cannot be submitted yet.', $event)"> Submit </button>
methods: {
  warn: function (message, event) { // 现在我们可以访问原生事件对象
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}

//可以使用 v-on:click 指令捕获 Click 事件。该指令也可以用缩写符号 @click 表示
<a v-on:click=”clickHandler”>Launch!</a>
<a @click=”clickHandler”>Launch!</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。

  • .stop - 调用 event.stopPropagation()
  • .prevent - 调用 event.preventDefault()
  • .capture - 添加事件侦听器时使用 capture 模式。
  • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
  • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
  • .native - 监听组件根元素的原生事件。
  • .once - 只触发一次回调。 可以绑定多个方法
  • .left - (2.2.0) 只当点击鼠标左键时触发。
  • .right - (2.2.0) 只当点击鼠标右键时触发。
  • .middle - (2.2.0) 只当点击鼠标中键时触发。
  • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,

v-on:click.prevent.self 会阻止所有的点击;

v-on:click.self.prevent 只会阻止对元素自身的点击

不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你想阻止事件的默认行为。

<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
<!--处理函数只会在 $event.key 等于 PageDown 时被调用-->
<input v-on:keyup.page-down="onPageDown">

<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

//希望使用户能够按下键盘上的Enter键,来将内容提交给名为 “storeComment” 的方法
<textarea @keyup.enter="storeComment"></textarea>
new Vue({
  el: '#app',
  methods: {
    storeComment(event) {
      //using event.target.value or use v-model to bind to a data property
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

使用 Vue 时调用 event.preventDefault() 的最佳方式

在事件侦听器上调用 event.preventDefault() 的最佳方式是.prevent 修饰符与 v-on 指令一起使用

<a @click.prevent=”doSomethingWhenClicked”>Do Something</a>
1

# 表单输入(v-model)双向绑定

尽管有些神奇,v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。在表单控件或者组件上创建双向绑定。

# v-model 的原理【要点】

v-model 本质上不过是语法糖; v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段value 作为 prop 并将 change 作为事件

上面是具体实现,内部实现还是:vue数据双向绑定实现原理解析 详见下面部分介绍;

以 input 表单元素为例:

<input v-model="searchText">
<!--等价于:-->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

<input type="text" v-model="nameInput">
<input type="text" :value="nameInput" @keyup="nameInput = $event.target.value">
1
2
3
4
5
6
7
8
9
<template>//Parent
    {{num}}
    <Child v-model="num">
</template>
export default {
    data(){
        return {
            num: 0
        }
    }
}
<template>//Child
    <div @click="add">Add</div>
</template>
export default {
    props: ['value'],
    methods:{
        add(){
            this.$emit('input', this.value + 1)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,(详见后面自定义事件);

<ModelChild v-model="message"></ModelChild> //父组件:
<div>{{value}}</div> //子组件:
props:{
    value: String
},
methods: {
  test1(){
     this.$emit('input', 'samy')
  },
},
1
2
3
4
5
6
7
8
9
10
<!--文本-->
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

<!--多行文本-->
<!--在文本区域插值 (<textarea>{{text}}</textarea>) 并不会生效,应用 v-model 来代替-->
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
1
2
3
4
5
6
7
8
9
10
<!--复选框-->
<input type="checkbox" id="checkbox" v-model="checked"><!--单个复选框-->
<label for="checkbox">{{ checked }}</label>
<div id='example-3'> <!--多个复选框,绑定到同一个数组-->
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames">
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  <label for="mike">Mike</label>
  <br>
  <span>Checked names: {{ checkedNames }}</span>
</div>
new Vue({
  el: '#example-3',
  data: {
    checkedNames: []
  }
})

<!--单选按钮-->
<div id="example-4">
  <input type="radio" id="one" value="One" v-model="picked">
  <label for="one">One</label>
  <br>
  <input type="radio" id="two" value="Two" v-model="picked">
  <label for="two">Two</label>
  <br>
  <span>Picked: {{ picked }}</span>
</div>
new Vue({
  el: '#example-4',
  data: {
    picked: ''
  }
})

<!--选择框-->
<div id="example-5">
  <select v-model="selected">
  <!--<select v-model="selected" multiple style="width: 50px;"> -->
    <option disabled value="">请选择</option> <!--提供一个值为空的禁用选项-->
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>
new Vue({
  el: '...',
  data: {
    selected: ''
  }
})
<!--用 v-for 渲染的动态选项-->
<select v-model="selected">
  <option v-for="option in options" v-bind:value="option.value">
    {{ option.text }}
  </option>
</select>
<span>Selected: {{ selected }}</span>
new Vue({
  el: '...',
  data: {
    selected: 'A',
    options: [
      { text: 'One', value: 'A' },
      { text: 'Two', value: 'B' },
      { text: 'Three', value: 'C' }
    ]
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# 值绑定

对于单选按钮,复选框及选择框的选项,v-model 绑定的值通常是静态字符串 (对于复选框也可以是布尔值):

<!-- 当选中时,`picked` 为字符串 "a" -->
<input type="radio" v-model="picked" value="a">

<!-- `toggle` 为 true 或 false -->
<input type="checkbox" v-model="toggle">

<!-- 当选中第一个选项时,`selected` 为字符串 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>
1
2
3
4
5
6
7
8
9
10

但是有时我们可能想把值绑定到 Vue 实例的一个动态属性上,这时可以用 v-bind 实现,并且这个属性的值可以不是字符串。

<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no"
>
<!-- vm.toggle === 'yes'// 当选中时 vm.toggle === 'no' // 当没有选中时 -->

<input type="radio" v-model="pick" v-bind:value="a"><!-- vm.pick === vm.a // 当选中时 -->

<select v-model="selected">
  <option v-bind:value="{ number: 123 }">123</option><!-- 内联对象字面量 -->
</select>
<!-- // 当选中时
typeof vm.selected // => 'object'
vm.selected.number // => 123
-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 修饰符

<input v-model.lazy="msg" > <!-- 在“change”时而非“input”时更新 -->
<input v-model.number="age" type="number"> <!-- 自动将用户的输入值转为数值类型 -->
<input v-model.trim="msg"> <!-- 自动过滤用户输入的首尾空白字符 -->
1
2
3

示例: 控制输入内容为整型的方式

oninput = "value=value.replace(/[^\d]/g,'')"    //只能输入数字
oninput = "value=value.replace(/[^0-9.]/g,'')"  //只能输入数字和小数

<!--可以使用 v-model.number 修饰符将用户输入转化为数字 但是这个无法阻止用户输入非数字
也可以 <el-input type="number"/> 这个就只能输入数字了 (包括小数)-->
<input v-model.number="age" type="number">
<!-- 同时设置;
因为即使在 type="number" 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat() 解析,则会返回原始的值。
-->

<el-input maxlength="8" oninput = "value=value.replace(/[^\d.]/g,'')" v-model="temp.price" placeholder="Please input price" />
1
2
3
4
5
6
7
8
9
10
11

# 在组件上使用 v-model

HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 v-model 一起使用!

详见下面组件中组件的自定义事件部分的讲解;

# vue的常用修饰符

  • 事件修饰符:
    • .stop stopPropagation 阻止冒泡
    • .prevent preventDefault 阻止默认行为
    • .self 事件作用在自己身上才触发
    • .once 事件只触发一次; 可以绑定多个方法
  • v-model 指令修饰符
    • .lazy 由监听oninput事件转为onchange事件
    • .number 尽量将文本框中的值转为数字,能转就转,不能转就不转
    • .trim 去掉字符串的首尾空格
  • 键盘修饰符
    • .enter 回车键
    • .esc 退出键

# 双向数据绑定/响应原理【重点】

vue 实现数据双向绑定主要是:Vue 采用 数据劫持 结合 发布者-订阅者 模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter 以及 getter,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。在数据变动时发布消息给订阅者,触发相应的监听回调。【要点】

具体就是【要点】:Vue实现数据双向绑定的效果,需要三大模块; MVVM作为绑定的入口,整合Observer,Compile和Watcher三者(OCWD),通过Observe来监听model的变化,通过Compile来解析编译模版指令,最终利用Watcher搭起Observer和Compile之前的通信桥梁,从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。【要点】

Vue 数据双向绑定主要是指:数据变化更新视图视图变化更新数据即:

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

data

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:(OCWD) Observer ,Compile, Watcher, Dep

  1. 第一步:实现一个监听器 Observer:需要 Observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

  2. 第二步:实现一个编译解析器 Compile;Compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新数据。

  3. 第三步:实现一个订阅者 Watcher(被观察者);Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情有:

    1. 在自身实例化时往属性订阅器(dep)里面添加自己。
    2. 自身必须有一个 update() 方法
    3. 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
  4. 第四步:实现一个订阅器 Dep(观察者);订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理

具体就是【要点】:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

数据劫持实现:(源码精简) src/core/observer/index.js 老版本通过 Object.defineProperty 递归可以实现

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
      }
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

就是劫持了对象的get和set方法。在所代理的属性的get方法中,当dep.Target存在的时候会调用 dep.depend()

最新版可以通过 Proxy 实现

Proxy(data, {
  get(target, key) {
    return target[key];
  },
  set(target, key, value) {
    let val = Reflect.set(target, key, value);
      _that.$dep[key].forEach(item => item.update());
    return val;
  }
})
1
2
3
4
5
6
7
8
9
10

劫持了对象的get和set方法。在数据劫持之外最重要的部分就是 DepWatcher,这其实是一个观察者模式

# 观察者模式实现:(源码精简)

三步骤:

  1. 通过 watcher.evaluate() 将自身实例赋值给 Dep.target
  2. 调用 dep.depend() 将dep实例将 watcher 实例 push 到 dep.subs中
  3. 通过数据劫持,在调用被劫持的对象的 set 方法时,调用 dep.subs 中所有的 watcher.update()
class Dep {// 观察者
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    depend() {
        if (Dep.target) { 
            Dep.target.addDep(this);
        }
    }
    notify() {
        this.subs.forEach(sub => sub.update())
    }
}

class Watcher {// 被观察者
    constructor(vm, expOrFn) {
        this.vm = vm;
        this.getter = expOrFn;
        this.value;
    }
    get() {
        Dep.target = this;
        var vm = this.vm;
        var value = this.getter.call(vm, vm);
        return value;
    }
    evaluate() {
        this.value = this.get();
    }
    addDep(dep) {
        dep.addSub(this);
    }
    update() {
        console.log('更新, value:', this.value)
    }
}
// 观察者实例
var dep = new Dep();
//  被观察者实例
var watcher = new Watcher({x: 1}, (val) => val);
watcher.evaluate();//通过 `watcher.evaluate()` 将自身实例赋值给 `Dep.target`

// 观察者监听被观察对象
dep.depend()//调用 `dep.depend()` 将dep实例的 watcher 实例 push 到 dep.subs中
dep.notify()//通过数据劫持,在调用被劫持的对象的set方法时,调用 dep.subs 中所有的 `watcher.update()`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

js 实现简单的双向绑定

<body>
    <div id="app">
        <input type="text" id="txt">
        <p id="show"></p>
    </div>
    <script>
        window.onload = function() {
            let obj = {};
            Object.defineProperty(obj, "txt", {
                get: function() {
                    return obj;
                },
                set: function(newValue) {
                    document.getElementById("txt").value = newValue;
                    document.getElementById("show").innerHTML  = newValue;
                }
            })
            document.addEventListener("keyup", function(e) {
                obj.txt = e.target.value;
            })
        }
    </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Vue 框架实现对象和数组的监听

Vue 怎么实现数据双向绑定? 属性、对象和数组;

  • 属性通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持;

  • 对象和数组:Vue 能检测到对象和数组(部分方法的操作)的变化, Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。 Vue 源码部分如下:

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])  // observe 功能为监测数据的变化
    }
  }
  /**
   * 对属性进行递归遍历
   */
  let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
1
2
3
4
5
6
7
8
9
10
11
12

# 禁止 Vue 劫持我们的数据呢?

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty (opens new window) 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

Vue 在遇到像 Object.freeze() 这样被设置为不可配置之后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法参考 Vue 源码 (opens new window)

可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。对长列表性能优化有用(有些时候我们的组件就是纯粹的数据展示,不会有任何改变);对于长列表还可以通过分页的优化;

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};
1
2
3
4
5
6
7
8
9

# defineProterty和proxy的对比【要点】

Object.defineProperty 接收三个参数:对象,属性名,配置对象; 这里使用的是 Object.defineProperty,这是 Vue 2.0 进行双向数据绑定的写法。在 Vue 3.0 中,它使用 Proxy 进行数据劫持。

1.defineProterty是es5的标准,proxy是es6的标准;

2.proxy可以监听到数组索引赋值,改变数组长度的变化;

3.proxy是监听对象,不用深层遍历,defineProterty是监听属性;

4.利用defineProterty实现双向数据绑定(vue2.x采用的核心)

5.利用proxy实现双向数据绑定(vue3.x会采用)

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;【1】
  • Proxy 可以直接监听数组的变化;【1】
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;【1】
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

# 为什么 Vue 3.0 中使用 Proxy

  1. Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、unshift()、shift()、splice()、sort()、reverse() 这八种方法,其他数组的属性检测不到。【PPUSSSR】
  2. Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
  3. 为什么 Proxy 在 Vue 2.0 编写的时候出来了,尤大却没有用上去?因为当时 es6 环境不够成熟,兼容性不好,尤其是这个属性无法用 polyfill 来兼容。(polyfill 是一个 js 库,专门用来处理 js 的兼容性问题-js 修补器)因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

# 组件

组件本质上是 Vue 实例,它们封装模板、逻辑和可选的本地响应性数据属性,能够提供可重新使用的自定义构建元素。可重用性是构建组件的核心。单文件组件使用 Webpack 等模块捆绑器进行编译。

使用单文件组件构建应用程序时,组件在扩展名为 .vue 的文件中定义。单文件组件包含三个部分

  • 模板部分定义了该组件的 HTML 布局
  • 脚本部分定义了数据、属性和逻辑单元(如方法)并将内容导出为 Vue 组件
  • 还有一个样式部分,用于定义组件的样式表

# 子父组件声明周期

# 父子组件生命周期钩子函数执行顺序

可以归类为以下 4 部分:

  • 加载渲染过程; 这个特别点;后面的mount操作顺序;

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程

    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

    父 beforeUpdate -> 父 updated

  • 销毁过程

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# 父组件监听到子组件的生命周期

有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,

方式一:以上需要手动通过 $emit 触发父组件的事件; 可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
  this.$emit("mounted");
}
1
2
3
4
5
6

方式二:更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},    
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     
1
2
3
4
5
6
7
8
9
10
11
12

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created, updated 等都可以监听。

示例优化:

mounted: function () {//优化后
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}
1
2
3
4
5
6
7
8
9

# 组件基础

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝

把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty (opens new window) 把这些属性全部转为 getter/setter (opens new window)Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

简单示例:

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
<div id="components-demo">
  <button-counter></button-counter>
</div>
new Vue({ el: '#components-demo' })

<!--组件复用-->
<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 组件中 data 为什么是一个函数?

组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象;

data() {// data
  return {
	message: "子组件",
	childName:this.name
  }
}
new Vue({// new Vue
  el: '#app',
  router,
  template: '<App/>',
  components: {App}
})
1
2
3
4
5
6
7
8
9
10
11
12

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题

# 监听子组件事件

# $emit

【父集监听:v-on:xx="postFontSize += 0.1"; 子集发送:v-on:click="$emit('xx')"

v-on:input="$emit('input', $event.target.value)" 详细可见自定义事件;

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      v-bind:key="post.id"
      v-bind:post="post"
      v-on:enlarge-text="postFontSize += 0.1"
    ></blog-post> <!-- 可优化传入;传入一个对象的所有属性 -->
  </div>
</div>

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button v-on:click="$emit('enlarge-text')">
        Enlarge text
      </button>
      <div v-html="post.content"></div>
    </div>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用事件抛出一个值

父集:v-on:xx="postFontSize += $event" ;子集:v-on:click="$emit('xx', 0.1)"

<button v-on:click="$emit('enlarge-text', 0.1)">
  Enlarge text
</button>
<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"></blog-post>

<!-- 或者,如果这个事件处理函数是一个方法:-->
<blog-post
  ...
  v-on:enlarge-text="onEnlargeText"></blog-post>
methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

<!--示例二: 注意这里随机数的用法-->
Vue.component('magic-eight-ball', {
  data: function () {
    return {
      possibleAdvice: ['Yes', 'No', 'Maybe']
    }
  },
  methods: {
    giveAdvice: function () {
      var randomAdviceIndex = Math.floor(Math.random() * this.possibleAdvice.length)
      this.$emit('give-advice', this.possibleAdvice[randomAdviceIndex])
    }
  },
  template: `
    <button v-on:click="giveAdvice">
      Click me for advice
    </button>
  `
})
<div id="emit-example-argument">
  <magic-eight-ball v-on:give-advice="showAdvice"></magic-eight-ball>
</div>
new Vue({
  el: '#emit-example-argument',
  methods: {
    showAdvice: function (advice) {
      alert(advice)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 在组件上使用v-model

详见自定义事件部分介绍;

# 解析 DOM 模板时的注意事项

有些 HTML 元素,诸如 ul, ol, table , select ,对于哪些元素可以出现在其内部是有严格限制的。

而有些元素,诸如 litroption,只能出现在其它某些特定的元素内部。

<table>
  <blog-post-row></blog-post-row>
</table>
<!--这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 is attribute 给了我们一个变通的办法: -->
<table>
  <tr is="blog-post-row"></tr>
</table>
1
2
3
4
5
6
7

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在

# 组件注册

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册。组件名字:使用 kebab-case[推荐] 和 使用 PascalCase

全局注册: 记住全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

Vue.component('my-component-name', { /* ... */ }) //推荐
Vue.component('MyComponentName', { /* ... */ })
1
2

局部注册 : 调用组件步骤

  • 第一步:在components目录新建组件文件(smithButton.vue),script一定要export default 将组件导出。

  • 第二步:在需要用的页面(组件)中导入:import smithButton from ‘…/components/smithButton.vue’

  • 第三步:注入到vue的子组件的components属性上面,components:{smithButton}

  • 第四步:在template视图view中使用; 注意事项:smithButton命名,使用的时候则smith-button

var ComponentA = { /* ... */ }
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA
  }
})
1
2
3
4
5
6
7

注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentAComponentB 中可用,则你需要这样写:

var ComponentA = { /* ... */ }
var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

//通过 Babel 和 webpack 使用 ES2015 模块
import ComponentA from './ComponentA.vue'
export default {
  components: {
    ComponentA
  },
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 模块系统

这种情况下是:PascalCase方式写法;

//局部注册
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

//全局注册
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
  './components', // 其组件目录的相对路径
  false,// 是否查询其子目录
  /Base[A-Z]\w+\.(vue|js)$/// 匹配基础组件文件名的正则表达式
)
requireComponent.keys().forEach(fileName => { // 获取组件配置
  const componentConfig = requireComponent(fileName)
  const componentName = upperFirst(// 获取组件的 PascalCase 命名
    camelCase( // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )
  // 全局注册组件
  Vue.component(
    componentName,
   // 如果这个组件选项是通过export default导出的, 那么就会优先使用.default, 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'

//基础组件
export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

import Vue from 'vue'
import App from './layout/App.vue'
import router from './router'
import store from './store'
import ElementUi from 'element-ui'
import '../static/theme/index.css'
import './assets/scss/index.scss'
import * as filters from './filters'
//基础组件;全局引入ElementUi
Vue.use(ElementUi)
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
Vue.config.productionTip = false
const app = new Vue({
  el: '#app',
  router,
  store,
  ...App
})
export default app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# Vue 组件间通信几种方式【要点】

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

总括:常用的三种: props / $emit, EventBus ($emit / $on), Vuex ;

(1)props / $emit 适用 父子组件通信

这种方法是 Vue 组件的基础。

父组件向子组件传值:

  • 子组件在props中创建一个属性,用来接收父组件传过来的值;

  • 在父组件中注册子组件;

  • 在子组件标签中添加子组件props中创建的属性;

  • 把需要传给子组件的值赋给该属性

子组件向父组件传值:

  • 子组件中需要以某种方式(如点击事件)的方法来触发一个自定义的事件;

  • 将需要传的值作为$emit的第二个参数,该值将作为实参传给响应事件的方法;

  • 在父组件中注册子组件并在子组件标签上绑定自定义事件的监听。

(2)ref$parent / $children 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用指向组件实例
  • $parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

(4)$attrs/$listeners 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

  •   onClickLeft(){
            const parentLeftClick = this.$listeners.leftClick
            if (parentLeftClick && typeof parentLeftClick === 'function') {
                this.$emit('leftClick')
                return
            }
            this.$router.go(-1);
          },
    
    1
    2
    3
    4
    5
    6
    7
    8

(5)provide / inject 适用于 隔代组件通信

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。(详细见下面的页面刷新方式)

export default {
  inject: ['reload'],
  data () {return {}}
  sockets: {
    sRefresh: function (value) {
      // this.reload()
      const index = this.list.findIndex(item => value.id === item.id)
      if (index >= 0) {
        this.list.splice(index, 1, value)
      }
    }
  }, 
1
2
3
4
5
6
7
8
9
10
11
12

(6)Vuex 适用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

示例:$emit $on ; 使用 bus.js 实现非父子组件通讯

假设在工作中,有三个 .vue 文件:A.vue、B.vue、C.vue。A.vue 是主页面,B.vue 和 C.vue 类似于头部导航条和底部导航栏。现在,B.vue 点击会切换路由,C.vue 需要获取 B.vue 传递的信息。

//A.vue
<template>
  <div>
    <top-nav></top-nav>
    <div class="container">
      <router-view></router-view>
    </div>
    <bottom-nav></bottom-nav>
  </div>
</template>

//bus.js
import Vue from 'vue';
const bus = new Vue();// 使用 Event Bus
export default bus;

//B.vue
<template>
  <div class="bottom-nav">
    <div class="nav-one" @click="goToPage({path: '/HomeIndex', meta:'首页'})">
      <i class="icon-home"></i>
      <span>首页</span>
    </div>
  </div>
</template>
<script>
  import bus from '../utils/bus'
  export default {
    methods: {
      goToPage(route) {
        this.$router.push(route.path);
        bus.$emit('meta', route.meta);
      }
    }
  }
</script>

//C.vue
<template>
  <div class="top-nav">
    <span class="title">{{title}}</span>
  </div>
</template>
<script>
  import bus from '../utils/bus'
  export default {
    data() {
      return {
        title: ''
      }
    },
    created() {
      bus.$on('meta', msg=> {
        this.title = msg;
      })
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

bus简单版本实现

1、给Vue的原型上添加一个bus属性

main.js:Vue.prototype.$bus = new Vue()

2、home页面进行修改个人资料操作时触发事件,

home.vue: changeProfile () {this.$bus.$emit('change')}

3、页面1里监听如果执行了操作,就调取页面1需要重新加载的数据接口。

mounted () {
 this.$bus.$on('change', ()=> {
  this.doSomething()
 })
},
1
2
3
4
5

# VUE几种页面刷新方法【要点】

  • this.$router.go(0):这种方法页面会一瞬间的白屏,体验不是很好,虽然只是一行代码的事;

  • location.reload():这种也是一样,画面一闪,效果总不是很好;

  • 跳转空白页再跳回原页面

    在需要页面刷新的地方写上:this.$router.push('/emptyPage'),跳转到一个空白页。在emptyPage.vue里beforeRouteEnter 钩子里控制页面跳转,从而达到刷新的效果

    beforeRouteEnter (to, from, next) {
       next(vm => {
        vm.$router.replace(from.path)
       })
    }
    
    1
    2
    3
    4
    5

    这种画面虽不会一闪,但是能看见路由快速变化。

  • 控制<router-view>的显示隐藏

    • 默认<router-view v-if="isRouterAlive" />isRouterAlive肯定是true,在需要刷新的时候把这个值设为false,接着再重新设为true:
    • 然后在需要刷新的页面引入依赖:inject: ['reload'];在需要的地方直接调用方法即可:this.reload()。
    <template>
      <div id="app">
        <router-view v-if="isRouterAlive"></router-view>
      </div>
    </template>
    <script>
      export default {
        name: 'app',
        provide(){
            return{
               reload:this.reload
            }
        },
        data(){
            return{
               isRouterAlive:true
            }
        },
        methods:{
            reload(){
               this.isRouterAlive =false;
               this.$nextTick(function(){
                    this.isRouterAlive=true
               })
            }
        }
      }
    </script>
    
    <template>
        <button @click="refresh"></button>
    </template>
    <script>
        export default{
            name: 'refresh',
            inject: ['reload'],
            methods: {
                  refresh () {
                      this.reload()
                  }
            }
        }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
  • 如果只是纯是更新页面数据的话,直接用 spliceVue.set (object, propertyName, value)、或者eventBus;

      sockets: {
        sRefresh: function (value) {
          // location.reload()
          // this.reload()
          const index = this.list.findIndex(item => value.id === item.id)
          if (index >= 0) {
            this.list.splice(index, 1, value)
          }
        }
      },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# 组件化设计思路

# 设计原则【要点】

  • 专一性
  • 可配置性
  • 生命周期
  • 事件传递

# 好处

  • 组件化是对实现的分层,是更有效地代码组合方式
  • 组件化是对资源的重组和优化,从而使项目资源管理更合理
  • 组件化有利于单元测试
  • 组件化对重构较友好

# Vue 组件和插件的关系

Vue组件(component):用来构成你的App的业务模块,目标是App.vue。

Vue插件(plugin) :用来增强你的技术栈的功能模块, 目标是Vue本身(插件是对Vue的功能的增强和补充)

Vue组件:通常在src的目录下,新建一个component文件夹来存放公共的组件,在我们要使用组件的页面中引入组件,并且进行一个说明。组件个调用它的页面之间的通信,就是父组件和子组件的通信方式

import Rule from '../../components/Rule.vue'
export default {
  components: {
    Rule,
  },
   data () {
    return {
    }
   }
}
1
2
3
4
5
6
7
8
9
10

Vue插件:vue插件的编写方法的四类:

export default {
    install(Vue, options) {
        Vue.myGlobalMethod = function () {  // 1. 添加全局方法或属性,如:  vue-custom-element
            // 逻辑...
        }  
        Vue.directive('my-directive', {  // 2. 添加全局资源:指令/过滤器/过渡等,如 vue-touch
            bind (el, binding, vnode, oldVnode) {
                // 逻辑...
            }
            ...
        })
        Vue.mixin({// 3. 通过全局 mixin方法添加一些组件选项,如: vuex
            created: function () {
                // 逻辑...
            }
            ...
        })    
 		// 4. 添加实例方法,通过把它们添加到 Vue.prototype 上实现
        Vue.prototype.$myMethod = function (options) { 
            // 逻辑...
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 自定义一个插件

我们在src/componnets的文件夹中,建立一个 toast.vue

<template>
    <div v-show="show" class="container">
        <div class="mask"></div>
        <div class="dialog">
            <div class="head">{{head}}</div>
            <div class="msg">{{msg}}</div>
            <div class="btn">
                <a class="btn_default" @click="cancel">取消</a>
                <a class="btn_main" @click="confirm">确认</a>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    data () {
        return {
            show: false,
            msg: '',
            head: '',
            callback: ''
        }
    },
    methods: {
        cancel () {
            this.show = false;
        },
        confirm () {
            this.callback();
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

在src/plugins 中创建一个 toast.js

/*eslint-disable*/
import ToastComponent from '@/components/Toast.vue'
// let $vm
export default {
    install (_Vue, options) {
        const ToastConstructor = _Vue.extend(ToastComponent) 
        const instance = new ToastConstructor()   //创建alert子实例
        instance.$mount(document.createElement('div')) //挂载实例到我们创建的DOM上
        document.body.appendChild(instance.$el)
        _Vue.prototype.$showToast = (head, msg, callback) => {
            instance.head = head;
            instance.msg = msg;
            instance.show = true;
            instance.callback = callback
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在main.js 中使用插件

import ToastPlugin from './plugins/toast.js'
Vue.use(ToastPlugin)
1
2

这样我们就可以在全局使用了。

this.$showToast ('弹窗标题', '这里显示提示的内容')
1

# Prop

Prop 的大小写: 在 HTML 中是 kebab-case 的; 在 JavaScript 中是 camelCase 的

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
1
2
3
4
5
6
7

# Prop类型

type 可以是下列原生构造函数中的一个:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor,
  date: Date,
	func: Function,
  symbol: Symbol
}
1
2
3
4
5
6
7
8
9
10
11
12

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
Vue.component('blog-post', {
  props: {
    author: Person
  }
})//来验证 `author` prop 的值是否是通过 `new Person` 创建的。
1
2
3
4
5
6
7
8
9

# Prop验证

当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告

注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的

Vue.component('my-component', {
  props: {
    propA: Number,// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propB: [String, Number],// 多个可能的类型
    propC: {// 必填的字符串
      type: String,
      required: true
    },
    propD: { // 带有默认值的数字
      type: Number,
      default: 100
    },
    propE: { // 带有默认值的对象
      type: Object,
      default: function () {// 对象或数组默认值必须从一个工厂函数获取
        return { message: 'hello' }
      }
    },
    propF: {// 自定义验证函数
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 传递静态或动态 Prop

给 prop 传入一个静态的值【静态prop】; 可以通过 v-bind 动态赋值【动态prop】 ;

当使用 v-bind 指令为 prop 分配值作为绑定到属性的函数时,被称为动态 prop。这与静态硬编码值相反。这种绑定始终是单向的,这意味着数据可以从父组件流到子组件,而绝不会反过来。

传入一个字符串

<!-- 静态 -->
<blog-post title="My journey with Vue"></blog-post>

<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>
<blog-post :title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post v-bind:title="post.title + ' by ' + post.author.name"></blog-post>
1
2
3
4
5
6
7
8
9

传入一个数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
1
2
3
4
5
6

传入一个布尔值 : 包含该 prop 没有值的情况在内,都意味着 true

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>
1
2
3
4
5
6
7
8
9

传入一个数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
1
2
3
4
5
6

传入一个对象

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>
1
2
3
4
5
6
7
8
9
10
11

传入一个对象的所有属性

如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。例如,对于一个给定的对象 post

post: {
  id: 1,
  title: 'My Journey with Vue'
}
1
2
3
4

下面的模板:

<blog-post v-bind="post"></blog-post>

<!-- 等价于 -->
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>
1
2
3
4
5
6
7

# 单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

这里有两种常见的试图改变一个 prop 的情形:

  1. **这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。**在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值

    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    
    1
    2
    3
    4
    5
    6
  2. **这个 prop 以一种原始的值传入且需要进行转换。**在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
    
    1
    2
    3
    4
    5
    6

注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态

相关事件,可以详见后面的自定义事件的 .sync 修饰符

# 其他处理

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源

推荐以 update:myPropName 的模式触发事件取而代之; 父组件可以监听那个事件并根据需要更新一个本地的数据属性

this.$emit('update:title', newTitle)
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

<!-- 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:-->
<text-document v-bind:title.sync="doc.title"></text-document>
1
2
3
4
5
6
7
8

# 非 Prop 的 Attribute

替换/合并已有的 Attribute: classstyle attribute 会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark

<input type="date" class="form-control">
<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>
1
2
3
4
5

禁止 Attribute继承: 希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false; 这尤其适合配合实例的 $attrs 属性使用,该属性包含了传递给一个组件的 attribute 名和 attribute 值;

注意 inheritAttrs: false 选项不会影响 styleclass 的绑定。 这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 自定义事件

事件名不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。因此,我们推荐你始终使用 kebab-case 的事件名

this.$emit('myEvent')
<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>
1
2
3

# 自定义组件的 v-model

<input v-model="searchText">
<!--等价于:-->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>
1
2
3
4
5
6

自定义组件中使用: 当用在组件上时,v-model 则会这样:

为了让它正常工作,这个组件内的 <input> 必须

  • 将其 value attribute 绑定到一个名叫 value 的 prop 上;
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出;

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})
<custom-input v-model="searchText"></custom-input>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突: 注意你仍然需要在组件的 props 选项里声明 checked 这个 prop

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

<base-checkbox v-model="lovingVue"></base-checkbox>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 将原生事件绑定到组件

想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on.native 修饰符:

<base-input v-on:focus.native="onFocus"></base-input>
1

再升级处理:子组件套了一层;

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>
1
2
3
4
5
6
7
8

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。为了解决这个问题,Vue 提供了一个 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        this.$listeners,// 我们从父级添加所有的监听器
        {// 然后我们添加自定义监听器,或覆写一些监听器的行为
          input: function (event) { // 这里确保组件配合 `v-model` 的工作
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 `` 元素一样使用了:所有跟它相同的 attribute 和监听器的都可以工作。

# sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源

推荐以 update:myPropName 的模式触发事件取而代之; 父组件可以监听那个事件并根据需要更新一个本地的数据属性

this.$emit('update:title', newTitle)
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

<!-- 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:-->
<text-document v-bind:title.sync="doc.title"></text-document>
1
2
3
4
5
6
7
8

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>
1

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

# vm.$attrs

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

# vm.$listeners

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

# 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListener等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}
1
2
3
4
5
6

事件移的优化注意事项:相同事件绑定和解除,需要使用共用函数;绑定和解除事件时 事件没有"on" 即onclick写成click; 共用函数不能带参数;

document.body.addEventListener('touchmove', function (event) {
    event.preventDefault();
},false);
document.body.removeEventListener('touchmove', function (event) {
    event.preventDefault();
},false);
//以上为错误写法;下面为正确写法;
function bodyScroll(event){
    event.preventDefault();
}
document.body.addEventListener('touchmove',bodyScroll,false);
document.body.removeEventListener('touchmove',bodyScroll,false);
1
2
3
4
5
6
7
8
9
10
11
12

# 插槽

插槽允许你定义可以封装和接受子 DOM 元素的元素。组件模板中的 <slot> </ slot> 元素作为通过组件标签捕获的所有DOM元素的出口

有三种:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

# 插槽内容

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案 (opens new window),将 <slot> 元素作为承载分发内容的出口。 通过插槽分发内容

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})
<alert-box> Something bad happened. </alert-box>
1
2
3
4
5
6
7
8
9

当组件渲染的时候,<slot></slot> 将会被替换为“Your Profile”。

插槽内可以包含任何模板代码,包括 HTML; 甚至其它的组件; 如果 <navigation-link> 没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

<!-- 在 <navigation-link> 的模板中 -->
<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

<navigation-link url="/profile">
  Your Profile
</navigation-link>
<navigation-link url="/profile">
  <span class="fa fa-user"></span><!-- 添加一个 Font Awesome 图标 -->
  Your Profile
</navigation-link>
<navigation-link url="/profile">
  <font-awesome-icon name="user"></font-awesome-icon><!-- 添加一个图标的组件 -->
  Your Profile
</navigation-link>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 编译作用域

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>
<!--该插槽跟模板的其它地方一样可以访问相同的实例属性 (也就是相同的“作用域”),而不能访问navigation-link的作用域-->

<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
  <!-- 
  这里的 `url` 会是 undefined,因为 "/profile" 是传递给<navigation-link> 的;
  而不是在 <navigation-link> 组件内部定义的。
  -->
</navigation-link>
1
2
3
4
5
6
7
8
9
10
11
12

# 后备(默认)内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。

<button type="submit">
  <slot>Submit</slot>
</button>
<submit-button></submit-button>
<submit-button>Save</submit-button>
1
2
3
4
5

# 具名插槽(v-slot)【要点】

用来访问被插槽分发 (opens new window)的内容; 一个不带 name<slot> 出口**会带有隐含的名字“default”。**每个具名插槽 (opens new window) 有其相应的属性 (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <template>
<!-- <template v-slot:default> -->
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>
  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

注意 v-slot 只能添加在 <template> (除了只有**独占默认插槽的缩写语法**情况),这一点和已经废弃的 slot attribute (opens new window)不同。

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法

<!--<current-user v-slot:default="slotProps">-->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</current-user>

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。然而,和其它指令一样,该缩写只在其有参数的时候才可用。例如 v-slot:header 可以被重写为 #header

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

<!-- 这样会触发一个警告 -->
<current-user #="{ user }">
  {{ user.firstName }}
</current-user>
<!-- 如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:-->
<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 作用域插槽/插槽Prop

让插槽内容能够访问子组件中才有的数据是很有用的;

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
1
2
3
4
5
6
7
8
9
10

作用域插槽的内部工作原理将你的插槽内容包括在一个传入单个参数的函数里; 这意味着 v-slot值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。

function (slotProps) { // 插槽内容 }
1
<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

<!--可以定义后备内容,用于插槽 prop 是 undefined 的情形-->
<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>
1
2
3
4
5
6
7
8
9
10
11
12

# 其他示例

动态插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
1
2
3
4
5

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
   > <!--我们为每个 todo 准备了一个插槽,将 `todo` 对象作为一个插槽的 prop 传入。-->
    <slot name="todo" v-bind:todo="todo">
      {{ todo.text }}<!-- 后备内容 -->
    </slot>
  </li>
</ul>

<todo-list v-bind:todos="todos">
  <template v-slot:todo="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# v-slotslotslot-scope的比较

v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slotslot-scope attribute 的 API 替代方案v-slot 完整的由来参见这份 RFC (opens new window)。在接下来所有的 2.x 版本中 slotslot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中。

  • v-slot 只能添加在 <template> (除了只有**独占默认插槽的缩写语法**情况); 而 slotslot-scope可以用在非 <template> 元素 (包括组件)上;

  • 带有 slot attribute 的具名插槽; 带有 slot-scope attribute 的作用域插槽

跟上面使用 v-slot的示例比较;

<base-layout>
  <template slot="header">
    <h1>Here might be a page title</h1>
  </template>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <template slot="footer">
    <p>Here's some contact info</p>
  </template>
</base-layout>
<!-- 或者直接把 slot attribute 用在一个普通元素上: -->
<base-layout>
  <h1 slot="header">Here might be a page title</h1>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <p slot="footer">Here's some contact info</p>
</base-layout>

<slot-example>
  <span slot-scope="slotProps">
    {{ slotProps.msg }}
  </span>
</slot-example>
<slot-example>
  <span slot-scope="{ msg }">
    {{ msg }}
  </span>
</slot-example>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

<todo-list> 作为示例,与它等价的使用 slot-scope 的代码是: 可见 v-slot:todo="{ todo }"简洁方便;

<todo-list v-bind:todos="todos">
  <template slot="todo" slot-scope="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>
<!-- 对比-->
<todo-list v-bind:todos="todos">
  <template v-slot:todo="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 动态组件/异步组件

# 动态组件

简单判断;在一个多标签的界面中使用 is attribute 来切换不同的组件:每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例。currentTabComponent 可以包括 已注册组件的名字,或 一个组件的选项对象 ;

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
1
2

请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器 (opens new window)

# keep-alive

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。为了解决这个问题,我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。

<keep-alive><!-- 失活的组件将会被缓存!-->
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>
1
2
3

注意:

  • 这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
  • <keep-alive> 是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for 则不会工作。多个条件性的子元素,要求同时只有一个子元素被渲染。
  • <keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例
# 带有 keep-alive的生命周期

详见开始部分的声明周期详解;

使用keep-alive标签后,会有两个生命周期函数分别是:activated、deactivated

activated:页面渲染的时候被执行。deactivated:页面被隐藏或页面即将被替换成新的页面时被执行。

当引入keep-alive的时候,页面第一次进入,钩子的触发顺序 created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。

# 组件中写 name 的作用
  • 项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
  • DOM 做递归组件时需要调用自身 name
  • vue-devtools 调试工具里显示的组见名称是由vue中组件name决定
# 实现原理【要点】
  • keep-alive 的创建阶段: created钩子会创建一个cache对象,用来保存vnode节点。
  • 在销毁阶段:destroyed 钩子则会调用pruneCacheEntry方法清除cache缓存中的所有组件实例。
created () {
    this.cache = Object.create(null) /* 缓存对象 */
},
destroyed () {
    for (const key in this.cache) {
        pruneCacheEntry(this.cache[key])
    }
},
1
2
3
4
5
6
7
8

使用

在 2.2.0 及其更高版本中,activateddeactivated 会在 <keep-alive> 树内的所有嵌套组件中触发。

  • Props

    • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
    • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
    • max - 数字。最多可以缓存多少组件实例。
  • 用法

    <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

    <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

    当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

    在vue-router写着keep-alive,keep-alive的含义:如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个keep-alive指令

案例: 比如有一个列表和一个详情,那么用户就会经常执行 打开详情=>返回列表=>打开详情… 这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染

<keep-alive><!-- 基本 -->
  <component :is="view"></component>
</keep-alive>

<keep-alive><!-- 多个条件判断的子组件 -->
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<transition><!-- 和 `<transition>` 一起使用 -->
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

**include and exclude **max 注:当使用正则表达式或者数组时,一定要使用v-bind

<keep-alive include="a,b"><!-- 逗号分隔字符串 -->
  <component :is="view"></component>
</keep-alive>
// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子只缓存a组件
<keep-alive include="a,b" exclude="b"> 
  <component />
</keep-alive>

<keep-alive :include="/a|b/"><!-- 正则表达式 (使用 `v-bind`) -->
  <component :is="view"></component>
</keep-alive>

<keep-alive :include="['a', 'b']"><!-- 数组 (使用 `v-bind`) -->
  <component :is="view"></component>
</keep-alive>

<keep-alive :max="10"> // 如果缓存的组件超过了max设定的值10,那么将删除第一个缓存的组件
  <component :is="view"></component>
</keep-alive>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 配合router使用

router-view也是一个组件,如果直接被包在keep-alive里面,那么所有路径匹配到的视图组件都会被缓存,如下:

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

router-view里面的某个组件被缓存

  • 使用 include/exclude
  • 使用 meta 属性

1.使用 include (exclude例子类似)

//只有路径匹配到的 name 为 a 组件会被缓存
<keep-alive include="a">
    <router-view></router-view>
</keep-alive>
1
2
3
4

2.使用 meta 属性

export default [// routes 配置
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: {
      keepAlive: true // 需要被缓存
    }
  }, {
    path: '/profile',
    name: 'profile',
    component: Profile,
    meta: {
      keepAlive: false // 不需要被缓存
    }
  }
]
<keep-alive>
    <router-view v-if="$route.meta.keepAlive">
        <!-- 这里是会被缓存的视图组件,比如 Home! -->
    </router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
    <!-- 这里是不会被缓存的视图组件,比如 Profile! -->
</router-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 防坑指南

1.keep-alive 先匹配被包含组件的 name 字段,如果 name 不可用,则匹配当前组件 components 配置中的注册名称。 2.keep-alive 不会在函数式组件中正常工作,因为它们没有缓存实例。 3.当匹配条件同时在 includeexclude 存在时,以 exclude 优先级最高(当前vue 2.4.2 version)。比如:包含于排除同时匹配到了组件A,那组件A不会被缓存。 4.包含在 keep-alive 中,但符合 exclude ,不会调用activateddeactivated

# 异步组件

Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    resolve({// 向 `resolve` 回调传递组件定义
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
//这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。

//一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的`require`语法将会告诉 webpack 自动将你的构建代码切割成多个包,这些包会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

//也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:
Vue.component(
  'async-webpack-example',
  () => import('./my-async-component')// 这个 `import` 函数会返回一个 `Promise` 对象。
)

//当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:
new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})
//如果你是一个 Browserify 用户同时喜欢使用异步组件,很不幸这个工具的作者明确表示异步加载“并不会被 Browserify 支持”,至少官方不会。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 处理加载状态
const AsyncComponent = () => ({
  component: import('./MyComponent.vue'),// 需要加载的组件 (应该是一个 `Promise` 对象)
  loading: LoadingComponent,//异步组件加载时使用的组件
  error: ErrorComponent, //加载失败时使用的组件
  delay: 200, //展示加载时组件的延时时间。默认值是 200 (毫秒)
  timeout: 3000 //如果提供了超时时间且组件加载也超时了,则使用加载失败时使用的组件。默认值是:`Infinity`
})
1
2
3
4
5
6
7

注意: 如果你希望在 Vue Router (opens new window) 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本

vue2.3+ 对应vue-router2.4.0 相应配套;

# 处理边界情况

# 访问元素 & 组件

# $root

在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex (opens new window) 来管理应用的状态

new Vue({// Vue 根实例
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})
<!--所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。-->
this.$root.foo// 获取根组件的数据
this.$root.foo = 2// 写入根组件的数据
this.$root.bar// 访问根组件的计算属性
this.$root.baz()// 调用根组件的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# $parent

属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。使用 $parent 属性无法很好的扩展到更深层级的嵌套组件上

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>
<!-- 那么在 <google-map-markers> 内部你可能发现自己需要一些类似这样的 hack: -->
var map = this.$parent.map || this.$parent.$parent.map
1
2
3
4
5
6
7

很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐依赖注入 (opens new window)的原因

# $refs

访问子组件实例或子元素; 尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。

<input ref="input">
//甚至可以通过其父级组件定义方法:
methods: {// 用来从父级组件聚焦输入框
  focus: function () {
    this.$refs.input.focus()
  }
}

<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput
//这样就允许父级组件通过下面的代码聚焦 <base-input> 里的输入框:
this.$refs.usernameInput.focus()
1
2
3
4
5
6
7
8
9
10
11
12

refv-for 一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

# ref的作用
  1. 获取dom元素;this.$refs.box
  2. 获取子组件中的data;this.$refs.box.msg
  3. 调用子组件中的方法;this.$refs.box.open()

依赖注入: 使用 $parent 属性无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provideinject。相比 $parent 来说,这个用法可以让我们在任意后代组件中访问 getMap,而不需要暴露整个 <google-map> 实例; 同时这些组件之间的接口是始终明确定义的,就和 props 一样。

provide 选项允许我们指定我们想要提供给后代组件的数据/方法inject 选项来接收指定的我们想要添加在这个实例上的属性

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里
<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>
provide: function () {
  return {
    getMap: this.getMap
  }
}
inject: ['getMap']
1
2
3
4
5
6
7
8
9
10
11
# provide / inject

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

var Provider = {// 父级组件提供 'foo'
  provide: {
    foo: 'bar'
  },
  // ...
}
var Child = {// 子组件注入 'foo'
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

//利用 ES2015 Symbols、函数 provide 和对象 inject:
const s = Symbol()
const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}
const Child = {
  inject: { s },
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 程序化的事件侦听器

知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

注意 Vue 的事件系统不同于浏览器的 EventTarget API (opens new window)。尽管它们工作起来是相似的,但是 $emit$on, 和 $off 并不是 dispatchEventaddEventListenerremoveEventListener 的别名。

mounted: function () {// 一次性将这个日期选择器附加到一个输入框上; 它会被挂载到 DOM 上。
  this.picker = new Pikaday({// Pikaday 是一个第三方日期选择器的库
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
beforeDestroy: function () {// 在组件被销毁之前,也销毁这个日期选择器。
  this.picker.destroy()
}
//缺点:它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物; 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

//优化后
mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}
//使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:
mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的 <input-datepicker> 组件。

# 循环引用

递归组件:当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。

Vue.component('unique-name-of-my-component', {
  // ...
})
1
2
3

稍有不慎,递归组件就可能导致无限循环:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
1
2

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 falsev-if)。

循环引用

<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>

<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>
<!-- 如果你使用一个模块系统依赖/导入组件,例如通过 webpack 或 Browserify,你会遇到一个错误: -->
<!-- Failed to mount component: template or render function not defined. -->

<!-- 为了解决这个问题,我们需要给模块系统一个点,在那里“A 反正是需要 B 的,但是我们不需要先解析 B。” -->
<!-- 把 <tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它:-->
beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
<!-- 或者,在本地注册组件的时候,你可以使用 webpack 的异步 import: -->
components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 模板定义的替代品

inline-template(内联模板):当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容; 内联模板需要定义在 Vue 所属的 DOM 元素内

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>
1
2
3
4
5
6

不过,inline-template让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板

X-Template:在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。 x-template 需要定义在 Vue 所属的 DOM 元素外。跟inline-template的不同之处;

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})
1
2
3
4
5
6

这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开

# 控制更新

感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。

强制更新:你可能还没有留意到数组 (opens new window)对象 (opens new window)的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过$forceUpdate 来做这件事。

# vm.$forceUpdate()

迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。也是一种备用的更新方式;

其他更新方式:

方式一:Vue.set(vm.items, indexOfItem, newValue)vm.$set(vm.items, indexOfItem, newValue);使用 vm.$set实例方法,该方法是全局方法 Vue.set 的一个别名:

方式二:vm.items.splice(indexOfItem, 1, newValue) , vm.items.splice(newLength)

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

通过 v-once 创建低开销的静态组件:渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})
1
2
3
4
5
6
7
8

再说一次,试着不要过度使用这个模式。**当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。**例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

# v-once

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

<span v-once>This will never change: {{msg}}</span><!-- 单个元素 -->
<div v-once><!-- 有子元素 -->
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<my-component v-once :comment="msg"></my-component><!-- 组件 -->
<ul><!-- `v-for` 指令-->
  <li v-for="i in list" v-once>{{i}}</li>
</ul>
1
2
3
4
5
6
7
8
9

# 过渡&动画

# 进入/离开 & 列表过渡

# 概述

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

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

在这里,我们只会讲到进入、离开和列表的过渡

# 单元素/组件的过渡

# transition

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

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

# 状态过渡

Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效,比如:

  • 数字和运算
  • 颜色的显示
  • SVG 节点的位置
  • 元素的大小和其他的属性

这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态

# 可复用性&组合

# 混入 mixins

混入 (mixin) 提供了一种非常灵活的方式,**来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。**当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

注意:Vue.extend() 也使用同样的策略进行合并。

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

//数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = {
  data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}
new Vue({
  mixins: [mixin],
  data: function () {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created: function () {
    console.log(this.$data)
    // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

//同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}
new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"

//值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
  methods: {
    foo: function () {
      console.log('foo')
    },
    conflicting: function () {
      console.log('from mixin')
    }
  }
}
var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 全局混入

混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑

Vue.mixin({// 为自定义的选项 'myOption' 注入一个处理器。
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})
new Vue({
  myOption: 'hello!'
})
// => "hello!"
1
2
3
4
5
6
7
8
9
10
11
12

tips: 请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件 (opens new window)发布,以避免重复应用混入

# 自定义选项合并策略

自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies 添加一个函数

Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {// 返回合并后的值}
1

对于多数值为对象的选项,可以使用与 methods 相同的合并策略:

var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
1
2

可以在 Vuex (opens new window) 1.x 的混入策略里找到一个更高级的例子:

const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
  if (!toVal) return fromVal
  if (!fromVal) return toVal
  return {
    getters: merge(toVal.getters, fromVal.getters),
    state: merge(toVal.state, fromVal.state),
    actions: merge(toVal.actions, fromVal.actions)
  }
}
1
2
3
4
5
6
7
8
9
10

# component / extend / mixins / extends 的区别

  • Vue.component 注册全局组件,为了方便
  • Vue.extend 创建组件的构造函数,为了复用
  • mixins、extends 为了扩展

如果按照优先级去理解,当你需要继承一个组件时,可以使用Vue.extend().当你需要扩展组件功能的时候,可以使用extends,mixins

# mixins (opens new window)

mixins 选项接受一个混合对象的数组。这些混合实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。

# extends (opens new window)

这和 mixins 类似,区别在于,组件自身的选项会比要扩展的源组件具有更高的优先级.

官方文档是这么写的,除了优先级,可能就剩下接受参数的类型吧,mixins接受的是数组.

# 自定义指令directives

除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

示例:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {// 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    el.focus()// 聚焦元素
  }
})
//如果想注册局部指令,组件中也接受一个 directives 的选项:
directives: {
  focus: {// 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}
//然后你可以在模板中任何元素上使用新的 v-focus 属性,如下:
<input v-focus>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 函数简写/对象字面量

在很多时候,你可能想在 bindupdate 时触发相同行为,而不关心其它的钩子。比如这样写:

Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})
1
2
3

如果指令需要多个值,可以传入一个 Js对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text)  // => "hello!"
})
1
2
3
4
5

# 钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选)

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

我们会在稍后 (opens new window)讨论渲染函数 (opens new window)时介绍更多 VNodes 的细节。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

接下来我们来看一下钩子函数的参数 (即 elbindingvnodeoldVnode)。

# 参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnodeVue 编译生成的虚拟节点。移步 VNode API (opens new window) 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

注意事项:除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset (opens new window) 来进行。

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})
new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})
//name: "demo"
//value: "hello!"
//expression: "message"
//argument: "foo"
//modifiers: {"a":true,"b":true}
//vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 动态指令参数

指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用

例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令

<div id="baseexample">
  <p>Scroll down the page</p>
  <p v-pin="200">Stick me 200px from the top of the page</p>
</div>
Vue.directive('pin', {
  bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    el.style.top = binding.value + 'px'
  }
})
new Vue({
  el: '#baseexample'
})
1
2
3
4
5
6
7
8
9
10
11
12
13

这会把该元素固定在距离页面顶部 200 像素的位置。但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新

<div id="dynamicexample">
  <h3>Scroll down inside this section ↓</h3>
  <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
Vue.directive('pin', {
  bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
})
new Vue({
  el: '#dynamicexample',
  data: function () {
    return {
      direction: 'left'
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

项目中示例:

//注册
Vue.directive('enterToNext', {
  inserted: function (el) {
    console.log('enterToNext...')
    // let frm = el.querySelector('.el-form');
    const inputs = el.querySelectorAll('input')
    console.log(inputs)
    for (var i = 0; i < inputs.length; i++) {
      inputs[i].setAttribute('keyFocusIndex', i)
      inputs[i].addEventListener('keyup', (ev) => {
        if (ev.keyCode === 13) {
          const targetTo = ev.srcElement.getAttribute('keyFocusTo')
          if (targetTo) {
            this.$refs[targetTo].$el.focus()
          } else {
            var attrIndex = ev.srcElement.getAttribute('keyFocusIndex')
            var ctlI = parseInt(attrIndex)
            if (ctlI < inputs.length - 1) { inputs[ctlI + 1].focus() }
          }
        }
      })
    }
  }
})
//使用
<el-form
ref="postForm"
v-enterToNext="true"
:model="postForm"
:rules="rules"
label-position="left"
label-width="110px"
size="mini"
status-icon
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 自定义指令(v-check,v-focus)的方法有哪些,它有哪些钩子函数, 还有哪些钩子函数参数

全局定义指令:在vue对象的directive方法里面有两个参数,一个是指令名称,另外一个是函数。组件内定义指令:directives

钩子函数:bind(绑定事件触发)、inserted(节点插入的时候触发)、update(组件内相关更新)

钩子函数参数:el、binding

# 渲染函数 & JSX

# 简介

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

案例:这个例子里 render 函数很实用。假设我们要生成一些带锚点的标题:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>
<anchored-heading :level="1">Hello world!</anchored-heading>
1
2
3
4
5
6
//当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现:
//缺点:这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>,在要插入锚点元素时还要再次重复。
<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
//使用渲染函数优化后; 这样代码精简很多,但是需要非常熟悉 Vue 的实例属性。
Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

你需要知道,向组件中传递不带 v-slot 指令的子节点时,比如 anchored-heading 中的 Hello world!,这些子节点被存储在组件实例中的 $slots.default。 详见后面的完整示例;

# vm.$slots

用来访问被插槽分发 (opens new window)的内容。每个具名插槽 (opens new window) 有其相应的属性 (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

注意: v-slot:foo 在 2.6 以上的版本才支持。对于之前的版本,你可以使用废弃了的语法 (opens new window).

在使用渲染函数 (opens new window)书写一个组件时,访问 vm.$slots 最有帮助。

<blog-post>
  <template v-slot:header>
    <h1>About Me</h1>
  </template>
  <p>vm.$slots.default, because it's not inside a named slot.</p>
  <template v-slot:footer>
    <p>Copyright 2016 Evan You</p>
  </template>
  <p>If I have some content down here, it will also be included in vm.$slots.default.</p>.
</blog-post>
Vue.component('blog-post', {
  render: function (createElement) {
    var header = this.$slots.header
    var body   = this.$slots.default
    var footer = this.$slots.footer
    return createElement('div', [
      createElement('header', header),
      createElement('main', body),
      createElement('footer', footer)
    ])
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 节点、树以及虚拟 DOM

在深入渲染函数之前,了解一些浏览器的工作原理是很重要的;当浏览器读到这些代码时,它会建立一个“DOM 节点”树 (opens new window)来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>
1
2
3
4
5

上述 HTML 对应的 DOM 节点树如下图所示:

DOM 树可视化

**每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。**就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

<h1>{{ blogTitle }}</h1>
//或者一个渲染函数里:
render: function (createElement) {
  return createElement('h1', this.blogTitle)
}
1
2
3
4
5

在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。

# template 编译

Vue 中 template 就是先转化成 AST 树,再得到 render 函数返回 VNode(Vue 的虚拟 DOM 节点)。

  1. 通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree - 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。
  2. AST 会经过 generate(将 AST 语法树转换成 render function 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有标签名、子节点、文本等待。

# 真实DOM和其解析流程

流程图: 所有的浏览器渲染引擎工作流程大致分为5步:创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting 【D R 渲布绘】 【DCJ渲布绘】

  • 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
  • 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
  • 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
  • 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
  • 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。

# Virtual(虚拟) DOM/VNode【要点】

渲染真实的DOM开销是很大的,修改了某个数据,如果直接渲染到真实dom上会引起整个DOM树的重绘和重排。 Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。

虚拟 DOM 就是用来模拟 DOM 的一个对象,这个对象拥有一些重要属性,并且更新 UI 主要就是通过对比(DIFF)旧的虚拟 DOM 树 和新的虚拟 DOM 树的区别完成的。当Virtual DOM某个节点的数据改变后生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode.

# VNode简介

Vue 在 rendercreateElement 的时候,并不是产生真实的 DOM 元素,实际上 createElement 描述为 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点。因此,我们将这样的节点描述为 “虚拟节点”(Virtual Node),简称 VNode。“虚拟 DOM” 是我们对由 Vue 组件树建立的整个 VNode 树的称呼

注意:在采取diff算法比较的时候,只会在同层级进行,不会跨层级比较

# 通知过程

当数据发生改变时,set方法会让调用Dep.notify()方法通知所有订阅者Watcher,订阅者就会调用patch函数给真实的DOM打补丁,更新响应的试图。

# 实现原理

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树
# 优缺点

优点:最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。可以渲染到 DOM 以外的端,使得框架跨平台,比如 ReactNative,React VR 等。
  • Virtual DOM 在牺牲(牺牲很关键)部分性能的前提下,增加了可维护性,这也是很多框架的通性。 实现了对 DOM 的集中化操作,在数据改变时先对虚拟 DOM 进行修改,再反映到真实的 DOM中,用最小的代价来更新DOM,提高效率(提升效率要想想是跟哪个阶段比提升了效率,别只记住了这一条)。
  • 打开了函数式 UI 编程的大门。
  • 可以更好的实现 SSR,同构渲染等。这条其实是跟上面一条差不多的。
  • 组件的高度抽象化。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。【代码更多,体积更大】
  • 虚拟 DOM 需要在内存中的维护一份 DOM 的副本(更上面一条其实也差不多,上面一条是从速度上,这条是空间上)。【内存占用增大】
  • 如果虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。但对于大多数单页面应用,这应该都会更快。【小量的单一的dom修改使用虚拟dom成本反而更高,不如直接修改真实dom快】

# vdom核心函数

  • h('标签名', {...属性名...}, [...子元素...])
  • h('标签名', {...属性名...}, '.........')
  • patch(container, vnode)
  • patch(vnode, newVnode)

# innerHTML vs. Virtual DOM 的重绘性能消耗:

innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)

Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

# createElement参数

如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 深入数据对象

有一点要注意:正如 v-bind:classv-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM 属性 (这会覆盖 v-html 指令)。

{
  'class': {// 与 `v-bind:class` 的 API 相同, // 接受一个字符串、对象或字符串和对象组成的数组
    foo: true,
    bar: false
  },
  style: { // 与 `v-bind:style` 的 API 相同,// 接受一个字符串、对象,或对象组成的数组
    color: 'red',
    fontSize: '14px'
  },
  attrs: {// 普通的 HTML attribute
    id: 'foo'
  },
  props: {// 组件 prop
    myProp: 'bar'
  },
  domProps: {// DOM 属性
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 约束:VNode 必须唯一

组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [// 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}
1
2
3
4
5
6

如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
1
2
3
4
5
6
7
# 完整示例

有了这些知识,我们现在可以完成我们最开始想实现的组件:

var getChildrenTextContent = function (children) {
  return children.map(function (node) {
    return node.children
      ? getChildrenTextContent(node.children)
      : node.text
  }).join('')
}

Vue.component('anchored-heading', {
  render: function (createElement) {
    // 创建 kebab-case 风格的 ID
    var headingId = getChildrenTextContent(this.$slots.default)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^-|-$)/g, '')

    return createElement(
      'h' + this.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, this.$slots.default)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# diff算法相关【要点】

Virual DOM是用JS对象记录一个dom节点的副本,当dom发生更改时候,先用虚拟dom进行diff,算出最小差异,然后再修改真实dom

总的来说,VNode 就是一个 JavaScript 对象,用 JavaScript 对象的属性来描述当前节点的一些状态,用 VNode 节点的形式来模拟一棵 Virtual DOM 树

vue的virtual dom的diff算法是基于snabbdom算法改造而来,与react的diff算法一样 仅在同级的vnode间做diff,递归的进行同级vnode的diff,最终实现整个DOM树的更新。

简单的说就是新旧虚拟dom 的比较,如果有差异就以新的为准,然后再插入的真实的dom中,重新渲染

特点

  • 只会做同级比较,不做跨级比较

  • 比较后几种情况

    • if (oldVnode === vnode),他们的引用一致,可以认为没有变化。
    • if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text
    • if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点,这是diff的核心
    • else if (ch),只有新的节点有子节点,调用createEle(vnode)vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。
    • else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。
  • 首先diff只会比较同层节点,不能跨层。【要点】

    1、如果都有文本节点且不相同,则将真实dom(el)的文本节点设置为vnode的文本节点

    2、如果旧vnode有子节点新vnode没有,则将el的子节点删除

    3、如果旧vnode没有子节点新vnode有,则el添加子节点

    4、如果两者都有子节点,则执行updateChildren函数比较子节点

  • //TODO:

# 自己实现一个简单的VNode

class VNode {
    constructor (tag, data, children, text, elm) {
        /*当前节点的标签名*/
        this.tag = tag;
        /*当前节点的一些数据信息,比如props、attrs等数据*/
        this.data = data;
        /*当前节点的子节点,是一个数组*/
        this.children = children;
        /*当前节点的文本*/
        this.text = text;
        /*当前虚拟节点对应的真实dom节点*/
        this.elm = elm;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

比如我目前有这么一个 Vue 组件。

<template>
  <span class="demo" v-show="isShow">
    This is a span.
  </span>
</template>
1
2
3
4
5

用 JavaScript 代码形式就是这样的。

function render () {
    return new VNode(
        'span',{
            directives: [/* 指令集合数组 */
                {
                    rawName: 'v-show',/* v-show指令 */
                    expression: 'isShow',
                    name: 'show',
                    value: true
                }
            ],
            staticClass: 'demo' /* 静态class */
        },
        [ new VNode(undefined, undefined, undefined, 'This is a span.') ]
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

看看转换成 VNode 以后的情况。

{
    tag: 'span',
    data: { /* 指令集合数组 */
        directives: [
            {/* v-show指令 */
                rawName: 'v-show',
                expression: 'isShow',
                name: 'show',
                value: true
            }
        ],
        staticClass: 'demo'/* 静态class */
    },
    text: undefined,
    children: [/* 子节点是一个文本VNode节点 */
        {
            tag: undefined,
            data: undefined,
            text: 'This is a span.',
            children: undefined
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然后我们可以将 VNode 进一步封装一下,可以实现一些产生常用 VNode 的方法。

  • 创建一个空节点
function createEmptyVNode () {
    const node = new VNode();
    node.text = '';
    return node;
}
1
2
3
4
5
  • 创建一个文本节点
function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val));
}
1
2
3
  • 克隆一个 VNode 节点
function cloneVNode (node) {
    const cloneVnode = new VNode(
        node.tag,
        node.data,
        node.children,
        node.text,
        node.elm
    );
    return cloneVnode;
}
1
2
3
4
5
6
7
8
9
10

将真实dom数据结构通过js对象以树状图形式模拟出来:

<div>
    <p>123</p>
</div>
var Vnode = {
tag: 'div',
children: [{ tag: 'p', text: '123' }]
}
1
2
3
4
5
6
7

# 使用 JavaScript 代替模板功能

# v-if/ v-for/ v-model

比如,在模板中使用的 v-ifv-for

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

//这些都可以在渲染函数中用 JavaScript 的 if/else 和 map 来重写:
props: ['items'],
render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

渲染函数中没有与 v-model 的直接对应——你必须自己实现相应的逻辑:

<input v-model="searchText">
<!--等价于:-->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

//**这就是深入底层的代价,但与 `v-model` 相比,这可以让你更好地控制交互细节**。
props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 事件 & 按键修饰符

对于 .passive.capture.once 这些事件修饰符, Vue 提供了相应的前缀可以用于 on

修饰符 前缀
.passive &
.capture !
.once ~
.capture.once.once.capture ~!

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 (opens new window))
修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)

示例:

on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}
//这里是一个使用所有修饰符的例子:
on: {
  keyup: function (event) {
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 插槽

可以通过 this.$slots (opens new window) 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}
1
2
3
4

也可以通过 this.$scopedSlots (opens new window) 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:

props: ['message'],
render: function (createElement) {
  // `<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}
1
2
3
4
5
6
7
8
9

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`; 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}
1
2
3
4
5
6
7
8
9
10
11
12

# JSX

有一个 Babel 插件 (opens new window),用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

//如果你写了很多 render 函数,可能会觉得下面这样的代码写起来很痛苦:
createElement(
  'anchored-heading', {
    props: {
      level: 1
    }
  }, [
    createElement('span', 'Hello'),
    ' world!'
  ]
)
//特别是对应的模板如此简单的情况下:
<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

//jsx
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本 (opens new window)开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。

项目示例: main.js 【推荐】

const app = new Vue({
  el: '#app',
  router,
  store,
  // ...App  //一种方式;
  render: h => h(App)
})
export default app
1
2
3
4
5
6
7
8

或者简化版本: 主动 $mount调用;

// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 函数式组件 functional

之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。 在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据 (opens new window)),也没有实例 (没有 this 上下文)。 一个函数式组件就像这样:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop

当使用函数式组件时,该引用将会是 HTMLElement,因为他们是无状态的也是无实例的。

在 2.5.0 及以上版本中,如果你使用了单文件组件 (opens new window),那么基于模板的函数式组件可以这样声明:

<template functional>  </template>
1

组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children: VNode 子节点的数组
  • slots: 一个函数,返回了包含所有插槽的对象
  • scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象 (opens new window),作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections: (2.3.0+) 如果使用了 inject (opens new window) 选项,则该对象包含了应当被注入的属性。

在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children(还是有区别的,详见下面slots()children 对比),然后将 this.level 更新为 context.props.level

因为函数式组件只是函数,所以渲染开销也低很多

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

  • 程序化地在多个组件中选择一个来代为渲染;
  • 在将 childrenpropsdata 传递给子组件之前操作它们。

下面是一个 smart-list 组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items
      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList
      return UnorderedList
    }
    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 向子元素或子组件传递 attribute 和事件

在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并 (opens new window)。然而函数式组件要求你显式定义该行为:

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透传任何 attribute、事件监听器、子节点等。
    return createElement('button', context.data, context.children)
  }
})
//通过向 createElement 传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的 attribute 和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native 修饰符。
1
2
3
4
5
6
7
8

可以使用 data.attrs 传递任何 HTML attribute,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>
1
2
3
4
5
6
7
8
9
# slots()children 对比

你可能想知道为什么同时需要 slots()childrenslots().default 不是和 children 类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>
1
2
3
4
5
6

对于这个组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签slots().foo 会传递第一个具名段落标签。同时拥有 childrenslots(),因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children,移交给其它组件去处理

# 模板编译

你可能会有兴趣知道,Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile 来实时编译模板字符串的简单示例:

# Vue.compile( template )

将一个模板字符串编译成render函数只在完整版时可用

var res = Vue.compile('<div><span>{{ msg }}</span></div>')
new Vue({
  data: {
    msg: 'hello'
  },
  render: res.render,
  staticRenderFns: res.staticRenderFns
})
1
2
3
4
5
6
7
8

# 插件

# 分类

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者属性。如: vue-custom-element (opens new window)
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch (opens new window)
  3. 通过全局混入来添加一些组件选项。如 vue-router (opens new window)
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router (opens new window)

# Vue.use( plugin )

  • 参数

    • {Object | Function} plugin
  • 用法

    安装 Vue.js 插件。**如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。**install 方法调用时,会将 Vue 作为参数传入。

    该方法需要在调用 new Vue() 之前被调用。

    当 install 方法被同一个插件多次调用,插件将只会被安装一次。Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件

# 使用插件

通过全局方法 Vue.use() 使用插件, 调用 MyPlugin.install(Vue)它需要在你调用 new Vue() 启动应用之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
  // ...组件选项
})
//也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
1
2
3
4
5
6
7
# 调用方式
  • 隐式调用:Vue.js 官方提供的一些插件 (例如 vue-router)在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()

  • 显示调用:然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use()

// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
1
2
3
4
5
# 步骤
  1. 采用ES6的import ... from ...语法或CommonJSd的require()方法引入插件

  2. 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

import router from './router'
import store from './store'
import ElementUi from 'element-ui'
import * as filters from './filters'
Vue.use(ElementUi)//显示调用
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
const app = new Vue({
  el: '#app',
  router,//隐式调用;
  store,
  ...App
})
export default app
1
2
3
4
5
6
7
8
9
10
11
12
13

routet/index.js

import Vue from 'vue'
import Router from 'vue-router'
const login = () => import(/* webpackChunkName: "login" */ '../pages/login')
Vue.use(Router)
const routes = [{
    path: '/login',
    name: '登陆',
    component: login,
    meta: { requiresAuth: false }
  }]
const router = new Router({
  mode: 'history',
  base: __dirname,
  routes
})
router.beforeEach((to, from, next) => {
  NProgress.start()
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!loginIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

router.afterEach(transition => {
  NProgress.done()
})
export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 过滤器 filter

过滤器是在 Vue 程序中实现自定义文本格式的一种非常简单的方法。Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

# 说明

  • 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。

  • 过滤器可以串联

​ 在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

  • 过滤器是 JavaScript 函数,因此可以接收参数:

    这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

# 设置方式

全局安装:或者在创建 Vue 实例之前全局定义过滤器

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
  // ...
})

import * as filters from './filters'
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
1
2
3
4
5
6
7
8
9
10
11

局部使用:你可以在一个组件的选项中定义本地的过滤器: 当全局过滤器和局部过滤器重名时,会采用局部过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

<div id="app">{{ title | reverseText }}</div>
App
new Vue({
    el: '#app',
    data: {
      title: 'This is a title'
    },
    filters: {
      reverseText(text) {
        return text.split('').reverse().join('');
      }
    }
});//eltit a si sihT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!--`v-bind`-->
<div v-bind:id="rawId | formatId"></div>
1
2
3
4
5

项目示例:时间格式问题;

//filters/index.js
export function format (time, format = 'YYYY-MM-DD HH:mm:ss') {
  if (!time) return null
  var date = new Date(time)
  var o = {
    'M+': date.getMonth() + 1, // month
    'd+': date.getDate(), // day
    'h+': date.getHours(), // hour
    'm+': date.getMinutes(), // minute
    's+': date.getSeconds(), // second
    'q+': Math.floor((date.getMonth() + 3) / 3), // quarter
    S: date.getMilliseconds() // millisecond
  }
  if (/(y+)/.test(format)) {
    format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
  }
  for (var k in o) {
    if (new RegExp('(' + k + ')').test(format)) {
      format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
    }
  }
  return format
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//main.js
//Vue.filter('formatDate', function(value, pattern = 'YYYY-MM-DD HH:mm:ss') {
Vue.filter('formatDate', function (value) {
  return Moment(value).format('YYYY-MM-DD HH:mm:ss')
})
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
//直接写在filters或者方法中;
dateFormat: function (time, pattern = 'YYYY-MM-DD HH:mm:ss') {
   return moment(time).format(pattern)
}
1
2
3
4
5
6
7
8
9
10
<el-table-column label="更新时间" align="center">
   <template slot-scope="scope">{{ scope.row.updatedAt | formatDate }}</template>
</el-table-column>
<td>{{ dateFormat(item.updated_at) }}</td>
1
2
3
4

# 内在

# 深入响应式原理

# 检测变化跟踪

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty (opens new window) 把这些属性全部转为 getter/setter (opens new window)Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools (opens new window) 来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

data

# 注意事项

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的

var vm = new Vue({
  data:{
    a:1
  }
})// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
//对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。例如,对于:

Vue.set(vm.someObject, 'b', 2)
//您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject,'b',2)

//有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值

var vm = new Vue({
  data: {
    message: '' // 声明 message 为一个空值字符串
  },
  template: '<div>{{ message }}</div>'
})
vm.message = 'Hello!'// 之后设置 `message`
1
2
3
4
5
6
7

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的属性。

这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构 (schema)。提前声明所有的响应式属性,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

# 异步更新队列

Vue 在更新 DOM 时是异步执行的。**只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。**如果同一个 watcher 被多次触发,只会被推入到队列中一次。

这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,**在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。 ** Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。

虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。**为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。**这样回调函数将在 DOM 更新完成后被调用;

示例:多数情况不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

//在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
//因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# Vue.nextTick

  • 用法:Vue.nextTick( [callback, context] )

  • 参数:

    • {Function} [callback]
    • {Object} [context]
  • 说明:Vue 是异步修改 DOM 的并且不鼓励开发者直接接触 DOM,但有时候业务需要必须对数据更改--刷新后的 DOM 做相应的处理,这时候就可以使用 Vue.nextTick(callback)这个 api 了。

    • 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
  • 用途:

    在 created 和 mounted 阶段,如果需要操作渲染后的试图,也要使用 nextTick 方法

    官方文档说明: 注意 mounted 不会承诺所有的子组件也都一起被挂载如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

    mounted: function () {
      this.$nextTick(function () {
        // Code that will run only after the entire view has been rendered
      })
    }
    
    1
    2
    3
    4
    5

    其他使用场景

    //案例:点击按钮显示原本以 v-show = false 隐藏起来的输入框,并获取焦点。
    showsou(){
      this.showit = true //修改 v-show
      document.getElementById("keywords").focus()  //在第一个 tick 里,获取不到输入框,自然也获取不到焦点
    }
    //修改为:
    showsou(){
      this.showit = true
      this.$nextTick(function () {// DOM 更新了
        document.getElementById("keywords").focus()
      })
    }
    
    //案例:点击获取元素宽度。
    <div id="app">
        <p ref="myWidth" v-if="showMe">{{ message }}</p>
        <button @click="getMyWidth">获取p元素宽度</button>
    </div>
    getMyWidth() {
        this.showMe = true;
        //this.message = this.$refs.myWidth.offsetWidth;
        //报错 TypeError: this.$refs.myWidth is undefined
        this.$nextTick(()=>{//dom元素更新后执行,此时能拿到p元素的属性
           this.message = this.$refs.myWidth.offsetWidth;
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

# 异步说明

Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。

具体来说,异步执行的运行机制如下。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

示例:

//改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    console.log(vm.$el.textContent) //可以得到'changed'
})
1
2
3
4
5
6
7
8
9
10

解析

第一个 tick(图例中第一个步骤,即'本次更新循环'):

  1. 首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及 DOM 。
  2. Vue 开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

第二个 tick(图例中第二个步骤,即'下次更新循环'):

同步任务执行完毕,开始执行异步 watcher 队列的任务,更新 DOM 。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel 方法,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

第三个 tick(图例中第三个步骤):

此时就是文档所说的

下次 DOM 更新循环结束之后

此时通过 Vue.nextTick 获取到改变后的 DOM 。通过 setTimeout(fn, 0) 也可以同样获取到

总括

同步代码执行 -> 查找异步队列,推入执行栈,执行Vue.nextTick[事件循环1] ->查找异步队列,推入执行栈,执行Vue.nextTick[事件循环2]...

总之,异步是单独的一个tick,不会和同步在一个 tick 里发生,也是 DOM 不会马上改变的原因。

# 工具

# 单文件组件

# 简介

在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素

这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:

  • 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
  • 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \
  • 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
  • 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

文件扩展名为 .vue 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具

# 针对高级用户

CLI 会为你搞定大多数工具的配置问题,同时也支持细粒度自定义配置项 (opens new window)

有时你会想从零搭建你自己的构建工具,这时你需要通过 Vue Loader (opens new window) 手动配置 webpack。关于学习更多 webpack 的内容,请查阅其官方文档 (opens new window)Webpack Academy (opens new window)

# 单元测试

# TS支持

# 生成环境部署

以下大多数内容在你使用 Vue CLI (opens new window) 时都是默认开启的。该章节仅跟你自定义的构建设置有关。

# 开启生产环境模式

# 不使用构建工具

如果用 Vue 完整独立版本,即直接用 <script> 元素引入 Vue 而不提前进行构建,请记得在生产环境下使用压缩后的版本 (vue.min.js)。两种版本都可以在安装指导 (opens new window)中找到。

# 使用构建工具

webpack

在 webpack 4+ 中,你可以使用 mode 选项:

module.exports = {
  mode: 'production' //默认
}
1
2
3

但是在 webpack 3 及其更低版本中,你需要使用 DefinePlugin (opens new window)

var webpack = require('webpack')
module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
}
1
2
3
4
5
6
7
8
9
10

# 模板预编译

当使用 DOM 内模板JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。

预编译模板最简单的方式就是使用单文件组件 (opens new window)——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数

# 提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段“无样式内容闪烁 (fouc)”。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存

查阅这个构建工具各自的文档来了解更多:

简单的方法:requires vue-loader@^12.0.0 and webpack@^2.0.0

// webpack.config.js
var ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
  // other options...
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          extractCSS: true
        }
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("style.css")
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上述内容将自动处理 *.vue 文件内的

上次更新: 2022/04/15, 05:41:27
×