Vue2.0 侦听器模拟实现

2,506 阅读6分钟

Object.defineProperty 的使用

第一个参数,需要定义属性的对象

第二个参数,需要在第一个参数对象上定义的属性名

第三个参数,一些配置(描述符

  • configurable
    • 描述符能否改变(能否被Object.defineProperty多次定义),是否能被 delete 操作符删除
    • 默认 false,不可重定义,不可删除
// =============== 例1 ===============
const obj = {}
// 当其为 false 时,不可再次使用 Object.defineProperty 进行定义
Object.defineProperty(obj, 'a', {
    configurable: false
})
// 且不可被删除
delete obj.a // 该表达式返回 false,删除失败
Object.defineProperty(obj, 'a', { // TypeError: Cannot redefine property: a
    configurable: true
})
// =============== 例2 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
    configurable: true
})
Object.defineProperty(obj, 'a', { // 可以重新设置描述符
    configurable: true
})
// 为 true 时,可以通过 delete 操作符进行删除
delete obj.a // true,obj 为空对象
  • enumerable
    • 是否可被枚举
    • 默认 false,不可被枚举
// =============== 例1 ===============
const obj = {}
// 当其为 false 时,不可被枚举
Object.defineProperty(obj, 'a', {
    enumerable: false
})
obj.propertyIsEnumerable('a') // false
for (const key in obj) { // 没有打印任何东西
    console.log(key)
}
// =============== 例2 ===============
const obj = {}
// 当其为 true 时,可被枚举
Object.defineProperty(obj, 'a', {
    enumerable: true
})
obj.propertyIsEnumerable('a') // true
for (const key in obj) { // 打印了一个字符串 a
    console.log(key)
}
  • value
    • 该属性对应的值
    • 默认 undefined
    • 注:不能与 get、set 共存
// =============== 例1 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
    value: 'b'
})
console.log(obj.a) // 'b'
  • writable
    • 默认 false
    • 注:不能与 get、set 共存
// =============== 例1 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
    writable: false,
    value: 'b'
})
obj.a = 'c'
console.log(obj.a) // 打印输出结果还是 'b'
// =============== 例2 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
    writable: true,
    value: 'b'
})
obj.a = 'c'
console.log(obj.a) // 可以修改了,结果是 'c'
  • get
    • 取值时,调用 get 函数,取到的值就是 get 函数的返回值
    • 默认 undefined
    • 注:不能与 value、writable 共存
  • set
    • 赋值时,调用 set 函数,赋值符右侧的值会作为参数传入
    • 默认 undefined
    • 注:不能与 value、writable 共存
// =============== 例1 ===============
const obj = {}
// get set 不能与 value、writable 共存
Object.defineProperty(obj, 'a', { // Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
    value: 'b',
    get: () => 1,
    set: (val) => void 0,
})
// =============== 例2 ===============
const obj = {}
let a = null // get 和 set 需要借助一个额外的变量
Object.defineProperty(obj, 'a', {
    get: () => a,
    set: (val) => (a = val),
})
obj.a = '123'
console.log(obj.a, a) // '123' '123'
// =============== 例3 ===============
const obj = {}
let a = null
Object.defineProperty(obj, 'a', {
    get: () => {
        console.log('this is get') // 在 get 时做一些操作
        return a
    },
    set: (val) => {
        console.log('this is set')
        a = val
    },
})
obj.a = '123'
console.log(obj.a, a) 
// 按如下顺序打印 先 set 里,再 get 里的,再上面一行的
// 'this is set'
// 'this is get'
// '123' '123'

实现 Vue2.0 类似的 侦听器

// 有这样一个对象
const obj = {
    a: 1,
    b: '2',
    c: {
        c1: 3,
    },
    d: [1, 2, 3]
}
// 以及一个这样的函数
function update() {
    console.log('update')
}
// 我想要执行下面代码的时候,能打印 update
// obj.a = 2
// obj.c.c1 = 4
// obj.d.push(4)

Object.defineProperty 一次只能定义一个对象上的属性,如果要定义 obj 的所有属性,就需要对其进行遍历

const obj = {
    a: 1,
    b: '2',
    c: {
        c1: 3,
    },
    d: [1, 2, 3]
}
function update() {
    console.log('update')
}

/**
 * 对象侦听
 * @param {object} observedObject  需要被观测的对象
 * @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
 */
function observer(observedObject, callback) {
    // 首先判断它是不是对象,如果不是,直接返回它本身
    if (observedObject === null) return observedObject
    if (typeof observedObject !== 'object') return observedObject
    
    // 对属性进行遍历
    for (const key in observedObject) {
        let otherValue = observedObject[key] // Object.defineProperty 的 get set 需要用到一个额外的变量,它的默认值为当前 observedObject 被遍历到的属性
        
        Object.defineProperty(observedObject, key, {
            configurable: true,
            enumerable: true,
            get() {
                return otherValue
            },
            set(val) {
                if (typeof callback === 'function')
                    callback()
                otherValue = val
            }
        })
    }
    return observedObject
}

observer(obj, update)
// 接下来对 obj 上的属性进行赋值操作
// obj.a = 2
// obj.b = 3
// 都可以打印一次 update
// 但是 obj.c.c1 = 4 就触发不了了
// 因为我们还只处理了一层

我们需要通过一个递归来处理更深层的属性观测

// 在 observer 函数的 for 循环内部,最后面加上一个判断
// 如果 observedObject 的当前属性对应值仍为一个对象,则把它的值作为参数继续调用 observer 函数自身
for (const key in observedObject) {
    let otherValue = observedObject[key]

    Object.defineProperty(observedObject, key, {
        configurable: true,
        enumerable: true,
        get() {
            return otherValue
        },
        set(val) {
            if (typeof callback === 'function')
                callback()
            otherValue = val
        }
    })
    // =============== 新增逻辑 ===============
    // 非对象,忽略后续步骤
    if (otherValue === null) continue
    if (typeof otherValue !== 'object') continue
    
    observer(observedObject[key], callback)
}

// 这下执行下面代码,update 也会打印出来了
// obj.c.c1 = 4
// 但是有这么一种情况,如果给 obj.b 赋值一个新对象,新对象并不会被观测到
// obj.b = { e: 1 }; obj.b.e = 2; // 后者不会再打印 update
// 但是在赋值对象的过程中,我们可以在 set 函数的 val 参数上进行判断,如果 val 是一个对象,那么对 val 进行观测

那我们对 set 函数进行如下改进

set(val) {
    if (typeof callback === 'function')
        callback()
    
    otherValue = val
    // =============== 以下新增逻辑 ===============
    if (val === null) return
    if (typeof val !== 'object') return
    
    observer(val, callback)
}
// 这样 obj.b = { e: 1 }; obj.b.e = 2; 就会打印两次 update 了

对于 obj 里的数组,obj.d[0] = 3 这样赋值,可以打印出 update ,因为数组的索引也属于它的属性,但是 obj.d.push(4) 并不能触发打印,我们需要对数组实例上的数组变更方法,进行包裹,这种处理方式就是面向切片的编程方式,例如装饰器

// 列举出所有的数组变更方法的函数名
const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arr = new Array()
function update() {
    console.log('update')
}

arrChangeMethodsKeys.forEach(methodsKey => {
    const prevMethods = arr[methodsKey] // 将原来的方法保存到 prevMethods 上
    arr.__proto__[methodsKey] = function() { // 改写成新的函数
        update() // 先调用 update
        prevMethods.apply(arr, arguments) // 再调用原来的方法,注意,数组的实例方法中都使用了 this,要手动绑定一次 this 指向
    }
})

arr.push(1) // 先打印了 update,后打印 arr 结果得到 [1]

上面这部分逻辑我们也需要加在 observer 函数的 for 循环后面

for (const key in observedObject) {
    if (otherValue === null) continue
    if (typeof otherValue !== 'object') continue
    
    observer(observedObject[key], callback)
    
    if (Array.isArray(observedObject[key])) {
        const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
        const arr = observedObject[key]
        arrChangeMethodsKeys.forEach(methodsKey => {
            const prevMethods = arr[methodsKey]
            arr.__proto__[methodsKey] = function() {
                if (typeof callback === 'function')
                    callback()
                prevMethods.apply(arr, arguments)
            }
        })
    }
}

// 这样执行数组的变更方法时,update 也会被打印了
// obj.d.push(4) // update 被打印

完整代码

  • 上文中的完整示例代码
const obj = {
    a: 1,
    b: '2',
    c: {
        c1: 3,
    },
    d: [1, 2, 3]
}
function update() {
    console.log('update')
}

/**
 * 对象侦听
 * @param {object} observedObject 需要被观测的对象
 * @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
 */
function observer(observedObject, callback) {
    // 首先判断它是不是对象,如果不是,直接返回它本身
    if (observedObject === null) return observedObject
    if (typeof observedObject !== 'object') return observedObject
    
    // 对属性进行遍历
    for (const key in observedObject) {
        let otherValue = observedObject[key]
        
        Object.defineProperty(observedObject, key, {
            configurable: true,
            enumerable: true,
            get() {
                return otherValue
            },
            set(val) {
                if (typeof callback === 'function')
                    callback()
                    
                otherValue = val
                
                if (val === null) return
                if (typeof val !== 'object') return
    
                observer(val, callback)
            }
        })
        
        if (otherValue === null) continue
        if (typeof otherValue !== 'object') continue

        observer(observedObject[key], callback)
        
        if (Array.isArray(observedObject[key])) {
            const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
            const arr = observedObject[key]
            arrChangeMethodsKeys.forEach(methodsKey => {
                const prevMethods = arr[methodsKey]
                arr.__proto__[methodsKey] = function() {
                    if (typeof callback === 'function')
                        callback()
                    prevMethods.apply(arr, arguments)
                }
            })
        }
    }
    return observedObject
}

observer(obj, update)
  • 上图中一个函数中写的内容过于臃肿了,拆分一下
function isFunction(val) {
    return typeof val === 'function'
}

function isObject(val) {
    return typeof val === 'object' && val !== null
}

function defineArrayObserver(arr, callback) {
    if (Array.isArray(arr)) {
        const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
        arrChangeMethodsKeys.forEach(methodsKey => {
            const prevMethods = arr[methodsKey]
            
            arr.__proto__[methodsKey] = function() {
                if (isFunction(callback)) callback()
                prevMethods.apply(arr, arguments)
            }
        })
    }
}

function defineObjectObserver(obj, key, otherValue, callback) {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: true,
        get: () => otherValue,
        set(val) {
            if (isFunction(callback)) callback()
            otherValue = val

            if (!isObject(val)) return
            observer(val, callback)
        }
    })

    observer(obj[key], callback)
    defineArrayObserver(obj[key], callback)
}

/**
 * 对象侦听
 * @param {object} observedObject 需要被观测的对象
 * @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
 */
function observer(observedObject, callback) {
    if (!isObject(observedObject)) return observedObject

    for (const key in observedObject) {
        defineObjectObserver(observedObject, key, observedObject[key], callback)
    }
    return observedObject
}