双向数据绑定 - pod4g/tool GitHub Wiki
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
console.log('this.subs:', this.subs)
this.subs.forEach(watcher => watcher.update())
}
}
class Watcher {
constructor(node, vm, name) {
Dep.target = this
this.node = node
this.vm = vm
this.name = name
this.update()
Dep.target = null
}
get() {
this.value = this.vm[this.name]
}
update() {
this.get()
if(this.node.nodeType === Node.ELEMENT_NODE) {
console.log('this.node.tagName:', this.node.tagName, this.value)
if (this.node.tagName === 'INPUT') {
this.node.value = this.value
} else {
this.node.textContent = this.value
}
} else if(this.node.nodeType === Node.TEXT_NODE) {
this.node.nodeValue = this.value
}
}
}
class Vue {
constructor(el, vm) {
this.el = document.getElementById(el)
this.vm = vm
this.observe(vm)
if (this.el) {
const fragment = this.nodeToFragment()
this.compile(fragment)
this.el.appendChild(fragment)
}
}
observe(vm) {
if(!vm || typeof vm !== 'object') return
Object.keys(vm).forEach(key => this.defineReactive(vm, key, vm[key]))
}
defineReactive(vm, key, value) {
const dep = new Dep()
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
set(newValue) {
value = newValue
dep.notify()
this.observe(newValue)
},
get() {
if(Dep.target) dep.addSub(Dep.target)
return value
}
})
}
nodeToFragment() {
const fragment = document.createDocumentFragment()
let firstchild
while (firstchild = this.el.firstChild) {
fragment.appendChild(firstchild)
}
return fragment
}
compile(fragment) {
const reg = /\{{2}(.*)\}{2}/
Array.from(fragment.childNodes).forEach(node => {
const { TEXT_NODE, ELEMENT_NODE } = Node
const { nodeType, nodeValue, attributes } = node
let dataName
if (nodeType === TEXT_NODE) {
const match = nodeValue.trim().match(reg)
if(match) {
dataName = match[1]
const data = this.vm[dataName]
node.nodeValue = node.nodeValue.replace(reg, data)
new Watcher(node, this.vm, dataName)
}
} else if(nodeType === ELEMENT_NODE) {
Array.from(attributes).forEach(({name, nodeValue}) => {
dataName = nodeValue
const data = this.vm[nodeValue]
switch(name) {
case 'v-model':
node.addEventListener('input', event => this.vm[nodeValue] = event.target.value)
node.value = data
case 'v-text':
node.textContent = data
case 'v-html':
node.innerHTML = data
}
})
new Watcher(node, this.vm, dataName)
this.compile(node)
}
})
}
}
使用
<!DOCTYPE html>
<html lang="ZH-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1"/>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no">
<meta name="description" content=""/>
<meta name="keywords" content=""/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
<title>测试vue</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name">
<p v-text="name"></p>
{{name}}
</div>
<script src="./lib/vue2.js"></script>
<script>
const vm = {
name: '李彦峰'
}
new Vue('app', vm)
</script>
</body>
</html>
双向绑定中的对象关键点:
- compiler 用来解析模板,取出指令或text节点,把相应的数据赋值给dom
- observer 用来劫持数据
- dep 依赖收集,一个依赖就是一个watcher对象,dep对象用一个数据来存放依赖(即watcher),这个对象其实就是一个简易的发布订阅对象的实现
- watcher 绑定依赖,当数据发生变化时,更新视图,new一个watcher,就是声明了一个依赖
- 使用Dep.target暂时保存每一个新new的watcher对象,在数据劫持的getter中,把watcher添加到dep对象的数组中,数据劫持的getter其实是由watcher触发(这个设计很巧妙)
关键代码:
class Watcher {
get() {
this.value = this.vm[this.name] // 触发数据劫持的getter
}
}
defineReactive(vm, key, value) {
const dep = new Dep()
Object.defineProperty(vm, key, {
get() {
if(Dep.target) dep.addSub(Dep.target) // 由watcher的get方法触发,一触发就进入dep数组,所以本质上,进入dep数组还是由watcher自己控制
return value
}
})
}