首先我们先解释一下下面用到的几个函数和对象
1、observe 观察者
2、Watcher 监听者
3、dep 监听者容器,就是一个数据的监听者(Wacher)都放在里面
4、notifiy 用来通知所有某数据的所有监听者,即通知数据的 dep的所有对象
5、updater 更新器包含多个更新器 根据不同的数据类型,执行不同的更新器。如 文本节点 指向TextUpdater 的逻辑
6、Combine 编译器 获取dom节点 将里面的模板数据 替换为真实数据。如
<div> {{name}} </div> 编译之后 变为 <div> 小明</div>
我也是初学者,这里看了别人的原码 加上了自己的理解
源码博客请看源码的博客地址
这些函数具体怎么用,怎么配合,请看下面:
一、数据代理
function MVVM(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
var _this = this;//当前实例vm
// 数据代理
// 实现 vm._data.xxx -> vm.xxx
Object.keys(data).forEach(function(key) {
_this._proxyData(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this);//定义编译器且初始化
}
MVVM.prototype = {
//数据代理函数实现
_proxyData: function(key) {
var _this = this;
if (typeof key == 'object' && !(key instanceof Array)){//这里只实现了对对象的监听,没有实现数组的
this._proxyData(key);
}
Object.defineProperty(_this, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return _this._data[key];
},
set: function proxySetter(newVal) {
_this._data[key] = newVal;
}
});
},
};
这里使用了 Object.defineProperty(对象,属性,配置)
来实现数据代理,为什么要数据代理呢 ?
1、不想暴露内部逻辑
2、方便访问 如 vm._data.xxx -> vm.xxx
而且这里采用递归的方式 为对象以及对象的属性都添加代理
二、观察者
为数据添加观察者,即上面代码的observe(data, this);
function observe(data){
if (typeof data != 'object') {
return ;
}
return new Observe(data);
}
function Observe(data){
this.data = data;
this.walk(data);//遍历 data
}
Observe.prototype = {
walk: function(data){
let _this = this;
for (key in data) {
if (data.hasOwnProperty(key)){//如果这个属性自身存在
let dep = new Dep();
let value = data[key];
if (typeof value == 'object'){
observe(value);
}
_this.defineReactive(data,key,data[key]);
}
}
},
defineReactive: function(data,key,value){//为这个属性设置get方法,同时生成一个dep数组订阅器,
let dep = new Dep();//在data的属性key内形成闭包,因此每个key都对应一个唯一的订阅器
Object.defineProperty(data,key,{
enumerable: true,//可枚举
configurable: false,//不能再define
get: function(){
//console.log('你访问了' + key);//测试代码,忽略
//如果是是初始化一个Watcher时引起的,则添加进订阅器
if (Dep.target){
dep.addSub(Dep.target);
}
//console.log(dep);//测试代码,忽略
return value;
},
set: function(newValue){
//console.log('你设置了' + key);//测试代码,忽略
if (newValue == value) return;
value = newValue;
observe(newValue);//监听新设置的值
//console.log(dep);//测试代码,忽略
dep.notify();//通知所有的订阅者
}
})
}
}
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub) {
sub.update();//每个监听器都有 update 函数
})
}
}
这段代码为每一个数据都添加get/set函数,当数据发生改变时,做出一些响应。为什么要做出响应啊?为了当该数据
改变的时候去通知关注该数据的那些节点
让他更新。但是这时候dep数组
里面没有对象,我怎么知道谁使用它了呢? 其实dep里的对象是编译的时候添加进去的。。
三、编译模板
编译就是为了把dom中绑定的变量 编译为真实的数据,并且添加对该数据的监听,这里说说其中的一些要点。
1、let fragment = document.createDocumentFragment();
fragment 的appendChild(child); 会把child剪切到内存中,对其进行操作,当然原来的child所指向的节点就会从dom树移除。
那么为什么要这么做呢??
我们都知道操作dom树是相当耗时的,因为我们对一个节点修改时,浏览器需要计算该元素相对于全局的位置、属性、样式等,而且当我们修改时会出现回流或者重绘,这是相当影响性能的。这里我们选择将要操作的dom 移动到内存中操作,就不会影响其他的dom元素,我们修改完成之后,在将该节点插入到dom树中,只计算一次,性能更好
2、我们是如何将节点添加到数据的监听者里的?
这里我们以所编译的节点为文本节点为例,如{{name}} ,看看是如何操作的。
(1)、首先用正则表达式/\{\{(.*)\}\}/
将变量取出,即name,
(2)、调用compileUtil 的text函数,text函数中又调用了bind方法
将节点绑定到dep数组,通过代码中通过new Watcher
new Watcher(vm,exp,function(value){ // wacher 把自己挂载到 vm的 订阅者里
updaterFn && updaterFn(node,value)//更新函数
});
我们看watcher,这个操作的意思就是把该节点弄成一个监听者对象,如何添加到dep数组呢?因为我们之前已经对name添加了get方法,并且那个get方法里
if (Dep.target){
dep.addSub(Dep.target);
}
我们在watcher的get函数里操作,通过Dep.target将自己传送到 name的get方法中,如下操作:因为有name有get方法,所以 this.vm[name]就会被调用
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm[this.exp]; // 强制访问自己,执行defineProperty里的get函数
Dep.target = null; // 释放自己
return value;
}
//Watcher
function Watcher(vm, exp, cb) {
this.vm = vm;
this.cb = cb;//更新函数
this.exp = exp;
this.value = this.get();//将自己添加进订阅器
};
Watcher.prototype = {
update: function(){
this.run();
},
run: function(){
const value = this.vm[this.exp];
//console.log('me:'+value);//测试代码,忽略
if (value != this.value){
this.value = value;
this.cb.call(this.vm,value);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm[this.exp]; // 强制访问自己,执行defineProperty里的get函数
Dep.target = null; // 释放自己
return value;
}
}
function Compile(el,vm){
this.$vm = vm;//vm为当前实例
this.$el = document.querySelector(el);//获得要解析的根元素
if (this.$el){
this.$fragment = this.nodeToFragment(this.$el);//将节点添加到文档碎片
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
nodeToFragment: function(el){
let fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild){
fragment.appendChild(child);//append相当于剪切的功能
}
return fragment;
},
init: function(){//初始编译
this.compileElement(this.$fragment);
},
compileElement: function(node){
let childNodes = node.childNodes;//获取子节点
const _this = this;
let reg = /\{\{(.*)\}\}/;
[].slice.call(childNodes).forEach(function(node){
if (_this.isElementNode(node)){//如果为元素节点
_this.compile(node);
} else if (_this.isTextNode(node) && reg.test(node.textContent)){
//如果为文本节点,并且包含data属性(如{{name}}),则进行相应操作
_this.compileText(node,reg.exec(node.textContent)[1]);
}
if (node.childNodes && node.childNodes.length){
//如果节点内还有子节点,则递归继续解析节点
_this.compileElement(node);
}
})
},
compileText: function(node,exp){
compileUtil.text(node,this.$vm,exp);
},
isElementNode: function(node){
return (node.nodeType == 1);
},
isTextNode: function(node){
return node.nodeType == 3;
},
isDirective: function(attr){
return (attr.indexOf('v-') == 0);
},
isEventDirective: function(attr){
return attr.indexOf('on') == 0;
},
isLinkDirective: function(attr){
return attr.indexOf('bind') == 0;
},
//编译节点内的属性
compile: function(node) {
console.log('编译节点的属性')
var nodeAttrs = node.attributes,//获取当前节点的属性,v-on:,v-bind:,v-model,class等属性
_this = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
if (_this.isDirective(attrName)) {//判断是否为vue指令
var exp = attr.value;
var dir = attrName.substring(2);
// 事件指令: v-on:
if (_this.isEventDirective(dir)) {
//node:当前节点, _this.$vm:当前实例, exp:指令属性值(表达式或函数), dir:指令类型(on)
compileUtil.eventHandler(node, _this.$vm, exp, dir);
}
if (_this.isLinkDirective(dir)){
let attr = dir.split(':')[1];
compileUtil[attr] && compileUtil[attr](node, _this.$vm, exp);
}else {
//其它指令: v-bind:,v-model
compileUtil[dir] && compileUtil[dir](node, _this.$vm, exp);
}
}
});
},
};
let updater = {
textUpdater: function(node,value){
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function(node, value) {
node.value = typeof value == 'undefined' ? '' : value;
},
classUpdater: function(node, value) {
var className = node.className;
node.className = className + value;
},
}
let compileUtil = {
eventHandler: function(node,vm,exp,dir){
let eventType = dir.split(':')[1];
let fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn){
node.addEventListener(eventType,fn.bind(vm),false);
}
},
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
model: function(node,vm,exp){
this.bind(node,vm,exp,'model');
let _this = this;
let value = node.value;
node.addEventListener('input',function(e){
let newValue = e.target.value;
if (value == newValue) return;
vm[exp] = newValue;//设置exp属性值,触发视图的更新
},false);
},
text: function(node,vm,exp){
this.bind(node,vm,exp,'text');
},
_getVMVal: function(vm,exp) {
let arr = exp.split('.');
let value = vm;
arr.forEach(function(item){//a.b -->arr = [a,b]-->value = ->> value = value[b]-->a[b]
value = value[item];//经过了数据代理,所以data里的数据直接绑定到了实例上
console.log('设置节点值',item,value)
})
//console.log(value);
return value;
},
bind: function(node,vm,exp,dir){//初始化相关
let updaterFn = updater[dir + 'Updater'];//获取更新器
updaterFn && updaterFn(node,this._getVMVal(vm,exp));//初始化首先去 vm实例拿值
new Watcher(vm,exp,function(value){ // wacher 把自己挂载到 vm的 订阅者里
updaterFn && updaterFn(node,value)//更新函数
});
//console.log('实例化了一个Watcher');//测试代码,忽略
}
};
至此,当我们修改某个数据的值的时候,就会自动调用该数据的监听者的updatefn函数,执行更新逻辑。