Vue的双向绑定是通过访问器属性来实现(Object.defineProperty中的get、set,具体使用方法请google),具体通过代码示例解释
html元素
<div id="app">
<input type="text" v-model="text"></input>
<span>{{text}}</span>
<div>
<p> {{food1}} </p>
<p> {{food2}} </p>
<p> {{favourite}} </p>
<div>
<p> <span> {{note}} </span> </p>
<p> <span> {{text}} </span> </p>
</div>
</div>
</div>
初始化
初始化Vue时将vue中的data绑定到view层面
- 创建一个Vue对象
class Vue {
constructor (option) {
Object.assign(this, option)
var dom = nodeToFragment(document.getElementById('app'), this)
}
}
var vm = new Vue({
el: 'app',
data: {
text: 'Hello World!',
food1: 'fish',
food2: 'meat',
favourite: 'you guess!',
note: '注意不能与禁忌食物同食'
}
})
示例中定义了一个Vue对象,根节点为id为app的元素
- 通过Object.assign把option相应的配置(data等)都分配给vm
- nodeToFragment方法实现从根节点开始遍历html元素节点,将data元素绑定至view层
nodeToFragment方法的实现如下
function nodeToFragment(node, vm) {
var child
var vmDataName
var fragment = document.createDocumentFragment()
while (child = node.firstChild) {
// 节点为元素
if (child.nodeType === 1) {
vmDataName = child.getAttribute('v-model')
if (vmDataName) {
child.value = vm.data[vmDataName]
}
nodeToFragment(child, vm)
} else if (child.nodeType === 3){ // 文本节点
if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
vmDataName = RegExp.$1
child.nodeValue = vm.data[vmDataName]
}
}
fragment.append(child)
}
node.append(fragment)
}
通过while (child = node.firstChild)
循环遍历node的所有子节点
a. 如果是元素节点,如果获取到v-model属性,更新元素的value值,然后继续遍历child的子节点
b. 如果是文本节点,匹配‘{{}}’中的内容,替换成对应的data值
c. 将更新后的节点append至fragment中
d. 将最后生成的节点全部append至node元素下
View至Model层的更新
对上面代码进行修改,使得对input元素的修改能够反映到Vue中
function nodeToFragment(node, vm) {
while (child = node.firstChild) {
// 节点为元素
if (child.nodeType === 1) {
vmDataName = child.getAttribute('v-model')
if (vmDataName) {
child.value = vm.data[vmDataName]
bindViewToModel(child, vm.data, vmDataName) // View层的变化反应到model层
}
nodeToFragment(child, vm)
}
……
}
node.append(fragment)
}
function bindViewToModel(node, obj, key) {
node.addEventListener('input', e => {
obj[key] = e.target.value
})
}
添加了一个bindViewToModel方法,通过监听input事件,更新model
Model至Vue的更新
class Dep {
constructor() {
this.subs = []
}
addSubs(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.bindModelToView()
});
}
}
function definePropertyOfData(obj) {
for(var key in obj) {
(function(dep) {
var value = obj[key]
Object.defineProperty(obj, key, {
get: () => {
if (Dep.target) {
dep.addSubs(Dep.target) //订阅
}
return value
},
set: val => {
value = val
dep.notify()
}
})
})(new Dep())
}
}
对于obj的每一个属性,每次有对象访问这个属性时,如果这个对象订阅了这个属性(Dep.target为true),就将对象添加至订阅列表(dep.addSubs)。在属性值发生改变时,对所有订阅列表中的对象发生一个通知(notify)。对象接受到通知以后,就将更新后的属性值反应到View中
现在对html中每一个访问了data元素值的元素,都订阅它绑定的属性值
class Watcher {
constructor (node, obj, key) {
this.node = node
this.obj = obj
this.key = key
Dep.target = this
node.nodeValue = obj[key]
Dep.target = null
}
bindModelToView () {
this.node.nodeValue = this.obj[this.key]
}
}
function nodeToFragment(node, vm) {
……
while (child = node.firstChild) {
……
else if (child.nodeType === 3){ // 文本节点
if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
vmDataName = RegExp.$1
new Watcher(child, vm.data, vmDataName)
}
}
fragment.append(child)
}
node.append(fragment)
}
通过Dep.target = this
获取订阅者, node.nodeValue = obj[key]
会调用对象的get方法,在get方法中,发现订阅者(Dep.target)存在,就会通过dep.addSubs(
Dep.target)
将该订阅者添加至订阅列表,之后将Dep.target 设置为空。obj属性值发送改变时(调用set方法),通过dep.notify()
发送通知。接收到通知后,Dep.target
调用bindModelToView
方法将model改变反应至View中
完整代码
var dom = `
<div id="app">
<input type="text" v-model="text"></input>
<span>{{text}}</span>
<div>
<p> {{food1}} </p>
<p> {{food2}} </p>
<p> {{favourite}} </p>
<div>
<p> <span> {{note}} </span> </p>
<p> <span> {{text}} </span> </p>
</div>
</div>
</div>`
document.body.innerHTML = dom
// View层的变化反应到model层:通过addEventListener监视vue层事件,更新model
class Vue {
constructor (option) {
Object.assign(this, option)
definePropertyOfData(this.data)
var dom = nodeToFragment(document.getElementById('app'), this)
}
}
function definePropertyOfData(obj) {
for(var key in obj) {
(function(dep) {
var value = obj[key]
Object.defineProperty(obj, key, {
get: () => {
if (Dep.target) {
dep.addSubs(Dep.target) //订阅
}
return value
},
set: val => {
value = val
dep.notify()
}
})
})(new Dep())
}
}
class Watcher {
constructor (node, obj, key) {
this.node = node
this.obj = obj
this.key = key
Dep.target = this
node.nodeValue = obj[key]
Dep.target = null
}
bindModelToView () {
this.node.nodeValue = this.obj[this.key]
}
}
class Dep {
constructor() {
this.subs = []
}
addSubs(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.bindModelToView()
});
}
}
var vm = new Vue({
el: 'app',
data: {
text: 'Hello World!',
food1: 'fish',
food2: 'meat',
favourite: 'you guess!',
note: '注意不能与禁忌食物同食'
}
})
function nodeToFragment(node, vm) {
var child
var vmDataName
var fragment = document.createDocumentFragment()
while (child = node.firstChild) {
// 节点为元素
if (child.nodeType === 1) {
vmDataName = child.getAttribute('v-model')
if (vmDataName) {
child.value = vm.data[vmDataName]
bindViewToModel(child, vm.data, vmDataName) // View层的变化反应到model层
}
nodeToFragment(child, vm)
} else if (child.nodeType === 3){ // 文本节点
if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
vmDataName = RegExp.$1
new Watcher(child, vm.data, vmDataName)
}
}
fragment.append(child)
}
node.append(fragment)
}
function bindViewToModel(node, obj, key) {
node.addEventListener('input', e => {
obj[key] = e.target.value
})
}
window.setTimeout(() => {
vm.data.favourite = 'rice'
console.log(vm.data)
}, 5000)
在谷歌中,将代码黏贴纸console中,即可尝试看到效果。