初版定稿时间:2025年3月16日22:35:55
Vue 的双向绑定是核心特性之一,能够让数据和 UI 保持同步更新、
什么是双向绑定
双向绑定让数据模型和视图之间形成双向连接
- 数据变更 -> 视图更新
- 视图变更 -> 数据同步
在 Vue 中,双向绑定主要依赖 v-model
指令,它简化了数据和表单元素之间的绑定过程。
Vue2 双向绑定原理
(1) Vue2 如何实现双向绑定?
Vue 2 使用 Object.defineProperty() 结合 getter 和 setter 进行响应式数据劫持
js
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
val = newValue
}
})
}
const data = {}
defineReactive(data, 'message'. 'Hello Vue');
console.log(data.message) // Hello Vue
data.message = 'Hi Vue' // Hi Vue
(2) Vue2 的核心机制
- Observer(观察者):监听数据变化
- Watcher(订阅者):依赖收集,通知更新
- Dep(依赖管理): 管理 Watcher 依赖
Vue 2 的响应式系统核心代码
js
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if(Dep.target) {
this.subscribers.push(Dep.target)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
function defineReactive(obj, key, val){
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal;
dep.notify()
}
})
}
class Vue {
constructor(options){
this.$data = options.data;
Object.keys(this.$data).forEach(key => {
defineReactive(this.$data, key, this.$data[key])
})
}
}
Dep.target = null
const vm = new Vue({
data: {
message: "hello Vue!"
}
})
vm.$data.message = 'Vue 2 双向绑定'
(3) v-model 实现原理
Vue2 中 v-model
本质上是 :value
和@input
事件的简写
vue
<input v-midel="message">
<!-- 等价于下面 -->
<input :value="message" @input="message = $event.target.value">
疑问点,Dep.target 为什么 = null
在 Vue 2 的响应式系统中,Dep.target = null
并不会影响依赖收集的正常工作,因为 Vue 采用的是 依赖收集(Dep)+ 订阅者模式(Watcher) 来管理数据变更。
Dep.target
的作用
在 Vue 2 中,Dep
代表 依赖管理器,用于追踪依赖(Watcher)。
- 当组件渲染时,Vue会访问响应式数据 ,触发
getter
,收集当前的Watcher
作为依赖。 Dep.target
始终指向 当前正在计算的 Watcher ,从而完成 依赖收集。- 当数据变化时 ,
Dep
通知所有 Watcher 重新计算,触发视图更新。
核心流程
- Dep 负责管理依赖。
- Watcher 代表具体的订阅者,负责在数据变更时重新执行更新逻辑。
Dep.target
临时存放当前正在计算的 Watcher。
为什么 Dep.target = null
在 Vue2 的响应式机制中,Dep.target
只是一个 全局的临时变量 ,用于标记当前收集依赖的 Watcher
。在一次收集完成后,Vue需要清除 Dep.target
,防止错误的依赖收集,所以Vue总是在依赖收集完成后将 Dep.target
置为 null
Vue 2 的依赖收集完整流程
(1) Watcher
创建时,设置 Dep.target
在 Vue 2 初始化渲染时,Vue 创建一个 Watcher
监听组件的 render()
函数 ,并在 Watcher
执行 get()
方法时,把 Dep.target
设置为当前 Watcher
js
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.getter = expOrFn;
this.cb = cb;
this.get() // 立即执行 get() 完成依赖收集
}
get() {
Dep.target = this; // 设置当前 Watcher
this.getter(); // 执行渲染函数,访问响应式数据
Dep.target = null; // 依赖收集完成,清空
}
update() {
console.log("数据变更,触发更新");
this.get()
}
}
(2)访问响应式数据时,触发 getter
进行依赖收集
Vue 拦截 data
的 getter
,收集当前 Watcher
作为依赖
js
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个数据 key 维护一个依赖管理器
Object.defineProperty(obj, key, {
get() {
if(Dep.target){
dep.depend() // 依赖收集
}
return val;
},
set(newVal) {
val = newVal;
dep.notify(); // 触发更新
}
})
}
(3) notify()
触发所有 Watcher 更新
当数据变化时,Vue 触发 setter
, 通知 dep.notify()
, 更新所有订阅 Watcher
js
const data = {};
defineReactive(data, "message", "Hello Vue");
const watcher = new Watcher(null, () => console.log("数据变更,重新渲染"));
data.message = "Vue 2 响应式"; // 触发 setter,调用 notify()
总结一波
Dep.target
是 Vue 2 依赖收集的核心,它存储当前正在收集依赖的Watcher
。- Vue 在访问响应式数据时,通过
getter
收集Dep.target
,然后立即清空Dep.target
以防止误收集。 - 当数据变化时,Vue 通知
dep.notify()
触发所有 Watcher 重新计算,从而更新 UI。
Vue 3 双向绑定原理
(1) Vue 3 的响应式变化
Vue 3 使用 Proxy 代替 Object.defineProperty()
, 提高了性能和扩展性
js
const data = new Proxy({ message: "Hello Vue 3" }, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value;
return true;
}
})
(2) Vue 3 的核心源码实现
js
const bucket = new WeakMap() // 存储依赖关系
let activeEffect = null; // 当前正在执行的副作用
function reactive(obj) {
return new Proxy(obj, {
get(target, key){
track(target, key); // 依赖收集
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key); // 触发更新
return true;
}
})
}
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return;
const effects = depsMap.get(key)
if(effects) {
effects.forEach(fn => fn())
}
}
// 创建一个副作用函数,在依赖变更时执行回调
function effect(fn) {
activeEffect = fn;
fn() // 立即执行一次, 触发 getter,完成依赖收集
activeEffect = null;
}
默认情况下是通过 effect()
显式注入的,computed 自动追踪依赖,watch 手动监听特定的响应式数据,在Vue组件中的 setup 里,模版渲染时自动触发依赖收集
(3) Vue 3 响应式 API
Vue 3 提供了 reactive
和 ref
两种方式
js
import { reactive, ref } = 'vue';
const state = reactive({message: "Hello Vue 3"});
const count = ref(0)
state.message = "响应式对象";
count.value++;
(4) Vue 3 v-model 语法
Vue 3 允许 v-model
绑定多个值
vue
<Child v-model:name="userName" v-model:age="userAge" />
<!-- 等价于下面 -->
<Child :name="userName" @update:name="val => userName = val" :age="userAge" @update:age="val => userAge = val" />
Vue 2 & Vue 3 双向绑定的实际应用
(1) Vue 2 组件中的 v-model
Vue
<!-- Parent.vue -->
<template>
<CustomInput v-model="message" />
</template>
<script>
import CustomInput from './CustomInput.vue'
export default {
data() {
return { message: 'Hello' }
},
components: { CustomInput }
}
</script>
<!-- CustomInput.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value'] // Vue 2 中 默认的 v-model 绑定 prop
}
</script>
组件
必须
使用value
作为prop,input
作为事件,才能正确绑定v-model
(2) Vue 3 组件中的 v-model
vue
<!-- Parent.vue -->
<template>
<CustomInput v-model="message" />
</template>
<script setup>
import CustomInput from './CustomInput.vue'
import { ref } from 'vue';
const message = ref("Hello Vue 3");
</script>
<!-- CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps(['modelValue'])
</script>
关系密切的 生命周期
生命周期回顾
Vue 组件的 生命周期 是组件从创建到销毁的整个过程,分为不同的阶段,每个阶段都有对应的钩子函数
Vue 2 的生命周期
- beforeCreate:实例初始化前,
data
和methods
还未创建 - created:实例创建完成,响应式
data
已初始化,但 DOM 未渲染 - beforeMount:模版编译完成,虚拟 DOM生成,但未挂载到真实 DOM
- mounted:组件挂载到 DOM,页面已渲染,可操作DOM
- beforeUpdate:组件数据即将更新,DOM 还未更新
- updated:数据更新完成,DOM 已同步更新
- beforeDestory:组件销毁前,仍然可以访问到
data
和methods
- destroyed:组件销毁后,所有监听器和子组件都已移出
Vue 3 的生命周期
Vue 3 生命周期 与 Vue 2 想听,但推荐使用 onMounted
、 onUpdated
等组合式API
js
import {onMounted, onUpdated, onBeforeUnmount } from 'vue'
onMounted(() => {
console.log("组件已挂载")
})
onUpdated(() => {
console.log("组件已更新")
})
onBeforeUnmount(() => {
console.log("组件即将卸载")
})
生命周期与双向绑定的关系
Vue的 双向绑定依赖于响应式系统 ,而 响应式系统的初始化和更新受生命周期控制,因此在不同阶段,Vue 的双向绑定行为也不同
(1)初始化阶段:beforeCreate
和created
Vue2
- 在
beforeCreate
阶段,Vue实例尚未初始化,data
和computed
还不可用 - 在
created
阶段,Vue完成了data
和watcher
依赖收集,双向绑定开始生效
示例
js
export default {
data(){
return { message: "hello vue" }
},
beforeCreate(){
console.log(this.message) // undefined, 因为 data 还未初始化
},
created(){
console.log(this.message) // "hello vue" data已经初始化
}
}
Vue3
使用setup()
时,data
和ref
在setup
期间已经初始化
js
import { ref, onBeforeMount, onMounted } from "vue";
export default {
setup() {
const message = ref("Hello Vue 3")
console.log(message.value); // setup 时已经可以访问
onBeforeMount(() => {
console.log("组件即将挂载")
})
onMounted(() => {
console.log("组件已挂载")
})
return { message }
}
}
(2)挂载阶段:beforeMount
和 mounted
- 在
beforeMount
阶段,Vue已经编译了模版,但还未真正挂载到DOM - 在
mounted
阶段,Vue组件真正挂载到 DOM,v-model
绑定的表单可以正常交互。
示例
vue
<template>
<input ref="inputRef" v-model="message" />
</template>
<script>
export default {
data() {
return { message: "Hello Vue" }
},
mounted() {
console.log(this.$refs.inputRef)
}
}
</script>
(3)更新阶段:beforeUpdate
和 updated
beforeUpdate
:数据更新,但 DOM 还未重新渲染updated
:数据更新后,DOM 也同步更新
Vue 2
js
export default {
data() {
return { message: "Hello Vue" }
},
watch: {
message(newVal) {
console.log("message 变更为:", newVal)
}
},
beforeUpdate(){
console.log("数据即将更新:", this.message)
},
updated(){
console.log("数据已更新:", this.message)
}
}
Vue 3
js
import { ref, watch, onBeforeUpdate, onUpdated } from 'vue'
export default {
setup(){
const message = ref("Hello Vue 3");
watch(message, (newVal) => {
console.log("message 变更为:", newVal)
})
onBeforeUpdate(() => {
console.log("数据即将更新:", message.value)
})
onUpdated(() => {
console.log("数据已更新:", message.value)
})
return { message };
}
}
(4)销毁阶段:beforeDestroy
和 destroyed
beforeDestroy
:组件即将被销毁,data
仍然可访问destroyed
:组件已销毁,所有依赖和监听器已清除
Vue 2
js
export default {
data(){
return { message: "Hello Vue" }
},
beforeDestroy(){
console.log("组件即将销毁", this.message);
},
destroyed(){
console.log("组件已销毁")
}
}
Vue 3
js
import { onBeforeUnmount, onUnmounted } from 'vue';
export default {
setup() {
onBeforeUnmount(() => {
console.log("组件即将销毁")
})
onUnmounted(() => {
console.log("组件已销毁")
})
}
}
总结一波
- Vue 2 依赖
Object.defineProperty()
实现数据劫持,而 Vue 3 使用Proxy
提高性能。 v-model
是 Vue 2 和 Vue 3 的核心双向绑定机制,Vue 3 支持多个v-model
绑定。- 生命周期影响 Vue 的双向绑定
- 创建阶段:初始化数据,绑定
v-model
- 挂载阶段:DOM 渲染,表单元素可交互
- 更新阶段:数据变更 -> 视图更新
- 销毁阶段:移除事件监听和响应式数据
- 创建阶段:初始化数据,绑定