双向绑定原理
- 双向绑定
- 思考:
- 一句话描述原理
- DocuemntFragment(碎片化文档)
- Object.defineProperty(数据劫持)
- 发布订阅者模式
- Vue 双向绑定图示
- Vue 双向绑定完整实现代码
双向绑定
vue中 data定义的数据会添加双向绑定的功能,即数据更新后,页面内容会同步更新;页面内容更新后,数据也会同步更新。
思考:
- 初始化,如何将 data 中的数据更新到DOM模板中? - 碎片化文档
- 页面更新,如何更新数据? - input事件监听
- 数据变了更新页面,那如何知道数据变了呢? - 数据劫持,Object.defineProperty()
- 已知数据变了(发布者),如何更新跟这个数据相关的页面内容 {{}}、属性绑定、v-model(订阅者)呢? - 发布订阅者模式
一句话描述原理
Vue 数据双向绑定是通过数据劫持结合发布订阅者模式的方式来实现的。使用 DocuemntFragment(碎片化文档)获取所有子节点,将 v-model {{}} 类似语法的值进行填充,监听页面元素的 input 事件,当 val 变更时,更新 data 中的数据,给 data 通过 object.defineProperty 添加响应监听,当 val 变化时,会触发 set 方法,通过发布订阅模式,触发订阅者的更新方法,更新视图。
DocuemntFragment(碎片化文档)
function nodeToFragment(node){
var fragment = document.createDocumentFragment();
var child = null;
while(child = node.firstChild){
fragment.appendChild(child)
}
return fragment
}
Object.defineProperty(数据劫持)
var obj = {}; // 定义一个空对象
Object.defineProperty(obj, 'val', { // 定义要修改对象的属性
get: function () {
console.log('获取对象的值')
},
set: function (newVal) {
console.log('设置对象的值:最新的值是'+newVal);
}
});
obj.hello = 'hello world'
js通过Object.defineProperty方法简单的实现双向绑定:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="app">
<span id="childSpan"></span>
</body>
<script>
var obj = {}
var initValue='初始值'
Object.defineProperty(obj,'initValue',{
get(){
console.log('获取obj最新的值');
return initValue
},
set(newVal){
initValue = newVal
console.log('设置最新的值');
// 获取到最新的值 然后将最新的值赋值给我们的span
document.getElementById('childSpan').innerHTML = initValue
console.log(obj.initValue);
}
})
document.addEventListener('keyup', function (e) {
obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj
})
</script>
</html>
发布订阅者模式
发布订阅者模式又叫 观察者模式,他定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得将得到通知。
// 实现发布订阅模式
// 事件容器:
let handlers = {}
// 添加事件:
handlers['onmsg'].push(fn1)
// 触发事件:
this.handlers['onmsg'].forEach(handler => { handler(...params) })
学习参考:JavaScript设计模式 -发布订阅者模式
Vue 双向绑定图示
Vue 双向绑定完整实现代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
测试双向绑定demo
<input type="text" v-model="text" /> {{text}}
</div>
</body>
<script type="text/javascript">
//编译函数
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/; // 来匹配{{xxx}}中的xxx
//如果是元素节点
if(node.nodeType === 1) {
var attr = node.attributes;
//解析元素节点的所有属性
for(let i = 0; i < attr.length; i++) {
if(attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue //看看是与哪一个数据相关
node.addEventListener('input', function(e) { //将与其相关的数据改为最新值
vm[name] = e.target.value
})
node.value = vm.data[name]; //将data中的值赋予给该node
node.removeAttribute('v-model')
}
}
}
//如果是文本节点
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 nodeToFragment(node, vm) {
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}
// Vue构造函数
// 观察data中的所有属性值,注意增添了observe
function Vue(options) {
this.data = options.data;
observe(this.data, this)
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this)
//处理完所有节点后,重新把内容添加回去
document.getElementById(id).appendChild(dom)
}
//实现一个响应式监听属性的函数。一旦有赋新值就发生变化
function defineReactive(obj, key, val) {
var dep = new Dep(); //观察者实例
Object.defineProperty(obj, key, {
get: function() {
if(Dep.target) { //每一个观察着都是唯一的
dep.addSub(Dep.target)
}
return val
},
set: function(newVal) {
if(newVal === val) {
return
}
val = newVal;
console.log('新值' + val);
//一旦更新立马通知
dep.notify();
}
})
}
//实现一个观察者,对于一个实例 每一个属性值都进行观察。
function observe(obj, vm) {
for(let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}
// Watcher监听者
function Watcher(vm, node, name) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
this.node.nodeValue = this.value //更改节点内容的关键
},
get() {
this.value = this.vm[this.name] //触发相应的get
}
}
//dep构造函数
function Dep() {
this.subs = [] // 观察主题添加订阅者
}
Dep.prototype = {
// 添加订阅者
addSub(sub) {
this.subs.push(sub)
},
// 发布通知
notify() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
var vm = new Vue({
el: 'app',
data: {
text: '赵刚'
}
})
</script>
</html>