本文github地址 https://github.com/zz-fe/vue_MVVM
1.angular 脏值检查 $watch() $apply()/ $digest(),
2.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:'前端架构之路'
})
首先我们要了解这些属性 然后我们在进行值修改,但有的时候我们会用到 get set方法 跟vue当的一样
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方法触发 就能监听到了数据的变化,如果数据类型是{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)
},
})
}
}
在项目中 我们会遇到一些比较复杂的数据结构 例如 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 。 深度响应 因为每次赋予一个新对象 会增加数据截止
模板编译是把我们通过{{}}中的属性值 用我们的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 执行
},
})
说到订阅 那么问题来了,谁是订阅者?怎么往订阅器添加订阅者? 在dep-subs.js中我指定了 Wathcher是订阅者 首先要增加Wathcher是订阅者 把订阅者放到订阅器(subs)中 当值发生变化的时候 订阅器就会调用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);
}
当我们调用dep.notify()的时候 其实就是调用update方法 在compile.js中模板重新赋值
发布订阅模式开启
new Watcher(vm, RegExp.$1, function(newValue){
node.textContent = text.replace(reg,newValue)
})
以上4步完事之后,就可以实现了vue当中的发布订阅模式
<input type="text" v-model='gongzhonghao'>
在编译的时候我要要判断节点 当nodeType == 1 的时候 我们获取DOM的属性来判断type类型 如果是我们想要的v-model 的话我们去就监听当前元素 并开启发布订阅模式去监听变化
//当前为标签的时候
if(node.nodeType == '1'){
var nodeAttrs = node.attributes //获取当前节点DOM属性
//console.log(nodeAttrs) //NamedNodeMap {0: type, 1: v-model, type: type, v-model: v-model, length: 2}
Array.from(nodeAttrs).forEach((attr) => {
var name = attr.name
var key = attr.value //v-model = 'value'
if (name.indexOf('v-') == 0) {
node.value = vm[key]
}
//发布订阅模式开启
new Watcher(vm, key, function(newValue){
node.value = newValue
})
node.addEventListener('input',function(e){
var newValue = e.target.value
vm[key] = newValue
})
})
在工作当中很多面试的人都会去问computed计算与methods的计算 有什么区别 ?
methods是一种交互方法,通常是把用户的交互动作写在methods中;而computed是一种数据变化时mvc中的module 到 view 的数据转化映射。 简单点讲就是methods是需要去人为触发的,而computed是在检测到data数据变化时自动触发的,还有一点就是,性能消耗的区别,这个好解释。 首先,methods是方式,方法计算后垃圾回收机制就把变量回收,所以下次在要求解筛选偶数时它会再次的去求值。而computed会是依赖数据的,就像闭包一样,数据占用内存是不会被垃圾回收掉的,所以再次访问筛选偶数集,不会去再次计算而是返回上次计算的值,当data中的数据改变时才会重新计算。简而言之,methods是一次性计算没有缓存,computed是有缓存的计算。其实代码实现起来很是简单
首先要是去获取当前computed值。
function computed() { //具有缓存功能
let computed = this.$options.computed; // {key:value}
let self = this
Object.keys(computed).forEach(function(key){ //拿到key 值
Object.defineProperty(self, key,{
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set(){},
})
})
}
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
let mvvm = new Mvvm({
el: '#app',
data:{ gongzhonghao:'前端架构之路', msg:{vx:214464812,creator: 'zhangzhen' }},
//computed 可以缓存 只是把数据挂在mvvm上
computed:{
say(){
return this.gongzhonghao + '--------' + this.msg.creator
}
}
})
function Mvvm(options = {}) {
//将所有的数据绑定在$options
this.$options = options
var data = this._data = this.$options.data;
//增加发布订阅
observe(data)
//数据代理方式
proxyData(data,this)
//计算
computed.call(this)
//模板编译
compile(options.el,this)
}
vue中的MVVM 这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了this._data的属性值,
本文主要围绕着 实现Observer , Compile , computed ,proxyData 几个方式 并且根据自己的思路来实现的,有问题可以联系我 VX:zz214464812 或在公总号:"前端架构之路上" 联系我