数据双向绑定 MVVM
- MVVM:
- M:数据模型
- V:视图
- VM:视图模型
数据双向绑定:逻辑 -> 数据 <-> 视图
- 数据 -> 通过 Object.defineProperty()、Proxy 实现响应式的数据
- 视图 -> 通过 DOM 增加事件处理函数监听(input/keyup)实现视图的响应
原理
通过数据劫持和发布订阅模式实现数据双向绑定,使用 Object.defineProperty() 或 Proxy 实现数据劫持,劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调,触发对应的监听渲染视图,从而实现数据的双向绑定。 第一步:observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。 第二步:compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。 第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: 在自身实例化时往属性订阅器(dep)里面添加自己,自己有变化的时候,接收到属性订阅器的通知,调用自身的 update 方法,触发 Compile 中绑定的回调。 第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
源码
js
// 订阅器类
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach((sub) => sub.update())
}
}
// 观察者类
class Observer {
constructor(data) {
this.walk(data)
}
walk(obj) {
Object.keys(obj).forEach((key) => {
this.defineReactive(obj, key, obj[key])
})
}
defineReactive(obj, key, val) {
const dep = new Dep()
let childObj = observe(val) // 递归子属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
childObj = observe(newVal) // 如果新值是对象,继续劫持
dep.notify()
}
})
}
}
function observe(value) {
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
// 订阅者类
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
this.value = this.get()
}
get() {
Dep.target = this
const value = this.vm.$data[this.key] // 触发 getter
Dep.target = null
return value
}
update() {
const newValue = this.vm.$data[this.key]
if (newValue !== this.value) {
this.value = newValue
this.cb.call(this.vm, newValue)
}
}
}
// 编译类
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
;[...childNodes].forEach((node) => {
const reg = /\{\{(.*)\}\}/
const text = node.textContent
if (this.isElementNode(node)) {
this.compile(node)
} else if (this.isTextNode(node) && reg.test(text)) {
this.compileText(node, reg.exec(text)[1])
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
}
compile(node) {
const nodeAttrs = node.attributes
;[...nodeAttrs].forEach((attr) => {
const attrName = attr.name
if (this.isDirective(attrName)) {
const exp = attr.value
const dir = attrName.substring(2)
if (this.isEventDirective(dir)) {
this.compileEvent(node, this.vm, exp, dir)
} else {
this.compileModel(node, this.vm, exp, dir)
}
node.removeAttribute(attrName)
}
})
}
compileText(node, exp) {
const initText = this.vm.$data[exp]
this.updateText(node, initText)
new Watcher(this.vm, exp, (value) => {
this.updateText(node, value)
})
}
compileEvent(node, vm, exp, dir) {
const eventType = dir.split(':')[1]
const cb = vm.methods && vm.methods[exp]
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false)
}
}
compileModel(node, vm, exp, dir) {
let val = vm.$data[exp]
this.modelUpdater(node, val)
new Watcher(vm, exp, (value) => {
this.modelUpdater(node, value)
})
node.addEventListener('input', (e) => {
const newValue = e.target.value
if (val === newValue) {
return
}
vm.$data[exp] = newValue
val = newValue
})
}
updateText(node, value) {
node.textContent = typeof value === 'undefined' ? '' : value
}
modelUpdater(node, value) {
node.value = typeof value === 'undefined' ? '' : value
}
isDirective(attr) {
return attr.indexOf('v-') === 0
}
isEventDirective(dir) {
return dir.indexOf('on:') === 0
}
isElementNode(node) {
return node.nodeType === 1
}
isTextNode(node) {
return node.nodeType === 3
}
}
// MVVM 类
class MVVM {
constructor(options) {
this.$data = options.data
this.methods = options.methods
// 1. 数据劫持
observe(this.$data)
// 2. 模板编译
new Compile(options.el, this)
// 3. 数据代理
this.proxyData(this.$data)
}
proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
}
// 使用示例
const vm = new MVVM({
el: '#app',
data: {
message: 'Hello MVVM',
inputText: ''
},
methods: {
handleClick() {
alert('Button clicked!')
}
}
})