【Vue】源码分析--双向数据绑定的实现
总结Vue的双向数据绑定主要通过Object.defineProperty来实现,先为所有的属性加上get/set的监控,这样当属性值改变时就会触发对应的set方法,然后再在set方法中通过观察者来更新视图,同时在get方法中进行依赖收集。Github地址:https://github.com/ns2250225/vue-analyze极简版的实现index....
·
总结
Vue的双向数据绑定主要通过Object.defineProperty来实现,先为所有的属性加上get/set的监控,这样当属性值改变时就会触发对应的set方法,然后再在set方法中通过观察者来更新视图,同时在get方法中进行依赖收集。
极简版的实现
- 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
})
- 分析
(1)通过Object.defineProperty的方法为属性加上get/set的监控
(2)通过EventListener监听属性的改变,不断触发属性的set方法,从而实现数据的双向绑定
稍微复杂版的实现
- 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
更多推荐
已为社区贡献4条内容
所有评论(0)