vue数据双向绑定是通过数据劫持结合发布-订阅模式实现的,具体不再赘述,一图以蔽之:
1. 前言
每当被问到Vue数据双向绑定原理的时候,大家可能都会脱口而出:Vue内部通过Object.defineProperty
方法属性拦截的方式,把data
对象里每个数据的读写转化成getter
/setter
,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以通俗易懂的方式剖析Vue内部双向绑定原理的实现过程。
2. 思路分析
所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。如下图:
也就是说:
- 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
- data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。
要实现这两个过程,关键点在于数据变化如何更新视图,因为视图变化更新数据我们可以通过事件监听的方式来实现。所以我们着重讨论数据变化如何更新视图。
数据变化更新视图的关键点则在于我们如何知道数据发生了变化,只要知道数据在什么时候变了,那么问题就变得迎刃而解,我们只需在数据变化的时候去通知视图更新即可。
3. 使数据对象变得“可观测”
数据的每次读和写能够被我们看的见,即我们能够知道数据什么时候被读取了或数据什么时候被改写了,我们将其称为数据变的‘可观测’。
要将数据变的‘可观测’,我们就要借助前言中提到的Object.defineProperty
方法了,关于该方法,MDN上是这么介绍的:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
在本文中,我们就使用这个方法使数据变得“可观测”。
首先,我们定义一个数据对象car
:
let car = {
'brand':'BMW',
'price':3000
}
我们定义了这个car
的品牌brand
是BMW
,价格price
是3000。现在我们可以通过car.brand
和car.price
直接读写这个car
对应的属性值。但是,当这个car
的属性被读取或修改时,我们并不知情。那么应该如何做才能够让car
主动告诉我们,它的属性被修改了呢?
接下来,我们使用Object.defineProperty()
改写上面的例子:
let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
get(){
console.log('price属性被读取了')
return val
},
set(newVal){
console.log('price属性被修改了')
val = newVal
}
})
通过Object.defineProperty()
方法给car
定义了一个price
属性,并把这个属性的读和写分别使用get()
和set()
进行拦截,每当该属性进行读或写操作的时候就会出发get()
和set()
。如下图:
可以看到,car
已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个car
的数据对象已经是“可观测”的了。
为了把car
的所有属性都变得可观测,我们可以编写如下两个函数:
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable (obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) =>{
defineReactive(obj,key,obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
Object.defineProperty(obj, key, {
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
现在,我们就可以这样定义car
:
let car = observable({
'brand':'BMW',
'price':3000
})
car
的两个属性都变得可观测了。
4. 依赖收集
完成了数据的'可观测',即我们知道了数据在什么时候被读或写了,那么,我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
现在,我们需要创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。
创建消息订阅器Dep:
class Dep {
constructor(){
this.subs = []
},
//增加订阅者
addSub(sub){
this.subs.push(sub);
},
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
},
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;
有了订阅器,再将defineReactive
函数进行改造一下,向其植入订阅器:
function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}
从代码上看,我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target
,这是一个全局唯一 的Watcher
,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher
被计算,另外它的自身属性 subs
也是 Watcher
的数组。
我们将订阅器Dep添加订阅者的操作设计在getter
里面,这是为了让Watcher
初始化时进行触发,因此需要判断是否要添加订阅者。在setter
函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
到此,订阅器Dep设计完毕,接下来,我们设计订阅者Watcher.
5. 订阅者Watcher
订阅者Watcher
在初始化的时候需要将自己添加进订阅器Dep
中,那该如何添加呢?我们已经知道监听器Observer
是在get
函数执行了添加订阅者Wather
的操作的,所以我们只要在订阅者Watcher
初始化的时候出发对应的get
函数去执行添加订阅者操作即可,那要如何触发get
的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty( )
进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者Watcher
初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target
上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher
的实现如下:
class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
},
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
},
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
过程分析:
订阅者Watcher
是一个 类,在它的构造函数中,定义了一些属性:
- vm:一个Vue的实例对象;
- exp:是
node
节点的v-model
或v-on:click
等指令的属性值。如v-model="name"
,exp
就是name
; - cb:是
Watcher
绑定的更新函数;
当我们去实例化一个渲染 watcher
的时候,首先进入 watcher
的构造函数逻辑,就会执行它的 this.get()
方法,进入 get
函数,首先会执行:
Dep.target = this; // 缓存自己
实际上就是把 Dep.target
赋值为当前的渲染 watcher
,接着又执行了:
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
在这个过程中会对 vm
上的数据访问,其实就是为了触发数据对象的getter
。
每个对象值的 getter
都持有一个 dep
,在触发 getter
的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher
订阅到这个数据持有的 dep
的 subs
中,这个目的是为后续数据变化时候能通知到哪些 subs
做准备。
这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target
恢复成上一个状态,即:
Dep.target = null; // 释放自己
因为当前vm
的数据依赖收集已经完成,那么对应的渲染Dep.target
也需要改变。
而update()
函数是用来当数据发生变化时调用Watcher
自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前get()
获得的旧数据进行比较,如果不一样,则调用更新函数cb
进行更新。
至此,简单的订阅者Watcher
设计完毕。
6. 测试
完成以上工作后,我们就可以来真正的测试了。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1 id="name"></h1>
<input type="text">
<input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
function myVue (data, el, exp) {
this.data = data;
observable(data); //将数据变的可观测
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
var ele = document.querySelector('#name');
var input = document.querySelector('input');
var myVue = new myVue({
name: 'hello world'
}, ele, 'name');
//改变输入框内容
input.oninput = function (e) {
myVue.data.name = e.target.value
}
//改变data内容
function changeInput(){
myVue.data.name = "xfcao"
}
</script>
</body>
</html>
但是还有一个细节问题,我们在赋值的时候是这样的形式 ' myVue.data.name = "xfcao" ' 而我们理想的形式是' myVue.name = "xfcao" '为了实现这样的形式,我们需要在new myVue的时候做一个代理处理,让访问myVue的属性代理为访问myVue.data的属性,实现原理还是使用Object.defineProperty( )对属性值再包一层:
index.js
function myVue (data, el, exp) {
var self = this;
this.data = data;
Object.keys(data).forEach(function(key) {
self.proxyKeys(key); // 绑定代理属性
});
observe(data);
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
myVue.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function proxyGetter() {
return self.data[key];
},
set: function proxySetter(newVal) {
self.data[key] = newVal;
}
});
}
}
observer.js
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable (obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) =>{
defineReactive(obj,key,obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}
class Dep {
constructor(){
this.subs = []
}
//增加订阅者
addSub(sub){
this.subs.push(sub);
}
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
}
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;
watcher.js
class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}
最后,我们来实现一个编译器
编译器:扫描和解析每个节点元素,替换模版数据,绑定事件监听函数,初始化订阅者
/**
* 编译器
* @param {String} el 根元素
* @param {Object} vm vue对象
*/
function Compile(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.fragment = null;
this.init();
}
Compile.prototype = {
constructor: Compile,
init: function() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el); // 移除页面元素生成文档碎片
this.compileElement(this.fragment); // 编译文档碎片
this.el.appendChild(this.fragment);
} else {
console.log('DOM Selector is not exist');
}
},
/**
* 页面DOM节点转化成文档碎片
*/
nodeToFragment: function(el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while(child) {
fragment.appendChild(child); // append后,原el上的子节点被删除了,挂载在文档碎片上
child = el.firstChild;
}
return fragment;
},
/**
* 编译文档碎片,遍历到当前是文本节点,则编译文本节点;如果当前是元素节点,并且存在子节点,则继续递归遍历
*/
compileElement: function(fragment) {
var childNodes = fragment.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{\s*((?:.|\n)+?)\s*\}\}/g;
var text = node.textContent;
if (self.isElementNode(node)) {
self.compileAttr(node);
} else if (self.isTextNode(node) && reg.test(text)) { // test() 方法用于检测一个字符串是否匹配某个模式
reg.lastIndex = 0
self.compileText(node, reg.exec(text)[1]); // exec() 方法用于检索字符串中的正则表达式的匹配
}
if (node.childNodes && node.childNodes.length) { // 递归遍历
self.compileElement(node);
}
})
},
/**
* 编译属性
*/
compileAttr: function(node) {
var self = this;
var nodeAttrs = node.attributes;
Array.prototype.forEach.call(nodeAttrs, function(attr) {
var attrName = attr.name; // attrName是DOM属性名,而exp是vue对象属性名
if (self.isDirective(attrName)) { // 只对vue本身指令进行操作
var exp = attr.value; // 属性名或函数名
if (self.isOnDirective(attrName)) { // v-on指令
self.compileOn(node, self.vm, attrName, exp);
} else if (self.isBindDirective(attrName)) { // v-bind指令
self.compileBind(node, self.vm, attrName, exp);
} else if (self.isModelDirective(attrName)) { // v-model
self.compileModel(node, self.vm, attrName, exp);
}
node.removeAttribute(attrName);
}
})
},
/**
* 编译v-on指令
*/
compileOn: function(node, vm, attrName, exp) {
var onReg = /^v-on:|^@/;
var eventType = attrName.replace(onReg, '');
var cb = vm.methods[exp];
node.addEventListener(eventType, cb.bind(vm), false);
},
/**
* 编译v-bind指令
*/
compileBind: function(node, vm, attrName, exp) {
var bindReg = /^v-bind:|^:/;
var attr = attrName.replace(bindReg, '');
node.setAttribute(attr, vm.data[exp]);
new Watcher(vm, exp, function(val) {
node.setAttribute(attr, val);
});
},
/**
* 编译v-model指令
*/
compileModel: function(node, vm, attrName, exp) {
var self = this;
var modelReg = /^v-model/;
var attr = attrName.replace(modelReg, '');
var val = vm.data[exp];
self.updateModel(node, val); // 初始化视图
new Watcher(vm, exp, function(value) { // 添加一个订阅者到订阅器
self.updateModel(node, value);
});
node.addEventListener('input', function(e) { // 绑定input事件
var newVal = e.target.value;
if (val == newVal) {
return;
}
self.vm.data[exp] = newVal;
}, false);
},
/**
* 属性是否是vue指令,包括v-xxx:,:xxx,@xxx
*/
isDirective: function(attrName) {
var dirReg = /^v-|^:|^@/;
return dirReg.test(attrName);
},
/**
* 属性是否是v-on指令
*/
isOnDirective: function(attrName) {
var onReg = /^v-on:|^@/;
return onReg.test(attrName);
},
/**
* 属性是否是v-bind指令
*/
isBindDirective: function(attrName) {
var bindReg = /^v-bind:|^:/;
return bindReg.test(attrName);
},
/**
* 属性是否是v-model指令
*/
isModelDirective: function(attrName) {
var modelReg = /^v-model/;
return modelReg.test(attrName);
},
/**
* 编译文档碎片节点文本,即对标记替换
*/
compileText: function(node, exp) {
var self = this;
var initText = this.vm.data[exp];
this.updateText(node, initText); // 初始化视图
new Watcher(this.vm, exp, function(val) {
self.updateText(node, val); // node?
});
},
/**
* 更新文本节点
*/
updateText(node, val) {
node.textContent = typeof val == 'undefined'? '': val;
},
updateModel(node, val, oldVal) {
node.value = typeof val == 'undefined'? '': val;
},
/**
* 判断元素节点
*/
isElementNode(node) {
return node.nodeType == 1;
},
/**
* 判断文本节点
*/
isTextNode(node) {
return node.nodeType == 3;
}
}