觉得这个系列任务还是很有趣的,这是一种性能很差的实现,也许之后会尝试使用虚拟dom、改善其中的遍历。

首先分析一下要干嘛:可以看出Vue是个构造函数;因为传入的对象可能有很多层对象,所以需要一个遍历传入对象的方法;双向绑定打算通过访问器属性实现、需要刷新dom,所以要有能刷新dom的方法、以及把传入Vue构造函数的对象中的值转换成Vue实例中的带有刷新dom回调的访问器的方法;最后还要一个初始化的方法,来保证没触发set时的初次访问的渲染;
// Vue构造函数将会把入口(entry)和data实例化
let Vue = function (obj) {
this.entry = document.querySelector(obj.el);
this.data = {};
this.init(obj);
};
// 初始化,遍历传入的对象转换成实例上的访问器,渲染一下页面
Vue.prototype.init=function (obj) {
this.walk(this.data, obj.data);
this.render(this.data, this.entry);
};因为渲染时需要入口,所以把相应的入口实例化;把用来双向绑定的数据放到data里,这样语义化一点(之前任务要求);调用一下Vue原型对象上的init方法。通用的方法没必要实例化,放在原型对象上就好。
// 用来遍历输入的对象
Vue.prototype.walk = function (output, input) {
for (let key in input) {
if (input.hasOwnProperty(key)) {
if (typeof input[key] !== 'object' || input[key] === null) {
this.convert(output, key, input[key]);
} else {
this.walk(output[key] = {}, input[key]);
}
}
}
};遍历输入上的每一个属性,使用hasOwnProperty判断如果是它自己的属性进入下一步判断,用来过滤掉继承的属性。如果传入的属性不是对象,则直接把该属性转换成输出上的访问器属性,如果是对象的话,递归调用walk去深层遍历。
// 将输入转换成Vue实例上的访问器属性
Vue.prototype.convert =
function (ins, key, value) {
let _value = value;
let that = this;
Object.defineProperty(ins, key, {
configurable: true,
enumerable: true,
get: function () {
return _value;
},
set: function (newVal) {
if (newVal === null || typeof newVal !== 'object') {
_value = newVal;
that.render(that.data, that.entry);
} else {
delete ins[key];
that.walk(ins[key], newVal);
that.render(that.data, that.entry);
}
}
})
};转换方法convert就是把对应的键和值通过Object.defineProperty转换成实例对象上访问器属性,使用一个变量_value来保存get返回的值。其中配置属性(configurable)要设置为true(默认为false),这样需要需要重写同名属性的时候可以把它的值给删除。set里如果设置的值不是对象则直接改变闭包的_value,使用render方法来刷新页面。如果设置的值是对象的话要把现在的访问器给删除。使用walk来遍历调用convert来设置新设置的值,然后刷新页面。
// 用来渲染页面
Vue.prototype.render = (function () {
let domCache;
return function (data, entry) {
console.log('render...');
domCache = domCache || entry.innerHTML;
let domInnerHtml = domCache;
let reg = /{{.*}}/g;
let templateArr = [];
let matchCache;
let keyCache;
let value;
while (matchCache = reg.exec(domCache)) {
templateArr.push(matchCache[0]);
}
templateArr.forEach(item => {
keyCache = item.slice(2, -2).split('.');
value = this.find(keyCache, data);
if (value !== undefined && (typeof value !== 'object' || value === null)) {
reg = new RegExp('{{' + keyCache.join('.') + '}}', 'g');
domInnerHtml = domInnerHtml.replace(reg, value);
}
});
entry.innerHTML = domInnerHtml;
}
}());// 查询某个属性在data中的值
Vue.prototype.find = function (key, data) {
for (let i = 0, len = key.length; i < len; i++) {
if (data.hasOwnProperty(key[i])) {
data = data[key[i]];
} else {
return undefined;
}
}
return data;
};使用一个闭包保存一个单例的domCache来保存dom缓存,防止{{}}被替换后,找不到双向绑定的位置。不过也有个问题就是不能动态地改变双向绑定的dom,不过我发现Vue也不了就释然了……创建一个dom缓存的副本,避免直接修改缓存,使用正则找到双花括号的位置,用正则的exec方法产生的数组的第一个元素就是匹配到的元素,所有的匹配保存到一个数组里。对匹配到的元素数组遍历。遍历中用slice返回一个切掉{{和}}的副本再以.分割,得到每一层属性名组成的数组,通过find方法用这个数组对data迭代判断、操作得到最终的值,创建正则,把dom缓存副本中属性替换成对应的值。遍历完后把修改过的dom缓存副本赋值给真实的dom,完成渲染。