vue 简明教程

793 阅读38分钟

目前前端开发已经非常类似GUI开发,前端人员需要了解大量业务逻辑,通过不同的页面交互行为给用户展示不同的界面或者引导用户进行不同的操作,这必然造成前端逻辑的繁重化,如何简单高效的维护开发代码是每个前端开发人员应该思考的问题。

本文会假定你有一定的前端基础,主要目的是构建快速复习的手册(按官方教程顺序进行知识梳理)。会不定时更新,和官方版本保持一致,目前是2.x的版本。

出自官网信息会标注,未标注的是自己的理解

hello world

结合 webpack 以 npm 的方式安装 vue,此处不使用 vue-cli,便于理解内部实现细节

使用完整版

注意此时要引入dist目录下的完整版,通过 import 默认导入的只包括运行时的版本

Vue 完整版 包括编译器和运行时

// js 文件
import Vue from "vue/dist/vue";

new Vue({
  el: "#app",
  data: {
    msg: "hello world"
  }
});
...
// html 文件
...
  <body>
    <div id="app">{{ msg }}</div>
  </body>
...  

使用运行时

使用完整版,引入的库比运行时大约大 30% ,按官方的说法不建议使用,开发中我们通常是使用 vue-loader 解析 vue 模版,这里有两个地方需要处理下

  • 引入开发依赖 vue-template-compiler ,这个会直接读取处理 .vue 文件 SFC 内容,将 .vue 文件处理为一个 AST
  • webpack 配置中,除了配置 loader 外,需要再引用其中的插件
const VueLoaderPlugin = require('vue-loader/lib/plugin');
...
plugins: [
    new VueLoaderPlugin()
]

经过这些配置,就可以直接使用 .vue 文件

import App from "./components/app";

new vue({
  el: "#app",
  render: h => h(App)
});

原理

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统(来源官网)

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM(来源官网)

虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发(来源官网)

vue 源码: **src/core/vdom/patch.js** (注明 vue 的虚拟DOM是基于Snabbdom原理实现)
Virtual DOM patching algorithm based on Snabbdom...

Vue 是一个M-V-VM (Model View ViewModel) 风格框架,这种风格的框架,View 通常可以理解成是一个页面,M 就是封装了核心数据和逻辑功能的模型,ViewModel 是针对 View 进行特殊定制的 Model

  • 视图(View):用户界面
  • 视图模型(ViewModel):业务逻辑
  • 模型(Model):数据保存
// M-V-VM的例子
// View 层,用户界面
<body>
    <div id="app">{{ msg }}</div>
</body>
...
// Model 层,数据信息,真实应用中,我们通常会从后端接口获取页面展示需要的数据
let modelData = {
		msg: 'hello'
}
...
// ViewModel,vue文件
import Vue from "vue/dist/vue";

let vm = new Vue({
		el: '#app',
		data: modelData
})

当我们通过 modelData.msg 来修改相关数据时,页面对应 DOM 元素的数据就会更新。

Vue 实例被创建时,它将 data 对象中的所有的属性加入到 Vue 的响应式系统中。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值(来源官网)

MVVM 的优点,就是让我们只需关注 Model 的变化,框架会自动更新 DOM 的状态,把开发者从操作 DOM 的繁琐步骤中解脱出来。

在实际开发中,我们更多考虑的也是如何对数据进行处理。页面发生了某次交互后,需要触发什么事件,从而达到更新数据,更新页面的目的(比如计算属性、过滤器、watch、指令等特性,都是设定好相关的信息,对应 DOM 层就自动更新,而我们无须关注Vue是怎么实现的)

模版语法

data

  • 在构造函数中直接使用
new Vue({
		el: '#app',
		data: {
			msg: 'hello'
		}
})
  • 在组件内则必须是函数

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

data () {
	return {
		msg: 'hello'
	}
}

插值

文本插值

通过 Mustache 语法的 {{ }} 设置

  • 持续有效
<div id="app">{{ msg }}</div>
  • 一次性插值
<div id="app" v-once>{{ msg }}</div>

属性 (Attribute)

使用 v-bind(简写为**:**) 指令

  • 普通属性
<div :id="idInfo">{{ msg }}</div>
...
{
	idInfo: 'idInfo'
}
/// 解析为
<div id='idInfo'>
  • 布尔属性
<button :disabled="btnDisabled">按钮</button>
...
{
	btnDisabled: true
}
/// 解析为
<button disabled="disabled">按钮</button>

插值与属性之间的表达式

<span>{{ `${msg} !` }}</span>
<div :id="`list_${id}`"></div>
...
{
	msg: "hello worlds",
	id: "info"
}
/// 解析为
<span>hello worlds !</span>
<div id="list_info"></div>

插值和属性对应的值除了可以是属性键值外,还可以是合法的表达式

/// 来源官网的例子
{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

原样HTML

某些情况下,我们需要原样使用重构给出的 html 文件,此时可以使用 v-html

<span v-html="iconInfo"></span>
...
{
	iconInfo: '<i class="iconfont icon-up"></i>'
}
/// 解析为
<span><i class="iconfont icon-up"></i></span>

指令

指令 (Directives) 是带有 v- 前缀的特殊 attribute,预期是单个 JavaScript 表达式。指令的职责是,当表达式的值改变时,响应式地作用于 DOM(来源官网,稍改)

指令按我的理解是模版功能的加强,给模板添加了编程的能力,让开发者可预期的控制 DOM 的操作。

常用指令

指令 说明
v-html 慎用
v-show 根据表达式之真假值,切换元素的 display CSS 属性(来源官网)
v-if 根据表达式的值有条件地渲染元素,在切换时元素及它的数据绑定 / 组件 会被销毁、重建(来源官网,稍改)
v-else 不可单独使用,与 v-ifv-else-if 配套使用
v-else-if 不可单独使用,与 v-ifv-else-if 配套使用
v-for 基于源数据多次渲染元素或模板块(来源官网)
v-on 绑定事件监听器,事件类型由参数指定:普通 DOM ,监听原生 DOM 事件;组件可以监听自定义事件
v-bind 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式(来源官网)
v-model 在表单控件或者组件上创建双向绑定(来源官网)
v-slot 提供具名插槽或需要接收 prop 的插槽,缩写: #(来源官网)
v-pre 跳过这个元素和它的子元素的编译过程,跳过大量没有指令的节点会加快编译(来源官网)
v-once 只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能(来源官网)

指令参数

  • 直接使用
<span v-html="iconInfo"></span>
<div v-if="flag">{{ flag }}</div>
  • 固定参数 在指令后直接跟随参数
<div :id="idInfo">{{ msg }}</div>
<button @click="clickButton">点击</button>

@是 v-on: 的简写

  • 动态参数 用方括号括起来的 JavaScript 表达式作为一个指令的参数
<div :[attributeName]="idInfo">{{ msg }}</div>
...
{
	attributeName: 'id'
}

修饰符

类比装饰器,算是对指令功能的加强,让指令可以有更灵活的使用

<input v-model="info" type="text">
...
<input v-model.lazy="info" type="text">

.lazy 就是一个修饰符,控制 v-modelinput 失焦时,才更新数据( 本质是取代 input 监听 change 事件)。

v-model 相关
  • .lazy
  • .trim 过滤首尾空格
  • .number 限定输入数据只能为数字

先输入数字,修饰符启用;

先输入非数字则无效,.特殊处理,所以可识别 .01

v-on相关
  • .stop 调用 event.stopPropagation(),阻止冒泡
<div @click="outEvt">
	<button @click="innerEvt">按钮</button>
</div>
...
innerEvt () {
	console.log('inner')
},
outEvt () {
	console.log('outer')
}
...
/// inner
/// outer

上面是一个事件冒泡的例子,下面使用 .stop 阻止冒泡

<div @click="outEvt">
	<button @click.stop="innerEvt">按钮</button>
</div>
  • .prevent 调用 event.preventDefault() ,阻止默认事件的行为
<a @click.prevent href="https://www.baidu.com">百度</a>
  • .capture 在事件捕获阶段添加事件
<div @click="outEvt">
  <div @click="midEvt">
  	<p @click="innerEvt">info</p>
  </div>
</div>
...
/// innerEvt midEvt outEvt

Vue 事件监听默认是按事件冒泡的方式进行,所以展示的执行顺序如上。如果使用了 .capture 则事件会在捕获阶段执行,如下:

<div @click="outEvt">
  <div @click.capture="midEvt">
  	<p @click="innerEvt">info</p>
  </div>
</div>
...
/// midEvt innerEvt outEvt

JavaScript 的事件传播会有三个阶段:事件捕获阶段事件目标阶段事件冒泡阶段,在执行中时,事件捕获阶段早于事件冒泡阶段,所以当使用这个修饰符后 midEvt 顺序会被提前。

/// 事件冒泡、事件捕获的原生实现
<div id="out">
  <div id="mid">
  	<p id="inner">原生</p>
  </div>
</div>
...
let out = document.getElementById('out');
let mid = document.getElementById('mid');
let inner = document.getElementById('inner');
function addEvt (elm, fn, flag = false, evtType = 'click') {
    elm.addEventListener(evtType, fn, {
    	capture: flag
    });
}
...
/// 冒泡
addEvt(out, () => {
    console.log('out')
});
addEvt(mid, () => {
    console.log('mid')
});
addEvt(inner, () => {
    console.log('inner')
});
/// inner mid out
...
/// 捕获
addEvt(mid, () => {
    console.log('mid')
}, true);
/// mid inner mid out

addEventListener 事件的第三个参数是针对 listener 监听事件的可选参数对象,capture 对应的值,就是表示事件的执行阶段是在冒泡阶段还是在捕获阶段,false 表示是冒泡阶段,true 捕获阶段。

从第二个捕获的例子,可以看出这个事件的执行顺序,先捕获,再冒泡。

捕获 --- 从window元素开始发生,一直到目标元素

window --> document --> html --> body --> ... --> 目标元素

冒泡 --- 从目前元素开始发生,一直到window元素

目标元素 ... --> body --> html --> document --> window

  • .self 限定事件是从侦听器绑定的元素本身触发时才触发回调(源于官网)
<div @click.self="outEvt">
	<button @click="innerEvt">按钮</button>
</div>

使用这个修饰符后,按钮的点击事件不会触发外层事件

  • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调(源于官网)

键盘事件: keydown:用户按键盘上的任意键(字符键或功能键)时触发,如果按的字符键,按住时会一直触发该事件 keypress:用户按键盘上的字符键时触发 keyup: 用户释放键盘时触发

对于字符键,键盘事件的执行顺序为:keydownkeypresskeyup,如果按住键盘,在 keypress 后会一直触发 keydown,直到松开触发 keyup

空格键回车键盘是按住时,是keydownkeypress 循环交替执行。

对于功能键,键盘事件的执行顺序为:keydownkeyup,功能键中除了 箭头键 按住时会重复触发 keydown 事件,其他不会重复触发

/// keyCode 32对应的就是空格键,.space就是对应键位别名
<input type="text" @keydown.32="innerEvt" />
...
<input type="text" @keydown.space="innerEvt" />

下面是在比较稳定的键位和码值的对应表

键位 码值 键位 码值
0~9(数字键) 48~57 A~Z(字母键) 65~90
Backspace(退格键) 8 Tab(制表键) 9
Enter(回车键) 13 Space(空格键) 32
Shift 16
Left arrow(左箭头键) 37 Top arrow(上箭头键) 38
Right arrow(右箭头键) 39 Down arrow(下箭头键) 40

keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持。(源于官网)

不要用 .keyCode 的形式

  • .native 监听组件根元素的原生事件(源于官网)
/// 假定根组件为 App.vue ,Child 是其根组件
<Child @click.native="midEvt"></Child>

使用了 .native 修饰符,就能监听根元素的事件

  • .once 只触发一次
<button @click.once="innerEvt">按钮</button>
  • .left 只当点击鼠标左键时触发。
  • .right 只当点击鼠标右键时触发。
  • .middle 只当点击鼠标中键时触发。
/// left right middle 都是鼠标按钮修饰符
<div @click.right="innerEvt">鼠标事件</div>
  • .ctrl
  • .alt
  • .shift
  • .meta

以上四个修饰符是系统修饰键,主要是实现仅在按下相应按键时才触发鼠标或键盘事件的监听器

/// 下面就表示必须按住alt键,再点击才会触发事件
<button @click.alt="printInfo">打印信息</button>
  • .exact 粒化系统修饰符组合触发的事件
/// 虽然下面会表示必须按住alt键,再点击才会触发事件,但是如果你按了 shift + alt 一样触发,如果希望更精细的控制,要求只有按住alt才出发,可以使用这个修饰符
<button @click.alt.exact="printInfo">打印信息</button>
  • .passive{ passive: true } 模式添加侦听器(源于官网)

prevent 是拦截默认事件,passive 是不拦截默认事件,两个修饰符不可同时使用

{ passive: true } 这个值也是原生方法 addEventListener 的第三个参数,此时表示监听的函数永远不会调用 preventDefault

浏览器内核针对每次事件的触发,都会去查询下是否有 preventDefault 阻止该次事件的默认行为,当设定了 passive 就是告诉浏览器不用查询了,该事件没有阻止默认行为。

这个修饰符一般用在 scrolltouchmove 滚动监听事件中,因为滚动监听的过程中,每移动一次像素都会产生一次事件,每次都使用内核进行查询会使滑动产生卡顿感,添加后能提升流程度。

v-bind 相关
  • .prop 作为一个 DOM property 绑定而不是作为 attribute 绑定。(源于官网)
    • property 是节点对象在内存中存储的属性,是 JavaScript 里的对象
    • attributeHTML 标签上的特性,它的值只能够是字符串,HTML 标签中定义的属性和值会保存 DOM 对象的 attributes 属性里面(包括自定义)

常用的 attribute,例如 idclasstitle 等,已经被作为 property 附加到 DOM 对象上,可以和 property 一样取值和赋值。

<div id="div1" title1="divTitle1" data-info="datainfo" :text-content="divText"></div>

attribute 可以用通过 getAttribute 取值,setAttribute 设置值

document.getElementById('div1').getAttribute('title1');
// divTitle1
document.getElementById('div1').setAttribute('title1', 'title1');

property 按普通对象方式使用

document.getElementById('div1').id;
document.getElementById('div1').id = 'div2';

默认 Vue 会按 attribute 绑定

document.getElementById('div1').attributes['text-content']; 
// text-content="div info"
document.getElementById('div1').textContent;
// ""

我们在页面端看到的信息是由 DOM 对象上的 textContent 控制,而不是标签上的 text-content,针对上面的问题,我们可以把其理解成一个对象上不同字段,使用这个修饰符可以强制 DOM 对象上存在的相关属性进行更新

:text-content.prop="divText"

attribute 的更新和 property 并不是双向绑定,我们常用的属性,应该是特殊处理过

divElem.setAttribute('text-content', 'new div');
...
divElem.getAttribute('text-content');
// new div
divElem.textContent;
// ''
...
/// 如果是这么去设置id,此时表现的就是同步更新
divElem.setAttribute('id', 'newId');
...
divElem.getAttribute('text-content');
divElem.textContent;
// newId
  • .camel 将 kebab-case attribute 名转换为 camelCase(源于官网)
/// 保持不转变
<svg :viewBox='msg'></svg>
...
/// 转换
<svg :view-box.camel='msg'></svg>
/// svg上可以直接绑定使用,除非你就喜欢第二种写法,要强转下
  • .sync 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器(源于官网)
<Child :info="msg" @update:info="func"></Child>
...
/// Child 组件中如果要触发相关事件需要调用
this.$emit('update:info', 'childInfo');

如果你只是对 msg 形成类似双向绑定的行为,在根组件对应事件只进行简单的赋值操作。

func (val) {
	this.msg = val;
}

可以采用这个修饰符进行处理

<Child :info.sync="msg"></Child>

子组件传递的事件名必须为 update:myPropName

计算属性和监听器

Vue 双向数据绑定的实现

Vue 是采用 数据劫持 结合 发布者-订阅者模式 的方式实现数据绑定

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。(源于官网)(数据劫持 - observe
  • 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。(源于官网)(dep
  • 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。(源于官网)(发布者-订阅者模式 - watcher

observe:遍历 data 中的属性,使用 Object.definePropertyget/set 方法对其进行数据劫持

dep:实现处理依赖关系的对象

为什么进行依赖收集

<template>
    <div>
        <p>{{info}}</p>
    </div>
</template>
...
data () {
	return {
		info: 'child info',
		flag: true
	}
}

比如上面的例子,flag 只是定义在 data 中的无用数据(并未使用),如果我们修改flag 也去对 虚拟DOM进行一次遍历,这就是浪费资源,所以才需要对依赖进行收集

依赖收集

入口以及数据劫持的代码段
/// Vue源码路径,只引关键部分代码说明逻辑 并加入相应的代码注释说明
/// vue/src/core/instance/index.js 入口
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 入口
  this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
...
/// vue/src/core/instance/init.js
let uid = 0

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) { // 加挂 _init 方法
    const vm: Component = this
    // a uid
    vm._uid = uid++
    ...
    initState(vm) // data 初始化的入口
    ...
/// vue/src/core/instance/state.js 
export function initState (vm: Component) {
...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */) // 执行 observe 方法
  }
...
function initData (vm: Component) {
  ...
  // proxy data on instance
	...
	// observe data
  observe(data, true /* asRootData */) // 执行 observe 方法
...

涉及三个源码文件:

  • core/instance/index.js
  • core/instance/init.js
  • core/instance/state.js

initData 方法中的 引用的 observe 方法,是对 options.data 进行数据劫持的关键方法

observe 数据劫持
/// vue/src/core/observer/index.js
// state.js 引用的方法 此处调整了源码中 observe 方法和 Observe 类之间的顺序
export function observe (value: any, asRootData: ?boolean): Observer | void {
 		...
    ob = new Observer(value)  // 把data变成可观察对象
...    
export class Observer {
...
	constructor (value: any) {
    this.value = value
    this.dep = new Dep() // 新建Dep对象,用来收集依赖
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) // 如果是数组执行 oberverArray
    } else {
      this.walk(value) // 如果是 JSON 执行 walk
    }
  }
  
  // 循环 Obj 对象,确保全部属性转为可观察对象 
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 执行 defineReactive 方法
    }
  }

  // 循环数组,执行 oberve 确保数组的每一个 child 都转为可观察对象 
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
...
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep(); // 创建 dep 对象收集依赖,每个 key 都会新建一个属于自己的 Dep 对象
	...
	Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 全局的target,值为 Vue 初始化的 Watcher 对象
        dep.depend()  // 收集依赖,确保只有使用的值才会被收集
        if (childOb) {
          childOb.dep.depend() // 收集依赖,确保只有使用的值才会被收集
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
        ...
    },
    set: function reactiveSetter (newVal) {
      ...
      childOb = !shallow && observe(newVal) // 新加的数据会被追加为可观察对象
      dep.notify() // 通知关联 watcher 更新操作
    }
  })
...

涉及以下源码:

  • core/observer/index.js 分析源码的 observe 方法和 Observer 类,可以看到会对 data 的数据进行遍历,保证每个属性值的都会被注入 getter 以及 setter 的逻辑,在 getter 中收集依赖(dep.depend),在 setter 中分分发依赖(dep.notify

dep 依赖收集

/// vue/src/core/observer/dep.js
export default class Dep {
	...
	constructor () {
    this.id = uid++
    this.subs = []
  }
	...
  depend () {
    if (Dep.target) { // 此处的 Dep.target 是一个实例化的 Watcher 对象(见下)
      Dep.target.addDep(this) // 此处相当于调用 Watcher.addDep(this),记住此处的 this 是 defineReactive 下针对每一个需要观察的 Key 新建的 Dep 的实例化对象
    }
  }
  notify () {
    ...
  }
...

Dep.target 是一个实例化的 Watcher 对象代码分析

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)

入口文件指明的函数执行顺序,上面分析过 initMixin 的代码,而 Watcher 实例化的逻辑在 lifecycleMixin

/// vue/src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  ...
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */) // 实例化 Watcher,updateComponent 是函数
...
/// vue/src/core/observer/watcher.js
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    ...
    if (typeof expOrFn === 'function') {
    	//data 依赖收集走此处
      this.getter = expOrFn // 把之前的 updateComponent 赋给 getter
    } else {
      //watch 依赖走此处
      this.getter = parsePath(expOrFn)
    ...
    this.value = this.lazy
      ? undefined
      : this.get() // 初始化时,我们并没有设置 lazy,所以直接执行get方法
    ...
  }  
  
  get () {
    pushTarget(this) // 给 Watcher 对象添加 target 属性的方法
    ...
  }
}
...
/// vue/src/core/observer/dep.js
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target // 把传入的 Watcher 对象,赋给 Dep.target
}
...
/// 这里还有个简单的分辨方法,查看Dep类的定义,静态属性已经说明是 Watcher 类型
export default class Dep {
  static target: ?Watcher;

Watcher与Dep

dep 依赖收集 段落,最终是在 Dep.target.addDep(this) 停止,下面是后续的逻辑

/// vue/src/core/observer/watcher.js
  addDep (dep: Dep) { // dep 是 Dep 类型
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep) // 给 watcher.newDeps 属性 push 进 dep 对象,也就是针对 data[key] 实例化的 Dep 对象
      if (!this.depIds.has(id)) {
        dep.addSub(this) // 依赖收集传入一个 Watcher 对象,此处需要明白 addDep 方法执行时,是基于Dep.target作为上下文环境,根据闭包原理,此时的this就是 Dep.target,也就是一个 Watcher 的实例对象
      }
    }
  }
...
/// vue/src/core/observer/dep.js
	addSub (sub: Watcher) { // sub 是 Watcher 类型
    this.subs.push(sub) // 在 dep 的 subs 数组推入一个 Watcher 对象,subs 就是一个依赖收集的数组,如果这个对应的 data key 有更新,就遍历这个数组通知其订阅者,更新数据 
  } 

依赖收集完成后,initData 的逻辑就执行完成,到此最直观的感觉就是: 通过 renderWatcher(名称源于 isRenderWatcher ) 我们对 data 进行劫持,在其内部建立起了一个消息订阅的模式,不管是获取,或者设置,Vue都能追踪其依赖,通知订阅者

此处关系有些绕,可以简化记忆为,watcher 实例对象下的 newDeps 数组收集了 dep,dep实例对象的数组 subs 记录自己被什么 watcher 收集

image

视图更新

/// vue/src/core/observer/index.js
export function defineReactive (
 ...
 const dep = new Dep()
 ...
 set: function reactiveSetter (newVal) {
 	...
 	dep.notify() // 数据劫持时的 set 方法触发 notify
 }
 ...
/// vue/src/core/observer/dep.js
export default class Dep {
	...
	notify () {
    const subs = this.subs.slice() // this 是闭包实例化的 dep 对象,参考上文说的 dep.subs 存储的是 watcher 对象
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 相当于是触发 watcher 对象的 update 方法
    }
  }
...
/// vue/src/core/observer/watcher.js
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this) // 按 data 中数据为例,此时代码应该是走到这,watcher队列 异步更新
    }
  }
/// vue/src/core/observer/scheduler.js  queueWatcher 更新
export function queueWatcher (watcher: Watcher) {
	const id = watcher.id
	...
			if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
...
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 如果设定了 before 也会执行
    }
    id = watcher.id
    has[id] = null
    watcher.run() // 执行 watcher 对象的 run 方法
...
/// vue/src/core/observer/watcher.js 回头来看 run 方法
...
此处把renderWatcher的定义提到此处,方便后面的方法说明
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
...
get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // this.getter = expOrFn 此处的getter 方法 renderWatcher 的第二个参数 updateComponent
    } 
    ...
}

run () {
    if (this.active) {
      const value = this.get() // 调用 watcher 对象的 get 方法,获取新的 value值,也就是上面标注的 get 方法
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
        	// watcher 监听走这里
          try {
            this.cb.call(this.vm, value, oldValue) // 执行 watcher 的回调函数,renderWatcher 中的 noop,并传入新旧值进行 diff 比较
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
        	// data 监听走这里
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  ...
/// 基于代码分析,专注点回到定义时的函数,看看对应的函数到底是什么
/// vue/src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
        ...

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      ...
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
	/// 分析上文 updateComponent 方法,都会触发 vm._update 
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
...
/// vue/src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ...
    if (!prevVnode) {
      // initial render
      // 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      // 更新时
      vm.$el = vm.__patch__(prevVnode, vnode) // 触发 vm.__patch__ 进行更新
    }
...
/// 代码会走到 vm.__patch__ 方法,该方法会执行虚拟DOM的diff算法,更新界面
/// vue/src/core/vdom/patch.js
export function createPatchFunction (backend) {
  ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ...

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue) // oldVnode 为空时,直接创建新的 root element,初始化时执行
    } else {
      const isRealElement = isDef(oldVnode.nodeType) // oldVnode 是id为app的dom节点
      // 剩余代码就是新旧Vnode比较,以及 diff 算法实现,最终映射成 dom
      ...
    return vnode.elm
  }
}
...
  • 当有数据变更时触发 setter 函数,触发 dep.notify() ,通知订阅者,通过 watcher.update 触发渲染逻辑,并最终渲染到页面。

此处参考资料

下面是说明,computedwatcherdata之间关系的建立

/// vue/src/core/instance/state.js
export function initState (vm: Component) {
...
	if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
...
const computedWatcherOptions = { lazy: true } // 留意此处的lazy

function initComputed (vm: Component, computed: Object) {
	const watchers = vm._computedWatchers = Object.create(null)
  ...

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get // 如果对应的value是函数,则直接取用,否则取value.get
    ...

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 此处是为每一个属性添加Watch,注意与 renderWatcher 不同,传入到vm._computedWatchers 
      watchers[key] = new Watcher(
        vm,
        getter || noop, // 传入的第二个函数正常就是计算属性本身
        noop,
        computedWatcherOptions
      )
    }
		if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      ...
    }
    ...
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
	const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  }
  ...
  // 针对计算属性进行数据劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // 此处就是上文说关注 {lazy:true} 使用的地方,保证 computed data 中引用的 data 发生改变后,是不是立马重新计算
        watcher.evaluate() // 当其watcher对象时组件时,这个方法最终还是会执行到watcher.get()
      }
      if (Dep.target) {
        watcher.depend() // 收集 computed data 中用到的 data 的依赖,实现当 computed data 中引用的 data 发生更改时,也能触发 render function 重新执行
      }
      return watcher.value
    }
  }
}
/// vue/src/core/observer/watcher.js
export default class Watcher {
	...
	constructor (
    vm: Component, // 当前这个 watcher 所属的 vueComponent
    expOrFn: string | Function, // 需要监听的方法/表达式
    cb: Function, // 当 getter 中引用到的 data 发生改变时,触发该回调
    options?: ?Object, // 额外参数,deep 为 true 时,会对getter返回的对象在做一次深度遍历,进行进一步依赖收集;user 用于标记这个监听是否由用户通过 $watcher 调用; lazy 用于标记 watcher 是否为懒执行,该属性是给 computend data 用的,当 data 中的值更改时,不会立即计算 getter 获取新的值,而是给该 watcher 标记为 dirty,当该 computed data 被引用时才会执行,从而返回新的 computed data,减少计算量;sync 表示当 data 值更改时,watcher 是否同步更新数据,如果是 true,就会立即更新,否则在nextTick 中更新;before
    isRenderWatcher?: boolean
  ) {
    ...
    if (options) {
    	...
      this.lazy = !!options.lazy
      ...
    }
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      ...
    }
    this.value = this.lazy
      ? undefined
      : this.get()
    ...
...
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象(源于MDN定义)

Object.defineProperty(obj, prop, descriptor)

obj:要定义属性的对象 prop:要定义或修改的属性的名称或 Symbol descriptor:要定义或修改的属性描述符

descriptor可设置的值:

  • configurable 是否可被删除 默认为 false
  • enumerable 是否可被枚举 默认为 false
  • value 属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)默认为 undefined
  • writable 是否可以被赋值运算符改变 默认为 false
  • get 属性的 getter 函数 默认为 undefined
  • set 属性的 setter 函数 默认为 undefined

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符

类型 configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以
let obj = {};
/// 数据描述符
Object.defineProperty(obj, 'info', {
    value: 'info',
    writable: true
})
/// 存取描述符
Object.defineProperty(obj, 'foo', {
    configurable: true,
    enumerable: true,
    get () {
        return `${this.info} foo`
    },
    set (newValue) {
        this.info = newValue
    }
})

blog.seosiwei.com/detail/36 blog.seosiwei.com/detail/22 blog.seosiwei.com/detail/24 blog.seosiwei.com/detail/35 juejin.cn/post/684490… juejin.cn/post/684490… segmentfault.com/a/119000001… segmentfault.com/a/119000001…

computed

对于任何复杂逻辑,你都应当使用计算属性(源于官网)

<template>
    <div>
        <div>{{ indexInfo }}</div>
        <button @click="modifyBtn">修改info</button>
    </div>
</template>
...
data () {
	return {
		msg: 'idInfo',
		idInfo: 'index'
	}
}
computed: {
	indexInfo () {
		return `${this.msg} - ${this.idInfo}`;
	}
},
methods: {
	modifyBtn () {
		this.msg = 'new msg';
	}
}

计算属性 indexInfo 会加挂到 vm 上,并且把其函数值,作为 vm.indexInfogetter 值,在依赖发生变化时(vm.msg 或者 vm.idInfo),会触发这个函数

根据之前的源码分析,如果对应的计算属性时存取描述,则当其获取时执行 getter,设置时执行 setter

  • 计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值,只要相关依赖不变,多次访问同一个计算属性会立即返回之前的计算结果,而不必再次执行函数(源于官网)

  • 方法将总会再次执行函数(源于官网)

watch

watch 是一种更通用的方式来观察和响应 Vue 实例上的数据变动(源于官网)

data () {
	return {
		msg: 'msg',
		idInfo: 'idInfo',
		indexInfo: '',
	}
}
watch: {
	msg (newValue, oldValue) {
    console.log('oldValue', oldValue);
    console.log('newValue', newValue);
    this.indexInfo = `${this.msg} - ${this.idInfo}`
  }
},

watch 与 computed

  • computed 是计算一个新的属性,并将该属性挂载到 vm(Vue实例) 上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以 watch 可以监听 computed 的变化(其它还有dataprops
  • computed 会基于它们的响应式依赖进行缓存的,只有在相关响应式依赖发生改变,或是第一次访问 computed 属性,才会计算新的值。而 watch 则是当数据发生变化便会调用执行函数
  • computed 适用一个数据被多个数据影响(对应数据被哪些数据影响,这些数据只要一变化,这个计算属性就会更新),而 watch 适用一个数据影响多个数据(当前改变的数据是什么,影响到了哪些数据都可以在定义时,直观查看)

class 与 style

和绑定属性一样的操作,做了一些加强

class

  • class 属性和绑定属性一起使用
<div class="msg" :class="classObj">{{ msg }}</div>
...
/// 绑定属性的值直接是 data
data () {
	return {
		classObj: 'newMsg'
	}
}
...
/// 也可以是计算属性
/// 绑定class能识别对象语法 { newMsg: this.active, 'msg-info': !this.active}
computed: {
  classObj () {
  	return {
  		newMsg: this.active, // active 是个 boolean 型的 data 属性
  		'msg-info': !this.active
  	}
  }
}
  • 数组语法 个人推荐使用,把可变的部分放到计算属性中
<div :class="[classObj, 'msg']">{{ msg }}</div>

style

尽量不要用,不好维护,如果要使用尽量使用数组的形式

<div :class="[classObj, 'msg']" :style="[baseStyle]">{{ msg }}</div>
...
computed: {
	 baseStyle () {
     return {
     	color: 'red'
     }
   }
}

条件渲染

增强模板逻辑功能的指令

v-if v-else v-else-if

<div>
  <div v-if="age > 30">壮年</div>
  <div v-else-if="age > 18">青年</div>
  <div v-else>孩童</div>
</div>
...
data () {
	return {
		age: 20
	}
}

v-show

 <div v-show="age > 18"></div>

v-if 与 v-show 区别

  • v-show 的元素始终会被渲染并保留在 DOM 中,v-show 只是简单地切换元素的 CSS 属性 display
  • v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
  • v-show 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
  • v-if 是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。
  • v-if 有更高的切换开销,v-show 有更高的初始 渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
  • v-show 不支持 <template> 元素,也不支持 v-else

以上区别说明来源官网

列表渲染

v-for

v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。(源于官网) v-for 还支持一个可选的第二个参数,即当前项的索引。(源于官网)

<ul>
	<li v-for="(item, $index) in list" :key="item.toString()">{{ $index }} - {{ item }}</li>
</ul>
...
data () {
	return {
		list: ['苹果', '菠萝', '香蕉', '火龙果']
	}
}

item of items 可以用 of 替代 in 作为分隔符

key

key 的特殊属性主要用在 Vue虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes

如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。(源于官网)

而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”,只需添加一个具有唯一值的 key 属性即可(源于官网)

相同父元素子元素必须有独特的 key。重复的 key 会造成渲染错误。(源于官网)

一般我们会在 v-for 中使用 key 属性,其他还有一些情况也可以使用:

  • 完整地触发组件的生命周期钩子
  • 触发过渡
<transition>
  <span :key="text">{{ text }}</span>
</transition>
...
<span>{{ text }}</span>
/// 对比上面的两种使用形式,使用了key值的,在text 值更新时,能明显的感觉出在重新渲染
...
<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>
/// 在使用 v-if 切换元素时,如果不加 key 相近的元素会被复用提升渲染速度,比如例子中的 label,但是这情况下如果复用了input就会造成数据的混乱,这时添加 key 的标注可以标明两个元素相互独立,不要复用

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。(源于官网)

不要在 v-for 里使用对象

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

存在兼容性问题的东西,尽量不要使用

数组更新

变异数组方法: push()、pop()、shift()、unshift()、splice()、sort()、reverse() 以上方法放心用,它们都会改变原始数组,怎么用都没问题

	<button @click="changeArray">改变</button>
	...
	methods: {
        changeArray () {
            this.list.pop()
        }
	}

其他使用其他方法是,需要主动替换

changeArray () {
	this.list = this.list.concat(['葡萄', '李子'])
}

对数据源特殊处理

v-for="(item, $index) in filterArray(list)"
...
filterArray (arr) {
	return arr.filter(item => item.length < 3)
}
/// 循环之前,可以先特殊处理

不要在同一个元素上 使用 v-for 与 v-if

事件处理

可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。(源于官网)

<button @click="printInfo">打印信息</button>
...
methods: {
	printInfo () {
  	console.log('打印信息')
  }
}

javascript组成:

  • ECMAScript 基本语法
  • BOM (浏览器对象模型:使用对象描述了浏览器的各个部分的内容)
    • Window 对象
    • Navigator 对象
    • Screen 对象
    • History 对象
    • Location 对象
    • 存储对象
  • DOM (文档对象模型)
    • HTML DOM Document 对象
    • HTML DOM 元素对象
    • HTML DOM 属性对象
    • HTML DOM 事件对象
    • Console 对象
    • CSSStyleDeclaration 对象
    • DOM HTMLCollection

DOM

DOM(文档对象模型):是 HTMLXML 文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将 web 页面和脚本或程序语言连接起来。

DOM 是 web 页面的完全的面向对象表述,它能够使用如 JavaScript 等脚本语言进行修改。

API (web 或 XML 页面) = DOM + JS (脚本语言)

以上均源于MDN定义

DOM事件

DOM0

DOM0 是指 IE4Netscape4.0 这些浏览器最初支持的 DHTML,在官方的说法中,并没有形成所谓的 DOM0 事件,只不过我们现在一般会把下面的使用方式称为 DOM0 事件

element.onclick = function(){...}
<button onclick="add()"></button>

DOM1

DOM1 是 1998年10月 成为 W3C 推荐标准,DOM1DOM CoreDOM HTML 两个模块组成。DOM Core 是规定如何映射基于 XML 的文档结构,以便简化对文档中任意部分的访问和操作,DOM HTML 模块则是在 DOM 核心的基础上加以扩展,添加了针对 HTML 的对象和方法。在 DOM1 阶段并未针对事件做特殊添加

DOM2

DOM2 中新增了多个模块,很多我们属性的模块都是在这时添加的,比如:

  • DOM Events:定义了事件和事件处理接口
element.addEventListener('click', function() {}, 'false') 
  • DOM Style:定义了基于 css 为元素的应用样式接口
  • DOM Traversal and Range:定义了遍历和操作文档树的接口

DOM3

DOM3 是对 DOM 进一步扩展,相应的其对事件也做了增强,用法于 DOM2 一样

相关事件

鼠标事件
属性 描述 DOM
onclick 当用户点击某个对象时调用的事件句柄。 2
oncontextmenu 在用户点击鼠标右键打开上下文菜单时触发
ondblclick 当用户双击某个对象时调用的事件句柄。 2
onmousedown 鼠标按钮被按下。 2
onmouseenter 当鼠标指针移动到元素上时触发。 2
onmouseleave 当鼠标指针移出元素时触发 2
onmousemove 鼠标被移动。 2
onmouseover 鼠标移到某元素之上。 2
onmouseout 鼠标从某元素移开。 2
onmouseup 鼠标按键被松开。 2
键盘事件
属性 描述 DOM
onkeydown 某个键盘按键被按下。 2
onkeypress 某个键盘按键被按下并松开。 2
onkeyup 某个键盘按键被松开。 2
框架/对象(Frame/Object)事件
属性 描述 DOM
onabort 图像的加载被中断。 ( <object>) 2
onbeforeunload 该事件在即将离开页面(刷新或关闭)时触发 2
onerror 在加载文档或图像时发生错误。 ( <object>, <body>和 <frameset>)
onhashchange 该事件在当前 URL 的锚部分发生修改时触发。
onload 一张页面或一幅图像完成加载。 2
onpageshow 该事件在用户访问页面时触发
onpagehide 该事件在用户离开当前网页跳转到另外一个页面时触发
onresize 窗口或框架被重新调整大小。 2
onscroll 当文档被滚动时发生的事件。 2
onunload 用户退出页面。 ( <body> 和 <frameset>) 2
表单事件
属性 描述 DOM
onblur 元素失去焦点时触发 2
onchange 该事件在表单元素的内容改变时触发(<input>, <keygen>, <select>, 和 <textarea>) 2
onfocus 元素获取焦点时触发 2
onfocusin 元素即将获取焦点时触发 2
onfocusout 元素即将失去焦点时触发 2
oninput 元素获取用户输入时触发 3
onreset 表单重置时触发 2
onsearch 用户向搜索域输入文本时触发 ( <input="search">)
onselect 用户选取文本时触发 ( <input> 和 <textarea>) 2
onsubmit 表单提交时触发 2
剪贴板事件
属性 描述 DOM
oncopy 该事件在用户拷贝元素内容时触发
oncut 该事件在用户剪切元素内容时触发
onpaste 该事件在用户粘贴元素内容时触发
打印事件
属性 描述 DOM
onafterprint 该事件在页面已经开始打印,或者打印窗口已经关闭时触发
onbeforeprint 该事件在页面即将开始打印时触发
拖动事件
属性 描述 DOM
ondrag 该事件在元素正在拖动时触发
ondragend 该事件在用户完成元素的拖动时触发
ondragenter 该事件在拖动的元素进入放置目标时触发
ondragleave 该事件在拖动元素离开放置目标时触发
ondragover 该事件在拖动元素在放置目标上时触发
ondragstart 该事件在用户开始拖动元素时触发
ondrop 该事件在拖动元素放置在目标区域时触发
多媒体(Media)事件
属性 描述 DOM
onabort 事件在视频/音频(audio/video)终止加载时触发。
oncanplay 事件在用户可以开始播放视频/音频(audio/video)时触发。
oncanplaythrough 事件在视频/音频(audio/video)可以正常播放且无需停顿和缓冲时触发。
ondurationchange 事件在视频/音频(audio/video)的时长发生变化时触发。
onemptied 当期播放列表为空时触发
onended 事件在视频/音频(audio/video)播放结束时触发。
onerror 事件在视频/音频(audio/video)数据加载期间发生错误时触发。
onloadeddata 事件在浏览器加载视频/音频(audio/video)当前帧时触发触发。
onloadedmetadata 事件在指定视频/音频(audio/video)的元数据加载后触发。
onloadstart 事件在浏览器开始寻找指定视频/音频(audio/video)触发。
onpause 事件在视频/音频(audio/video)暂停时触发。
onplay 事件在视频/音频(audio/video)开始播放时触发。
onplaying 事件在视频/音频(audio/video)暂停或者在缓冲后准备重新开始播放时触发
onprogress 事件在浏览器下载指定的视频/音频(audio/video)时触发。
onratechange 事件在视频/音频(audio/video)的播放速度发送改变时触发。
onseeked 事件在用户重新定位视频/音频(audio/video)的播放位置后触发。
onseeking 事件在用户开始重新定位视频/音频(audio/video)时触发。
onstalled 事件在浏览器获取媒体数据,但媒体数据不可用时触发。
onsuspend 事件在浏览器读取媒体数据中止时触发。
ontimeupdate 事件在当前的播放位置发送改变时触发。
onvolumechange 事件在音量发生改变时触发。
onwaiting 事件在视频由于要播放下一帧而需要缓冲时触发。
动画事件
属性 描述 DOM
animationend 该事件在 CSS 动画结束播放时触发
animationiteration 该事件在 CSS 动画重复播放时触发
animationstart 该事件在 CSS 动画开始播放时触发
过渡事件
属性 描述 DOM
transitionend 该事件在 CSS 完成过渡后触发。
触摸事件
属性 描述 DOM
touchstart 手指放在一个触点时触发
touchend 当用户将一个手指离开触摸平面时触发
touchmove 当用户在触摸平面上移动触点时触发
touchcancel 当触点由于某些原因被中断时触发
其他事件
属性 描述 DOM
onmessage 该事件通过或者从对象(WebSocket, Web Worker, Event Source 或者子 frame 或父窗口)接收到消息时触发
ononline 该事件在浏览器开始在线工作时触发。
onoffline 该事件在浏览器开始离线工作时触发。
onpopstate 该事件在窗口的浏览历史(history 对象)发生改变时触发。
onshow 该事件当 <menu> 元素在上下文菜单显示时触发
onstorage 该事件在 Web Storage(HTML 5 Web 存储)更新时触发
ontoggle 该事件在用户打开或关闭 <details> 元素时触发
onwheel 该事件在鼠标滚轮在元素上下滚动时触发

自定义事件

可以通过 Event 构造函数,新建一个自定义事件,自定义事件通过 element.dispatchEvent() 触发

<div id="idInfo">idInfo</div>
...
let vmElem = document.getElementById('idInfo')
let customEvt = new Event('custom')
vmElem.addEventListener('custom', function() {
    console.log('自定义事件')
})
vmElem.addEventListener('click', function() {
    this.dispatchEvent(customEvt);
    console.log('click')
})

Vue 此处在虚拟DOM上绑定事件的行为,其实是和之前的事件代理的绑定规则有些冲突,只不过这些操作行为都是在虚拟dom上进行,按官方的说法,事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上

表单输入绑定

表单上可以使用 v-model 创建双向数据绑定,它会根据控件类型自动选取正确的方法来更新元素

v-model 会忽略所有表单元素的 value、checked、selected attribute 的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。(源于官网)

input

绑定的是 inputvalue

text

<input type="text" v-model="info">
<p>{{info}}</p>
...
data () {
	return {
		info: ''
	}
}

checkbox

  • 单个值使用,v-model 绑定到布尔值
/// 此时selInfo会在布尔值直接切换
<input type="checkbox" v-model="selInfo" id="names">
<label for="names">{{selInfo}}</label>
...
selInfo: false
  • 多个复选框,绑定到同一个数组
<input type="checkbox" v-model="selInfo" id="red" value="red">
<label for="red">red</label>
<input type="checkbox" v-model="selInfo" id="yellow" value="yellow">
<label for="yellow">yellow</label>
<p>{{selInfo}}</p>
...
selInfo: []

radio

<input type="radio" v-model="selInfo" id="red" value="red">
<label for="red">red</label>
<p>{{selInfo}}</p>
...
selInfo: ''

select

绑定的是 optionvalue

单选

<select v-model="selInfo">
  <option disabled value="">请选择颜色</option>
  <option value="red">red</option>
  <option value="yello">yello</option>
</select>
<p>{{selInfo}}</p>
...
selInfo: ''

多选

/// 同单选使用,多加multiple属性
<select v-model="selInfo" multiple>
...
selInfo: []

textarea

<textarea cols="30" rows="10" v-model="info"></textarea>

在文本区域插值 ({{text}}) 并不会生效,应用 v-model 来代替。

组件

  • 每个组件必须只有一个根元素
  • 组件的注册分为全局注册和局部注册
  • 父组件通过 Prop 向子组件传递数据
  • 子组件可以通过调用内建的 emit** 方法 并传入事件名称来触发一个事件,**emit 的第二个参数可以传递参数值

单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。(源于官网) 关于组件名,官网说两种命名都可以,不过参考隔壁 react 的规范,建议始终以大写单词开头

/// app.vue
<template>
  <div> // 这里的 div 元素必须保留,必须要有一个根元素
    <Msg /> // 注册后的组件,就可以像普通元素一样使用
  </div>
</template>
<script>
import { mapState } from "vuex";
import Msg from "./msg";

export default {
  components: { // 根组件中这种注册子组件的方式是局部注册
    Msg
  }
};
</script>
...
/// msg.vue
<template>
  <div> {{ msg }} </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "msg"
    };
  }
};
</script>
...
/// 全局注册
/// 在入口的 js 通过 Vue.component 进行注册
import rootMsg from "./components/root-msg";

Vue.component("root-msg", rootMsg);

let vm = new Vue({
  el: "#app",
  store,
  render: h => h(App)
});
...
/// 通过全局注册的组件,就无须在其他注册内再次注册,可直接使用
/// app.vue
<template>
  <div>
    <Msg />
    <root-msg />
  </div>
</template>

组件间传值

Prop

Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。我们可以用一个 props 选项将其包含在该组件可接受的 prop 列表中(源于官网)

父组件 -> 子组件

/// app.vue
<template>
  <div>
    <Msg title="msg title" />
...    
/// msg.vue
<template>
  <div>
    {{ msg }} - {{ title }}
  </div>
</template>
<script>
export default {
  props: ["title"],

组件 prop 可以传入一个静态值,也可以是个动态值

<Msg :title="msgTitle" />
...
{
	msgTitle: 'msg title'
}
prop 的类型

目前支持:String, Number, Boolean, Array, Object, Function, Date, Symbol

prop 验证
  • 直接指定一个或者多个类型
props: {
    propA: Number,
    propB: [String, Number],
}    
  • 使用对象的形式,设定多个值
props: {
	propA: {
		type: String, // 类型
		required: true, // 是否为必填
		default: 100 // 默认值,default 可以是一个具体的字面量,针对 Object 或者 Function 它也可以是一个工厂函数
	}
}
  • 使用自定义验证函数
propF: {
  validator: function (value) { // 函数参数是接收的值
  	return ['success', 'warning', 'danger'].indexOf(value) !== -1
  }
}

vue 的校验是运行时校验,所以你不按设定值去传值,在编译阶段也不会报什么错误,之后版本使用的 ts 能比较完美的解决这个问题

非 prop 属性,组件会直接追加到根元素上

<Msg :title="msg" id="msgId" class="msgClass" />
/// 在 Msg 组件的根元素会直接追加 id 和 class

在官方文档中提到了替换/合并已有的 Attribute,但我自己感觉,这种思路会破坏组件的封装。

禁用 Attribute 继承
<Msg :title="msg" id="msgId" class="msgClass" info="info" />

这样在 Msg组件 的根元素上就会包含id class info这些属性值,虽然可能这些值完全无用,如果想禁止这种行为可以在组件中设定 inheritAttrs: false

如果 Msg组件 这么设定,最终只会有 class 被添加到根元素上(style 属性也不受这个参数的控制)

不使用 : 的情形

:v-bind 的简写形式,但是当我要在组件上绑定一个对象的所有属性时,就要使用完整格式,组件上有多值需要传递时使用这种方式会比较便利

<Msg v-bind="info" />
...
info: {
  name: "msg",
  title: "msg title"
}
/// 这样写相当于
<Msg :name="info.name" :title="info.title" />

配合inheritAttrs、props、$attrs一起使用,能达到简化代码的功能

$attrs:包含了父作用域中,除去 props 能被识别 (且获取) 的 attribute (class 和 style 除外)

/// info的值稍微修改下
info: {
  name: "msg",
  title: "msg title",
  id: "msgClass"
}
...
/// 在 Msg 组件中设置inheritAttrs:false、props: ['title']
/// 在 Msg 组件内,把需要绑定的外部属性的标签,直接使用 v-bind="$attrs"
/// 这样 name id 就作为属性直接绑定到指定标签上
<p v-bind="$attrs">p info</p>
...
/// 转化为 <p name="msg" id="msgClass">p info</p>
改变 prop 的场景

按照比较合理的组件设计原则,我们是希望在组件中直接使用传入的数据,而不是改造后的数据。如果尝试在组件中直接修改 prop 对应的值,会报错,提醒你使用 datacomputed(相当于内部再拷贝一份)。

就比如一个原生的DOM元素,id 就是 id,不要设定了 id 后,内部还是对 id 值进行一个特殊处理(小写全部转大写)

props: ["prvTitle", "name", "title"],
...
computed: {
  rootTitle() {
  	return this.title;
  }
},
...
data() {
  return {
    rootTitle: this.title
  };
},
/// 第一种使用场景更为广泛,第二种按官网的说法,感觉直接使用 Vuex 更适合跨组件传值

$emit

子组件触发父组件的事件,可以传递参数给父组件的事件,在父组件中接受这个值,从而达到子组件向父组件通信的能力

/// app.vue
<Msg v-bind="info" @change="changeMsgInfo" />
{{ msgInfo }}
...
msgInfo: "msg info",
...
changeMsgInfo(val) {
	this.msgInfo = val;
}
/// msg.vue
<button @click="setInfo">setInfo</button>
...
setInfo(e) {
	console.log(e); // e 事件触发的 dom 信息
	this.$emit("change", "new info"); // 第一个参数是触发的事件名,第二个是传参,参数可以多个
}

我们推荐你始终使用 kebab-case 的事件名(源于官网)

事件名支持两种形式:kebab-casecamelCased ,不过个人更加习惯 camelCased 的写法(隔壁的 react 也推荐以这种方式定义事件名)

在组件上使用 v-model

  • v-model 默认会利用名为 valueprop 和名为** input** 的事件(源于官网)
  • v-model 本质上不过是语法糖(源于官网)
/// app.vue
<Msg :value="msgInfo" @input="msgInput"></Msg>
{{ msgInfo }}
...
msgInfo: 'msg',
...
msgInput (val) {
	this.msgInfo = val;
}
/// msg.vue
{{ value }}
<button @click="setInfo">setInfo</button>
...
props: ['value'], // value 的 props
...
setInfo () {
	this.$emit('input', 'new info') // 触发 input 事件
}

针对上面的写法,可用简化使用 v-model

<Msg v-model="msgInfo"></Msg>

$listeners

$listeners 是一个对象,里面包含了作用在这个组件上的所有监听器。上例的v-model,在组件内查看这个属性值时会看到 {input: ƒ}

<Msg v-model="msgInfo" @change="change" @set="set"></Msg>
...
/// {change: ƒ, set: ƒ, input: ƒ}

基于这个特性可以改写出一个不正常使用的例子

与 v-bind 类似,v-on 如下那么编写可以绑定多个事件

<Msg v-model="msgInfo" v-on="msgEvt()"></Msg>
...
msgEvt () {
  return {
    change () {},
    set () {}
  }
}

参考官网的例子,改写一个更为通用的例子

/// app.vue
<Msg @click="showMsg"></Msg>
/// msg.vue
<div>
  <button>setInfo</button>
  <button v-on="btnEvt">showMsg</button>
</div>
...
inheritAttrs: false,
computed: {
  btnEvt () {
  	// 把父组件的监听和自己要进行的监听进行合并
  	return {
      ...this.$listeners,
      dblclick: () => {
        this.innerInfo();
      }
		}
	}
},
methods: {
  innerInfo () {}
}

插槽

<slot>元素是 Vue 实现的内容分发 API,可以作为承载分发内容的出口。这套 API 的设计灵感源自 Web Components 规范草案

/// app.vue
<Msg>
	slot 的使用
</Msg>
...
/// msg.vue
<div>
	<slot></slot>
</div>

在自定义组件内的内容,会被默认渲染到<slot></slot>,插槽中的内容也可以是HTML元素,或者是其他组件

/// Child 是另一个自定义组件
<Msg>
  <p>html 元素</p>
  <Child/>
</Msg>

编译作用域

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的(源于官网)

/// app.vue
<Msg>
  <p>html 元素</p>
  {{ msgInfo }}
  <Child/>
</Msg>
...
msgInfo: 'msg root' // msgInfo 是根组件中的值
/// msg.vue
msgInfo: 'msg component'
/// 本例中 {{ msgInfo }} 最终会被渲染为 msg root,虽然 msgInfo 是 Msg 的插槽中,但其作用域还是在根组件中,不会访问到子组件的作用域

插槽的后备内容

/// msg.vue
<div>
  <slot>
    <p>默认信息</p>
  </slot>
</div>
/// app.vue
<Msg></Msg>
/// 组件没有提供内容的时候,就会渲染插槽中的后备内容

具名插槽

由于组件的复杂度不同,对于大型组件而言,内部结构复杂可能会需要多个插槽

/// msg.vue 
<div>
  <header>
  	<slot></slot>
  </header>
  <footer>
  	<slot></slot>
  </footer>
</div>
/// 此时两个 slot 代表的都是 name="default",两者会进行一样的内容分发

可以使用 name 去定义具名插槽

<slot name="header"></slot>

2.6+ 版本,我们可以在 template 的标签上使用 v-slot 的指令向具名插槽提供内容,v-slot 只能用在 template

<Msg>
  <template v-slot:header>
    <p>header</p>
  </template>
  <template v-slot:footer>
    <p>footer</p>
  </template>
  <p>slot</p>
</Msg>

作用域插槽

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里(源于官网)

  • 如果你想要在父组件中直接使用子组件中的数据,可以通过 插槽 prop 来实现
/// app.vue
<Msg>
  <template v-slot:info="slotProps">
  	<span>{{ slotProps.user.lastName }}</span>
  </template>
  <span>: 您好</span>
</Msg>
/// msg.vue
<div>
	<slot name="info" v-bind:user="cusName">{{ cusName.firstName }} 先生/女士:</slot>
	<slot></slot>
</div>
...
cusName: {
  firstName: "李",
  lastName: "世民"
}

Msg 组件中 v-bind:user="cusName" ,表示向这个插槽绑定一个 user 的 prop,App 组件中 v-slot:info="slotProp" 这个是在父组件接收prop的别名(这里可以是任意的名字),在内部区域,{{ slotProp.user.lastName }} 使用子组件中的数据

官方文档中提到针对 v-slot:default 使用作用域插槽,可以再进行相关简化,不过我不建议去简化,应该始终保持 template v-slot:slotName 的写法,这样做,虽然表面会造成代码的冗余,但却会使我们的代码可读性更高,能很明显的知道对应插槽是做什么用的

  • slotProps 可以是任何能够作为函数定义中的参数的 JavaScript 表达式
// 解构的写法
<template v-slot:default="{ user }">
  <p>{{ user.lastName }}</p>
</template>

关于动态插槽名,我感觉还是要慎用,会造成代码的混乱可读性差

  • v-slot: 可以简写为 #
<template #default="{ user }">
  • 插槽prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。(源于官网)
/// app.vue
<Msg>
  <template #default="{ user }">
    <p v-if="user.lastName" class="red">{{ user.lastName }}</p>
    <p v-else class="yellow">尊敬的用户</p>
  </template>
</Msg>
/// 默认分发的内容,在父组件内变成了可复用模版,不同的组件引用 Msg 组件都可以灵活的定义这部分内容

动态组件

使用元组件 <component> 配合 is 动态决定哪个组件需要被渲染,从而实现动态组件的功能

<select v-model="animal">
  <option value="Dog">狗</option>
  <option value="Cat">猫</option>
  <option value="Mouse">鼠</option>
</select>
<component :is="animal"></component>
...
data () {
	return {
		animal: 'Cat'
	}
}
...
components: {
	Cat,
  Dog,
  Mouse
}
/// 假定有三个组件,此处就可以通过控制 animal 的值动态的在元组件那里渲染指定组件

keep-alive

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。它是一个抽象组件:自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中,主要用于保留组件状态或避免重新渲染(源于官网)

/// dog.vue
<template>
    <div>
        <p>dog</p>
        <p>eat {{ food }}</p>
        <button @click="changeFood">换狗粮</button>
    </div>
</template>
<script>
export default {
    data () {
        return {
            food: '骨头'
        }
    },
    mounted () {
        console.log('dog mounted')
    },
    methods: {
        changeFood () {
            this.food = '馒头'
        }
    }
}
</script>

这里以 Dog 组件为例,默认 food 为骨头,可以通过按钮改变为馒头。如果此时再切换组件,变回 Dog 组件时,组件重新渲染,状态又变回了默认状态,这种销毁后重新渲染大多数情况下是比较合理,但难免碰到上述情况,此时就需要使用 <keep-alive> 这个内置组件

<keep-alive>
	<component :is="animal"></component>
</keep-alive>
/// 此时如果你再修改 Dog 组件的状态,进行切换时,组件就不会重新渲染,组件将会被缓存
include和exclude

includeexclude 属性允许组件有条件地缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示(源于官网)

如上例,我们最终只希望 Dog 组件进行缓存,其他组件还是会重新渲染,可以使用这两个属性值控制

<keep-alive include="Dog">
/// 这样只有 Dog 会被缓存
...
<keep-alive :include="cashComponent">
...
cashComponent: 'Dog'
/// 可以使用 v-bind 方式绑定动态值,多个组件时使用正则或者数组
...
cashComponent: /Dog|Cat/ // 正则
cashComponent: ['Dog', 'Cat'] // 数组
max

最多可以缓存多少组件实例

<keep-alive :include="cashComponent" max="10"> 

异步组件

Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义,Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数(源于官网)

以刚刚的动态组件为例,默认进入的情况下,我们只需要使用 Dog 组件,Cat、Mouse 组件此时是无需加载的,可以使用异步组件实现这个要求

/// app.vue
components: {
  Msg,
  Cat: () => import(/* webpackChunkName: "cat" */ './cat'),
  Dog: () => import(/* webpackChunkName: "dog" */ './dog'),
  Mouse: () => import(/* webpackChunkName: "mouse" */ './mouse')
}
  • import()ES Module 的动态导入,这种导入会返回一个 promise 同时也支持 await
  • /* webpackChunkName: "cat" */ 这个是 webpack 在拆分代码时使用的参数,如果在其中加入这个注释,这个组件对应的 js 就会被命名为 cat.js 如不设置,会默认按0.js、1.js这样去命名

如此设置后,默认加载时只会加载 Dog 组件对应的 js 文件,进行切换后才会加载其他组件对应的 js 文件

/// 还支持如下写法
 Cat: () => ({
 	component: import(/* webpackChunkName: "cat" */ './cat'),
  /// 其他参数参考官网
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
 }),

边界情况

$root

在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问(源于官网)

官网建议不要用这个属性,跨组件通信通过 Vuex 进行处理

// index.js
let vm = new vue({
    data: {
        root: 'root info'
    },
...
/// 在内部任何组件,都可以通过 this.$root.root 访问

$parent

$root 类似,都可以访问祖先组件的实例,区别在于,其只能访问父级,按官网的说法也是不要用这个属性,容易造成数据流向的混乱

$refs

这个属性是提供了一种在父组件直接访问子组件的方式,正好与前面介绍的两个属性相反,官方建议这个属性有条件的使用

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

/// app.vue
<Msg ref="msg">
...
mounted () {
	console.log('msg msgInfo', this.$refs.msg.msgInfo);
}
/// msg.vue
msgInfo: 'msg component',

依赖注入

设计思路就像不想把跨组件的 prop 透传往下带

/// app.vue
<Msg :use="'app'">
...
/// msg.vue
<Info :use="use"></Info>
...
props: ['use'],
...
/// info.vue
<div>
	{{ use }}
</div>
...
props: ['use']

如例,在 App 组件内有一个值需要传到 Msg 组件内的一个子组件 Info 中,而在 Msg 组件中其实完全没有用到这个值,完全就是透传,针对这种情况才设计了依赖注入

隔壁 react 是 redux 实现了类似的功能,Vue 这是官方实现

provide / inject
  • provide 选项应该是一个对象或返回一个对象的函数,该对象包含可注入其子孙的属性(源于官网)
  • inject 接收指定的我们想要添加在这个实例上的 property
/// app.vue
<Msg></Msg>
...
export default {
  provide: {
    use: "app info"
  },
...
/// info.vue
inject: ["use"]
/// 可以看到Msg 组件中没有的透传代码不需要再添加了,在info中直接通过 inject 获取

至于官方文档中提到的弊端,我感觉可以参考隔壁家 react 中 redux 的做法,你如果要使用我这个功能,就要遵守一定的使用限制

过渡

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

  • 控制 css 的方式
  • 控制 js 的方式

单元素/组件的过渡

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

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点 (以上源于官网)
<button @click="showFlag = !showFlag">toggle info</button>
<transition name='fade'>
	<p v-if="showFlag">info</p>
</transition>
...
showFlag: true,
...
/// msg.css
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名
  2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(来源官网)

过渡的类名

如果处在 transition 组件内的元素发生了插入或者删除的操作,Vue 会自动判断这个元素是否应用了 CSS 过渡或者动画。如果应用了,就会自动根据当前的行为添加或删除 CSS 类名

添加时(元素插入时)Enter
类名 过渡状态 生效时间 移除时间 触发顺序
v-enter 过渡进入:开始 元素被插入之前生效 元素被插入之后的下一帧移除 0
v-enter-active 过渡进入:生效 整个进入过渡的阶段,元素被插入之前生效 在过渡/动画完成之后移除 1
v-enter-to 过渡进入:结束 在元素被插入之后下一帧生效(与此同时 v-enter 被移除) 在过渡/动画完成之后移除 2
删除时(元素移除时)Leave
类名 过渡状态 生效时间 移除时间 触发顺序
v-leave 过渡离开:开始 在离开过渡被触发时立刻生效 下一帧被移除 0
v-leave-active 过渡离开:生效 整个离开过渡的阶段,在离开过渡被触发时立刻生效 在过渡/动画完成之后移除 1
v-leave-to 过渡离开:结束 在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除) 在过渡/动画完成之后移除 2

v-enter-activev-leave-active 可以控制进入/离开过渡的不同的缓和曲线

可以简单记忆为:

  • v-enter-activev-leave-active 一直有效,v-enter-tov-leave-to几乎一直有效,v-enterv-leave 理解成只有一帧有效时间
  • v-enter-active 有效时间为 v-enter + v-enter-tov-leave-active 有效时间为 v-leave + v-leave-to
  • v-enterv-leave 设置过渡的开始属性,v-enter-tov-leave-to 设置过渡的结束属性,v-enter-activev-leave-active 设置过渡的曲线,时间等相关属性
.fade-enter {
  color: #00f; // 开始属性
}
.fade-enter-active {
  transition: color 5s; // 过渡属性
}
.fade-enter-to {
  color: #f00; // 结束属性
}

css 的 transition

属性 描述 CSS
transition 简写属性,用于在一个属性中设置四个过渡属性。 3
transition-property 规定应用过渡的 CSS 属性的名称。 3
transition-duration 定义过渡效果花费的时间。默认是 0。 3
transition-timing-function 规定过渡效果的时间曲线。默认是 "ease"。 3
transition-delay 规定过渡效果何时开始。默认是 0。 3

在设定 transition 时至少需要给定 transition-property、transition-duration,多个变化值可以用逗号隔开

transition: width 2s, height 2s, transform 2s;
transition-property

不是所有的CSS样式值都可以过渡,只有具有中间值的属性才有过渡效果

  • 颜色: color、background-color、border-color、outline-color
  • 位置: backround-position、left、right、top、bottom
  • 长度:
    • max-height、min-height、max-width、min-width、height、width
    • border-width、margin、padding、outline-width、outline-offset
    • font-size、line-height、text-indent、vertical-align
    • border-spacing、letter-spacing、word-spacing
  • 数字: opacity、visibility、z-index、font-weight、zoom
  • 组合: text-shadow、transform、box-shadow、clip
  • 其他: gradient
transition-timing-function
描述
linear 规定以相同速度开始至结束的过渡效果(等于 cubic-bezier(0,0,1,1))。
ease 规定慢速开始,然后变快,然后慢速结束的过渡效果(cubic-bezier(0.25,0.1,0.25,1))。
ease-in 规定以慢速开始的过渡效果(等于 cubic-bezier(0.42,0,1,1))。
ease-out 规定以慢速结束的过渡效果(等于 cubic-bezier(0,0,0.58,1))。
ease-in-out 规定以慢速开始和结束的过渡效果(等于 cubic-bezier(0.42,0,0.58,1))。
cubic-bezier(n,n,n,n) 在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值。

css 动画

  • css中使用 @keyframes 规则创建动画,在 @keyframes 中不同阶段规定某项 CSS 样式,就能创建由当前样式逐渐改为新样式的动画效果
  • 使用 animation 绑定动画
/// 实现 css transition 一样的效果
.fade-enter-active {
  animation: enteranimation 5s;
}
@keyframes enteranimation {
  0% {
    color: #00f;
  }
  100% {
    color: #f00;
  }
}
@keyframes

@keyframes animationname {keyframes-selector {css-styles;}}

  • animationname 名称
  • keyframes-selector 动画百分比
@keyframes mymove {
  0%   {top:0px;}
  50%  {top:100px;}
  100% {top:0px;}
}
animation
属性 描述 CSS
animation 所有动画属性的简写属性,除了 animation-play-state 属性。 3
animation-name 规定 @keyframes 动画的名称。 3
animation-duration 规定动画完成一个周期所花费的秒或毫秒。默认是 0。 3
animation-timing-function 规定动画的速度曲线。默认是 "ease"。 3
animation-delay 规定动画何时开始。默认是 0。 3
animation-iteration-count 规定动画被播放的次数。默认是 1。 3
animation-direction 规定动画是否在下一周期逆向地播放。默认是 "normal"。 3
animation-play-state 规定动画是否正在运行或暂停。默认是 "running"。 3
animation-fill-mode 规定对象动画时间之外的状态。 3
animation-direction
描述
normal 默认值。动画应该正常播放
alternate 动画在奇数次(1、3、5...)正向播放,在偶数次(2、4、6...)反向播放。
alternate-reverse 动画在奇数次(1、3、5...)反向播放,在偶数次(2、4、6...)正向播放。
reverse 动画反向播放。

animation-direction 值是 "alternate",则动画会在奇数次数(1、3、5 等等)正常播放,而在偶数次数(2、4、6 等等)向后播放

animation-play-state
描述
paused 规定动画已暂停。
running 规定动画正在播放。

animation-play-state 属性规定动画正在运行还是暂停。可以在 JavaScript 中使用该属性,这样就能在播放过程中暂停动画

animation-fill-mode
描述
none 不改变默认行为。
forwards 在动画结束后(由 animation-iteration-count 决定),动画将应用该属性值。
backwards 动画将应用在 animation-delay 定义期间启动动画的第一次迭代的关键帧中定义的属性值。这些都是 from 关键帧中的值(当 animation-direction 为 "normal" 或 "alternate" 时)或 to 关键帧中的值(当 animation-direction 为 "reverse" 或 "alternate-reverse" 时)。
both 向前和向后填充模式都被应用。

animation-fill-mode 属性规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式。

默认情况下,CSS 动画在第一个关键帧播放完之前不会影响元素,在最后一个关键帧完成后停止影响元素。animation-fill-mode 属性可重写该行为。

自定义动画类名

可以自己定义v-enter、v-enter-action等这些类对应的类名,而不是使用Vue transition 组件上 name属性值,自动映射的名称

  • enter-class 映射 v-enter
  • enter-active-class 映射 v-enter-active
  • enter-to-class (2.1.8+) 映射 v-enter-to
  • leave-class 映射 v-leave
  • leave-active-class 映射 v-leave-active
  • leave-to-class (2.1.8+) 映射 v-leave-to
<transition
  name="fade"
  leave-class="info-leave"
  leave-active-class="info-leave-active"
>
	<p v-if="showFlag">info</p>
</transition>
/// fade-leave 变为 info-leave,fade-leave-active 变为 info-leave-active

同时使用过渡和动画

当同时使用过渡和动画时,两者的执行时间由不相同,可以通过指定 transition 的 type 值。告知 Vue 监听哪个,避免动画未执行完,就进行类名变更(让 Vue 去监听时间长的)。

有效值为 "transition" 和 "animation"

 <transition
      name="fade"
      type="transition"

显性告诉持续时间

当动画或者过渡比较复杂时,可显性的告知 Vue 持续时间

<transition :duration="1000">
...
<transition :duration="{ enter: 500, leave: 800 }">

钩子函数

在相关过渡动画发生变化时,可以通过相关钩子函数去做更细化的控制

<transition
  name="fade"
  leave-class="info-leave"
  leave-active-class="info-leave-active"
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancCelled"
  
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled" // leaveCancelled 只用于 v-show 中
>

第一次渲染时的过渡

上面的 enter、leave 都是元素加载完成后,主动去触发的过渡行为,如果希望在第一次渲染初始化时就触发过渡,可以设置 appear 属性,如果不想使用默认的渲染行为还可以指定对应的类名

<transition
  name="fade"
  appear
  appear-class="foo-appear"
  appear-to-class="foo-appear-to"
  appear-active-class="foo-appear-active"      

同样有对应的钩子函数: before-appea、appear、after-appear、appear-cancelled

多元素过渡

当有相同标签名的元素切换时,需要通过 key attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践。(源于官网)

/// msg.vue
<transition name="fade">
  <button v-bind:key="docState" @click="btnEvt">
  	{{ buttonMessage }}
  </button>
</transition>
...
computed: {
  buttonMessage: function() {
  switch (this.docState) {
    case "saved":
    	return "Edit";
    case "edited":
    	return "Save";
    case "editing":
    	return "Cancel";
    }
  }
},
...
 btnEvt() {
   switch (this.docState) {
     case "saved":
       this.docState = "edited";
       break;
     case "edited":
       this.docState = "editing";
       break;
     case "editing":
       this.docState = "saved";
       break;
   }
 }
/// msg.css
.fade-enter-active {
  animation: enteranimation 0.5s;
}
.fade-leave-active {
  animation: enteranimation 0.5s reverse;
}
@keyframes enteranimation {
  0% {
    color: #00f;
  }
  100% {
    color: #f00;
  }
}

在运行此处代码时,会发现触发过渡时,页面会同时有两个按钮,过渡结束后页面才变回之前一个按钮,原因是 <transition> 的默认行为,就是进入和离开同时发生。可以通过设置 mode 来设置过渡模式

mode 控制离开/进入过渡的时间序列,有效的模式有 "out-in" 和 "in-out",默认同时进行。

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。

多组件过渡

类似多元素过渡,区别在于,因为是动态组件,所以不需要在 transition 内部使用 key,一样设置 mode

<label for="ComA">ComA</label>
<input type="radio" v-model="view" value="ComB" id="ComB" />
<label for="ComB">ComB</label>
<transition name="fade" mode="out-in">
	<component :is="view"></component>
</transition>
...
view: "ComA",

列表过渡

/// msg.vue
<button @click="add">新增</button>
<button @click="remove">删除</button>
<transition-group tag="ul" name="list">
  <li v-for="item in lists" :key="item.toString()" class="list-item">
  	{{ item }}
  </li>
</transition-group>
...
/// msg.css
.list-item {
  transition: all .5s;
}
.list-enter,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
.list-leave-active {
  position: absolute;
}
  • transition-group 会渲染成一个真实的 DOM,默认是 span ,可以通过 tag 去更换为其他元素
  • 内部元素总是需要提供唯一的 key attribute 值
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身

像上例中,name 是定义在 transition-group,但对应的过渡类确是绑定在内部元素 li 上

如果 transition-group 的内部元素添加了 transition 的动画,就会触发在相关元素的 FLIP 的动画,形成列表过渡

这个要添加的 FLIP 过渡的元素,不能设置 display: inline

混入

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。

  • 混入对象 的选项将被 “混合” 进入该组件本身的选项,当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”
    • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
    • 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
    • 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

(以上源于官网)

  • 不要在 混入对象 中使用生命周期相关的函数,这样会造成使用混入对象的组件生命周期混乱,不利于代码管理
  • 不要使用 全局混入,官方建议非插件作者不要使用全局混入
  • 混入在使用时,很容易造成命名冲突,混入对象中的值很容易被外部对象的值覆盖。并且如果在 data 中混入数据时,如果是对象,会要求被混入的组件中也要有同名对象
/// msg.mixin.js
export let msgMixin = {
    data () {
        return {
            mixinInfo: 'msg mix info',
            info: {
                foo: 'foo'
            }
        }
    },
    methods: {
        init () {
            console.log('msg mixin init');
        }
    }
}
...
/// msg.vue
{{ mixinInfo }} // 这里直接使用是没有问题
{{ info.foo }} // 这里就会要求 msg 的 data 下要有 info ,要不就会报错
...
import { msgMixin } from '../script/msg.mixin';

export default {
    mixins: [msgMixin],
    ...
    methods: {
        init () {
            console.log('msg init') // 这里的方法会覆盖 msg.mixin 的同名方法
        }
    },
    mounted () {
        this.init();
    }

混入的方式让组件编写具有了很多灵活性,上面的例子我们直接把相关代码逻辑写在 msg 组件中,也没什么问题,但是如果这是一些公共的逻辑,比如针对 loading 组件的显示隐藏控制、键盘组件的关闭逻辑或者是某些js动画事件,这种很通用并且和组件本身逻辑关联性不强的代码,就可以考虑通过混入的方式,减少一些代码拷贝黏贴

不过混入的方式容易被覆盖,而且引入多个混入时,混入对象不能很明显的告知使用者其对应路径,建议在定义时,通过注释的形式,明确说明混入了哪些值

/**
 * 通过 msgMixin 混入了
 * data: mixinInfo
 * methods: init
 */
 import { msgMixin } from '../script/msg.mixin';

自定义指令

自定义指令可以认为是对功能性代码块的复用,比如图片的懒加载、超出指定长度的文案截取、区域滚动到底部加载指定事件。

自定义指令分为:全局自定义和局部自定义

/// customDirective.js
export let cutWord = {
    // 指令的定义
    inserted (el, binding, vNode) {
        let innerText = el.innerText;
        let cutNum = binding.value;
        el.innerText = `${innerText.slice(0, cutNum)}...`
    }
}
/// msg.vue
 <p v-cutWord='3'>截取文案</p>
 ...
 import { cutWord } from '../script/customDirective';
 ...
 directives: {
   cutWord
 },
 /// 这就是一个超过制定长度,对文本进行截取的指令
 ...
 /// 如果想注册成全局指令就使用 Vue.directive 进行注册
Vue.directive('cutWord', cutWord)

渲染函数以及JSX

Vue 的模板实际上被编译成了渲染函数(渲染函数)

/// com-c.vue
<script>
export default {
  render(createElement) {
    return createElement(`h${this.leave}`, {
    	class: {
    		foo: true
    	}
    }, this.$slots.default);
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
};
</script>
/// msg.vue
<ComC :level="2">Hello world!</ComC>
...
components: {
	ComC
},
...
/// com-c中的render函数不能使用剪头函数,因为这里需 this 指向定义时的 组件而非运行时的对象
 render: createElement =>
    createElement(
      `h${this.level}`, // 标签名称
      this.$slots.default // 子节点数组
    ),

上面例中的 render 就是渲染函数,该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。

VNode 包含的节点信息,以及其子节点的描述信息。

Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。(源于官网)

createElement 参数

createElement 接收三个参数:

  • elem 标签名 {String | Object | Function}
  • elemAttribute 标签对应的属性对象 {Object}
  • childNode 子级虚拟节点 {String | Array}

elemAttribute 对应的参数形式可以实现 Vue 的模板功能,只不过这种写法比较繁琐,属于内部实现细节,可以使用 JSX 的形式

JSX 是 React 建立起来的规范,Vue 也支持

JSX

要在 Vue 中使用 JSX 需要按照相关依赖(Vue2.x 版本,babel7.x 版本)

npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props --save

修改 babel 的配置文件

const presets = [
    '@vue/babel-preset-jsx',
    '@babel/env'
];

上例的 render 函数改写为

/// com-c.vue
render (h) {
  const hElem = `h${this.leave}`;
  const vNode = <hElem>{ this.$slots.default }</hElem> // jsx的语法
  return (vNode)
},

当使用 JSX 语法后,可以不再使用 .vue 的文件

/// com-c.js
const ComC = {
    render (h) {
        const hElem = `h${this.leave}`;
        const vNode = <hElem>{ this.$slots.default }</hElem>
        return (vNode)
    },
    props: {
        leave: {
            type: Number
        }
    }
}
export default ComC;

而上面形式的组件,可以叫做函数组件,这里面没有生命周期相关的钩子函数,这种函数组件会有更低的性能消耗,可以用在无业务逻辑的 UI 组件上

不过上面的形式也只是类似,如果想让组件形成变成真正的函数组件,可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文),如果我们此时要获取相关参数信息就要借助 render 函数的第二个参数 context

函数式组件只是函数,所以渲染开销也低很多(源于官网)

const ComC = {
    functional: true,
    render (h, context) {
        const { props, slots, children } = context;
        const hElem = `h${props.leave}`;
        const vNode = <hElem>{ children }</hElem>
        return (vNode)
    }
}
export default ComC

context

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组(这个比 slots().default 在获取子节点时,更为准确)
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。

当你决定使用 JSX 时,就要忘记 Vue 模版下的指令、计算属性、watch等概念,更多的依赖 JS 的编程能力

插件

  • 插件通常用来为 Vue 添加全局功能
  • 通过全局方法 Vue.use() 使用插件
  • 插件需要在调用 new Vue() 之前使用
  • Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件 (以上源于官网)

过滤器

  • 过滤器可被用于一些常见的文本格式化,可以用在两个地方:双花括号插值和 v-bind 表达式。
  • 过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示
  • 过滤器定义分为全局和局部两种,当全局过滤器和局部过滤器重名时,会采用局部过滤器
  • 过滤器可以按管道的形式进行串写,或者以函数传参的形式调用
{{ cutText | cutWord }}
...
filters: {
  cutWord (value) {
    if (!value) {
    	return ''
    }
    return String(value).length > 3 ? `${String(value).slice(0,3)}...` : String(value)
  }
},
/// 在组件内使用 filters 是局部注册
/// Vue.filter(filterName, filterFunction) 这就是全局注册
/// 管道串写
{{ cutText | cutWord | copyright }}
...
copyright (value) {
	return `${value} @rede`
}
/// 函数传参
{{ cutText | cutWord | copyright('info') }}
...
copyright (value, arg) {
	return `${value} @rede - ${arg}`
}