重读 Vue2 教程
Vue(读音 /vjuː/,发音类似 view)是一套用于构建用户界面的渐进式 JavaScript 框架。
Vue 聚焦视图层,采用自底向上增量开发设计。它实现了 MVVM 模式的双向数据绑定,核心是 ViewModel,负责建立 View 视图与 Model 数据之间的关联,保障视图和数据状态保持同步。
MVVM 架构模式
MVVM 是 Model-View-ViewModel 的缩写,是前端常用软件架构模式。
核心实现 View 与 ViewModel 双向绑定:视图发生改变自动同步到数据模型;数据模型变更也会自动渲染到页面视图,也就是双向数据绑定。
- View(视图):对应 DOM 与 HTML 模板,承担 UI 展示,把数据渲染为用户可见界面。
- Model(数据模型):业务数据来源,对应组件
data、Vuex 仓库数据,也可以包含数据业务逻辑。 - ViewModel:Vue 实例的核心部分,监听 Model 数据变更、管控视图行为、处理用户交互;作为中间层连接 Model 与 View。
MVVM 脱胎于 MVC 架构,开发者只需要在模板书写指令,就可以做到 UI 界面和业务逻辑解耦,大量降低手动 DOM 操作。
MVVM 对比 MVC 两大核心差异:
- 实现数据与视图解耦分离
- 数据驱动视图,开发者只关心数据变化,DOM 操作由框架内部封装处理
注意:Vue2 并没有严格完全遵循纯粹 MVVM,例如 $refs 允许直接操作 DOM 视图。
响应式原理
把普通 JS 对象作为组件 data 传入 Vue 实例,Vue 会遍历对象全部属性,通过 Object.defineProperty 为每一个属性添加 getter、setter 访问器。
getter、setter 对开发者透明,框架内部依靠它们完成依赖追踪:属性被读取时收集依赖;属性被修改时触发变更通知。
每一个组件实例对应一个 Watcher 实例。组件渲染过程中,访问到的 data 属性会被记录为依赖;当属性 setter 执行触发变更,通知对应 Watcher,驱动组件重新渲染。
注意:Object.defineProperty 无法通过 polyfill 兼容实现,因此 Vue2 不支持 IE8 以及更低浏览器。
生命周期钩子
Vue2 组件完整生命周期,四个阶段:创建、挂载、更新、销毁。
- 创建阶段(实例化,DOM 还不存在)
beforeCreate:实例刚创建,data、methods还未初始化;拿不到this.xxx数据。created:data、methods、props已经初始化完成,DOM 还没有生成,$el 不存在;可以发起网络请求。
- 挂载阶段(渲染 DOM)
beforeMount:模板编译完成,挂载前;$el 存在但是是虚拟 DOM,页面还没渲染。mounted:真实 DOM 挂载完毕,可以操作 DOM、ref;可以做 DOM 相关操作、定时器。
- 更新阶段(响应数据变化,DOM 更新)
beforeUpdate:数据改变,虚拟 DOM 重新打补丁,真实 DOM 还没更新。updated:DOM 已经更新完成。
- 销毁阶段
beforeDestroy:实例销毁前,组件实例仍然可用;清除定时器、解绑事件、取消 axios 请求,放在这里。destroyed:实例完全销毁,全部指令、watcher 解除绑定。
其他钩子:
activated、deactivated:<keep-alive>缓存组件专属钩子;组件激活、失活时触发。errorCaptured:捕获后代组件抛出的错误。
注意:服务端渲染 SSR 环境,mounted、updated、beforeDestroy、destroyed 不会执行。
内置组件
keep-alive
缓存组件实例,组件不会执行销毁,避免重复创建销毁,提升性能。
组件属性:
include:字符串、正则,匹配的组件才缓存;exclude:匹配的组件不缓存;max:最大缓存组件实例数量,超出按照 LRU 淘汰。
<keep-alive include="User,Order" max="5">
<component :is="view"></component>
</keep-alive>
component
动态切换渲染哪个组件。
<component :is="组件名、组件对象"></component>
transition、transition-group
过渡动画组件。
<transition>:单个组件、元素进入离开动画;<transition-group>:多个列表元素,v-for列表动画,每一项必须要有 key。
transition 属性:
name:动画 class 前缀;appear:是否在组件初始渲染时执行进入动画,appear;enter-from-class、enter-active-class、enter-to-class;leave-from-class、leave-active-class、leave-to-class;mode:过渡模式,out-in(旧先出,新再进)、in-out(先进后出),用于两个组件切换。
transition-group 属性:
name:class 前缀;tag:指定渲染 DOM 标签;appear:初始渲染是否播放动画;- 同样支持 enter-class、leave-class 系列自定义 class 属性。
注意:这两个组件本身不会写动画,只是添加、移除 CSS class 类名,真正动画由 CSS 完成。
<template>
<div>
<button @click="show = !show">切换显示</button>
<transition name="fade">
<p v-if="show">hello vue transition</p>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: false
}
}
}
</script>
<style scoped>
.fade-enter-from { opacity: 0; }
.fade-enter-active { transition: opacity 0.4s; }
.fade-enter-to { opacity: 1; }
.fade-leave-from { opacity: 1; }
.fade-leave-active { transition: opacity 0.4s; }
.fade-leave-to { opacity: 0; }
</style>
slot 插槽
有默认插槽、具名插槽、作用域插槽。
注意:Vue2.6 之后统一新语法 v-slot;也兼容旧语法 slot="xxx"、slot-scope。
默认插槽:
<!-- 父组件 -->
<my-comp>
<div>默认插槽内容</div>
</my-comp>
<!-- 子组件 my-comp -->
<slot></slot>
具名插槽:
<!-- 父组件 -->
<my-comp>
<template v-slot:header>头部内容</template>
<template #footer>底部内容</template>
</my-comp>
<!-- 子组件 -->
<slot name="header"></slot>
<slot name="footer"></slot>
作用域插槽,即子组件向父插槽传数据:
<!-- 子组件 -->
<slot name="item" :row="rowData"></slot>
<!-- 父组件 -->
<template #item="slotProps">
{{ slotProps.row.name }}
</template>
Class 与 Style 绑定
v-bind 针对 class、style 做了特殊增强,除字符串外,支持对象语法、数组语法。
<!-- 字符串拼接写法 -->
<div v-bind:class="(isActive ? 'active' : '') + ' error'"></div>
<!-- 数组语法 -->
<div v-bind:class="[isActive ? 'active' : '', 'error']"></div>
<!-- 对象语法(最常用) -->
<div v-bind:class="{active: isActive, error: true }"></div>
<!-- 数组嵌套对象语法 -->
<div v-bind:class="[{ active: isActive }, 'error']"></div>
<!-- 对象语法 -->
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- 数组语法,应用多个样式对象 -->
<div v-bind:style="[
{ color: activeColor, fontSize: fontSize + 'px' },
{ border: '1px solid red'}
]"></div>
<!-- 多前缀值数组(2.3.0+),自动兼容不同浏览器内核前缀 -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
事件处理
v-on 指令基础
v-on 用于绑定 DOM 事件,可以直接执行行内 JS 代码、绑定方法、行内调用方法。
<!-- 行内直接执行代码 -->
<button v-on:click="window.alert('hello')">hello</button>
<!-- 绑定方法名,不带括号,会自动把 event 作为第一个入参 -->
<button v-on:click="hello">hello</button>
<!-- 行内调用方法,可以自定义传参 -->
<button v-on:click="hello()">hello</button>
注意:
v-on:click="hello":只写方法名,不传参,回调第一个参数原生event对象;v-on:click="hello()":主动调用函数,默认不会携带 event 对象;需要事件对象,使用内置特殊变量$event。
<button v-on:click="hello('lizh', $event)">hello</button>
自定义事件
vm.$on(eventName, callback):监听当前实例自定义事件,事件一般由 $emit 触发;回调接收 emit 传递全部参数。
vm.$on('test', msg => {
console.log(msg)
})
vm.$emit('test', 'hello')
// 输出 hello
绑定原生事件
组件标签直接写 v-on 默认监听组件自定义事件;要监听组件根元素原生 DOM 事件,使用 .native 修饰符。
<my-component v-on:focus.native="onFocus"></my-component>
如果不想绑定到组件根元素,而是组件内部某个子元素,可以使用 $listeners。$listeners 是对象,包含父组件传递给组件的全部事件监听器。配合 v-on="$listeners" 批量转发到内部指定 DOM。
// $listeners 结构示例
{
focus: function (event) { /* ... */ },
input: function (value) { /* ... */ }
}
<input v-on="$listeners"></input>
备注:2.4.0+,v-on 支持对象语法批量绑定事件,对象语法不支持任何修饰符。
事件修饰符
.stop:阻止事件冒泡.prevent:阻止事件默认行为.capture:开启事件捕获模式.self:只有event.target === 当前元素才触发回调.once:事件只会触发一次.passive:立即执行默认行为,优先级高于.prevent
修饰符书写顺序会影响执行结果:
v-on:click.prevent.self:会阻止全部点击;v-on:click.self.prevent:只会阻止元素自身的点击。
键盘按键修饰符
监听键盘事件,提供按键别名,兼容旧浏览器。
可用别名:.enter、.tab、.delete、.esc、.space、.up、.down、.left、.right。
<input @keyup.enter="submit" />
<input @keyup.page-down="onPageDown" />
组件注册
全局注册
Vue.component() 全局注册组件,注册之后,在任意 Vue 实例模板都可以直接使用。
Vue.component('my-component-name', {
// 组件配置选项
})
局部注册
只在当前组件内可用,其他组件无法直接使用。
const ComponentA = { /* 组件选项 */ }
const ComponentB = {
components: {
'component-a': ComponentA
}
}
自动化全局注册
借助 Webpack 的 require.context() 批量导入目录组件,完成自动化全局注册。
require.context(directory, useSubdirectories, regExp)
参数说明:
- directory:目标文件夹相对路径;
- useSubdirectories:是否递归查找子文件夹;
- regExp:文件名匹配正则。
返回值是一个函数,同时附带属性:
resolve():传入匹配文件相对路径,返回模块在项目内完整路径;keys():返回所有匹配成功的文件路径数组;id:上下文模块 id,用于热更新。
// main.js 入口文件示例
import Vue from 'vue'
const requireComponent = require.context(
'../../../components/form_items',
false,
/\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 提取组件名称
const componentName = fileName.split('/')[1].split('.')[0]
const componentConfig = requireComponent(fileName)
// 优先取 export.default 导出,兼容 CommonJS 模块
Vue.component(
componentName,
componentConfig.default || componentConfig
)
})
Prop 与 Attribute
props 用于接收父组件传递过来的数据,可以是数组或者对象;对象形式支持类型校验、默认值、必传、自定义校验器。
props: {
propB: {
type: [String, Array],
default: any || () => ({}),
required: Boolean,
validator: value => {
return boolean
}
}
}
prop 传值技巧
一次性传递对象全部属性,直接不带参数的 v-bind:
post: {
id: 1,
title: 'My Journey with Vue'
}
<!-- 完整展开对象属性 -->
<blog-post v-bind="post"></blog-post>
<!-- 等价写法 -->
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
自定义构造函数类型校验:
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
props: {
author: Person
}
<my-component :author="new Person('firstName', 'lastName')"></my-component>
备注:prop 内部使用 instanceof 做类型校验。
非 Prop Attribute
向组件传递属性,组件没有声明对应 prop,就是非 Prop Attribute。
- 普通属性:外部传入的值会覆盖组件根元素上同名属性;
- 例外:
class、style,不会覆盖,会自动合并。 - 在组件内部通过
$attrs访问全部非 Prop 属性; - 设置
inheritAttrs: false,阻止根元素自动继承非 Prop 属性,适合手动转发到内部子元素。
prop 单向数据流
所有 prop 遵循单向下行绑定:父组件 prop 更新向下流动到子组件;子组件内部不允许直接修改 prop。父组件数据更新,子组件 prop 会自动刷新。
子组件想要修改父组件数据,需要触发自定义事件通知父组件更新。
<!-- 父组件模板 -->
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event">
</text-document>
// 子组件内部
this.$emit('update:title', newTitle)
.sync 语法糖
.sync 是该模式的语法糖,等价于上面完整写法。
<text-document v-bind:title.sync="doc.title"></text-document>
备注:Vue2.7 仍然支持 .sync;Vue3 已经移除,统一使用 v-model。
自定义指令
Vue 内置 v-if、v-for、v-model、v-show 等指令,同时支持自定义指令,用于需要直接操作底层 DOM 的场景。
全局注册指令:
Vue.directive('focus', {
bind: function (el, binding, vnode) {},
inserted: function (el, binding, vnode) {
el.focus()
},
update: function (el, binding, vnode, oldVnode) {},
componentUpdated: function (el, binding, vnode, oldVnode) {},
unbind: function (el, binding, vnode) {}
})
局部注册指令:
directives: {
focus: {
inserted: function (el, binding, vnode) {
el.focus()
}
}
}
<input v-focus placeholder="自动聚焦此元素">
指令钩子函数
全部钩子都是可选:
bind:指令第一次绑定到 DOM 元素时,仅执行一次。inserted:绑定元素被插入父 DOM 节点(父节点存在,但不一定已经进入文档)。update:组件 VNode 更新触发,子 VNode 更新可能还没有完成。componentUpdated:组件 VNode 以及全部子 VNode 更新完成后触发。unbind:指令和 DOM 解绑,仅执行一次。
钩子参数
el:绑定指令真实 DOM 元素,可以操作 DOM,el 可写,其余参数只读。binding指令信息对象name:指令名称,不带v-前缀。value:指令绑定的值。oldValue:上一次绑定的值,仅update、componentUpdated可用。expression:原始字符串形式表达式。arg:指令参数,例v-my:foo,arg="foo"。modifiers:修饰符对象,例v-my.a.b得到{a:true,b:true}。
vnode:组件当前虚拟节点。oldVnode:上一轮虚拟节点,仅update、componentUpdated可用。
简写形式
只关心 bind、update 钩子,其余生命周期忽略,可以直接传入函数:
Vue.directive('my-directive', function (el, binding) {
el.style.backgroundColor = binding.value
})
过滤器 filter
过滤器用于文本格式化,可在插值表达式 {{ }}、v-bind 中使用,管道符 | 调用。
{{ message | capitalize }}
<div v-bind:id="rawId | formatId"></div>
<!-- 过滤器传参,第一个参数永远是管道左边表达式的值 -->
{{ message | filterA('arg1', arg2) }}
全局过滤器:
Vue.filter('capitalize', function (value) {
return value ? value.replace(/^\w/, a => a.toUpperCase()) : ''
})
局部过滤器:
filters: {
capitalize: function (value) {
return value ? value.replace(/^\w/, a => a.toUpperCase()) : ''
}
}
常用 API 选项
Vue.use(plugin)
安装 Vue 插件:
- 如果插件是对象,对象必须提供
install方法; - 如果插件是函数,直接作为 install 执行;
- install 执行的时候会把 Vue 构造函数作为参数传入;
- 必须在
new Vue()之前调用; - 同一个插件多次调用,只会安装一次。
provide、inject
跨层级祖先-后代组件依赖注入,不管中间嵌套多少层,后代组件都可以拿到祖先提供的数据。类似 React Context。
provide:对象,或者返回对象的函数,可以使用 Symbol 作为 key。inject:字符串数组;或者对象,配置from来源 key、default默认值。
// 祖先组件
export default {
provide () {
return {
provideInst: this
}
}
}
// 后代任意层级组件
export default {
inject: {
aliasInst: {
from: 'provideInst',
default: () => ({})
}
}
}
model 选项
自定义组件 v-model 对应的 prop 和事件。
默认规则:v-model 使用 value 作为 prop,input 作为触发事件。
<my-component v-model="name"></my-component>
<!-- 默认等价 -->
<my-component :value="name" @input="val => name = val"></my-component>
子组件配置 model 选项修改默认行为:
export default {
model: {
prop: 'testValue',
event: 'change'
}
}
<my-component v-model="name"></my-component>
<!-- 等价 -->
<my-component :testValue="name" @change="val => name = val"></my-component>
v-bind
动态绑定属性、prop。
修饰符:
.prop:作为 DOM Property 绑定,不是 HTML attribute,不会进入$attrs;.camel:把短横线属性转为驼峰;.sync:语法糖,扩展为update:*事件监听(Vue2.3+,Vue3 移除)。
<text-document v-bind:title.sync="doc.title"></text-document>
<!-- 相当于 -->
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event">
</text-document>
无参数对象批量绑定:
<my-component v-bind="$props"></my-component>
v-on
绑定事件,普通元素绑定原生 DOM 事件;组件上绑定自定义事件。
2.4.0 支持对象语法批量绑定事件,对象语法不支持修饰符。
<my-component v-on:click="ckEvent"></my-component>
<my-component v-on:click="ckEvent('a', $event)"></my-component>
<my-component v-on:click.native="ckEvent"></my-component>
<my-component v-on:myEvent="doSomething"></my-component>
<my-component v-on="myEventObject"></my-component>
myEventObject: {
click () => {},
change () => {},
myEvent () => {}
}
key
虚拟 DOM Diff 算法用来识别 VNode 节点;
- 有 key:根据 key 判断节点,移动、删除节点;
- 无 key:使用就地复用策略,尽量修改现有 DOM,列表顺序变化容易产生 bug。
注意:同一个父节点下子元素 key 必须唯一,禁止直接用数组 index 作为 key。
ref
获取 DOM 元素或者组件实例。
- 普通 DOM:ref 指向真实 DOM;
- 子组件:ref 指向组件实例;
v-for循环中使用 ref,$refs对应为数组。
常见问题
数据响应相关问题
- 根级响应属性最好在
data初始化就声明,哪怕初始值为空; - 无法检测对象新增、删除属性:初始化阶段才执行
defineProperty,新增属性不会变成响应式;解决方案:Vue.set、vm.$set。 - 数组部分操作无法响应:通过下标直接赋值、直接修改数组 length 不会触发更新。
备注:数组变异方法可以触发更新:push、pop、shift、unshift、splice、sort、reverse。
异步 DOM 更新 nextTick
Vue DOM 更新是异步。修改数据不会立刻更新 DOM,同一个事件循环多次变更会做队列缓冲。
修改数据后,要获取更新完成之后 DOM,使用 this.$nextTick(callback)。
this.visible = true
this.$nextTick(() => {
// DOM 已经更新完成,可以操作 DOM
const height = document.querySelector('#MyComponent').clientHeight
})
组件 data 为什么必须是函数
组件会被实例化多次。
- 如果 data 是普通对象,所有组件实例共享同一个对象引用;
- data 是函数,每次实例化调用函数,返回全新对象副本,实例之间数据隔离。
不推荐 v-for 和 v-if 写在同一节点
v-for 优先级高于 v-if。会先循环全部列表,每一项执行 v-if 判断,性能差。
备注:优先使用 computed 计算属性过滤数据,循环过滤后的数组。
computed: {
activeUsers () {
return this.users.filter(u => u.isActive)
}
}
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
this 指向问题
setTimeout、setInterval回调内部 this 不再指向 Vue 实例;- 不要在组件选项、watch 回调中使用箭头函数,箭头函数没有 this,向上作用域查找,造成 this 指向异常。
// 错误示例
created: () => console.log(this.a)
vm.$watch('a', newValue => this.myMethod())
computed、watch、methods 区别
- computed 计算属性:基于响应式依赖做缓存;多次读取不会重复执行函数;适合多数据推导一个值。
- methods 方法:没有缓存,每次调用都会执行;适合需要手动调用的逻辑。
- watch 侦听器:监听数据变化,适合数据变化后执行异步、开销大的操作;支持
immediate立即执行、deep深度监听。
执行顺序:beforeCreate → watch(immediate:true) → created → beforeMount → computed(访问才执行) → mounted。
computed 缓存实现原理
计算属性内部实例化一个 lazy:true 的 Watcher,依靠 dirty 标记实现缓存。
dirty=true:依赖已变更,读取计算属性时调用evaluate()执行用户 getter,结果缓存到watcher.value,置dirty=false。dirty=false:直接返回缓存值,不执行函数。- 当计算属性依赖的数据发生变化,仅将
dirty=true,不会立即重新计算;等到下一次读取计算属性才重新求值。 - 若模板没有使用该计算属性,则永远不会执行 getter,不收集依赖。
Vue 组件 8 种通信方式
- props:父向子传数据;
- $emit:子向父触发自定义事件;
- ref:获取子组件实例、DOM;
- $parent、$root:向上访问父、根实例;
- $attrs、$listeners:逐级透传属性与事件;
- provide、inject:跨多层祖先后代注入;
- Vuex:全局状态管理;
- EventBus 事件总线(适合兄弟组件通信)。
// Bus.js
class Bus {
constructor() {
this.callbacks = {}
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach(cb => cb(args))
}
}
}
// 挂载原型
Vue.prototype.$bus = new Bus()
// 也可以直接复用 Vue 实例作为 bus
// Vue.prototype.$bus = new Vue()
// 发送方
this.$bus.$emit('foo')
// 接收方
this.$bus.$on('foo', this.handle)