Summarize
Vue's two-way data binding is mainly implemented through Object.defineProperty. First, add get/set monitoring to all properties, so that when the property value changes, the corresponding set method will be triggered, and then pass the observer in the set method. to update the view while doing dependency collection in the get method.
- Github address: https://github.com/ns2250225/vue-analyze
Implementation of the minimalist version
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>极简双向数据绑定</title>
</head>
<body>
<input type="text" id="message" />
<div id="msg"></div>
<script src="app.js"></script>
</body>
</html>
- app.js
var obj = {}
Object.defineProperty(obj, "data", {
get: function () {
console.log("get")
},
set: function (newValue) {
document.getElementById("message").value = newValue
document.getElementById("msg").innerText = newValue
}
})
document.getElementById("message").addEventListener('keyup', function () {
obj.data = event.target.value
})
- analyze
(1)通过Object.defineProperty的方法为属性加上get/set的监控
(2)通过EventListener监听属性的改变,不断触发属性的set方法,从而实现数据的双向绑定
A slightly more complicated implementation
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>双向数据绑定</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message">
{{ message }}
</div>
<script src="dist/bundle.js"></script>
</body>
</html>
- main.js
import myVue from './myVue'
new myVue({
el: '#app',
data: {
message: "hello myVue"
}
})
- myVue.js
import Observer from './Observer'
import Compiler from './Compiler'
class myVue {
constructor(options) {
// 获取对象传入的数据
this.$options = options
this.$el = this.$options.el
this._data = this.$options.data
// 将传入的所有属性添加get/set的属性监听
// 属性值发生改变会触发set方法
Object.keys(this._data).forEach(key=>{
this.add_watch(key)
})
// 将所有属性加入订阅发布者模式的管理中
new Observer(this._data)
// 编译渲染页面
new Compiler(this.$el, this)
}
add_watch(key) {
var self = this
Object.defineProperty(this, key, {
get() {
return self._data[key]
},
set(value) {
self._data[key] = value
}
})
}
}
export default myVue
- Observer.js
import Dep from './Dep'
class Observer {
constructor(data) {
// 获取所有属性数据
this.data = data
// 为所有属性数据添加get/sete的属性监听
Object.keys(this.data).forEach(key=>{
this._bind(data, key, data[key])
})
}
_bind(data, key, val) {
var myDep = new Dep()
Object.defineProperty(data, key, {
get() {
// 如果是为订阅的对象,则添订阅
if(Dep.target) myDep.listen(Dep.target)
return val
},
set(newValue) {
if (newValue === val) return
val = newValue
// 如果数值改变,则发布更新
myDep.notify()
}
})
}
}
export default Observer
- Watcher.js
import Dep from './Dep'
class Watcher {
constructor(node, name, vm) {
this.node = node
this.name = name
this.vm = vm
Dep.target = this
this.update()
Dep.target = null
}
update() {
this.node.nodeValue = this.vm[this.name]
}
}
export default Watcher
- Dep.js
class Dep {
constructor() {
this.list = []
}
listen(subs) {
this.list.push(subs)
}
notify() {
for(var i=0; i<this.list.length; i++){
this.list[i].update()
}
}
}
Dep.prototype.target = null
export default Dep
- Compiler.js
import Watcher from './Watcher'
const REG = /\{\{(.*)\}\}/
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el)
this.vm = vm
// 创建文档片段,编译完成后,挂载到el元素上
this.frag = this._createFragment()
this.el.appendChild(this.frag)
}
_createFragment() {
var frag = document.createDocumentFragment()
var child
while (child = this.el.firstChild) {
this._compile(child)
frag.appendChild(child)
}
return frag
}
_compile(node) {
// 如果传入的是节点node
if(node.nodeType === 1) {
var attr = node.attributes
var self = this
if(attr.hasOwnProperty('v-model')){
var name = attr['v-model'].nodeValue
node.addEventListener('input', function(e) {
self.vm[name] = e.target.value
})
node.value = this.vm[name]
}
}
// 如果传入的是元素elemet
if (node.nodeType === 3) {
if(REG.test(node.nodeValue)) {
var name = RegExp.$1
name = name.trim()
new Watcher(node, name, this.vm)
}
}
}
}
export default Compiler