前言
随着 Vue2.0 的发布,前端入门的要求也越来越低,已至于 Vue 已经成为一个前端的标配,最近也面了很多前端开发工程师,发现大部分都停留在用的阶段上,建议大家看看源码,学学 Vue 的思想。
本文中,读者配合作者的 GitHub 去实践,相信会有很大的提升。
双向数据绑定 Model View ViewModel
$watch()
去监听值得变化,然后调用 $apply()/ $digest()
方法来实现。Vue 的双向数据绑定是通过数据劫持 + 发布订阅模式(不兼容低版本)+ 数据代理的方式来实现的。
今天主要讲 Vue
Vue 不兼容低版本,是因为低版本浏览器不兼容 Object.defineProperty 这个属性,我们首先了解一下正常情况下定义的对象。
var obj = {}
obj.公众号 = '内推猿', //console.log(obj) {"公众号","内推猿"}
delect obj.公众号 //console.log(obj) {}
如果我们用到了 Object.defineProperty 这个属性再去定义一个对象,在控制台中你就会发现不同了, 在原型链上多了一些方法。
var obj = {};
Object.defineProperty(obj, '公众号',{
configurable:true, //属性值可以被删除
writable:true, //对属性可进行编写
enumerable: true, //可枚举
value:'内推猿'
})
想要了解这些属性的全部参数的话,可以去 MDN 上查看一下。我们在进行值修改的时候,就会用到 get、set方法。
var obj = {};
Object.defineProperty(obj, '公众号',{
configurable:true, //属性值可以被删除
enumerable: true, //可枚举
get() { //获取obj.公众号值得时候 会调用get方法
return '内推猿'
} set() { //给obj 属性赋值
}
})
console.log(obj.公众号) //内推猿
我们通常写 Vue 的时候,都会这样写:
<body>
<div id="app">
{{gongzhonghao}} </div></body><script>
let mvvm = new Mvvm({ el: '#app', data:{ gongzhonghao:'内推猿'}
})</script>
那么如何去实现呢?我们用这个 Object.defineProperty 这个属性来实现数据劫持(Observer)。
数据劫持:观察对象,通过递归给每一个对象增加 Object.definePropery,在 set 方法中触发 observe 方法,就能监听到数据的变化,如果数据类型是 {a:{b:1}}多层的,那么就要用到递归去实现。
function observe (data) { return new Observe(data)
}function Observe (data) { if(!data || typeof data !== 'object') return
//把data属性 通过Object.definePropert 来定义属性
for (let key in data) {
let value = data[key]
//递归方式绑定所有属性 数据是 {a:{b:1}}
observe(value)
Object.defineProperty(data, key, {
enumerable:true, get() { return value
}, set(newValue) { //如果值没有发生改变的话
if(newValue == value) return
//重新赋值
value = newValue observe(value)
},
})
}
}
Vue 中的数据代理
我们会遇到一些比较复杂的数据结构,例如 data:{ gongzhonghao:'内推猿', msg:{vx:214464812,creator: 'zhangzhen' }}
。
如果你用的是我上面写的 observe 方法就会发现,我要获取 creator 字段的话,需要通过mvvm._data.msg.creator .....
的形式来获取值。遇到再复杂的数据结构就会更乱。然而我们想要通过mvvm.msg
方式来获取数据(去掉_data)。去掉复杂的查询方式,所以用到了数据代理的方式来处理以上问题,其中 this 代表的是整个数据。
//数据代理方式
for(let key in data ) {
Object.defineProperty(this, key ,{
enumerable:true, get() { return this._data[key];
}, set(newValue){ this._data[key] = newValue
}
})
}
Vue 特点,不能新增不存在的属性 ,因为不存在的属性没有 get、set 方法。而 Vue 当中的深度响应,会给每一个新对象增加数据劫持,从而去监控新对象的变化。
模板编译 Compile
Vue 项目中我们通过 {{}} 的方式来替换 data 值,首先我们通过 #el 来确定编译的范围,创建createDocumentFragment
标签,在内存中去更换我们的模板减少 DOM 操作,通过 nodeType 来判断当前的节点,利用正则来匹配 {{}} 通过递归的方式来更换每一个数据。
function Compile(el,vm) {
vm.$el = document.querySelector(el); var Fragment = document.createDocumentFragment(); //把模板放入内存当中
while (child = vm.$el.firstChild) {
Fragment.appendChild(child)
}
replace(Fragment,vm)
vm.$el.appendChild(Fragment)
}function replace (Fragment,vm){ //类数组转化成数组
Array.from(Fragment.childNodes).forEach(function(node){ var text = node.textContent ; var reg = /\{\{(.*)\}\}/; if(node.nodeType == '3' && reg.test(text)) { //console.log(RegExp.$1)
let ary = RegExp.$1.split('.') //console.log(ary) [msg, vx]
let val = vm
ary.forEach(function(key){ //取this.msg /this.gongzhonghao
val = val[key]
});
node.textContent = text.replace(reg,val)
} if(node.childNodes){
replace(node,vm)
}
})
}
发布订阅模式(重点)
ep.addSub(Dep.target) 是增加订阅,dep.notify 函数是发布事件。当值发生改变的时候我们去发布这个事件(调用dep.notify())。
observe(value)
Object.defineProperty(data, key, {
enumerable:true, get() {
console.log(Dep.target)
Dep.target && dep.addSub(Dep.target) //[增加watcher]
return value
}, set(newValue) { //如果值没有发生改变的话
if(newValue == value) return
//重新赋值
value = newValue observe(value)
dep.notify() //让所有的watcher的update 执行
},
})
如何去订阅一些事件
//绑定的方法都有一个update属性
function Dep (){ this.subs = [] //订阅器
} //增加订阅
Dep.prototype.addSub = function(watcher) { this.subs.push(watcher)
}
//通知Dep.prototype.notify = function() { this.subs.forEach(watcher => watcher.update())
}
function Watcher(vm,exp,fn){ this.vm = vm this.exp = exp this.fn = fn //添加到订阅中
Dep.target = this
var val = vm var arr = exp.split('.');
arr.forEach(function(key){
val = val[key]
}) //在这里调用objectDefineProperty中get方法
Dep.target = null}
Watcher.prototype.update = function() { //获取新值
var val = this.vm var arr = this.exp.split('.');
arr.forEach(function(key){
val = val[key]
}) this.fn(val);
}