数据劫持Observer
所谓数据劫持就是给对象的每一个属性增加get,set方法
1.观察对象,给对象增加Object.defineProperty
2.vue特点是不能新增不存在的属性,不存在的属性没有get和set
3.深度响应,因为每次赋予一个新对象时会给这个新对象增加defineProperty
// 创建一个Observer构造函数
function Observe(data) {
let dep = new Dep()
// 既然要给对象的每一个属性增加get、set,那就先遍历一遍对象
for(let key in data) {
let val = data[key]
// 递归继续向下找,实现深度的数据劫持
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get() {
// 当获取值的时候就会自动调用get方法,于是在数据劫持observe修改一下get方法,将watcher添加到订阅事件中
// Dep.target && dep.addSub(Dep.target)
if (Dep.target) {
dep.depend() // 和上面一行代码的意思是一样的
}
return val
},
set(newVal) {
// 如果设置的新值和以前的值一样,就不处理
if (val === newVal) {
return
}
val = newVal
// 当设置完新值后,也需要把新值再去数据劫持(不然新值的属性没有get和set方法)
observe(newVal)
// 让所有watcher的update方法执行
dep.notify()
}
})
}
}
数据代理
数据代理就是让我们每次取data里面的数据时,不用每次都写一长串,比如mvvm._data.album.name这种,我们可以直接写成mvvm.album.name这种显而易见的方式。
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key]
},
set(newVal) {
this._data[key] = newVal
}
})
}
数据编译Compile
options中的el参数,为我们指定了需要编译哪些内容,而我们需要做的仅仅是解析出通过v-model、v-text、{{}}等等标识和指令,然后获取绑定数据的值,替换掉标识的内容,并进行数据的变化监听watcher,当再有值发生变化时,可以及时通知其修改对应dom元素。
function Compile(el, vm) {
// 讲el挂载到实例上方便调用
vm.$el = document.querySelector(el)
// 创建一个新的空白的文档片段,在el范围里将内容都拿到,当然不能一个一个的拿,可以选择移到内存中去,然后放入文档碎片中,节省开销
// DocumentFragment是DOM节点,它不是DOM树的一部分,通常的用例是创建文档片段,讲元素附加到文档片段,然后将文档片段附加到DOM树,在DOM树中,文档片段将其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此使用文档片段通常会带来更多好的性能。
let fragment = document.createDocumentFragment()
while (child = vm.$el.firstChild) {
// 将el中的内容放入到内存中
fragment.appendChild(child)
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
// 正则匹配{{}}
let reg = /\{\{(.*?)\}\}/g
// 如果既是文本节点又有大括号
if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 我们需要订阅一个事件,当数据改变的时候需要重新刷新视图,这就需要在replace替换的逻辑来进行处理
// 通过new Watcher 把数据订阅一下,数据一变就执行改变内容的操作
// 监听变化,给watcher再添加两个参数,用来取新的值给回调函数
new Watcher(vm, placeholder, replaceTxt)
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
// 举个例子解释一下上面的代码
// 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
// 这里vm还是作为初始值传给val,进行第一次调用,返回的是vm['album'],然后将返回的vm['album']这个对象传给下一次调用的val
// 最后变成了vm['album']['name'] => '知足'
})
}
replaceTxt()
}
// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
vm.$el.appendChild(fragment)
}
发布订阅Dep、Watcher
就像买房的中介一样,用户(watcher)去买房,不可能天天去房地产开发商那边去问有没有房源,更多的是找一个中介(dep),然后把我们的需求和联系方式告诉中介(dep.depend()),中介一旦有满足需求的房源,便会打电话来通知我们dep.notify()。
我们需要一个订阅器Dep,它需要有收集需求和联系方式的功能,也需要有打电话通知的功能。
function Dep() {
// 定义一个数组,用来存放函数的事件池
this.subs = []
}
Dep.prototype = {
// 收集需求和联系方式的功能
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
},
addSub(sub) {
this.subs.push(sub)
},
// 发通知的功能
notify() {
// 绑定的方法,都有一个update方法
this.subs.forEach(sub => sub.update())
}
}
我们需要一个订阅者watcher,它包含接受通知的功能,以及建立与Dep关联的功能。
function Watcher(vm, exp, fn) {
// 将fn放到实例上
this.fn = fn
this.vm = vm
this.exp = exp
// 建立关联
Dep.target = this
let arr = exp.split('.')
// 这里取值,会触发value的get方法,所以需要在get方法里将联系人的方式给中介,代码47行get方法
let val = vm
// 取值,获取到this.album.name,默认就会调用get方法
arr.forEach(key => {
val = val[key]
})
// 释放关联
Dep.target = null
}
Watcher.prototype = {
// 接受通知的功能,收到消息后,进行更新数据的操作
update() {
// notify的时候值已经更改了,再通过vm,exp来获取新的值
let arr = this.exp.split('.')
let val = this.vm
arr.forEach(key => {
// 通过get获取到新的值
val = val[key]
})
// 将每次拿到的新值去替换{{}}的内容
this.fn(val)
},
addDep(dep) {
dep.addSub(this)
}
}
双向数据绑定
数据--------------->Dom
1.通过compile解析指令和数据,为其添加watcher
2.watcher触发对应的get方法,使其进行依赖收集,把对应的watcher进行收集
3.当数据发送变化的时候,触发set方法,使其通知watcher进行视图更新
Dom--------------->数据
1.通过compile解析指令和数据
2.监听Dom input等更新动作,当触发dom更新时,在对应回调函数中更新实例vm中的数据值
// 如果是元素节点
if (node.nodeType === 1) {
// 获取dom上的所有属性,是个类数组
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
let name = attr.name // v-model
let exp = attr.value // who
if (name.includes('v-')) {
node.value = vm[exp] // 获取this.who的值
}
// 监听变化
new Watcher(vm, exp, function (newVal) {
node.value = newVal
})
node.addEventListener('input', e => {
let newVal = e.target.value
// 相当于给this.who 赋了一个新值,而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal
})
})
}
以上就实现了一个MVVM模型
完整代码
Index.html
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app">
<h1>{{song}}</h1>
<p>《{{album.name}}》是{{singer}}2005年发行的专辑</p>
<p>主打歌为{{album.theme}}</p>
<input v-model="who" type="text">
</div>
<script src="mvvm.js"></script>
<script>
let mvvm = new Mvvm({
el: '#app',
data: {
song: '闲鱼',
album: {
name: '知足专辑',
theme: '知足主打歌'
},
singer: '五月天',
who: '五月天还是周杰伦'
}
})
</script>
</body>
mvvm.js
// 创建一个Mvvm构造函数,讲options赋一个初始值,防止没传,等同于options || {}
function Mvvm(options = {}) {
// 在vue上将所有的属性都挂载到了vm.$options 上,所以我们也同样实现,将所有属性挂载到了$options
this.$options = options;
// this._data这里也和vue一样
let data = this._data = this.$options.data;
// 一、数据劫持
observe(data)
// 二、数据代理
// 数据代理就是让我们每次取data里面的数据时,不用每次都写一长串,比如mvvm._data.album.name这种,我们可以直接写成mvvm.album.name这种显而易见的方式
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key]
},
set(newVal) {
this._data[key] = newVal
}
})
}
// 三、数据编译
new Compile(options.el, this)
}
// 一、数据劫持(所谓数据劫持就是给对象增加get,set)
// 为什么要做数据劫持?
// 1.观察对象,给对象增加Object.defineProperty
// 2.vue特点是不能新增不存在的属性,不存在的属性没有get和set
// 3.深度响应,因为每次赋予一个新对象时会给这个新对象增加defineProperty
function observe(data) {
// 如果不是对象的话就直接return掉,放置递归溢出
if(!data || typeof data !== 'object') return
return new Observe(data)
}
// 创建一个Observer构造函数
function Observe(data) {
let dep = new Dep()
// 既然要给对象的每一个属性增加get、set,那就先遍历一遍对象
for(let key in data) {
let val = data[key]
// 递归继续向下找,实现深度的数据劫持
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get() {
// 当获取值的时候就会自动调用get方法,于是在数据劫持observe修改一下get方法,将watcher添加到订阅事件中
// Dep.target && dep.addSub(Dep.target)
if (Dep.target) {
dep.depend() // 和上面一行代码的意思是一样的
}
return val
},
set(newVal) {
// 如果设置的新值和以前的值一样,就不处理
if (val === newVal) {
return
}
val = newVal
// 当设置完新值后,也需要把新值再去数据劫持(不然新值的属性没有get和set方法)
observe(newVal)
// 让所有watcher的update方法执行
dep.notify()
}
})
}
}
// 三、创建Compile构造函数
// options中的el参数,为我们指定了需要编译哪些内容,而我们需要做的仅仅是解析出通过v-model、v-text、{{}}等等标识和指令,然后获取绑定数据的值,替换掉标识的内容,并进行数据的变化监听watcher,当再有值发生变化时,可以及时通知其修改对应dom元素。
function Compile(el, vm) {
// 讲el挂载到实例上方便调用
vm.$el = document.querySelector(el)
// 创建一个新的空白的文档片段,在el范围里将内容都拿到,当然不能一个一个的拿,可以选择移到内存中去,然后放入文档碎片中,节省开销
// DocumentFragment是DOM节点,它不是DOM树的一部分,通常的用例是创建文档片段,讲元素附加到文档片段,然后将文档片段附加到DOM树,在DOM树中,文档片段将其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以讲子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此使用文档片段通常会带来更多好的性能。
let fragment = document.createDocumentFragment()
while (child = vm.$el.firstChild) {
// 将el中的内容放入到内存中
fragment.appendChild(child)
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
// 正则匹配{{}}
let reg = /\{\{(.*?)\}\}/g
// 如果既是文本节点又有大括号
if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 五、数据更新视图
// 我们需要订阅一个事件,当数据改变的时候需要重新刷新视图,这就需要在replace替换的逻辑来进行处理
// 通过new Watcher 把数据订阅一下,数据一变就执行改变内容的操作
// 监听变化,给watcher再添加两个参数,用来取新的值给回调函数
new Watcher(vm, placeholder, replaceTxt)
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
// 举个例子解释一下上面的代码
// 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
// 这里vm还是作为初始值传给val,进行第一次调用,返回的是vm['album'],然后将返回的vm['album']这个对象传给下一次调用的val
// 最后变成了vm['album']['name'] => '知足'
})
}
replaceTxt()
}
// 六、双向数据绑定
// 如果是元素节点
if (node.nodeType === 1) {
// 获取dom上的所有属性,是个类数组
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
let name = attr.name // v-model
let exp = attr.value // who
if (name.includes('v-')) {
node.value = vm[exp] // 获取this.who的值
}
// 监听变化
new Watcher(vm, exp, function (newVal) {
node.value = newVal
})
node.addEventListener('input', e => {
let newVal = e.target.value
// 相当于给this.who 赋了一个新值,而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal
})
})
}
// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
vm.$el.appendChild(fragment)
}
// 四、发布订阅
// 就像买房的中介一样,用户(watcher)去买房,不可能天天去房地产开发商那边去问有没有房源,更多的是找一个中介(dep),然后把我们的需求和联系方式告诉中介(dep.depend()),中介一旦有满足需求的房源,便会打电话来通知我们dep.notify()
// 我们需要一个订阅器Dep,它需要有收集需求和联系方式的功能,也需要有打电话通知的功能
// 发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行 如[fn1, fn2, fn3]
// 订阅器
function Dep() {
// 定义一个数组,用来存放函数的事件池
this.subs = []
}
Dep.prototype = {
// 收集需求和联系方式的功能
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
},
addSub(sub) {
this.subs.push(sub)
},
// 发通知的功能
notify() {
// 绑定的方法,都有一个update方法
this.subs.forEach(sub => sub.update())
}
}
// 我们需要一个订阅者watcher,它包含接受通知的功能,以及建立与Dep关联的功能
// 监听函数,通过watcher这个类创建的实例,都拥有update方法
// 订阅者
function Watcher(vm, exp, fn) {
// 将fn放到实例上
this.fn = fn
this.vm = vm
this.exp = exp
// 建立关联
Dep.target = this
let arr = exp.split('.')
// 这里取值,会触发value的get方法,所以需要在get方法里将联系人的方式给中介,代码47行get方法
let val = vm
// 取值,获取到this.album.name,默认就会调用get方法
arr.forEach(key => {
val = val[key]
})
// 释放关联
Dep.target = null
}
Watcher.prototype = {
// 接受通知的功能,收到消息后,进行更新数据的操作
update() {
// notify的时候值已经更改了,再通过vm,exp来获取新的值
let arr = this.exp.split('.')
let val = this.vm
arr.forEach(key => {
// 通过get获取到新的值
val = val[key]
})
// 将每次拿到的新值去替换{{}}的内容
this.fn(val)
},
addDep(dep) {
dep.addSub(this)
}
}
// 数据--------------->Dom
// 1.通过compile解析指令和数据,为其添加watcher
// 2.watcher触发对应的get方法,使其进行依赖收集,把对应的watcher进行收集
// 3.当数据发送变化的时候,触发set方法,使其通知watcher进行视图更新
// Dom--------------->数据
// 1.通过compile解析指令和数据
// 2.监听Dom input等更新动作,当触发dom更新时,在对应回调函数中更新实例vm中的数据值
网友评论