亲手实现一个简单的类 Vue 框架 —— 数据的监听

上一节说完了如何将一个外部传入的数据的绑定到我们自己的框架 Lue 的实例l上,但是这只是最基础的一步,下面我们就来实现 Vue 中的另一个基础功能——数据变动的监听

同时,这一节将会推翻上一节的所有内容:)因为我们将会有一个更好地方法来实现上一节的数据绑定

实现数据监听的基本思路

什么是数据监听?言简意赅得说,当被绑定的数据发生改变时,能够执行我们预先设定好的方法

说到这,机智的小伙伴肯定已经有了自己实现的想法,比如:事件监听器、广播、订阅等机制

但是学习过其他面向对象的编程语言的小伙伴肯定都知道还有这样一种机制:getter 和 setter

getter 和 setter

getter

当获取(get)一个对象的某个属性时执行的方法

setter

当设置(set)一个对象的某个属性时执行的方法

getter 和 setter 不需要我们手动去调用,只要我们对这个数据进行了操作,那这连个方法就会自动调用

那在 javascript 中能不能实现属性的 getter 和 setter 呢?答案是可以,目前 Vue 中使用的基本的数据监听也是这种方式,但是由于这种方式不支持 IE8 ,所以目前 Vue 已经不再支持 IE8

下面我们就尝试着为一个属性添加上 getter 和 setter,首先一起了解下下面这个 API

实现数据的监听 —— Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象

/**
 * @param obj 要在其上定义属性的对象
 * @param prop 要定义或修改的属性的名称
 * @param descriptor 将被定义或修改的属性的描述符
 *
 * @return 被传递给函数的对象
 */
Object.defineProperty(obj, prop, descriptor)

通过Object.defineProperty(),我们可以为一个对象添加 getter 和 setter

var obj = {}

Object.defineProperty(obj, 'name', {
    enumerable: true, // 是否可枚举
    configurable: true, // 是否可删除
    get: function () {
        console.log('get name = ', this.value)
        return this.value // 必须返回 this.value
    },
    set: function (newVal) {
        console.log('set name = ', newVal)
        if (this.value !== newVal) this.value = newVal
     }
})

上面的代码为obj对象定义了一个name属性,同时为这个属性添加了 getter 和 setter. 运行上面的代码,在控制台分别输入如下代码:

obj.name
// get name = undefined
// undefined
// 作者注释:调用了 name 属性的 getter,此时的 name 值尚未定义,所以返回了 name 的值为 undefined

obj.name = 'xiaoming'
// set name = xiaoming
// "xiaoming"
// 作者注释:调用了 name 属性的 setter,为 name 的值设为 xiaoming, 并,返回所设置的值

obj.name
// get name = xiaoming
// "xiaoming"

从上面控制台的输出,我们可以看到已经成功地为obj.name添加了 getter 和 setter

机智的你一定也已经发现了,原本obj对象上是没有name属性的,但是通过Object.defineProperty()以后,name就存在了,那是不是就以为着,我们上一节的数据绑定可以通过它来实现呢?

是的没错,这就是开头说,要推翻上一节所有内容的原因,因为我们有了一个更好地方法,能够同时完成两个我们所需要的功能!

想要更详细了解这个 API 的小伙伴可以点击右边链接查看哦 Object.defineProperty() - JavaScript | MDN 同时上面代码中出现的 enumerableconfigurable等特性,也可以在 MDN 上查看到

改写上节代码

从这节开始,代码量会逐渐增多,为了清晰起见,我们先不直接将数据绑定到实例l上,而是它的一个属性$data

依旧先定义一个l实例,但是在实际中,未定义 Lue之前是无法定义它的实例l

    var l = new Lue({
        data: {
            name: 'xiaohong',
            age: 17
        },
        ready: function () {
           // 注意:数据已经绑定到 $data 上
            console.log(this.$data.name)
            console.log(this.$data.age)
        }
    })

定义Lue

    function Lue(opts) {
        this.$data = {}

        if (typeof opts.data === 'object') {
            for (var key in opts.data) {
                var value = opts.data[key]
                if (typeof value !== 'function') {
                    this._bindData(this.$data, key, value)
                }
            }
        }

        if (typeof opts.ready === 'function') {
            opts.ready.call(this)
        }
    }

    Lue.prototype = {
        constructor: Lue,
          // 在 Lue 的原型链上定义一个绑定函数 用来绑定数据到 $data 和 设置 getter setter
        _bindData: function (obj, key, val) {
            Object.defineProperty(obj, key, {
                enumerable: true, // 是否可枚举
                configurable: true, // 是否可删除
                get: function () {
                    console.log('get', key, val)
                    return val
                },
                set: function (newVal) {
                    console.log('set', key, newVal)
                    if (val !== newVal) val = newVal
                }
            })
        }
    }

打印结果

get name = xiaohong
xiaohong
get age = 17
17

对深层对象的优化

上面的代码对于普通的数据类型并没有问题,但是如果我们传入的数据是一个嵌套层次很多的对象时,由于我们只对该对象的最外层进行了绑定监听,当该对象内部数据改变后我们就无法监听到了,这时候该怎么办呢?

解决办法很简单 —— 当该数据为对象时,再对这个对象进行遍历,通过递归的方式为每一个对象设置监听

优化代码

     function Lue(opts) {
         this.$data = {}

         if (typeof opts.data === 'object') {
             // 需要将 opts.data 的数据绑定到 $data 上,所以目标对象为 $data,源对象为 opts.data
             this._each(this.$data, opts.data)
         }

         if (typeof opts.ready === 'function') {
             opts.ready.call(this)
         }
     }

     Lue.prototype = {
         constructor: Lue,

         /**
          * 遍历深层数据对象
          * @param aimObj 需要被绑定到的目标对象
          * @param sourceObj 源对象
          * @private
          */
         _each: function (aimObj, sourceObj) {
             var value
             for (var key in sourceObj) {
                 value = sourceObj[key]
                 // 当遍历到的属性值是个对象时,对该对象再次进行遍历,此时目标对象和源对象相同,不需要改变 value 上属性所绑定的对象
                 if (typeof value === 'object') {
                     this._each(value, value)
                 }
                 // 当该属性值不为对象时,进行数据绑定
                 this._bindData(aimObj, key, value)
             }
         },

         _bindData: function (obj, key, val) {
             Object.defineProperty(obj, key, {
                 enumerable: true, // 是否可枚举
                 configurable: true, // 是否可删除
                 get: function () {
                     console.log('get', key, val)
                     return val
                 },
                 set: function (newVal) {
                     console.log('set', key, newVal)
                     if (val !== newVal) val = newVal
                 }
             })
         }
     }

l实例中的data属性改为深层属性,查看控制台输出:

var l = new Lue({
         data: {
             person: {
                 name: 'xiaohong',
                 age: 17
             }
         },
         ready: function () {
             console.log(this.$data.person.name)
             console.log(this.$data.person.age)
             this.$data.person.name = 'ligang'
             this.$data.person.age = 99
         }
     })

// get person Object{}
// get name xiaohong
// xiaohong
// get person Object{}
// get age 17
// 12
// get person Object{}
// set name ligang
// get person Object{}
// set age 99

是不是感觉略微有点样子了?是的,通过添加几行代码,我们增加了 Lue 这个框架的可用性,想想还有点小激动呢:)

But 问题又来了,person对象不可能一直不变啊,如果我们之后需要给它添加另一个属性sexjob的时候该怎么办呢?目前的代码只能对实例化时已经存在的数据进行操作使其拥有 getter 和 setter,而如果将整个类型为对象的数据修改的话,修改后的数据就不会被绑定到$data,也不会有 getter setter 了,如下面代码所示:

this.$data.person = {
    sex: 'male',
    married: false
}

上面代码中,将原先的person对象整个替换为了一个拥有sexmarried属性的对象,如果在控制台上打印出peoson的话,这两个属性是没有其所对应的 getter 和 setter 的

那要如何解决这个问题呢?很简单,往下看

针对对象类型数据被完全替换时的优化

优化的代码不多,只有一行,只需要修改下原先代码中的set方法

set: function (newVal) {
    console.log('set', key, newVal)
    if (val !== newVal) val = newVal
    // 设置属性的新值为对象时,执行原型链上的 _each 方法,self 已在 Object.defineProperty() 方法外指向 this
    // 需要绑定的目标对象为设置的新值 newVal,而需要被绑定的源对象也为 newVal
    if (typeof newVal == 'object') self._each(newVal, newVal)
}

通过上面最后增加的这行代码,我们就能够在为$data中属性设置新值时,也为其新值的属性添加 getter 和 setter 了,是不是很简单呢?

依旧存在的问题

l.$data.person.sex = 'male'

如上面代码所示,如果不是将person对象完全替换,而是通过这种方式为其增加了一个sex的属性,那么这个sex是不会有 getter 和 setter 的,这一点同 Vue 相同

那么有没有解决方法呢?小伙伴们可以尝试着自己来探索一下。这一节的《亲手实现一个简单的类 Vue 框架》到这里就先结束啦!感谢各位小伙伴的支持!

亲手实现一个简单的类 Vue 框架 —— 数据的监听_第1张图片
扫码关注前端周记公众号

你可能感兴趣的:(亲手实现一个简单的类 Vue 框架 —— 数据的监听)