造一个简单的vue双向绑定
参考 160实现vue的极简双向绑定 https://segmentfault.com/a/1190000015375217
之前虽然照着网络上的160行实现vue造过,但是过个一段日子就有点不清楚了,所以以这篇文章来巩固一下自己。
流程
init
初始化- 将数据进行
defineProperty
的数据劫持 - 解析指令通知给
watcher
,绑定到dep
- 如果数据有变化就告诉
watcher
,watcher
就去更新视图
开始实现它
首先定义一下如何去调用他
new Vue({
el: "#app", //dom
data: {
message: "123" //数据
}
})
开始实现一个这样的入口
class Vue{
constructor(option = {}){
//获取要绑定的dom
this.$el = document.querySelector(option.el);
//得到数据
this._data = option.data;
//watcher的观察集合
this._watcherTpl = {};
//劫持数据
this._observer(this._data);
//编译模板 发布订阅
this._compile(this.$el);
}
}
watcher
函数是一个数据的观察者,如果有数据发生了变动watcher
就会去更新视图
/*
el 订阅者dom
vm 这里指vue的实例
val 对应vue实例上data的值
attr 要改变dom的什么属性
*/
class Watcher{
constructor(el, vm, val, attr){
//挂载数据
this.el = el;
this.vm = vm;
this.val = val;
this.attr = attr;
//更新视图
this.update();
}
update(){
this.el[this.attr] = this.vm._data[this.val];
}
}
observer
数据的监听器,这里会监听劫持所有的数据,如果有数据变动就会通知给watcher
让它更新视图
class Vue{
//...
_observer(obj){
var _this = this;
//遍历所有的数据,给所有数据监听
Object.keys(obj).forEach((key) => {
// 每个数据的一个订阅池,就是说如果数据发生变化就会通知给他的订阅池`_directives`,让所有的订阅者发生变动
_this._watcherTpl[key] = {
_directives: []
}
var value = obj[key];
var watcherTpl = _this._watcherTpl[key];
// 开始绑定数据的get、set
Object.defineProperty(_this._data, key, {
//可以删除或添加
configurable: true,
//可以枚举
enumerable: true,
get(){
// 直接把它的值放回出去
return value;
},
set(newVal){
// 如果新的值全等于旧值就直接返回
if( newVal === value )return;
value = newVal;
//给所有的订阅者更新视图
watcherTpl._directives.forEach(item => {
item.update();
})
}
})
})
}
}
最后是实现compile
模板编译,这个函数的实现思路是深度遍历dom树,将里面的变量替换成真实的数据,将v-model
添加到对应的订阅池中
class Vue{
//...
_compile(el){
var _this = this,
nodes = el.children, //得到所有的子节点
len = nodes.length;
// 遍历所有的节点
for( var i = 0; i < len; i++ ){
var node = nodes[i];
// 如果这个子节点还有子节点的话就将其递归给自己
if( node.children.length ){
_this._compile(node)
}
// 为有v-model属性的input、textarea的元素绑定input事件
if( node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA') ){
var attrVal = node.getAttribute('v-model');
// 为attrVal这个属性添加订阅者
_this._watcherTpl[attrVal]._directives.push(new Watcher(
node,
_this,
attrVal,
'value'
))
//监听节点的input事件,发生变动就将其中的内容赋值给vue
node.addEventListener('input', (key => {
return function(){
_this._data[attrVal] = nodes[key].value;
}
})(i))
}
//给dom节点绑定vue的数据
if( node.hasAttribute('v-bind') ){
var attrVal = node.getAttribute('v-bind');
_this._watcherTpl[attrVal]._directives.push(new Watcher(
node,
_this,
attrVal,
'innerHTML'
))
}
var reg = /{{\s*([^}]+\S)\s*}}/g,
txt = node.textContent;
// 如果有匹配上面的正则的文本内容的话就将其替换成对应的vue数据
if( reg.test(txt) ){
// metched和placeholder分别是主匹配和附匹配,具体看exec的机制
node.textContent = txt.replace(reg, (metched, placeholder) => {
var getName = _this._watcherTpl[placeholder];
if( !getName._directives ){
getName._directives = [];
}
getName._directives.push(new Watcher(
node,
_this,
placeholder,
'innerHTML'
))
/* reduce
2个参数:
callback:4个参数
a: 上次调用返回的值
b: 这次执行时获取的值
c: 数组索引
d: 数组
init: 初始值
*/
return placeholder.split('.').reduce((val, key) => {
return _this._data[key];
}, _this.$el)
})
}
}
}
}
到这里就实现了vue的双向绑定功能了。
这篇文章是我边看源码(160行极简代码)边写的,通过写这篇文章我对这个功能熟悉了很多。
在最后,我发现这个代码实现的功能有点不全,就是在
return placeholder.split('.').reduce((val, key) => {
return _this._data[key];
}, _this.$el)
这一段,虽然写了分开obj.test
这样的字符串的代码,但是没有创建相应的_directives
,并且就算有,也没有绑定他的数据,不能进行双向绑定。
这个问题就交给下次实现更加完善的vue功能时再去实现吧。