简单实现vue双向绑定
实现 Vue 双向绑定的核心原理
Vue 的双向绑定主要通过数据劫持和发布-订阅模式实现。以下是一个简化版的实现示例:
数据劫持(Object.defineProperty)
function observe(data) {
if (!data || typeof data !== 'object') return
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() {
Dep.target && dep.addSub(Dep.target) // 收集依赖
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 通知更新
}
})
}
发布-订阅模式实现
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.vm[this.key] // 触发 getter 收集依赖
Dep.target = null
}
update() {
this.cb.call(this.vm, this.vm[this.key])
}
}
实现编译器(Compile)
class Compile {
constructor(el, vm) {
this.vm = vm
this.el = document.querySelector(el)
this.fragment = null
this.init()
}
init() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el)
this.compileElement(this.fragment)
this.el.appendChild(this.fragment)
}
}
nodeToFragment(el) {
const fragment = document.createDocumentFragment()
let child = el.firstChild
while (child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
}
compileElement(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
const text = node.textContent
const reg = /\{\{(.*?)\}\}/g
if (this.isTextNode(node) && reg.test(text)) {
this.compileText(node, RegExp.$1)
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
}
compileText(node, key) {
const initText = this.vm[key]
this.updateText(node, initText)
new Watcher(this.vm, key, value => {
this.updateText(node, value)
})
}
updateText(node, value) {
node.textContent = typeof value === 'undefined' ? '' : value
}
isTextNode(node) {
return node.nodeType === 3
}
}
整合实现简易 Vue
class MyVue {
constructor(options) {
this.data = options.data
observe(this.data)
new Compile(options.el, this)
Object.keys(this.data).forEach(key => {
this.proxyData(key)
})
}
proxyData(key) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.data[key]
},
set(newVal) {
this.data[key] = newVal
}
})
}
}
使用示例
<div id="app">
{{ message }}
</div>
<script>
const vm = new MyVue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
setTimeout(() => {
vm.message = 'Updated!'
}, 2000)
</script>
实现要点说明
- 通过
Object.defineProperty劫持数据属性的 getter 和 setter - 每个属性维护一个 Dep 实例来管理所有 Watcher
- Watcher 在初始化时触发 getter 将自己添加到 Dep 中
- 数据变化时通过 setter 通知 Dep,Dep 再通知所有 Watcher 更新视图
- Compile 解析模板中的指令和插值表达式
- 通过代理使 vm.key 可以直接访问 vm.data.key
这个实现展示了 Vue 双向绑定的核心原理,实际 Vue 的实现更加复杂,包含虚拟 DOM、diff 算法等优化。







