所谓双向绑定,指的是vue实例中的data与其渲染的DOM元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据。(数据变化更新视图,视图变化更新数据)
在vue中可以通过v-model实现双向绑定
<template>
<div id="app">
{{username}} <br/>
<input type="text" v-model="username">
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
username:''
}
}
}
</script>
但其实v-model只是一个语法糖,他实际做了两步动作:1、绑定数据元素;2、触发输入事件
ps: v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件;
也就是说其实v-model等同于如下代码:
<template>
<div id="app">
{{username}} <br/>
<input type="text" :value="username" @input="username=$event.target.value">
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
username:''
}
}
}
</script>
但为什么 这样写就会实现双向绑定?他的核心是什么?
Object.defineProperty()
方法Object.defineProperty(obj,prop,descriptor)使用:
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
descriptor:将被定义或修改的属性描述符。
descriptor的基本结构
{
value: 属性对应的值,默认为 undefined。
configurable: true | false, //属性是否可以被delete,或者再次修改descriptor
enumerable: true | false, //属性是否可以被for...in,Object.keys()枚举
writable: true | false, //对象是否可被赋值
get:function(){} | undefined,
set:function(){} | undefined
}
<body>
<div id="demo"></div>
<input type="text" id="inp">
</body>
<script type="text/javascript">
var obj = {};
var demo = document.querySelector('#demo')
var inp = document.querySelector('#inp')
Object.defineProperty(obj, 'name', {
get: function() {
return val;
},
set: function(newVal) { //当该属性被赋值的时候触发
inp.value = newVal;
demo.innerHTML = newVal;
}
})
inp.addEventListener('input', function(e) {
// 给obj的name属性赋值,进而触发该属性的set方法
obj.name = e.target.value;
});
obj.name = '测试'; //在给obj设置name属性的时候,触发了set这个方法
</script>
由上得出Object.defineProperty可以先实现简单的双向绑定,但是如果有100个、1000个dom,我们不可能一个一个设置其值,这样效率太低。这样我们就要运用到发布订阅模式
发布者-订阅者模式定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。
(ps:我们去商店买可乐时被老板告诉可乐售罄,但是老板告知你们可以添加到商店的微信群中,等可乐到货后,我在通知你们。)
这就是一个简单的发布者-订阅者模式,可乐是观察对象,我们是订阅者,老板是观察者,微信群是订阅器,当老板知道可乐到货后,就在微信群中通知我们,我们就回去买可乐。
同理vue也是这样做的:
我们new vue({})传入的data就是我们监听器(Observer )的观察对象,当初始化的时候,我们要把data的值默认渲染在dom中,在dom中使用({{}},v-model,v-bind)data的值就是订阅者,在初始化的时候就要把订阅者添加到订阅器(Dep)中,当data的值发生的改变时,会通知到去告诉订阅者们(Watcher)更新数据,最后指令解析器( Compile)解析对应的指令,进而会执行对应的更新函数,从而更新视图。
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
6、1监听器Observer
监听器的作用就是去监听数据的每一个属性,我们上面也说了使用 Object.defineProperty
方法,当我们监听到属性发生变化之后我们需要通知 Watcher 订阅者执行更新函数去更新视图,在这个过程中我们可能会有很多个订阅者 Watcher 所以我们要创建一个容器 Dep 去做一个统一的管理。
class Dep {
constructor() {
this.watchers = [];
}
//添加
add(Watcher) {
this.watchers.push(Watcher);
}
//通知
notify() {
this.watchers.forEach(watcher => {
watcher.update();
})
}
}
function observer(data) {
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach(key => {
initData(data, key, data[key]);
});
}
function initData(data,key,value) {
observer(value);
let reactiveValue = value;
const dep = new Dep();
Object.defineProperty(this, key, {
configurable: true,
enumerable:true,
get() {
Dep.target && dep.add(Dep.target)
return reactiveValue;
},
set(newVal) {
console.log("监听成功");
if(newVal != reactiveValue) {
reactiveValue = newVal;
//触发通知
dep.notify();
}
}
})
}
observer({
a:'11'
})
以上我们就创建了一个监听器 Observer,我们现在可以尝试一下给一个对象添加监听然后改变属性会有何变化。这是侯监听a,并修改a的值就会打印监听成功
6.2、订阅者Watcher
Watcher 主要是接受属性变化的通知,然后去执行更新函数去更新视图,所以我们做的主要是有两步:
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
this.vm = vm;
this.key = key;
Dep.target = this
this.value = this.vm[this.key]
Dep.target = null
}
update() {
this.callback();
}
}
6、3解析器Compile
Compile 的主要作用一个是用来解析指令初始化模板,一个是用来添加添加订阅者,绑定更新函数。
initVModel() {
const nodes = this.dom.querySelectorAll('[v-model]');
nodes.forEach(node =>{
const key = node.getAttribute('v-model');
node.value = this[key];
new Watcher(this,key,()=>{
node.value = this[key];
})
node.addEventListener('input', ev=>{
this[key] = ev.target.value;
})
})
}
<!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>
<div id="app">
<input type="text" v-model="msg">
</div>
</body>
<script>
//订阅器
class Dep {
constructor() {
this.watchers = [];
}
//添加
add(Watcher) {
this.watchers.push(Watcher);
}
//通知
notify() {
this.watchers.forEach(watcher => {
watcher.update();
})
}
}
//观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
this.vm = vm;
this.key = key;
Dep.target = this
this.value = this.vm[this.key];
Dep.target = null
}
update() {
this.callback();
}
}
class myVue {
constructor({el,data}) {
this.dom = document.querySelector(el);
this.data = data;
//初始化数据
this.initData();
//初始化v-model
this.initVModel();
}
initData() {
Object.entries(this.data).forEach(([key, value]) => {
let reactiveValue = value;
const dep = new Dep();
Object.defineProperty(this, key, {
configurable: true,
enumerable:true,
get() {
Dep.target && dep.add(Dep.target)
return reactiveValue;
},
set(newVal) {
if(newVal != reactiveValue) {
reactiveValue = newVal;
//触发通知
dep.notify();
}
}
})
});
}
initVModel() {
const nodes = this.dom.querySelectorAll('[v-model]');
nodes.forEach(node =>{
const key = node.getAttribute('v-model');
node.value = this[key];
new Watcher(this,key,()=>{
node.value = this[key];
})
node.addEventListener('input', ev=>{
this[key] = ev.target.value;
})
})
}
}
</script>
<script>
const vm = new myVue({
el: "#app",
data: {
msg: "abc"
}
})
</script>
</html>
首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
然后在编译的时候在该属性的数组dep中添加订阅者,Vue中的v-model会添加一个订阅者,{{}}也会,v-bind也会;
最后修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。