数据监听
上次我们实现了一个数据劫持,今天我们看一下数据的监听。
什么是监听呢?请看以下代码:
var o = {a: {b: 'jack'}}
observe(o)//数据劫持
//数据监听
new Watcher(o,'a.b',function(newVal,oldVal){
console.log(newVal)
console.log(oldVal)
})
劫持和监听的区别在于:
劫持是针对object的所有属性,而监听是针对某一个属性。
当对象o的某一个属性发生变化时,我们监听到了,就可以触发后面的回调函数了。
那如何实现呢?
直觉上是用观察者模式。
var o = {a: {b: {c: 'jack'}}}
observe(o)//劫持所有属性
function observe(data) {
Object.keys(data).forEach(function (key) {
if (typeof data[key] === 'object') {
observe(data[key]) //递归
} else {
defineReactive(data, key, data[key])
}
})
}
//观察者模式
function Dep() {
this.subs = []
}
Dep.prototype.on = function (fn) {
this.subs.push(fn)
}
//执行的是对象里面的方法
Dep.prototype.emit = function () {
this.subs.forEach(fn => {
fn()
})
}
function defineReactive(obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
dep.on(fn)//回调函数从哪里来?
return val
},
set (newval) {
console.log('object属性发生了变化')
val = newval
dep.emit()
}
})
}
问题来了:
订阅的回调函数是什么呢?
应该是Watcher的回调函数。
如何在defineProperty内部订阅Watcher的回调函数呢?
这里我们不妨换一种思路:观察者订阅的一定是回调函数么?可否订阅一个对象呢?然后触发这个对象的一个方法?
很明显是可以的,我们订阅的对象就是Watcher。
下面我们先搭出Watcher的架子看看:
var o = {a: {b: {c: 'jack'}}}
observe(o)//劫持所有属性
function observe(data) {
Object.keys(data).forEach(function (key) {
if (typeof data[key] === 'object') {
observe(data[key]) //递归
} else {
defineReactive(data, key, data[key])
}
})
}
//观察者模式
function Dep() {
this.subs = []
}
//往数组里面丢的是对象
Dep.prototype.on = function (obj) {
this.subs.push(obj)
}
//执行的是对象里面的方法
Dep.prototype.emit = function () {
this.subs.forEach(obj => {
obj.update()
})
}
/*
* new Watcher(o,'a.b.c',function(newVal,oldVal){
console.log()
})
* */
function Watcher(obj,exp,cb) {//exp是表达式a.b.c,cb是回调函数
this.obj = obj
this.exp = exp
this.cb = cb
this.value = this.getValue()//在new实例的时候,获取obj.a.b.c的值,触发defineProperty的get
}
Watcher.prototype.getValue = function(){
//触发defineProperty的get
let val = this.obj+'.' + this.exp//这里是错误的,[object Object].a.b.c,想想如何实现?
return val // 也就是obj.a.b.c
}
Watcher.prototype.update = function(){
//修改o.a.b.c的值,触发defineProperty的set
let oldValue = this.value;
let newValue = this.getValue()//重新获取新值
if(oldValue !==newValue){
this.cb(newValue,oldValue)
}
}
let watcher = new Watcher(o,'a.b.c',function(newVal,oldVal){
console.log('new>>>',newVal)
console.log('old>>>',oldVal)
})
function defineReactive(obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
dep.on(watcher)//订阅watcher对象
return val
},
set (newval) {
console.log('object属性发生了变化')
val = newval
dep.emit()
}
})
}
这段代码是报错的,存在这么几个问题:
1、Watcher在实例化的时候,就应该已经触发了defineProperty的get,但实际上defineProperty内部的get需要等待Watcher实例化才能订阅上。这就成死循环了。
2、如何在Watcher的getValue方法获取obj.a.b.c的值呢?
第一个问题比较重要,我们需要优先解决。
问题的关键就在于:
Watcher在实例化的时候,就需要通知到defineProperty的get。
要解决这件事并不难,我们可以利用对象本身的引用特性来实现:
Dep.target = null //这是订阅的对象
function Watcher(obj,exp,cb) {//exp是表达式,cb是回调函数
this.obj = obj
this.exp = exp
this.cb = cb
this.value = this.getValue()//调用自身的方法
}
Watcher.prototype.getValue = function(){
Dep.target = this //这是最关键的地方哈~~
//Dep.target已经有了实例,
let val = this.obj+'.' + this.exp//触发defineProperty的get,观察者会将Watcher实例丢进subs数组
return val // 也就是obj.a.b.c
}
我们用外部变量Dep.target存放Watcher的实例,然后defineProperty中订阅的对象就是Dep.target。当在Watcher实例化的时候,它就会通知到通知到defineProperty的get。
var o = {a: {b: {c: 'jack'}}}
observe(o)//劫持所有属性
function observe(data) {
Object.keys(data).forEach(function (key) {
if (typeof data[key] === 'object') {
observe(data[key]) //递归
} else {
defineReactive(data, key, data[key])
}
})
}
//观察者模式
function Dep() {
this.subs = []
}
//往数组里面丢的是对象
Dep.prototype.on = function(obj){
if(Dep.target){//如果Dep.target不为空,就丢进subs数组
this.subs.push(obj)
}
}
//执行的是对象里面的方法
Dep.prototype.emit = function(){
this.subs.forEach(obj=>{
obj.update()
})
}
/*
* new Watcher(o,'a.b.c',function(newVal,oldVal){
console.log()
})
* */
Dep.target = null //这是订阅的是Watcher实例
function Watcher(obj,exp,cb) {//exp是表达式a.b.c,cb是回调函数
this.obj = obj
this.exp = exp
this.cb = cb
this.value = this.getValue()//在new实例的时候,获取obj.a.b.c的值,触发defineProperty的get
}
Watcher.prototype.getValue = function(){
Dep.target = this //这是最关键的地方哈~~
//Dep.target已经有了实例,
let val = this.obj+'.' + this.exp//触发defineProperty的get,观察者会将Watcher实例丢进subs数组
return val // 也就是obj.a.b.c
}
Watcher.prototype.update = function(){
//修改o.a.b.c的值,触发defineProperty的set
let oldValue = this.value;
let newValue = this.getValue()//重新获取新值
if(oldValue !==newValue){
this.cb(newValue,oldValue)
}
}
new Watcher(o,'a.b.c',function(newVal,oldVal){
console.log('new>>>',newVal)
console.log('old>>>',oldVal)
})
function defineReactive(obj,key,val){
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
dep.on(Dep.target)//监听的是watcher实例
return val
},
set(newval){
console.log('object属性发生了变化')
val = newval
dep.emit()
}
})
}
下面解决第二个问题。
我们不妨把这个问题抽象出来,单独放在一个函数里解决。
var o = {a: {b: {c: 'jack'}}}
function parsePath(path){
let segs = path.split('.')
return function(obj){
for(let i=0;i<segs.length;i++){
if(typeof obj !=='object'){
break;
}
// console.log(segs[i])//a
// console.log(obj[segs[i]])//{ b: { c: 'jack' } }
obj = obj[segs[i]]
//console.log(obj)//{ b: { c: 'jack' } }
}
return obj
}
}
console.log(parsePath('a.b.c')(o))
这个函数用到了柯里化,我们再分析一下。
for循环的作用就是逐次往对象内部深入,直到找出非object的那个值来返回。
第一次循环:obj --->{ b: { c: 'jack' } }
第二次循环:obj--->{ c: 'jack' }
第三次循环:obj--->'jack',
跳出循环,返回值。
那个判断跳出for循环的条件其实不加也行,不过如果有人写成a.b.c.多带个点的话,那就会多一次循环,那这样的话判断条件就有用了。
好啦,解决了以上问题,我们就可以把最终代码写出来了:
var o = {a: {b: {c: 'jack'}}}
observe(o)//劫持所有属性
function observe(data){
Object.keys(data).forEach(function(key){
if(typeof data[key] === 'object'){
observe(data[key]) //递归
} else{
defineReactive(data,key,data[key])
}
})
}
//观察者模式
function Dep(){
this.subs = []
}
//往数组里面丢的是对象
Dep.prototype.on = function(obj){
if(Dep.target){
this.subs.push(obj)
}
}
//执行的是对象里面的方法
Dep.prototype.emit = function(){
this.subs.forEach(obj=>{
obj.update()
})
}
Dep.target = null //这是订阅的对象watcher
function Watcher(obj,exp,cb) {//exp是表达式,cb是回调函数
this.obj = obj
this.exp = exp
this.cb = cb
this.value = this.getValue()//调用自身的方法
}
Watcher.prototype.getValue = function(){
Dep.target = this
let value = parsePath(this.exp)(this.obj)//这里会触发defineProperty的get
Dep.target = null
return value
}
Watcher.prototype.update = function(){
//获取新值
let newVal = this.getValue()//这里会触发defineProperty的get
let oldVal = this.value
if(newVal !== oldVal){
this.value = newVal
this.cb.call(this.obj,newVal,oldVal)
}
}
function parsePath(path){
let segs = path.split('.')
return function(obj){
for(let i=0;i<segs.length;i++){
if(typeof obj !=='object'){
break;
}
// console.log(segs[i])//a
// console.log(obj[segs[i]])//{ b: { c: 'jack' } }
obj = obj[segs[i]]
//console.log(obj)//{ b: { c: 'jack' } }
}
return obj
}
}
//监听
new Watcher(o,'a.b.c',function(newVal,oldVal){
console.log('new>>>',newVal)
console.log('old>>>',oldVal)
})
o.a.b.c = 'lily'
o.a.b.c = 'lily2'
function defineReactive(obj,key,val){
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
console.log('get>>>')
console.log(Dep.target)
dep.on(Dep.target)
return val
},
set(newval){
console.log('object属性发生了变化')
val = newval
dep.emit()
}
})
}