对数据双向绑定一直都是半知半解,今天就打算自己写个简单的实现。
可能有理解不到位甚至是错误的地方,如果有人发现的话可以指正~谢谢
数据的双向绑定,即视图和数据之间
// html
<input type="text" id="input"/>
<p id="output"></p>
// js
var oInput = document.getElementById('input')
var oOutput = document.getElementById('output')
var data = {
content: ''
}
以上准备了一个用于修改数据的input输入框,用于展示数据的p标签以及用于存储相关数据的data对象;当input输入框的值改变时,值将同步更新于P标签。
- 视图 -> 数据
这个比较简单,,当输入的内容变化时,就将输入框内容复制给data对象
// js
oInput.addEventListener('keyup', function(e) {
data.content = e.target.value
},false)
- 数据 -> 视图
vue里用了Object.defineProperty(),结合订阅/发布模式实现
个人的理解如下图:
1. 将需要取值的元素定义为数据的订阅者,在上面的例子中就是P标签。
订阅者的任务就是更新视图,即更新P标签展示的内容。
function Watcher(obj, key, cb) {
/*
* 更新视图
*/
TARGET = this //TARGET为订阅者标志
this._obj = obj
this._key = key
this._cb = cb
this._value = obj[key]
TARGET = null
}
Watcher.prototype.update = function() {
let value = this._obj[this._key]
this._cb.call(this._obj, value)
}
2. 那么多订阅者由谁来统一管理?它们订阅的信息由谁来统一发布信息呢?对于a数据,可能有a1,a2,a3…订阅者,a1,a2,a3…都需要展示a数据,那么这些a1,a2,a3…就由发布者A来统一管理;同理对于b数据,就由发布者B来管理b的订阅者b1,b2,b3。
发布者的任务就是收集订阅者和通知订阅者更新视图
function Dep() {
/*
* 发布者有两件事要做:
* 1. 吸纳订阅者
* 2. 将数据更新信息告知订阅者
*/
//订阅者队列
this.subs = []
}
Dep.prototype.notify = function(newVal) {
//通知订阅者
for (var i=0; i<this.subs.length; i++) {
this.subs[i].update()
}
}
Dep.prototype.addSubs = function(watcher) {
//吸纳订阅者
this.subs.push(watcher)
}
3. 对data对象里每个数的取值、赋值操作进行拦截,当其中某个数据被取值、赋值时,相应地做出某些事;同时,也为每一个数据配备一个发布者,当这个数据有变化时,都由这个发布者统一发布。
function observe(obj) {
/*
* 观察者有两件事要做:
* 1. 当数据被取,通知中间件去吸纳订阅者
* 2. 当数据被设置更新,通知中间件数据更新了
* 那么如何知道数据被取或者被设置呢?用Object.defineProperty的get()、set()劫持
*/
this.obj = obj
for(var key in obj) {
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
get() {
//通知发布者去吸纳订阅者
if(TARGET) {
dep.addSubs(TARGET)
}
return val
},
set(newVal) {
//通知发布者要更新数据
val = newVal
dep.notify()
},
})
}
现在各路神仙已经到位啦!
1. 现在我们知道P标签要运用到的是data对象,先给data里的数据加上set/get来拦截存取操作;并添加观察者来更新P视图。
observe(data)
new Watcher(data, 'content', val => {
oOutput.innerHTML = val
})
在Watcher里,有一个操作this._value = obj[key]
,obj[key]在这里也就是data.content。
这个操作被get()发现了!get()赶紧通知content的发布者。
get:“dep!这里有人要取我的值,它是一个TARGET,快来吸纳它!”
dep:“我来啦!dep.addSubs(TARGET)
,我已经把它加入了我的订阅者数组里啦!”
2. 现在我们在输入框输入数据,触发了keyup事件,执行了data.content = e.target.value
- data.content = ???,这是要对content进行修改。之前我们已经对data里的数据进行了拦截,当要对它进行修改时,被set()发现了。
set:“dep,content被修改了,你快通知你的订阅者们”。
dep:“我来啦!dep.notify()
,我去订阅数组里看看都有谁,再一个个通知它们更新视图去!”
3. 相关的Watcher们都听到了dep的呼唤this.subs[i].update()
,乖乖更新视图去了。
这里的Watcher是手动添加的,在vue里,通过编译模板,识别了v-model以及{{}},自动添加Watcher
js
window.onload = function() {
var oInput = document.getElementById('input')
var oOutput = document.getElementById('output')
var data = {
content: ''
}
// Observe
function observe(obj) {
/*
* 观察者有两件事要做:
* 1. 当数据被取,通知中间件去吸纳订阅者
* 2. 当数据被设置更新,通知中间件数据更新了
* 那么如何知道数据被取或者被设置呢?用Object.defineProperty的get()、set()劫持
*/
this.obj = obj
for(var key in obj) {
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
get() {
//通知发布者去吸纳订阅者
if(TARGET) {
dep.addSubs(TARGET)
}
return val
},
set(newVal) {
//通知发布者要更新数据
val = newVal
dep.notify()
},
})
}
// Dep
function Dep() {
/*
* 发布者有两件事要做:
* 1. 吸纳订阅者
* 2. 将数据更新信息告知订阅者
*/
//订阅者队列
this.subs = []
}
Dep.prototype.notify = function(newVal) {
//通知订阅者
for (var i=0; i<this.subs.length; i++) {
this.subs[i].update()
}
}
Dep.prototype.addSubs = function(watcher) {
//吸纳订阅者
this.subs.push(watcher)
}
// Watcher
function Watcher(obj, key, cb) {
/*
* 更新视图
*/
TARGET = this //TARGET为订阅者标志
this._obj = obj
this._key = key
this._cb = cb
this._value = obj[key]
TARGET = null
}
Watcher.prototype.update = function() {
let value = this._obj[this._key]
this._cb.call(this._obj, value)
}
observe(data)
new Watcher(data, 'content', val => {
oOutput.innerHTML = val
})
oInput.addEventListener('keyup', function(e) {
data.content = e.target.value
},false)
}