手写简易Vue响应式:从原理到实现,带你吃透双向绑定的核心

作为前端开发者,你一定用过Vue的响应式数据,但你有没有想过:当我们修改data中的属性时,页面为什么能自动更新? 今天我们就从零开始手写一个简易版的Vue响应式(Vue 2版本核心原理),彻底搞懂这背后的魔法。


一、先抛出几个灵魂拷问(欢迎在评论区讨论)

在开始写代码之前,先考考大家:

  1. Vue 2的响应式是基于什么API实现的?为什么Vue 3要改用Proxy?
  2. 为什么直接给对象添加新属性不会触发响应式?Vue.set是怎么解决的?
  3. 数组的push/pop等方法为什么能触发更新?原生数组方法被做了什么手脚?

欢迎在评论区留下你的答案,我们实现完代码后再来验证!


二、核心原理拆解

Vue 2响应式的核心是数据劫持+依赖收集+发布订阅,三者的关系可以用下面的流程概括:

1. 初始化data时,用Object.defineProperty劫持属性的get/set
2. 模板渲染时,读取data属性会触发get,此时收集当前的渲染函数(依赖)
3. 当修改data属性时,触发set,此时通知所有收集的依赖重新执行(更新视图)

三、手写简易版响应式代码

我们将实现一个包含数据劫持、依赖收集、视图更新的最小响应式系统,代码不到100行,清晰易懂。

1. 完整可运行代码

Javascript
复制
// 1. 依赖收集器:存储当前属性对应的所有依赖(渲染函数)
class Dep {
constructor() {
this.subscribers = new Set() // 用Set避免重复收集
}

// 添加依赖
addSub(sub) {
if (sub && typeof sub.update === 'function') {
this.subscribers.add(sub)
}
}

// 通知所有依赖更新
notify() {
this.subscribers.forEach(sub => sub.update())
}
}

// 2. 观察者:封装渲染函数,触发更新时执行
class Watcher {
constructor(vm, key, cb) {
this.vm = vm // 当前Vue实例
this.key = key // 要观察的data属性名
this.cb = cb // 回调函数(更新视图)

// 触发一次get,将当前Watcher实例加入依赖收集器
Dep.target = this
this.vm[this.key] // 读取属性,触发get
Dep.target = null // 收集完成后清空
}

// 执行回调更新视图
update() {
this.cb.call(this.vm, this.vm[this.key])
}
}

// 3. 数据劫持:给data的所有属性添加get/set
function observe(data) {
if (typeof data !== 'object' || data === null) return

// 遍历对象属性,劫持get/set
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}

function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val)

const dep = new Dep() // 每个属性对应一个Dep实例

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖:如果当前有Watcher(Dep.target),就加入收集器
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 如果新值是对象,也要劫持它的属性
observe(newVal)
// 通知所有依赖更新
dep.notify()
}
})
}

// 4. 简易Vue类:整合以上所有功能
class MiniVue {
constructor(options) {
this.$options = options
this.$data = options.data || {}

// 1. 将data挂载到实例上,方便this.xxx访问
this.proxyData(this.$data)
// 2. 对data进行响应式处理
observe(this.$data)
// 3. 初始渲染视图
this.mount()
}

// 代理data到Vue实例
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}

// 模拟模板渲染:这里简化为直接更新DOM
mount() {
// 初始化渲染
this.updateView()
// 给每个data属性添加Watcher,监听变化
Object.keys(this.$data).forEach(key => {
new Watcher(this, key, () => {
this.updateView()
})
})
}

// 更新视图的具体逻辑
updateView() {
console.log('视图更新了!当前数据:', this.$data)
// 真实场景中这里会重新渲染模板,我们简化为打印日志+更新DOM
document.getElementById('app').innerText = `当前计数:${this.count}`
}
}

// 5. 测试代码
const vm = new MiniVue({
data: {
count: 0,
message: 'Hello Vue'
}
})

// 测试修改数据,看视图是否更新
setTimeout(() => {
vm.count = 1 // 会触发视图更新
console.log('修改count后:', vm.count)
}, 1000)

setTimeout(() => {
vm.message = 'Hello 响应式' // 会触发视图更新
console.log('修改message后:', vm.message)
}, 2000)

2. 代码运行效果

  1. 页面初始显示当前计数:0
  2. 1秒后,控制台打印视图更新了!当前数据:{count: 1, message: 'Hello Vue'},页面显示当前计数:1
  3. 2秒后,控制台打印视图更新了!当前数据:{count: 1, message: 'Hello 响应式'},页面显示当前计数:1(message变化但视图只绑定了count,所以页面计数不变)

四、关键部分解释

1. Dep类(依赖收集器)

  • 每个响应式属性对应一个Dep实例,用Set存储依赖(避免重复收集同一个Watcher)
  • addSub:添加依赖(渲染函数)
  • notify:通知所有依赖更新

2. Watcher类(观察者)

  • 每个渲染逻辑对应一个Watcher实例
  • 初始化时主动读取属性,触发get,将自己加入Dep的收集器
  • update方法:执行回调函数(更新视图)

3. Object.defineProperty(数据劫持)

  • 劫持属性的get/set,读取时收集依赖,修改时触发更新
  • 递归处理嵌套对象,确保所有子属性都被劫持

五、和Vue 2原生响应式的差距

我们的简易版本还有很多不足,比如:

功能点 简易版 Vue 2原生
数组响应式 不支持 重写了push/pop等7个方法
动态添加属性 不支持 提供Vue.set/this.$set方法
嵌套对象深度监听 支持 支持(但性能问题导致Vue 3改用Proxy)
计算属性/侦听器 不支持 完整支持

思考:为什么简易版不支持数组响应式?怎么修改代码实现?欢迎在评论区讨论!


六、回到开头的灵魂拷问

现在我们来回答最开始的问题:

  1. Vue 2基于Object.defineProperty,Vue 3改用Proxy是因为Object.defineProperty无法监听数组和对象的新增属性,而Proxy可以直接代理整个对象,更强大更灵活。
  2. 因为Object.defineProperty只能劫持已存在的属性,新增属性没有被劫持get/set,所以不会触发更新。Vue.set是通过手动给新增属性添加get/set,并通知Dep更新。
  3. Vue 2重写了数组的7个变异方法(push/pop/shift/unshift/splice/sort/reverse),在调用这些方法时,除了执行原生逻辑,还会通知Dep更新视图。

 

购买须知/免责声明
1.本文部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责。
2.若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
3.如果本站有侵犯、不妥之处的资源,请在网站右边客服联系我们。将会第一时间解决!
4.本站所有内容均由互联网收集整理、网友上传,仅供大家参考、学习,不存在任何商业目的与商业用途。
5.本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
6.不保证任何源码框架的完整性。
7.侵权联系邮箱:aliyun6168@gail.com / aliyun666888@gail.com
8.若您最终确认购买,则视为您100%认同并接受以上所述全部内容。

小璐导航资源站 站长资讯 手写简易Vue响应式:从原理到实现,带你吃透双向绑定的核心 https://o789.cn/25071.html

相关文章

猜你喜欢