使用js es6 中 Object.defineProperty
为我们自己定义的VM创建示例。同时这个方法通过提供了set
.get
方法的触发我们的监听事件。
分析
数据监听的基本要素
通过这样的思路来理解这里的模型。
- 事件的触发
- 事件的分发场所
- 事件的响应
1. 事件的触发
通过Object.defineProperty
方法定义的属性。在get
和set
方法内进行事件的触发。将事件分发给监听者watcher
(就是后面所说的事件分发的场所)。
这里通过Object.defineProperty
定义属性的方式来进行,对Array的方法,就没办法监听到。vue
对list的一些方法进行了hook,来触发。
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method)=> {
// 这里是原生Array的原型方法
let original = Array.prototype[method];
// 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
// 注意:是属性而非原型属性
arrayAugmentations[method] = function () {
//触发事件,分发给watcher
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
同样,事件的触发器只要能hook到就可以做到。后续的Proxy
2. 事件的分发场所
事件分发的场所。需要凑齐两个因素。
- 分发的事件
- 事件的处理者(真正的监听对象)
事件的处理者,在这里监听到到我们需要的事件。
所以初始化的时候,需要对事件监听的对象和监听的事件,都入场。
3. 事件的响应
事件的监听对象,对事件,进行自己的响应。
代码逻辑
针对上面的思路。来理一下代码结构
事件的触发
通过defineProperty
中的set
方法,进行事件的触发
//接下来需要实现_obserse函数,对data进行处理,重写data的set和get函数
myVue.prototype._obverse=function (obj) {
// body...
var value;
for (key in obj) { //遍历对象。给每个对象添加监听事件
if (obj.hasOwnProperty(key)) { //如果有这个属性
//为这个属性添加这个指令监听者
this._binding[key]={
_directives:[]
}
value=obj[key]
if (typeof value === 'object') { //如果这个值还是对象,则继续处理
this._obverse(value)
}
var binding =this._binding[key]
//定义Property,一直以来说的关键!!!
Object.defineProperty(this.$data,key,{
enumerable:true,
configurable:true,
//定义get方法。
get:function () {
console.log(`获取${value}`)
return value;
},
set:function (newVal) {
// body...
console.log(`更新${newVal}`)
if (value!==newVal) {
value=newVal;
//如果更新了数据了,就分发给监听者,同时改变数据
binding._directives.forEach(function(item){
item.update()
})
}
}
})
}
}
}
事件分发的场所
构造对象。
//写一个指令集Watcher,用来绑定更新函数,实现对Dom元素的更新
function Watcher(name,el,vm,exp,attr) {
//指令的名称。
this.name = name;
//指令对应的DOM元素
this.el = el;
//执行所属于的myVue实例
this.vm=vm;
//指令对应的值,
this.exp=exp;
//绑定的属性值。
this.attr=attr;
//这里的update其实是事件响应的方法。最后再来看
this.update();
// body...
}
看到我们需要响应的事件。其实是 vm.$data中对应的exp的变化。这些是变化,会当作事件传递到这里。
而另外一方面,真实监听这些事件的,就是dom得元素。可以通过this.el和attr来的到。
创建的时机
因为是通过v-bind
这样的指令进行绑定的。所以创建的时机,就是在遍历dom tree的时候。
//3.创建指令的编译器
myVue.prototype._compile = function(root) {
//root为id为app的Element元素。也就是我们的根元素
var _this = this
var nodes = root.children
//取出每个子节点。如果有子节点。则继续递归。并进行处理
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
this._compile(node)
//如果有v-on,则进行点击事件的监听
if (node.hasAttribute('v-on')) {
node.onclick=(function () {
//这里使用i。就马上调用,就不会出现问题。
var attrVal = nodes[i].getAttribute('v-on')
//bind是使用data的作用域与method函数的作用域保持一致
return _this.$methods[attrVal].bind(_this.$data)
})();
}
//继续,解析其他的
//v-model只能绑定 input
if (node.hasAttribute('v-model') && (node.tagName=='INPUT' || node.tagName=='TEXTAREA')) {
node.addEventListener('input',(function (key) {
var attrVal=node.getAttribute('v-model')
//如果是v-model就为属性值添加一个watcher
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function () {
//将data内的值,给成这里写的值。实现双向绑定
_this.$data[attrVal] =nodes[key].value
}
})(i))
}
//继续v-bind
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind')
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
};
可以看到,创建时,将我们的两个要素的组成部分,都传入了。
事件的触发
事件的触发,其实就是我们的update方法。来完成。
属性的变化
Watcher.prototype.update = function() {
// body...
this.el[this.attr]=this.vm.$data[this.exp]
};
方法的调用
之前在绑定时,已经添加了我们的监听事件了。
if (node.hasAttribute('v-on')) {
node.onclick=(function () {
//这里使用i。就马上调用,就不会出现问题。
var attrVal = nodes[i].getAttribute('v-on')
//bind是使用data的作用域与method函数的作用域保持一致
return _this.$methods[attrVal].bind(_this.$data)
})();
}
最后集成到一起。
//自定义myVue函数
function myVue(options){
//添加一个init属性
this._init(options)
}
//第一个版本。简单的init函数
myVue.prototype._init = function(options) {
//为上面使用时传入的结构体。包括el.data.methods
this.$options=options;//options
//el是#app,this.$el是id为app的element元素
this.$el=document.querySelector(options.el);
this.$data=options.data;
this.$methods=options.methods;
//_binding 保持这model和view之间的映射关系,也就算我们之前定义的
//wathcer.当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
this._binding ={}
//需要调用自己的observable方法来监听data
this._obverse(this.$data)
this._compile(this.$el);
};