nfssuzukaze / Blog

0 stars 0 forks source link

从 JS 的 Object.defineProperty 到 Vue 的数据响应式 #19

Open nfssuzukaze opened 3 years ago

nfssuzukaze commented 3 years ago

从 JS 的 Object.defineProperty 到 Vue 的数据响应式

1. Object.defineProperty 的用法

描述符 默认值
公有描述符 configurable false
公有描述符 enumerable false
数据描述符 value undefined
数据描述符 writable false
访问器描述符 get undefined
访问器描述符 set undefined

1.1 数据描述符

const obj1 = {}
const obj2 = {}

Object.defineProperty(obj1, 'name', {
    value: 'saber', // 数据描述符, 表示该属性的值
    writable: true  // 数据描述符, 表示该属性(此处是指 obj1.name 属性)是可以修改的
})

Object.defineProperty(obj2, 'name', {
    value: 'saber',
    writable: false
})

console.log(obj1.name) //=> saber
console.log(obj2.name) //=> saber
obj1.name = 'archer' 
console.log(obj1.name) //=> archer
obj2.name = 'berserker'   
console.log(obj2.name) //=> saber

1.2 访问器描述符

const obj = {}
obj._number = 0

Object.defineProperty(obj, 'number', {
    get() { 
        // 当 obj 的 number 属性被读取时, 触发该函数
        console.log('getter is touched')
        return obj._number
    },
    set(value) { 
        // 当 obj 的 number 属性被修改时, 触发该函数
        console.log('setter is touched')
        obj._number = value
    }
})

console.log(obj.number) //=> getter is touched, 0
obj.number = 1 //=> setter is touched
console.log(obj.number) //=> getter is touched, 1

3. 公用描述符

const obj = {}
obj.age = 18

Object.defineProperty(obj, 'name', {
    value: 'saber',
    writable: true,
    enumerable: false,  // 属性不能在 for...in 和 Object.keys() 中被枚举
    configurable: false // 除 value 和 writable 以外的描述符都不可更改
})

for (let key in obj) {
    console.log(key + ":" + obj[key])
} //=> age:18

console.log(Object.keys(obj)) //=> ["age"]

obj.name = 'archer'
console.log(obj.name) //=> archer

Object.defineProperty(obj, 'name', {
    enumerable: true
}) //=> Uncaught TypeError: Cannot redefine property: name

关于后续的 defineProperty 是否会改变其他描述符的值

const obj = {}

Object.defineProperty(obj, 'name', {
    get() {return 'saber'},
    configurable: true
})

console.log(Object.getOwnPropertyDescriptor(obj, 'name'))
//=> {set: undefined, enumerable: false, configurable: true, get: ƒ}

Object.defineProperty(obj, 'name', {
    set(value) {return false}
})

console.log(Object.getOwnPropertyDescriptor(obj, 'name'))
//=> {enumerable: false, configurable: true, get: ƒ, set: ƒ}

可知, 后续的 defineProperty 并不会改变除设置之外的描述符

2. Vue 的数据响应式

<template>
    <div>
        <div @click="addN1">
            {{n1}}
        </div>
        <div @click="addN2">
            {{n2}}
        </div>
        <div>
            {{result}}
        </div>    
    </div>
</template>
<script>
    export default {
        data() {
            return {
                n1: 0,
                n2: 0
            }
        },
        methods: {
            addN1() {
                this.n1 ++
            },
            addN2() {
                this.n2 ++
            }
        },
        computed: {
            result() {
                return this.n1 + this.n2
            }
        }
    }
</script>
<style scoped></style>

在此组件中, 第三个 div 中的数字会随着前两个 div 的数字的变化而变化. 这就是通过上面的 Object.defineProperty 定义 getter/setter 实现所实现的

Vue.js 文档所说:

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

2.1 Vue 不能保证数组和对象的响应式

2.1.1 对于对象
let vm = new Vue({
    data: {
        a: 1
    }
})

vm.b = 2 
// vm.b 并不是响应式的, 因为 Vue 并没有将其记录为依赖(没有 setter 和 getter)

解决方法一 : 在 data 中就定义该属性

let vm = new Vue({
    data: {
        a: 1,
        b: undefined
    }
})

vm.b = 2
// 此时的 vm.b 是响应式的

解决方法二 : 使用 Vue 提供的 Vue.set 方法 或 vm.$set 实例方法

let vm = new Vue({
    data: {
        obj: {
            a: 1
        }
    }
})

vm.$set(vm.obj, 'b', 2) 或
Vue.set(vm.obj, 'b', 2)
// 不能直接在 data 对象上修改
2.1.2 对于数组
let vm = new Vue({
    data: {
        items: ['a', 'b', 'c']
    }
})
vm.items[1] = 'd' // 不是响应式
vm.items.length = 5 // 不是响应式

解决方法一 : 通过 Vue.setvm.$set 为数组设置(更改或新增)属性

Vue.set(vm.items, property, newValue)

解决方法二 : 通过 vm.someArr.splice 方法来设置(更改或新增)属性

vm.someArr.splice(index, count, newValue)
2.1.3 注意

Vue 不允许动态添加根级响应式 property , 所以必须在初始化实例之前声明所有的根级响应式 property , 哪怕只是一个空值