从 0.5 开始造轮子
这系列文章主要以学习为主,讲述了如何从 0.5 开始 造一个轮子,为什么是0.5因为我查了很多资料,参考了很多。至于为什么第一个是 vue 可能是参考资料比较多,在一个目前在公司的技术栈是 vue ,于是先搁置了以前的技术栈, react 。后面空闲了准备捡起react ,开始 造轮子,虽然之前造过,但是 感觉有点 low,后面再说吧。。。
核心 -- 可爱的数据数据劫持
数据劫持怎么理解,其实很简单。相信写过 java 的应该很容易理解。其实就是javabeen, 对对象的属性添加 set,get,操作。在js里面可以通过 Object.defineProperty 来劫持对象属性的setter和getter操作,当然 es6 里和 vue 里目前已经替换成了 Proxy ,之后我们也会替换掉 。数据劫持“种下”一个钩子,当数据发生变化触发set函数做一些操作,get时候又会触发一个钩子。
具体看个例子吧:
let obj = {
name: 'mvvm'
};
let testname = 'vue';
Object.defineProperty(obj, 'name', {
// 1. value: '七里香',
configurable: true, // 2. 可以配置对象,删除属性
// writable: true, // 3. 可以修改对象
enumerable: true, // 4. 可以枚举
// ☆ get,set设置时不能设置writable和value,它们代替了二者且是互斥的
get() { // 5. 获取obj.name的时候就会调用get方法
return testname;
},
set(val) { // 6. 将修改的值重新赋给name
testname = val;
}
});
console.log(obj);
/*
{
name: 'vue',
set:function(val){},
get:function(){}
}
*/
开始造轮子
要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者也就是我们说的数据劫持
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数,说白了就是字符串解析器
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,实例,整合以上三者
从这篇文章中找了个图:
入口函数,轮子的开始
看看实现过程:
this.$options = options; // 配置挂载
this.$el = document.querySelector(options.el); // 获取dom
this._data = options.data;//数据挂载
this._watcherTpl = {};//watcher池 发布订阅
this._observer(this._data); //数据劫持
// this._compile(dom)
this._compile(this.$el);//渲染
Observer用来数据劫持
给数据添加 getter, setter, 并且在setter时候做一些事情,当然这里没有做深度劫持。下个章节加上。这里注意一下value,这里我们是使用 let 定义的,如果这里换成 var,就会导致对象的value被最后一个值覆盖。具体情况 百度一下 let 和 var 在循环中的区别就明白了。后续将替换为 Proxy
查看Observer部分实现:
// 重写data 的 get set 更改数据的时候,触发watch 更新视图
myVue.prototype._observer = function (obj) {
var _this = this;
for (key in obj){ // 遍历数据
//订阅池
// _this._watcherTpl.a = [];
// _this._watcherTpl.b = [];
_this._watcherTpl[key] = {
_directives: []
};
let value = obj[key]; // 获取属`性值
let watcherTpl = _this._watcherTpl[key]; // 数据的订阅池
Object.defineProperty(_this._data, key, { // 数据劫持
configurable: true, // 可以删除
enumerable: true, // 可以遍历
get() {
console.log(`${key}获取值:${value}`);
return value; // 获取值的时候 直接返回
},
set(newVal) { // 改变值的时候 触发set
console.log(`${key}更新:${newVal}`);
if (value !== newVal) {
value = newVal;
//_this._watcherTpl.xxx.forEach(item)
//[{update:function(){}}]
watcherTpl._directives.forEach((item) => { // 遍历订阅池
item.update();
// 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图
});
}
}
})
};
};
指令解析器Compile
由于这是个最简单的版本,所以我们暂时只考虑 v-model 和 v-bind 在 input 和 textarea 下的情况。其他情况我们后期迭代处理。
实现情况:
// 模板编译
myVue.prototype._compile = function (el) {
var _this = this, nodes = el.children; // 获取app的dom
for (var i = 0, len = nodes.length; i < len; i++) { // 遍历dom节点
var node = nodes[i];
if (node.children.length) {
_this._compile(node); // 递归深度遍历 dom树
}
// 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
node.addEventListener('input', (function (key) {
//attVal = data的值
var attVal = node.getAttribute('v-model'); // 获取绑定的data
//找到对应的发布订阅池
_this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
node,
_this,
attVal,
'value'
));
return function () {
//触发set nodes[i].value;
_this._data[attVal] = nodes[key].value; // input值改变的时候 将新值赋给数据 触发set=>set触发watch 更新视图
}
})(i));
}
if (node.hasAttribute('v-bind')) { // v-bind指令
var attrVal = node.getAttribute('v-bind'); // 绑定的data
_this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
node,
_this,
attrVal,
'innerHTML'
))
}
var reg = /\{\{\s*([^}]+\S)\s*\}\}/g,
txt = node.textContent; // 正则匹配{{}}
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, attVal) => {
// matched匹配的文本节点包括{{}}, attVal 是{{}}中间的属性名
var getName = _this._watcherTpl[attVal]; // 所有绑定watch的数据
if (!getName._directives) { // 没有事件池 创建事件池
getName._directives = [];
}
getName._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
node,
_this,
attVal,
'innerHTML'
));
return _this._data[attVal];
// return attVal.split('.').reduce((val, key) => {
// return _this._data[key]; // 获取数据的值 触发get 返回当前值
// }, _this.$el);
});
}
}
};
实现Watcher
也就是做为 Compile 和 Observer 的连接器,将dom和数据劫持联系起来。作为一个中间件。说白了就是根据一些条件更改真实 dom 的 attr。
// new Watcher() 为this._compile()发布订阅+ 在this._observer()中set(赋值)的时候更新视图
function Watcher(el, vm, val, attr) {
this.el = el; // 指令对应的DOM元素
this.vm = vm; // myVue实例
this.val = val; // data
this.attr = attr; // 真实dom的属性
this.update(); // 填入数组
}
Watcher.prototype.update = function () {
//dom.value = this.mvvm._data[data]
//调用get
this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图
};
这几段代码虽然很短可是可以多揣摩一下。总体下来其实就这些东西。
结语
其实核心思想大概就是这么3个模块,能实现一个小的mvvm,本文章的完整代码见:
下一章 将替换我们的劫持对象 Object.defineProperty 为 Proxy