目录

1. 什么是双向数据绑定?

数据双向绑定是指:数据的变化驱动视图的更新,视图的变化驱动数据的更新,如图所示:

【Vue】Vue数据双向绑定原理_dom

2. 双向绑定的原理是什么?

Vue是一种双向数据绑定的框架,整个框架由三部分组成:

  • 数据层(Model):应用的数据及业务逻辑,是开发者写的业务代码
  • 视图层(View):应用的页面展示效果,由页面模板和样式组成
  • 业务逻辑层(ViewModel):框架封装的核心,它的主要功能是将数据与视图关联起来

上面这三部分就是所谓的MVVM框架。而关键点就在于业务逻辑层(ViewModel) 部分。

ViewModel的主要职责就是在数据变化之后更新视图在视图变化之后更新数据。

数据双向绑定原理图:

【Vue】Vue数据双向绑定原理_dom_02


我们可以看出,数据双向绑定就是通过数据劫持结合发布者-订阅者模式 实现的。

原理: 通过​​Object.defineProperty​​​来劫持数据的​​setter​​​、​​getter​​,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。

3. 双向数据绑定的实现

注: 这部分代码不是本人写的,还没有那个水平,代码来自github,个人感觉写的非常不错,地址:​​链接直达​

通过以下四个步骤实现数据数据的双向绑定:

  1. 实现一个监听器​​Observer​​ ,对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者;
  2. 实现一个解析器​​Compile​​,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数;
  3. 实现一个发布订阅模型​​Watcher​​+​​Dep​​,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。;
  4. 实现一个​​MVVM​​,整个以上三者,作为一个入口函数;

其中:

  • ​Dep​​是发布订阅者模型中的发布者:get数据的时候,收集订阅者,触发Watcher的依赖收集;set数据时发布更新,通知Watcher 。一个Dep实例对应一个对象属性或一个被观察的对象,用来收集订阅者和在数据改变时,发布更新。
  • ​Watcher​​​是发布订阅者模型中的订阅者:订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。一个Watcher可以更新视图,如html模板中用到的{{test}},也可以执行一个​​$watch​​监督的表达式的回调函数(Vue实例中的watch项底层是调用的​​$watch​​实现的),还可以更新一个计算属性(即Vue实例中的computed项)。
3.1 监听器 Observer 的实现

Vue使用了​​Object.defineProperty()​​​方法来劫持数据属性的​​getter​​​和​​setter​​。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法: (​​MDN的相关解释​​)

Object.defineProperty(obj, prop, descriptor)

参数如下:

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符

主要注意的是,应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

数据监听功能实现:

使用​​Obeject.defineProperty()​​​来监听属性变动,就需要observe的数据对象进行递归遍历,包括子属性对象的属性,给它们都加上 ​​setter​​​和​​getter​​:

function observe(obj){
// 判断是否为对象、是否为空对象
if (!obj || typeof obj !== 'object') {
return;
}
// 遍历对象的所有属性,Object.keys会返回一个包含对象所有可枚举属性的数组
Object.keys(obj).forEach(function(key) {
defineReactive(obj, key, obj[key]);
});
}

function defineReactive(obj, key, value){
observe(value) // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 属性可枚举
get: function() {
return value;
},
set: function(newVal) {
if (value === newVal) return;
value = newVal;
}
});
}

这样observe的数据监听的功能就实现了,那么下面就来实现一下他的数据变化通知订阅者的功能, 我们只要定义一个数组,来收集订阅者,数据变化时,触发notify来通知所有的订阅者,在调用订阅者的数据更新功能:

// 首先要对上面定义的函数进行改造
function defineReactive(data, key, value) {
var dep = new Dep(); // 创建一个订阅器
observe(value); // 监听子属性

Object.defineProperty(data, key, {
enumerable: true, // 属性可枚举
get: function() {
if (Dep.target) {
dep.addSub(Dep.target); // 收集所有订阅者
}
return value;
},
set: function(newVal) {
if (value === newVal) return;
val = newVal;
dep.notify(); // 通知所有订阅者
}
});
}
// 定义订阅者
function Dep() {
this.subs = []; // 用来维护订阅者
}
Dep.prototype = {
// 收集订阅者
addSub: function(sub) {
this.subs.push(sub);
},
// 数据更新
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};

这样,数据变化通知订阅者的功能也就实现了。

发布-订阅者设计模式 又叫观察者模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。

3.2 解析器 Compile 的实现

解析器 Compile 的作用如下:

  • 解析模板指令,替换模板数据,然后初始化渲染页面视图
  • 将每个指令对应的节点添加监听数据的订阅者,并绑定更新函数

实现步骤如下:

function Compile(el) {
//传入的可能是 #app或者document.getElementById('app'),所以需要进行判断
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 1. 将DOM节点发给在文档片段fragment中
this.$fragment = this.nodeToFragment(this.$el);
// 2. 模板编译
this.init();
// 3. 将编译好的文档放在页面中
this.$el.appendChild(this.$fragment);
}
}

(1)将模板实例拷贝在​​fragment​​文档片段中,减少DOM操作。

Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
nodeToFragment: function(el) {
let fragment = document.createDocumentFragment();
let child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};

(2)编译:调用对应的指令渲染函数进行数据的渲染,并将对应的指令更新函数进行绑定。

.prototype = {
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; // 表达式文本
// 按元素节点方式编译
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;

[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}

node.removeAttribute(attrName);
}
});
},

compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
},

isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},

isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
},

isElementNode: function(node) {
return node.nodeType == 1;
},

isTextNode: function(node) {
return node.nodeType == 3;
}
};

// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},

html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},

model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');

var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}

me._setVMVal(vm, exp, newValue);
val = newValue;
});
},

class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},

bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];

updaterFn && updaterFn(node, this._getVMVal(vm, exp));

new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},

// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];

if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},

_getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},

_setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};


var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},

htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},

classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');

var space = className && String(value) ? ' ' : '';

node.className = className + space + value;
},

modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};

(3) 将编译好的文档放在页面中

this.$el.appendChild(this.$fragment);

这样解析器就完成了。

3.3 订阅者 Watcher 的实现

Watcher的使用场景:

  • 观察模板中的数据
  • 观察创建Vue实例时watch选项里的数据
  • 观察创建Vue实例时computed选项里的数据所依赖的数据

订阅者 Watcher 作为解析器和监听器的之间的桥梁,其主要作用是:

  • 在自身实例化时往属性订阅器(dep)里面添加自己
  • 自身必须有一个​​update()​​方法
  • 属性变动​​dep.notice()​​​通知时,能调用自身的​​update()​​方法,并触发Compile中绑定的回调
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.vm[exp]; // 触发getter,添加自己到属性订阅器中
Dep.target = null; // 添加完毕,重置
return value;
}
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 调用订阅者的update方法,通知变化
});
}
};

实例化Watcher的时候,调用​​get()​​​方法,通过​​Dep.target = watcherInstance​​​标记订阅者是当前​​watcher​​​实例,强行触发属性定义的​​getter​​​方法,​​getter​​​方法执行的时候,就会在属性的订阅器​​dep​​​添加当前​​watcher​​实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

3.4 实现一个MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,实现了数据的双向绑定。

function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data,
me = this;
// 属性代理,实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};

这里主要还是利用了​​Object.defineProperty()​​​这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了​​vm._data​​的属性值,达到需要的效果。