vue的双向绑定原理及实现
vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的。
通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情。
-
订阅者和发布者模式
订阅者和发布者模式,通常用于消息队列中.一般有两种形式来实现消息队列- 一是使用生产者和消费者来实现,
- 二是使用订阅者-发布者模式来实现。其中订阅者和发布者实现消息队列的方式,就会用订阅者模式。
-
原理以及基础
12653633-eead3f1968b694cd.png
Object.defineProperty-详细文档 -
小案例--在名字上加入括号
截屏2020-03-05下午9.55.21.png
let love = {};
let name = '';
Object.defineProperty(love,'name',{
set:function(value) {
name = value;
},
get:function() {
return '《'+name+'》';
}
});
love.name = '我的最爱';
console.log(love.name); //《我的最爱》
简单的mvvm双向绑定的demo
-
思路分析
12653633-fa21a82839e175a7.png
要想实现mvvm,主要包含两个方面,视图变化更新数据,数据变化更新视图。view变化更新data其实可以通过事件监听实现,比如input标签监听input事件,所有我们着重分析data变化更新view。data变化更新view的重点是如何知道view什么时候变化了,只要知道什么时候view变化了,那么接下来的就好处理了。这个时候我们上文提到的Object.defineProperty( )就起作用了。通过Object.defineProperty( )对属性设置一个set函数,当属性变化时就会触发这个函数,所以我们只需要将一些更新的方法放在set函数中就可以实现data变化更新view了。
-
实现过程
12653633-d82f70569b8c385f.png
首先要对数据进行劫持监听,所以首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新.属性可能是多个,所以会有多个订阅者,故需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理。以为在节点元素上可能存在一些指令,所以还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图。
1、 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { walk: function (data) { var self = this; Object.keys(data).forEach(function (key) { self.defineReactive(data, key, data[key]); }); }, defineReactive: function (data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function getter() { if (Dep.target) { dep.addSub(Dep.target); } return val; }, set: function setter(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); } }); } }; function observe(value, vm) { if (!value || typeof value !== 'object') { return; } return new Observer(value); }; function Dep() { this.subs = []; } Dep.prototype = { addSub: function (sub) { this.subs.push(sub); }, notify: function () { this.subs.forEach(function (sub) { sub.update(); }); } }; Dep.target = null;
2、 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己 var value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; } };
3、 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compile.prototype = { init: function () { if (this.el) { this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log('Dom元素不存在'); } }, nodeToFragment: function (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment; }, compileElement: function (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function (node) { var reg = /\{\{(.*)\}\}/; var text = node.textContent; if (self.isElementNode(node)) { self.compile(node); } else if (self.isTextNode(node) && reg.test(text)) { self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }, compile: function (node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function (attr) { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); if (self.isEventDirective(dir)) { // 事件指令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); } }); }, compileText: function (node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); new Watcher(this.vm, exp, function (value) { self.updateText(node, value); }); }, compileEvent: function (node, vm, exp, dir) { var eventType = dir.split(':')[1]; var cb = vm.methods && vm.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false); } }, compileModel: function (node, vm, exp, dir) { var self = this; var val = this.vm[exp]; this.modelUpdater(node, val); new Watcher(this.vm, exp, function (value) { self.modelUpdater(node, value); }); node.addEventListener('input', function (e) { var newValue = e.target.value; if (val === newValue) { return; } self.vm[exp] = newValue; val = newValue; }); }, updateText: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value; }, 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; } }
4、 index.js
function SelfVue(options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function (key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // 所有事情处理好后执行mounted函数 } SelfVue.prototype = { proxyKeys: function (key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function getter() { return self.data[key]; }, set: function setter(newVal) { self.data[key] = newVal; } }); } }
5、 index.html
{{title}}
{{name}}