1.响应式数据

1.1.Object.defineProperty

1.1.1.基础语法

定义一个对象可以使用构造函数或者字面量的形式,例如:

let obj = new Object()
obj.name = "xiaoqiang"
// 或者 使用字面量形式
let obj2 = {}
obj2.name = "xiaowang"

除了上面定义属性的方式外,还可以使用Object.defineProperty这种方式来定义或修改属性

语法:

Object.defineProperty(obj, prop, descriptor)
参数:

    obj要在其上定义属性的对象。

    prop要定义或修改的属性的名称。

    descriptor将被定义或修改的属性描述符。

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

1.1.2.描述详解

value: 设置属性的值
writable: 值是否可以重写。true | false
enumerable: 目标属性是否可以被枚举。true | false
configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false

举例:

var obj = {
    name:"nodeing"
}
//对象已有的属性添加特性描述
Object.defineProperty(obj,"name",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});
//对象新添加的属性的特性描述
Object.defineProperty(obj,"age",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});

下面详细解释描述符:

value

value: 只是用来设置当前需要添加或者修改属性的值,它可以是任意的类型

举例:

let obj = new Object()
 
Object.defineProperty(obj, "name", {
    value: "nodeing",
})

console.log(obj.name)   // nodeing  如果不设置value值,默认为undefined

writable

writeable: 这个用来控制属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false

举例:


let obj = new Object()
 
Object.defineProperty(obj, "name", {
     value:"nodeing",
     writable: false
})
obj.name = "nodeing.com"

// 当writeable默认为false的时候,修改name的值是无效的 依旧会输出nodeing
console.log(obj.name)

你可以将上面案例中的 writable的值改成true,再来运行看看结果

enumerable

enumerable: 作用是控制属性是否可以被枚举,即是否可以被(for in)或者 (Object.keys())这些方法遍历出来,默认
为false,设置为true以后,才可以被遍历出来

举例:

let obj = new Object()
 
Object.defineProperty(obj, "name", {
     value:"nodeing",
     enumerable: true
})
console.log(obj)
console.log(Object.keys(obj))

configurable

configurable: 作用有两个,1.是否可以使用delete删除目标属性,2.除 value 和 writable 特性外的其他特性是否可以被修改

举例:

let obj = new Object()
 
Object.defineProperty(obj, "name", {
     value:"nodeing",
     enumerable: true,
     configurable: false
     
})

delete obj.name

console.log(obj.name)  // 依然可以输出value值:nodeing
 

上面代码中,你可以将configurable的值改成true,再运行试试看

举例2:

let obj = new Object()
 
Object.defineProperty(obj, "name", {
     value:"nodeing",
     enumerable: true,
     configurable: false
     
})


Object.defineProperty(obj, "name", {
    value:"nodeing",
    enumerable: true,
    configurable: true
    
})

console.log(obj.name)  // TypeError: Cannot redefine property: name

上面代码中,你可以将第一次定义name属性的时候,给的configurable值改为true,再来运行看看输出结果

注意1: configurable 如果默认为false的时候,要修改writable的值,必须是从true改为false,如果是从false改为true,就会抛出异常

注意2:

如果不设置属性的特性,configurable、enumerable、writable这些值都默认为false

Object.defineProperty(obj,'name',{});

这种情况,configurable、enumerable、writable这些值都默认为false,value为undefined

1.1.3.存取器描述

前面我们学习了 writable和value这两种描述选项,它们是和设置属性值相关的,我们还可以使用存取器getter和setter来获取或者设置值

var obj = {};
Object.defineProperty(obj,"name",{
    get:function (){} | undefined,   //getter 是一种获得属性值的方法
    set:function (value){} | undefined  // setter是一种设置属性值的方法。
    configurable: true | false
    enumerable: true | false
});

注意:使用了getter和setter后,writable和value就不能使用了

举例:

let obj = new Object()
 
Object.defineProperty(obj, "name", {
     get: function(){
         console.log("你正在获取name的值")
         return "nodeing"
     },
     enumerable: true,
     configurable: true
     
})

console.log(obj.name)   // 触发get函数, 当前name属性的值为get函数返回的值
 

举例2:

let obj = new Object()
let initValue = "nodeing"
Object.defineProperty(obj, "name", {
     get: function(){
         console.log("你正在获取name的值")
         return initValue
     },
     set: function(value){
        console.log("你正在设置name的值")
        // 如果使用 obj.name = "nodeing.com"来重新设置值,此时的值 nodeing.com,会传给set函数里的第一个参数,
        // 意味着 value = "nodeing.com"
        initValue = value
        console.log("设置完毕")
     },
     enumerable: true,
     configurable: true
     
})
obj.name = "nodeing.com"
console.log(obj.name)
 

从上面两个例子中可以看出,get和set两个函数并不是一定要同时成对出现的

1.2.实现Vue中的响应式数据

1.2.1.回顾Vue中的数据响应式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <h1>{{msg}}</h1>
    </div>
    <script src="./node_modules/vue/dist/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                msg: "nodeing"
            }
        })
        // 当修改msg值的时候,视图会跟着变化
        vm.msg = "nodeing.com"


    </script>
</body>
</html>

1.2.2.实现Vue中的数据响应式

我们的需求是,当数据发生变化,视图跟着变化,下面我们通过Object.defineProperty来实现

首先,我们来实现最简单的数据劫持

// 监听的数据对象
let data = {
    msg: "nodeing"
}

// 把data渲染到视图
function render(){
    console.log("模拟msg被渲染")
}

// 观察对象

function observer(obj){
   // 在这里面,需要把每个属性劫持到geter和setter
   // 首先判断obj是不是一个对象
   if(Object.prototype.toString.call(obj) === "[object Object]"){
       // 循环obj的属性,分别修改成getter和setter的模式
       for(let key in obj){
          toRective(obj,key,obj[key])
       }
   }
   
}


function toRective(obj,key,initValue) {
    Object.defineProperty(obj,key,{
        get:function() {
            return initValue
        },
        set: function (value) {
            initValue = value
            render()
        }
    })
}
// 调用观察者方法 观察data变化
observer(data)
 

上面就是最简单的数据劫持原理,但是,上面代码只是实现了对象第一层属性的监听,当数据变成下面这样:

let data = {
    msg: "nodeing",
    goods: {
        name:"手机",
        price: 100
    }
}

我们没办法监听到上面更深层次的对象,因此,还需要修改代码

// 监听的数据对象
let data = {
    msg: "nodeing",
    goods: {
        name:"手机",
        price: 100
    }
}

// 把data渲染到视图
function render(){
    console.log("模拟msg被渲染")
}

// 观察对象

function observer(obj){
   // 在这里面,需要把每个属性劫持到geter和setter
   // 首先判断obj是不是一个对象
   if(Object.prototype.toString.call(obj) === "[object Object]"){
       // 循环obj的属性,分别修改成getter和setter的模式
       for(let key in obj){
          toRective(obj,key,obj[key])
       }
   }
   
}


function toRective(obj,key,initValue) {
    if (Object.prototype.toString.call(initValue) === "[object Object]"){
        observer(initValue)
    }
    Object.defineProperty(obj,key,{
        get:function() {
            return initValue
        },
        set: function (value) {
            initValue = value
            render()
        }
    })
}
// 调用观察者方法 观察data变化
observer(data)
data.goods.price = 20

当给data对象的属性设置一个新的对象的时候,这个新对象也是需要被监听的

data.goods = {
    a:"手机",
    b: 200
}
data.goods.a = 20

如果设置data属性变成上面这样,我们需要修改toRective方法


function toRective(obj,key,initValue) {
    if (isObjcet(obj)){
        observer(initValue)
    }
    Object.defineProperty(obj,key,{
        get:function() {
            return initValue
        },
        set: function (value) {
            // 当新设置的值和旧的值一样的时候,就不渲染
            if(initValue === value) return
            // 当新设置的value值也是一个对象的时候,也需要observer操作
            if(isObjcet(value)){
                observer(value)
            }
            initValue = value
            render()
        }
    })
}
// 定义一个判断对象的方法
function isObjcet(obj) {
    return Object.prototype.toString.call(obj) === "[object Object]"
}

1.2.3.set方法和变更方法

前面的代码,我们已经大致实现了数据响应式变化,但还有一些问题需要解决,比如,当新增对象属性的时候,无法实现响应式,实际上,在vue中,对于已经创建的实例,Vue不允许动态添加根级别的响应式property的,查看文档

例如:

let data = {
    msg: "nodeing",
    goods: {
        name:"手机",
        price: 100
    }
}

// 新增一个a属性 这个a属性不是响应式的
data.a = 200

在vue中,实现了一个Vue.set方法来对新增属性进行响应式监听,它有一个别名vm.$set

Vue.set(vm.someObject, 'b', 2)

基于前面的代码,我们也可以去实现这个set方法

function $set(obj, key, value) {
    toRective(obj, key, value)
}

Object.defineProperty是不支持数组的,Vue文档中,包裹了一些数组的方法,它们是支持响应式的,这些方法有:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

我们也需要继续完善代码,以支持数组的更新检测

// 监听的数据对象
let data = {
    msg: "nodeing",
    goods: {
        name:"手机",
        price: 100
    }
}

// 把data渲染到视图
function render(){
    console.log("模拟msg被渲染")
}


// 需要支持更新检测的方法
let arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 实现思路: 1 这些方法都是在数组原型上的  2 我们重新包裹这些原型上的方法,在调用了这些原型方法后,调用render方法更新视图
// 第一步,先把原型复制一份出来,以免对数组原型产生破坏
let newPrototype = Object.create(Array.prototype)
arrayMethods.forEach((method)=>{
    newPrototype[method] = function () {
        Array.prototype[method].call(this, ...arguments)
        render()
    }
})

// 观察对象

function observer(obj){
   // 先判断是不是数组
   if(Array.isArray(obj)){
       obj.__proto__ = newPrototype
       
       return
   }
   // 在这里面,需要把每个属性劫持到geter和setter
   // 首先判断obj是不是一个对象
   if(Object.prototype.toString.call(obj) === "[object Object]"){
       // 循环obj的属性,分别修改成getter和setter的模式
       for(let key in obj){
          toRective(obj,key,obj[key])
       }
   }
   
}

function toRective(obj,key,initValue) {
    if (isObjcet(obj)){
        observer(initValue)
    }
    Object.defineProperty(obj,key,{
        get:function() {
            return initValue
        },
        set: function (value) {
            // 当新设置的值和旧的值一样的时候,就不渲染
            if(initValue === value) return
            if(isObjcet(value)){
                observer(value)
            }
            initValue = value
            render()
        }
    })
}

function isObjcet(obj) {
    return Object.prototype.toString.call(obj) === "[object Object]"
}



function $set(obj, key, value) {
    toRective(obj, key, value)
}
let arr = [1,2,3]


// 调用观察者方法 观察data变化
observer(arr)

arr.push(4)

 

我们自定义的$set方法也需要支持数组

function $set(obj, key, value){
    if(Array.isArray(obj)){
        return obj.splice(key,1,value)
    }
    toReactive(obj,key,value)
}

1.3.proxy基础用法

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写

如果要访问一个目标对象,在中间加一层拦截器,可以对外界的访问进行过滤和修改,例如:

let obj = {
    a: 1
}
let proxy = new Proxy(obj, {
    get(target, prop ){
        console.log(target, prop)
        return Reflect.get(target, prop)
    },
    set(target, key, value){
        console.log("xxx")
        return Reflect.set(target, key, value)
    }
})

proxy.a
 

1.4.使用proxy来实现响应式数据

let data = {
  msg: "hello nodeing",
  goods: {
    name: "手机",
    price: 200,
  },
  arr: [1,2,3]
};
function isObjcet(obj) {
  return Object.prototype.toString.call(obj) === "[object Object]";
}
function render() {
    console.log("模拟视图更新")
}

let handler = {
    get(target, prop){
        if (Array.isArray(target[prop]) || isObjcet(target[prop])){
            return new Proxy(target[prop], handler)
        }
        return Reflect.get(target, prop)
    },
    set(target,key, value){
        if (key ==="length") return true
        Reflect.set(target, key, value)
        render()
        return true
    }
}
let proxy = new Proxy(data, handler)

// proxy.msg = "20"
proxy.arr.push(1)
// proxy.goods.price = 3000
proxy.goods.price = 200