一文浅读Vue的双向绑定

初版定稿时间: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 拦截 datagetter,收集当前 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 提供了 reactiveref 两种方式

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:实例初始化前,datamethods还未创建
  • created:实例创建完成,响应式 data 已初始化,但 DOM 未渲染
  • beforeMount:模版编译完成,虚拟 DOM生成,但未挂载到真实 DOM
  • mounted:组件挂载到 DOM,页面已渲染,可操作DOM
  • beforeUpdate:组件数据即将更新,DOM 还未更新
  • updated:数据更新完成,DOM 已同步更新
  • beforeDestory:组件销毁前,仍然可以访问到 datamethods
  • destroyed:组件销毁后,所有监听器和子组件都已移出

Vue 3 的生命周期

Vue 3 生命周期 与 Vue 2 想听,但推荐使用 onMountedonUpdated 等组合式API

js 复制代码
import {onMounted, onUpdated, onBeforeUnmount } from 'vue'

onMounted(() => {
    console.log("组件已挂载")
})

onUpdated(() => {
    console.log("组件已更新")
})

onBeforeUnmount(() => {
    console.log("组件即将卸载")
})

生命周期与双向绑定的关系

Vue的 双向绑定依赖于响应式系统 ,而 响应式系统的初始化和更新受生命周期控制,因此在不同阶段,Vue 的双向绑定行为也不同

(1)初始化阶段:beforeCreatecreated

Vue2
  • beforeCreate阶段,Vue实例尚未初始化,datacomputed还不可用
  • created阶段,Vue完成了datawatcher依赖收集,双向绑定开始生效

示例

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()时,datarefsetup期间已经初始化

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)挂载阶段:beforeMountmounted

  • 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)更新阶段:beforeUpdateupdated

  • 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)销毁阶段:beforeDestroydestroyed

  • 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 渲染,表单元素可交互
    • 更新阶段:数据变更 -> 视图更新
    • 销毁阶段:移除事件监听和响应式数据
相关推荐
Java技术小馆1 分钟前
Java中的Fork/Join框架
java·后端·面试
全马必破三4 分钟前
CSS 盒模型
前端·css
野生的程序媛11 分钟前
重生之我在学Vue--第12天 Vue 3 性能优化实战指南
前端·javascript·vue.js
vvilkim31 分钟前
Vue.js 中的计算属性、监听器与方法:区别与使用场景
前端·javascript·vue.js
乘风!33 分钟前
SpringBoot前后端不分离,前端如何解析后端返回html所携带的参数
前端·spring boot·后端
Anlici2 小时前
跨域解决方案还有优劣!?
前端·面试
uhakadotcom2 小时前
MaxCompute Python UDF开发指南:从入门到精通
后端·面试·github
苹果电脑的鑫鑫2 小时前
在使用element-ui时表单的表头在切换页面时第一次进入页面容易是白色字体解决方法
javascript·vue.js·ui
庸俗今天不摸鱼2 小时前
【万字总结】构建现代Web应用的全方位性能优化体系学习指南(二)
前端·性能优化·webp
潜龙在渊灬2 小时前
杂谈:前端 UI 框架和 UI 组件库的区别
javascript·vue.js·react.js