数据监听

作者: 姜治宇 | 来源:发表于2020-04-05 12:08 被阅读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()
            }
        })
    }
    

    相关文章

      网友评论

        本文标题:数据监听

        本文链接:https://www.haomeiwen.com/subject/jgnjphtx.html