重读 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 为每一个属性添加 gettersetter 访问器。

gettersetter 对开发者透明,框架内部依靠它们完成依赖追踪:属性被读取时收集依赖;属性被修改时触发变更通知。

每一个组件实例对应一个 Watcher 实例。组件渲染过程中,访问到的 data 属性会被记录为依赖;当属性 setter 执行触发变更,通知对应 Watcher,驱动组件重新渲染。

注意Object.defineProperty 无法通过 polyfill 兼容实现,因此 Vue2 不支持 IE8 以及更低浏览器

生命周期钩子

Vue2 组件完整生命周期,四个阶段:创建、挂载、更新、销毁。

  • 创建阶段(实例化,DOM 还不存在)
    • beforeCreate:实例刚创建,datamethods 还未初始化;拿不到 this.xxx 数据。
    • createddatamethodsprops 已经初始化完成,DOM 还没有生成,$el 不存在;可以发起网络请求。
  • 挂载阶段(渲染 DOM)
    • beforeMount:模板编译完成,挂载前;$el 存在但是是虚拟 DOM,页面还没渲染。
    • mounted:真实 DOM 挂载完毕,可以操作 DOM、ref;可以做 DOM 相关操作、定时器。
  • 更新阶段(响应数据变化,DOM 更新)
    • beforeUpdate:数据改变,虚拟 DOM 重新打补丁,真实 DOM 还没更新
    • updated:DOM 已经更新完成。
  • 销毁阶段
    • beforeDestroy:实例销毁前,组件实例仍然可用;清除定时器、解绑事件、取消 axios 请求,放在这里。
    • destroyed:实例完全销毁,全部指令、watcher 解除绑定。

其他钩子:

  • activateddeactivated<keep-alive> 缓存组件专属钩子;组件激活、失活时触发。
  • errorCaptured:捕获后代组件抛出的错误。

注意:服务端渲染 SSR 环境,mountedupdatedbeforeDestroydestroyed 不会执行。

内置组件

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-classenter-active-classenter-to-class
  • leave-from-classleave-active-classleave-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 支持对象语法批量绑定事件,对象语法不支持任何修饰符

事件修饰符

  1. .stop:阻止事件冒泡
  2. .prevent:阻止事件默认行为
  3. .capture:开启事件捕获模式
  4. .self:只有 event.target === 当前元素 才触发回调
  5. .once:事件只会触发一次
  6. .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。

  • 普通属性:外部传入的值会覆盖组件根元素上同名属性
  • 例外:classstyle,不会覆盖,会自动合并。
  • 在组件内部通过 $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-ifv-forv-modelv-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:上一次绑定的值,仅 updatecomponentUpdated 可用。
    • expression:原始字符串形式表达式。
    • arg:指令参数,例 v-my:fooarg="foo"
    • modifiers:修饰符对象,例 v-my.a.b 得到 {a:true,b:true}
  • vnode:组件当前虚拟节点。
  • oldVnode:上一轮虚拟节点,仅 updatecomponentUpdated 可用。

简写形式

只关心 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.setvm.$set
  • 数组部分操作无法响应:通过下标直接赋值、直接修改数组 length 不会触发更新。

备注:数组变异方法可以触发更新:pushpopshiftunshiftsplicesortreverse

异步 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 指向问题

  • setTimeoutsetInterval 回调内部 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) → createdbeforeMount → computed(访问才执行) → mounted

computed 缓存实现原理

计算属性内部实例化一个 lazy:true 的 Watcher,依靠 dirty 标记实现缓存。

  • dirty=true:依赖已变更,读取计算属性时调用 evaluate() 执行用户 getter,结果缓存到 watcher.value,置 dirty=false
  • dirty=false:直接返回缓存值,不执行函数。
  • 当计算属性依赖的数据发生变化,仅将 dirty=true不会立即重新计算;等到下一次读取计算属性才重新求值。
  • 若模板没有使用该计算属性,则永远不会执行 getter,不收集依赖。

Vue 组件 8 种通信方式

  1. props:父向子传数据;
  2. $emit:子向父触发自定义事件;
  3. ref:获取子组件实例、DOM;
  4. $parent、$root:向上访问父、根实例;
  5. $attrs、$listeners:逐级透传属性与事件;
  6. provide、inject:跨多层祖先后代注入;
  7. Vuex:全局状态管理;
  8. 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)

参考资料

Vue.js v2 官方教程

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-09-03 01:55:15

results matching ""

    No results matching ""