参考链接:https://www.cnblogs.com/kidney/p/6052935.html
黄轶的源码解读:https://github.com/DDFE/DDFE-blog/issues/7
一、双向数据绑定和单向数据绑定概念
双向数据绑定就是在单向数据绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model(js)和 view(视图),在单向数据绑定中,input输入元素中输入的内容可以通过js操作dom动态获取,js中改变的数据也需要再次操作dom反映到视图中。双向数据绑定通过watcher方法自动更新视图中的数据,省去了烦琐的dom操作;
二、访问器属性
var obj = {}
// 为obj对象定义一个名为hello的访问器属性
// 访问器属性是对象中的一种特殊属性,不能直接在对象中定义,只能通defineProperty方法定义
// 读取或设置访问器属性的值,实际上是调用其内部函数get或set方法
Object.defineProperty(obj, "hello", {
get: function() {},
set: function() {}
})
obj.hello // 调用get方法,并返回get方法的返回值
obj.hello = "123" // 赋值传参,调用set方法,参数是123
// 访问器属性会被优先访问,即访问器属性会覆盖同名属性
三、双向数据绑定的简化版
var obj = {}
Object.defineProperty(obj, "hello", {
get: function() {},
set: function(newVal) {
document.getElementById('a').value = newVal
document.getElementById('b').innerHTML = newVal
}
})
// 模拟watcher
document.addEventListener('keyup', function(e) {
obj.hello = e.target.value
})
四、将vue中的值单向绑定到dom中
1)DocumentFragment文档片断
可以看做是节点容器,它可以包含多个子节点,将其插入到dom中时,只有它的子节点会插入到目标节点;
使用DocumentFragment处理节点,速度和性能远远优于直接操作dom;
vue进行编译时,就是将挂载目标的所有子节点劫持(通过append方法,dom中的所有节点会被自动删除)到DocumentFragment中,处理后再将DocumentFragment整体返回插入挂载目标;
// html代码
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
// js操作
var dom = nodeToFragment(document.getElementById('app'))
console.log(dom)
function nodeToFragment(node) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
flag.appendChild(child) // 将子节点劫持到文档片断中
}
return flag
}
document.getElementById('app').appendChild(dom) // 返回到app中
2)dom编译和数据绑定
// html代码
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
// js代码
// 对dom进行编译,将输入框以及文本节点与data中的数据绑定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 获取v-model绑定的属性名
node.value = vm.data[name] // 将data的值赋给该node
node.removeAttribute('v-model')
}
}
}
// 节点类型为text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 获取匹配到的字符串
name = name.trim()
node.nodeValue = vm.data[name] // 将data的值赋给该node
}
}
}
// 将节点转换为文档片断
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 将子节点劫持到文档片断中
}
return flag
}
// vue绑定的完整操作
function Vue(options) {
this.data = options.data
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 编译完成后,将dom返回到app中
document.getElementById(id).appendChild(dom)
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
最终结果:
五、实现数据与dom双向绑定
在输入框中输入数据的时候,首先会触发input或者keyup事件,在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性,利用defineProperty将data中的text设置为vm的访问器属性,会触发set方法更新属性的值;
// html代码
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
// js代码
var obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get: function() {
return val
},
set: function(newVal) {
if (newVal === val) return
val = newVal
console.log(val)
}
})
}
// watcher
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key])
})
}
function Vue(options) {
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 编译完成后,将dom返回到app中
document.getElementById(id).appendChild(dom)
}
// 对dom进行编译,将输入框以及文本节点与data中的数据绑定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 获取v-model绑定的属性名
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 将data的值赋给该node
node.removeAttribute('v-model')
}
}
}
// 节点类型为text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 获取匹配到的字符串
name = name.trim()
node.nodeValue = vm[name] // 将data的值赋给该node
}
}
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 将子节点劫持到文档片断中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
结果如下:
六、实现数据与dom双向绑定
text 文本变化了,set方法触发了,使用订阅发布模式将绑定到text的文本节点同步变化,订阅发布模式是一种一对多的关系,即多个观察者同时监听一个主题对象,这个主题对象的状态发生变化时会通知所有观察者对象;
流程:发布者发出通知=》主题对象收到通知并推送给观察者=》订阅者执行相应操作
// 一个发布者publisher
var pub = {
publish: function() {
dep.notify()
}
}
// 三个订阅者subscribers
var sub1 = {
update: function() {
console.log(1)
}
}
var sub2 = {
update: function() {
console.log(2)
}
}
var sub3 = {
update: function() {
console.log(3)
}
}
// 一个主题对象
function Dep() {
this.subs = [sub1, sub2, sub3]
}
Dep.prototype.notify = function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
// 发布者发布消息,主题对象执行notif方法,进而触发订阅者执行update方法
var dep = new Dep()
pub.publish() // 1,2,3
七、双向数据绑定完整代码
监听数据的过程中,会为data中的每一个属性生成一个主题对象dep;
在编译html过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中;
发出通知dep.notify()=>触发订阅者的update方法=>更新视图;
function defineReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function() {
// 添加订阅者watcher到主题对象Dep
if (Dep.target) dep.addSub(Dep.target)
return val
},
set: function(newVal) {
if (newVal === val) return
val = newVal
// 作为发布者发出通知
dep.notify()
}
})
}
// watcher
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key])
})
}
// 一个主题对象
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
function Vue(options) {
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 编译完成后,将dom返回到app中
document.getElementById(id).appendChild(dom)
}
// 对dom进行编译,将输入框以及文本节点与data中的数据绑定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 获取v-model绑定的属性名
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 将data的值赋给该node
node.removeAttribute('v-model')
}
}
}
// 节点类型为text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 获取匹配到的字符串
name = name.trim()
// node.nodeValue = vm[name] // 将data的值赋给该node
new Watcher(vm, node, name)
}
}
}
function Watcher(vm, node, name) {
Dep.target = this
this.name = name
this.node = node
this.vm = vm
this.update()
Dep.target = null
}
Watcher.prototype = {
update: function() {
this.get()
this.node.nodeValue = this.value
},
// 获取data中的属性值
get: function() {
this.value = this.vm[this.name] // 触发相应属性的get
}
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 将子节点劫持到文档片断中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
结果如下: