Object.defineProperty是如何劫持get set 并且是如何深度监听、如何监听原生数组方法

对象的属性分为:数据属性和访问器属性。如果要修改对象的默认特性,必须使用Object.defineProperty方法,它接收三个参数:属性所在的对象、属性的名字、一个描述符对象。那么接下来先看看他如何深度监听原生数组的~~~~~~

observer

//先准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

其中的info需要深度监听 其次定义一个方法,这个方法判断了这个不是对象或数组 然后重新定义各个属性(for in 也可以遍历数组)

function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

其次重新定义属性 监听起来并且运用到了刚刚封装的监听方法 和核心API( 注意,value 一直在闭包中,value =newValue此处设置完之后,再 get 时也是会获取最新的值)

function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

其次监听原生数组方法

const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

记得别Array.prototype.push = function 这样子添加一个方法 这样会污染全局的Array原型
下面把完整的代码放一下~~

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

数据劫持

然后我们来看看数据劫持 我们先看看Object.defineProperty的定义。MDN(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)上是这么说的:

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

参数 说明
obj 要定义属性的对象。
prop 要定义或修改的属性的名称或 Symbol 。
descriptor 要定义或修改的属性描述符

返回值是被传递给函数的对象。

descriptor里提到了一个 属性描述符
属性描述符分两类,第一类为数据描述符,第二类为存储描述符

数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

键值 描述
configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

双向数据绑定原理
双向数据绑定是通过数据劫持(getter-setter)发布者-订阅者模式的方式来实现的。
发布者-订阅者还没部分知识还没理解透~~:,先看看数据劫持是怎么实现的。

代码简单实现:
html

<label>姓名:</label><span id="userName"></span>

JavaScript

let userName = document.getElementById('userName');

let user = {};
//通过get,set劫持name属性
Object.defineProperty(user,'name',{
    get() {
        console.log('get')
        //获取页面更新数据
        return userName.innerText
    },
    set(newName) {
       console.log('set')
       //数据发生改变时,更新页面
       userName.innerText = newName;
    }
})
user.name='jone'

是不是看不懂~~看不懂就对了 因为我也看不懂 ~ 哈哈哈哈~
Object.defineProperty是如何劫持get set 并且是如何深度监听、如何监听原生数组方法_第1张图片

你可能感兴趣的:(Object.defineProperty是如何劫持get set 并且是如何深度监听、如何监听原生数组方法)