本文通过github上的一个MVVM框架github.com/DMQ/mvvm作探索记录,与vue的双向数据绑定有很多类似的思想可以帮助理解vue。
该MVVM框架实现的原理图:
<div id="app">
<h1>{{student.name}}</h1>
<button v-on:click="changeData">change student.name</button>
</div>
<script>
const vm = new MVVM({
el: '#app',
data: {
student: { name:'jerry' }
},
methods: {
changeData(){
this.student.name = 'lisa'
}
}
});
</script>
初始化视图阶段
在实例化vm实例时首先进行通过_proxyData()应用Object.defineProperty实施数据代理,配置对象中this可以访问到data的数据,通过initComputed()简单代理compute计算属性
function MVVM(options) {
// 保存option到vm实例
this.$options = options || {};
// 保存data对象为_data到vm实例
var data = this._data = this.$options.data;
// 保存vm为me
var me = this;
// 数据代理
// 实现 vm.xxx -> vm._data.xxx
// 遍历data对象得到每个key,调用实例方法_proxyData(key),为每个data上的属性创建<访问器属性,get和set>与_data对象的属性联动,这些存取器都是定义在vm实例对象上的,完成了<数据代理>
Object.keys(data).forEach(function(key) {
me._proxyData(key);
});
// <初始computed>,跟上面数据代理很相像,遍历了computed对象,并在vm实例对象上绑定<访问器属性,get>与computed的属性联动,(未见访问器属性set的设置)
this._initComputed();
observe(data, this);
// 编译模板
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
constructor: MVVM,
$watch: function(key, cb, options) {
new Watcher(this, key, cb);
},
_proxyData: function(key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
},
_initComputed: function() {
var me = this;
var computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach(function(key) {
Object.defineProperty(me, key, {
get: typeof computed[key] === 'function'
? computed[key]
: computed[key].get,
set: function() {}
});
});
}
}
};
observe(data, this)开始数据劫持阶段
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
/*
数据劫持与监听器:劫持监听所有数据的变化
*/
function Observer(data) {
// 将vm实例的data对象,绑定一份在劫持者对象中,显然这是堆区中的同一个data对象
this.data = data;
// 进行数据劫持
this.walk(data);
}
Observer.prototype = {
constructor: Observer,
walk: function (data) {
var me = this;
// 遍历劫持数据对象 的 所有属性和值 -> 定义扩展属性访问器get、set来劫持对象
Object.keys(data).forEach(function (key) {
me.convert(key, data[key]);
});
},
convert: function (key, val) {
this.defineReactive(this.data, key, val);
},
defineReactive: function (data, key, val) {
// 新建一个消息订阅器
var dep = new Dep();
// 递归:对数据的所有层次进行数据劫持
var childObj = observe(val);
// 通过为每一项数据添加数据读取器get()、set()进行数据劫持
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function () {
// 当有watcher访问到该属性时
if (Dep.target) {
dep.depend();
}
return val;
},
set: function (newVal) {
//
if (newVal === val) {
return;
}
val = newVal;
// 对新的值补充劫持
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
};
/*
Dep:消息订阅器
它的结构:
{
id:消息订阅器的唯一标识(自动生成)
target:一个临时指向watcher的指针,当有新的watcher创建时,这个临时指针指向新创建的watcher,并且,watcher新建时访问了一次被劫持的属性,在那个时刻watcher与dep相互绑定(watcher调用addDep方法,在watcher的depIds容器中绑定Dep,在Dep的sbus容器中添加该新建的watcher)此过程称为<添加订阅者>
subs:[] 储存watcher的容器
}
Dep的生命周期:创建、添加订阅者、通知
从observer对数据劫持的过程可以知道:
1.Dep是进行数据劫持的时候创建的,data对象中的属性被深度劫持,
如果将data对象看做树状结构,那么树的每一个节点都对应创建了一个消息订阅器Dep
2.observer的数据劫持是动态的,set()访问器中补充了对新值的劫持:observer(newVal),如果新值为一个对象则劫持它的所有属性并新建Dep
3.Dep添加订阅者这个动作,是watcher新建的时刻进行的,数据劫持时为get()部署了绑定dep的流程(watcher新建时每访问一次get(),绑定流程就会执行一次,例如针对a.b.c这样的表达式,watcher访问了三次get,绑定了三个Dep)
从watcher的角度也可以知道:
1.Dep虽然为data对象树的每一个属性节点都创建了一次,但是它只绑定仅有的watcher
2.watcher 与 Dep,显然是一种多对多的关系;一个watcher有可能同时绑定了多个Dep(表达式a.b.c),一个Dep也可能绑定着多个watcher(一个属性)
*/
var uid = 0;
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
depend: function () {
// addDep方法是watcher的方法
Dep.target.addDep(this);
},
removeSub: function (sub) {
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
};
Dep.target = null;
随后this.$compile = new Compile(options.el || document.body, this)进行模板的编译,在编译阶段针对 普通指令v-text、v-html、v-class(这个是这个框架自己的)、{{}}等实例化对应的watcher(传入一个调用updater的回调)向Dep消息订阅者添加订阅者,并监听消息订阅器通知,编译的初始化结果挂载到真实DOM上完成初始化视图显示。
/*
Compile模板编译器
它上面将会挂载有
{ $vm:mvvm实例对象,
$el:真实DOM中的标记模板,
$fragment:内存中的模板,用于编译
}
*/
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 创建fragment容器并把模板中的所有节点抛入:此时DOM被清空,view视图空白
this.$fragment = this.node2Fragment(this.$el);
// 进入编译器,在fragment容器中编译其中的每一个节点
this.init();
// 在DOM原来的位置中插入已编译好的模板,此时DOM树被构建,view视图渲染
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
node2Fragment: function (el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function () {
this.compileElement(this.$fragment);
},
compileElement: function (el) {
var childNodes = el.childNodes,
// 保存一下编译器对象
me = this;
[].slice.call(childNodes).forEach(function (node) {
var text = node.textContent;
// 该正则用于匹配{{xxx}}
var reg = /\{\{(.*)\}\}/;
// 编译器的核心部分:判断节点类型(元素节点 / 文本节点(需要编译'{{}}'部分))
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
// 匹配{{}}
// RegExp.$1.trim()?? 表示第一个子匹配,即{{word}}中的word
me.compileText(node, RegExp.$1.trim());
}
// 如果该节点之下还有子节点,递归编译,直到编译完全部节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
/*
编译元素节点
主要工作:遍历属性->判断并确定指令类型(事件指令v-on / 普通指令[v-text、v-html、v-class])
*/
compile: function (node) {
// 获取该元素节点上所有的属性(节点)
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function (attr) {
// attr:如v-on:xxx = 'exp' 、v-text='exp'
//attrName: 如v-on:xxx 、 v-text
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);
}
});
},
/*
编译文本节点
主要工作:调用指令处理器compileUtil,取出当前vm实例的具体属性的值,更新到虚拟DOM上
*/
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');
},
// 双向数据绑定的处理:
// 该指令的设计类似于 事件指令+普通指令
// 相对于普通指令,它额外绑定了input事件监听,当input事件触发,调用被劫持属性的set()
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) {
// 更新器对象updater:
// 包含多种更新方法,具体根据指令名['text'、'html'、'class'、’model‘]等进行匹配更新方法
var updaterFn = updater[dir + 'Updater'];
// 获取了更新方法并执行了更新方法
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 新建了一个观察者???-》watcher.js
new Watcher(vm, exp, function (value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 在DOM上绑定事件监听
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是指令的表达式,
// 由于可能是word也可能是word.detail这样的形式,这里需要循环历经多次地从实例对象中取出该值
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;
}
});
}
};
// 多种指令下更新viwe视图的方法 对应各自的指令
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;
}
};
watcher.js
/*
watcher:观察者
在模板编译的时候,具体在bind()时刻(可见watcher对象 仅为 模板中的普通指令表达式所构建的,一个表达式对应一个watcher)
实例化了一个观察者 -> (传入了vm实例对象,指令表达式,一个回调函数<操作updater进行虚拟DOM更新>),
它的扼要结构
{
vm:vm实例
cb:一个调用updater的回调函数,当Dep通知该watcher时调用
expOrFn:指令表达式
depIds:储存Dep的容器
getter: 访问属性的方法,调用它会触发被劫持属性的get()
}
*/
function Watcher(vm, expOrFn, cb) {
// 回调函数
this.cb = cb;
// vm实例
this.vm = vm;
// 指令表达式
this.expOrFn = expOrFn;
// 一个储存器:储存了该watcher依赖的消息订阅器Dep
this.depIds = {};
// getter?
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
// 访问了一次data中对应属性的get(),并保存该值
this.value = this.get();
}
Watcher.prototype = {
constructor: Watcher,
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);
}
},
// 在depIds容器中添加消息订阅器Dep,同时在Dep中的subs[]容器中保存watcher
addDep: function(dep) {
// 1. 每次调用run()的时候会触发相应属性的getter
// getter里面会触发dep.depend(),继而触发这里的addDep
// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
// 则不需要将当前watcher添加到该属性的dep里
// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
// 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
},
get: function() {
Dep.target = this;
var value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
},
parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
数据更新阶段
当一些操作致使data中的数据发生改变,访问了Observer劫持属性的set(),此时调用dep.notify()遍历dep上绑定的所有订阅者watcher并调用watcher上的update方法,执行初始化视图阶段绑定的更新函数调用updater完成视图更新。
双向数据绑定的实现
主要关注v-model这个指令,模板编译时在dom上绑定input事件监听,事件被触发时(如输入框数据变化),则访问observer劫持的对应属性的set()访问器,继而触发视图更新
// 双向数据绑定的处理:
// 该指令的设计类似于 事件指令+普通指令
// 相对于普通指令,它额外绑定了input事件监听,当input事件触发,调用被劫持属性的set()
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;
});
}