Vue.js 是用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
Vue.js 使用简洁的模板语法声明式地将数据渲染进 DOM:
<template>
<div id="app">{{ message }}</div>
</template>
<script>
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
}
})
</script>当数据变化时,视图会自动更新——这是 Vue 最核心的特性:
// 打开浏览器控制台
app.message = "Hello World!"
// 页面自动更新响应式更新链路:
📖 响应式系统的完整源码级解析请参见:深入响应式原理
组件系统允许使用小型、独立、可复用的组件构建大型应用:
<template>
<div id="app">
<ol>
<todo-item
v-for="item in groceryList"
:todo="item"
:key="item.id"
></todo-item>
</ol>
</div>
</template>
<script>
Vue.component("todo-item", {
props: ["todo"],
template: "<li>{{ todo.text }}</li>"
})
var app = new Vue({
el: "#app",
data: {
groceryList: [
{ id: 0, text: "蔬菜" },
{ id: 1, text: "奶酪" }
]
}
})
</script>📖 关于组件系统的深入讲解,请参见:组件基础
| 特性 | Vue 组件 | Web Components |
|---|---|---|
| 数据传递 | 单向/双向绑定 | 属性传递 |
| 事件通信 | 自定义事件系统 | DOM 事件 |
| 生命周期 | 完整钩子函数 | 回调函数 |
| 浏览器支持 | 无需 polyfill | 现代浏览器已原生支持,旧浏览器需 polyfill |
Vue 2 已于 2023 年 12 月 31 日到达生命周期终点 (EOL)。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| CDN 引入 | 快速原型、学习、静态页面 | 无需构建工具,开箱即用 | 无法使用单文件组件 |
| NPM | 模块化、工程化项目 | 便于依赖管理 | 需构建配置 |
| Vue CLI | 大型单页应用 | 完整工程化方案 | 学习成本较高 |
<script> 标签开发环境推荐使用未压缩版(含完整警告和调试信息):
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>生产环境锁定版本号并使用压缩版:
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>npm install vue@^2import Vue from "vue"
// 或
const Vue = require("vue")npm install -g @vue/cli
vue create my-vue-app
cd my-vue-app
npm run serveVue 提供多种构建版本:
| UMD | CommonJS | ES Module (打包工具) | ES Module (浏览器) | |
|---|---|---|---|---|
| 完整版 | vue.js | vue.common.js | vue.esm.js | vue.esm.browser.js |
| 仅运行时 | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js | - |
template 选项直接写模板字符串。.vue 单文件组件(模板在构建阶段预编译)。这是 NPM 包默认导出版本。关键规则:代码中使用
template选项 → 需要完整版;使用.vue文件或render函数 → 仅运行时版即可。
下面是一个完整示例,展示 Vue 的核心功能:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>我的第一个 Vue 应用</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
</div>
<script>
new Vue({
el: "#app",
data: {
title: "欢迎学习 Vue.js",
count: 0
},
methods: {
increment() { this.count++ },
decrement() { this.count-- }
}
})
</script>
</body>
</html>| 概念 | 示例 | 详细文档 |
|---|---|---|
| 插值表达式 | {{ message }} | 模板语法 |
| 属性绑定 | :href="url" | 模板语法 |
| 事件处理 | @click="handler" | 事件处理 |
| 双向绑定 | v-model="input" | 表单输入绑定 |
| 条件渲染 | v-if="show" | 条件渲染 |
| 列表渲染 | v-for="item in list" | 列表渲染 |
Vue 应用通过 new Vue() 创建根实例:
var vm = new Vue({
el: "#app",
data: { message: "Hello" },
computed: {
reversedMessage() {
return this.message.split("").reverse().join("")
}
}
})核心要点:
data 中的属性会被加入响应式系统——数据变化时视图自动更新$ 为前缀(如 vm.$data、vm.$watch),区别于用户属性// ✅ 提前声明所有需要的属性
data: {
newTodoText: "",
visitCount: 0,
todos: []
}📖 关于 Vue 实例的完整 API 参考,请参见:Vue 实例
强烈推荐安装 Vue Devtools——审查组件树、状态和事件的强大工具。
Vue 实例是 Vue 应用的核心。每个 Vue 应用都是通过 Vue 函数创建一个新的 Vue 实例开始的:
var vm = new Vue({
// 选项
})虽然没有完全遵循 MVVM 模式,但 Vue 的设计受到其启发,因此文档中常用 vm(ViewModel 缩写)表示 Vue 实例。
Vue 2 通过 Object.defineProperty 将 data 属性转为 getter/setter,实现数据驱动的视图更新:
📖 响应式系统的完整源码级解析请参见:深入响应式原理
每个 Vue 实例从创建到销毁会经历一系列生命周期钩子:
关键要点:
this 不会指向组件实例mounted 不保证所有子组件都已挂载,需等待用 this.$nextTick()updated 不保证所有子组件都已重绘,同样用 this.$nextTick()📖 每个钩子的详细用法请参见:生命周期详解
类型:Object
Vue 实例观察的数据对象。通过 vm.a 可直接访问 vm.$data.a:
var vm = new Vue({
data: {
message: "Hello Vue!",
user: { name: "John", age: 25 }
}
})
console.log(vm.message) // "Hello Vue!"
console.log(vm.$data.message) // "Hello Vue!"
console.log(vm.$data === vm.$options.data) // true使用
Object.freeze()可以阻止响应式追踪,适用于不需要变化的大数据。
2.2.0 新增
类型:Object
当前组件接收到的 props 对象。Vue 实例代理了对其属性的访问:
Vue.component("user-profile", {
props: {
name: String,
age: { type: Number, default: 18 }
},
created() {
console.log(this.$props.name)
}
})⚠️ Props 是单向数据流,子组件不应修改。通过
$emit通知父组件修改。
类型:Element 只读
Vue 实例使用的根 DOM 元素:
var vm = new Vue({
template: '<div id="app">{{ message }}</div>',
data: { message: "Hello Vue!" }
})
vm.$mount()
console.log(vm.$el.tagName) // "DIV"服务端渲染时
$el为undefined。
类型:Object 只读
用于当前 Vue 实例的初始化选项。实用场景——存储自定义配置:
Vue.component("my-component", {
apiEndpoint: "/api/users",
methods: {
fetchData() {
return fetch(this.$options.apiEndpoint).then(res => res.json())
}
}
})类型:Vue instance 只读
父实例(根实例的 $parent 为 undefined)。
⚠️ 避免直接访问
$parent——会使组件耦合。推荐用 props/events 通信。
类型:Vue instance 只读
当前组件树的根 Vue 实例:
// 简单场景下访问全局状态
this.$root.globalState类型:Array<Vue instance> 只读
直接子组件。不保证顺序,也不是响应式的。
推荐使用
ref替代$children:
this.$refs.child1 // 直接引用子组件
this.$refs.items // v-for 中 ref 返回数组类型:{ [name: string]: ?Array<VNode> } 只读
访问被插槽分发的内容:
// 子组件中访问
this.$slots.header // 具名插槽
this.$slots.default // 默认插槽⚠️ 插槽不是响应式的。从 2.6.0 开始推荐通过
$scopedSlots访问。
2.1.0 新增
类型:{ [name: string]: props => VNode | Array<VNode> } 只读
访问作用域插槽,每个插槽对应一个返回 VNode 的函数:
Vue.component("todo-list", {
props: ["todos"],
render(createElement) {
return createElement("ul",
this.todos.map(todo =>
createElement("li", [this.$scopedSlots.default({ todo })])
)
)
}
})类型:Object 只读
持有注册过 ref 的所有 DOM 元素和组件实例:
<input ref="input" type="text" />
<user-profile ref="profile" />this.$refs.input.focus()
this.$refs.profile.sayHello()⚠️
$refs不是响应式的,避免在模板中或计算属性中使用。仅在组件渲染完成后可用。
类型:boolean 只读
是否运行于服务器端,用于 SSR 场景下区分环境:
if (!this.$isServer) {
window.addEventListener("resize", this.handleResize)
}2.4.0 新增
类型:{ [key: string]: string } 只读
包含父作用域中未被 prop 识别的属性(class 和 style 除外)。配合 inheritAttrs: false 实现属性透传:
Vue.component("base-input", {
inheritAttrs: false,
props: ["value"],
template: `<input v-bind:value="value" v-bind="$attrs" @input="$emit('input', $event.target.value)">`
})2.4.0 新增
类型:{ [key: string]: Function | Array<Function> } 只读
父作用域中的 v-on 事件监听器(不含 .native 修饰器),通过 v-on="$listeners" 透传:
Vue.component("base-button", {
template: `<button v-on="$listeners"><slot></slot></button>`
})返回值:{Function} unwatch
// 监听属性
vm.$watch("a", (newVal, oldVal) => {
console.log(`a: ${oldVal} -> ${newVal}`)
})
// 监听函数
vm.$watch(
() => this.a + this.b,
sum => console.log("sum:", sum)
)
// 深度监听对象
vm.$watch("user", callback, { deep: true })
// 立即执行
vm.$watch("a", callback, { immediate: true })
// 取消监听
const unwatch = vm.$watch("a", callback)
unwatch()deep 选项的底层实现——traverse 函数:
// src/core/observer/traverse.js
function traverse(val) {
// 递归遍历对象/数组的所有属性,触发 getter 收集依赖
const seenObjects = new Set() // 防止循环引用
function _traverse(val, seen) {
if (val.__ob__) { // 仅响应式对象(有 __ob__)才有 dep
if (seen.has(val.__ob__.dep.id)) return // 已遍历过
seen.add(val.__ob__.dep.id)
}
// 深度遍历子属性
if (Array.isArray(val)) {
val.forEach(item => _traverse(item, seen))
} else {
const keys = Object.keys(val)
for (let key of keys) {
_traverse(val[key], seen) // 递归触发 getter → 收集依赖
}
}
}
_traverse(val, seenObjects)
}原理:
deep: true时,Watcher 会调用traverse(this.value)递归读取对象/数组的所有子属性。在读取过程中触发每个子属性的 getter,从而将该 Watcher 加入到所有嵌套属性的依赖列表中。缺点是遍历大对象时性能开销大——推荐使用'obj.key'字符串路径替代。
向响应式对象中添加属性,确保响应式更新:
// 处理 Vue 无法检测的变更
vm.$set(vm.user, "age", 25) // 对象添加属性
vm.$set(vm.items, 1, "B") // 数组索引赋值为什么需要 $set? Vue 2 使用 Object.defineProperty,无法检测属性添加和数组索引直接赋值。Vue 3 使用 Proxy 解决了这些限制。
$set 源码实现(简化):
// src/core/observer/index.js
function set(target, key, val) {
// 1. 数组处理:调用 splice 触发响应式更新
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 2. 对象已有属性:直接赋值(已响应式)
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 3. 获取 Observer 实例
const ob = target.__ob__
// 4. 根数据对象不可新增属性
if (ob && ob.vmCount) {
warn('Avoid adding reactive properties to root data at runtime.')
return val
}
// 5. 非响应式对象:直接赋值
if (!ob) {
target[key] = val
return val
}
// 6. 核心:对新属性调用 defineReactive + 触发依赖通知
defineReactive(ob.value, key, val)
ob.dep.notify() // 通知所有依赖此对象的 Watcher
return val
}$delete 源码实现(简化):
// src/core/observer/index.js
function del(target, key) {
// 1. 数组处理:调用 splice
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = target.__ob__
// 2. 属性不存在:直接返回
if (!hasOwn(target, key)) return
// 3. 删除属性
delete target[key]
// 4. 非响应式对象:无需通知
if (!ob) return
// 5. 触发依赖通知
ob.dep.notify()
}删除属性并触发视图更新:
vm.$delete(vm.user, "age")
vm.$delete(vm.items, 1)监听实例上的自定义事件:
vm.$on("test", msg => console.log("Received:", msg))
vm.$emit("test", "Hello")
// 2.2.0+ 支持数组
vm.$on(["event1", "event2"], handler)只触发一次的监听器:
vm.$once("init", config => console.log("Initialized"))
// 之后再 emit 不会触发移除事件监听器:
触发实例上的事件,附加参数传给监听器回调:
// 子组件
this.$emit("give-advice", advice)
// 父组件模板
<magic-eight-ball @give-advice="showAdvice">手动挂载未挂载的实例:
var MyComponent = Vue.extend({
template: "<div>Hello!</div>"
})
// 三种挂载方式
new MyComponent().$mount("#app")
new MyComponent({ el: "#app" })
var component = new MyComponent().$mount()
document.getElementById("app").appendChild(component.$el)强制重新渲染。仅在非响应式数据变化时需要,应尽量避免使用。
三种使用方式:
// 回调
this.$nextTick(() => {
console.log(this.$el.textContent) // 已更新
})
// Promise(2.1.0+)
this.$nextTick().then(() => { ... })
// async/await
async updateMessage() {
this.message = "Updated"
await this.$nextTick()
console.log(this.$el.textContent) // 已更新
}完全销毁实例。触发 beforeDestroy 和 destroyed 钩子。
在大多数场景中不直接调用——使用
v-if以数据驱动的方式控制子组件生命周期。
// ✅ 推荐:提前声明所有响应式属性
data() {
return {
message: "",
user: null,
items: []
}
}
// ❌ 避免:动态添加属性
this.message = "Hello" // 非响应式(如果未在 data 中声明)// ✅ 推荐:使用 props 和 events
<child-component :data="value" @update="handler">
// ✅ 跨组件:Event Bus(小型应用)
var bus = new Vue()
bus.$emit("event", data)
bus.$on("event", handler)
// ✅ 大型应用:VuexbeforeDestroy() {
clearInterval(this.timer)
this.eventBus.$off("event", this.handler)
window.removeEventListener("resize", this.handleResize)
}Vue 2 无法检测 vm.items[1] = "B" 和 vm.items.length = 2。使用 vm.$set(vm.items, 1, "B") 或 vm.items.splice(2)。
$nextTick 在 DOM 更新后执行(优先使用微任务),setTimeout 在下一个宏任务中执行。获取更新后的 DOM 应使用 $nextTick。
在 beforeDestroy 中清理定时器、事件监听器、Event Bus 订阅。
创建高阶组件时透传属性和事件:
Vue.component("transparent-wrapper", {
inheritAttrs: false,
template: `<div class="wrapper"><input v-bind="$attrs" v-on="$listeners"></div>`
})