web前端

数据监听

2020-04-05  本文已影响0人  姜治宇

上次我们实现了一个数据劫持,今天我们看一下数据的监听。
什么是监听呢?请看以下代码:

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()
        }
    })
}
上一篇下一篇

猜你喜欢

热点阅读