手写Vue中MVVM专题三
专题一和专题二中已经实现模板解析和数据绑定。
还需要实现的是数据劫持及数据和视图的绑定。
1. 准备工作
js目录下新建Observer.js和Watcher.js文件,并在index.html中引入新建文件。
目录结构如下:
2. 双向绑定效果实现
1. 编写Vue.js文件
Vue.js 实例化Observer构造函数,并传入需要的参数data。
function Vue (opt) {
this.$el = opt.el;
this.$data = opt.data;
this.init();
}
Vue.prototype = {
init: function () {
new Observer(this.$data);
new Compile(this.$el, this);
}
}
2. 编写Observer.js文件
function Observer (data) {
this.data = data;
this.init();
}
Observer.prototype = {
init: function () {
}
}
3. 数据劫持处理
对数据递归遍历处理,利用Object.defineProperty()方法对属性进行定义,添加setter和getter方法,从而实现数据劫持的效果。
编写observe数据处理方法。该方法主要用于遍历数据,并为每个属性都定义getter和setter。
/**
* 数据处理
*
* @param {*} data
*/
observe (data) {
if (!data || typeof data != 'object') {
return;
}
var _self = this;
Object.keys(data).forEach(function (key) {
_self.defineReactive(data, key, data[key]);
_self.observe(data[key]); // 处理嵌套属性问题
});
}
编写用于数据劫持的defineReactive方法,方式主要用于监听数据变化。
对数据进行赋值或者取值操作时,会触发get和set方法。
/**
* 数据劫持
*
* @param {*} obj
* @param {*} key
* @param {*} value
*/
defineReactive (obj, key, value) {
var _self = this;
Object.defineProperty (obj, key, {
enumerable: true,
configurable: true,
get: function () {
return value;
},
set: function (newValue) {
if (newValue !== value) {
_self.observe(newValue); // 处理重新赋值问题 vm.$data.message = {}
value = newValue;
}
}
});
}
init函数中调用observer方法。
init: function () {
this.observe(this.data);
}
现在已经完成数据劫持,当数据变化的时候可以在get和set方法中检测到。
还需要做的是如何在数据改变的时候通知视图渲染。
4. 优化compileTools.js文件
compileTools文件用来处理页面模板编译后的赋值。
旧代码
var CompileTools = {
/**
* 处理v-model指令
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
model: function (node, vm, exp) {
var updaterFn = this.updater['modelUpdater'];
// value vm.$data[exp]
updaterFn && updaterFn(node, this.getVal(vm, exp));
},
/**
* 处理text节点
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
text: function (node, vm, exp) {
var updaterFn = this.updater['textUpdater'],
reg = /\{\{([^}]+)\}\}/g;
// {{ message.a }} -> message.a
var value = exp.replace(reg, function (node, key) {
return key;
});
updaterFn && updaterFn(node, this.getVal(vm, trimSpace(value)));
},
updater: {
modelUpdater: function (node, value) {
node.value = value;
},
textUpdater: function (node, value) {
node.textContent = value;
}
},
/**
* 对象取值
*
* @param {*} vm
* @param {*} exp
*/
getVal: function (vm, exp) {
exp = exp.split('.');
return exp.reduce(function (prev, next) {
return prev[next];
}, vm.$data);
}
}
将getVal方法和处理文本节点属性值的代码抽象成方法放入tools.js文件中。
/**
* 获取对象属性值
*
* @param {*} vm
* @param {*} exp
*/
function getVal (vm, exp) {
exp = exp.split('.'); // [x, y, z]
return exp.reduce(function (prev, next) {
return prev[next];
}, vm.$data);
}
/**
* 处理文本节点属性值 {{ message.a }} -> message.a
*
* @param {*} exp
*/
function getTextValue (exp) {
var reg = /\{\{([^}]+)\}\}/g;
var value = exp.replace(reg, function (node, key) {
return key;
});
return trimSpace(value);
}
在compileTools.js文件中调用上述方法。
新代码:
var CompileTools = {
/**
* 处理v-model指令
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
model: function (node, vm, exp) {
var updaterFn = this.updater['modelUpdater'];
// value vm.$data[exp]
updaterFn && updaterFn(node, getVal(vm, exp));
},
/**
* 处理text节点
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
text: function (node, vm, exp) {
var updaterFn = this.updater['textUpdater'],
value = getTextValue(exp);
updaterFn && updaterFn(node, getVal(vm, value));
},
updater: {
modelUpdater: function (node, value) {
node.value = value;
},
textUpdater: function (node, value) {
node.textContent = value;
}
}
}
5. 编写Watcher.js文件
Watcher中定义get和update方法,用于获取修改之前的数据和更新数据。
Watcher构造函数有3个参数。
1. 操作的虚拟节点
2. 获取属性值的表达式
3. 更新时触发的函数
function Watcher (vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype = {
/**
* 获取修改前的数据
*/
get () {
var value = getVal(this.vm, this.exp);
return value;
},
/**
* 更新数据
*
*/
update () {
var newValue = getVal(this.vm, this.exp),
oldValue = this.value;
if (newValue != oldValue) {
this.cb(newValue);
}
}
}
get用于获取之前的数据,保存到value属性中,当数据更新时,update方法会比较新旧数据,如果数据不相等,就触发更新的回调函数。
6. 在compileTools.js中使用Watcher
在处理元素节点和文本节点的方法中使用Watcher。
当模板编译时,会给解析出来的值逐一添加Watcher。
/**
* 处理v-model指令
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
model: function (node, vm, exp) {
var updaterFn = this.updater['modelUpdater'];
new Watcher(vm, exp, function (value) {
updaterFn && updaterFn(node, value);
});
// value vm.$data[exp]
updaterFn && updaterFn(node, getVal(vm, exp));
},
/**
* 处理text节点
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
text: function (node, vm, exp) {
var updaterFn = this.updater['textUpdater'],
value = getTextValue(exp);
new Watcher(vm, value, function (value) {
updaterFn && updaterFn(node, value);
});
updaterFn && updaterFn(node, getVal(vm, value));
}
7. 连接数据和视图
上述已经实现Watcher的初始化,也实现数据劫持,现在需要把两者结合起来。
Observer.js中定义Dep构造函数。
Dep用于管理多个Watcher,并且提供数据更新的操作。
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function (watcher) {
this.subs.push(watcher);
},
notify: function () {
this.subs.forEach(function (watcher) {
watcher.update();
});
}
}
在Watcher.js中使用Dep。由于每个数据都有自己的Watcher,我们可以给其设置自己的Dep。
/**
* 获取修改之前的数据
*/
get () {
Dep.target = this;
var value = getVal(this.vm, this.exp);
Dep.target = null;
return value;
}
在自定义的defineReactive的get和set中,定义Watcher的收集和Wachter的更新。
/**
* 数据劫持
*
* @param {*} obj
* @param {*} key
* @param {*} value
*/
defineReactive (obj, key, value) {
var _self = this,
dep = new Dep();
Object.defineProperty (obj, key, {
enumerable: true,
configurable: true,
get: function () {
Dep.target && dep.addSub(Dep.target);
return value;
},
set: function (newValue) {
if (newValue !== value) {
_self.observe(newValue);
value = newValue;
dep.notify();
}
}
});
}
现在实际上已经实现了数据的双向绑定。可以通过以下案例测试。
将之前的实例化的message数据提取到全局,在开发者工具中修改数据查看。
index.html
var message = {
a: 'HelloWorld'
}
new Vue({
el: '#app',
data: {
message: message
}
});
效果如下:
修改前:
修改后:
可以看到,当修改message.a的数据时,页面会重新渲染,展示新的数据。
8. 处理input输入框
数据双向绑定已经实现,但是还需要监听input的操作。
当数据变化时,同步修改数据,实现页面重新渲染。
tools.js 定义addEvent兼容性函数,用于添加事件处理函数。
/**
* 添加监听事件
*
* @param {*} el
* @param {*} type
* @param {*} fn
*/
function addEvent (el, type, fn) {
if (el.addEventListener) {
el.addEventListener(type, fn, false);
} else if (el.attachEvent) {
el.attachEvent('on' + type, function(){
handle.call(el);
})
} else {
el['on' + type] = fn;
}
}
tools.js 定义设置对象属性值的方法,用于input的赋值操作。
/**
* 设置对象属性值
*
* @param {*} vm
* @param {*} exp
* @param {*} value
*/
function setVal (vm, exp, value) {
exp = exp.split('.'); // [x, y, z]
return exp.reduce(function (prev, next, index) {
if (index === exp.length - 1) {
return prev[next] = value;
}
return prev[next];
}, vm.$data);
}
compileTools.js中的model方法中增加对input的处理。
/**
* 处理v-model指令
*
* @param {*} node
* @param {*} vm
* @param {*} exp
*/
model: function (node, vm, exp) {
var updaterFn = this.updater['modelUpdater'];
new Watcher(vm, exp, function (value) {
updaterFn && updaterFn(node, value);
});
addEvent(node, 'input', function (event) {
var e = event || window.event,
target = e.target || e.srcElement,
newValue = target.value;
setVal(vm, exp, newValue);
});
// value vm.$data[exp]
updaterFn && updaterFn(node, getVal(vm, exp));
},
监听input事件,并绑定事件处理函数。当数据变化时,更改数据,进而触发页面渲染。
3. 总结
通过自己实现MVVM的整个过程,让我更加理解Vue数据双向绑定的实现原理,以及理解MVVM这种架构模式带来的好处。最后,希望这篇文章对大家也有所帮助。
代码链接:ES5手写MVVM