Vue2数据响应式原理学习笔记
Vue
2021-08-23
401
0

Vue2的数据响应式是基于Object.defineProperty开实现的,因此我们要先学习这个方法。

Object.defineProperty()方法

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

兼容性

先来看看兼容性,Object.defineProperty是ECMAScript 5 新增的语法,IE9 开始完全兼容,基本上2011年后主流的浏览器都支持该语法。

image-20210606102125695

使用方法

let obj = {};

Object.defineProperty(obj, "a",{
    value: "123"
})

console.log(obj) // { a : 123 }

可以看出Object.defineProperty接受三个参数,第一个参数为要定义的对象,第二个参数为要定义或者修改的属性,第三个参数为属性的描述。

描述对象支持以下几个参数:

  • writable 表示属性是否可以被修改
  • enumerable 表示属性是否可以被枚举
  • configurable 表示属性的描述符是否能够被改变
  • value 表示属性的值
  • get 函数,访问该属性时,会调用此函数
  • set 函数,属性值被修改时,会调用此函数

writable

表示是否可写,默认为true。

let obj = {};

Object.defineProperty(obj, "a", {
    value: 123,
    writable: false
})

Object.defineProperty(obj, "b", {
    value: 123,
    writable: true
})

obj.a++;
obj.b++;

console.log(obj) // {a: 123, b: 124}

enumerable

表示属性是否可以被枚举,默认为false。设置为false之后将无法被for ... in ... 进行遍历到。

let obj = {};

Object.defineProperty(obj, "a", {
    value: 1,
    enumerable: true
})

Object.defineProperty(obj, "b", {
    value: 2,
    enumerable: true
})


Object.defineProperty(obj, "c", {
    value: 3,
    enumerable: false
})

for (key in obj) {
    console.log(key)  
}
// 这里只会输出a和b, c不会被遍历到
// a
// b

console.log(obj) // {a: 1, b: 2, c: 3}

configurable

表示属性的描述符是否能够被改变,例如被删除。

get与set函数

重点来了!!!getset 是实现响应式的核心函数。

  • get函数,访问该属性时,会调用此函数,该函数的返回值会被用做属性的值。
  • set函数,属性值被修改时,会调用此函数,该函数接受被赋予的新值作为参数。
let obj = {};

Object.defineProperty(obj, "a", {
    enumerable: true,
    configurable: true,
    get() {
        console.log("访问a");
        return 1;
    },
    set(a) {
        console.log("设置a", a);
    }
})

obj.a = 10;
console.log(obj.a)

// 会输出以下内容
/*
    设置a 10
    访问a
    1
*/

我们会发现,无论我们给obj.a赋予任何值,obj.a会永远返回1,这是因为get()的返回值为1。

封装defineReactive()函数

如果我们想去实现一个数据劫持,会发现不太好用,因为我们需要在外部定义一个变量并使用get和set来访问和修改它。所以我们可以去定义一个defineReactive函数,创建出一个闭包环境来存放这个变量。

let obj = {};

function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            console.log("访问a", value);
            return value;
        },
        set(newValue) {
            console.log("设置a为", newValue);
            value = newValue;
        }
    })

}

defineReactive(obj, "a", 1);

console.log(obj.a)
obj.a = 2;
console.log(obj.a)

//结果是
/*
    访问a 1
    1
    设置a为 2
    访问a 2
    2
*/

递归侦测对象所有属性

我们将defineReactive函数封装在一个js文件以方便调用:

defineReactive.js

import observe from "./observe.js"

export default function defineReactive(data, key, value) {
    if (arguments.length == 2) {
        value = data[key]
    }
    // 子元素要进行observe,这里有递归
    let childObj = observe(value);

    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,

        get() {
            console.log(`正在读取${key}属性`);
            return value;
        },
        set(newValue) {
            console.log(`正在设置${key}属性`);
            value = newValue;
            // 当设置了新值,这个新值也要被observe
            childObj = observe(newValue);
        }
    })
}

定义observe方法来让一个对象变成响应式。

observe.js

import Observer from "./Observer.js"

// 这个函数本质上是在给这个对象身上加上__ob__属性
export default function observe(value) {
    if (typeof value !== "object") return;

    let ob;
    if (typeof value.__ob__ !== "undefined") {
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob;
}

观察者类

Observer.js

import { def } from './utils'
import defineReactive from "./defineReactive.js"
import observe from './observe.js';

export default class Observer {
    constructor(value) {
        // 给value添加一个属性,值为创建出来的Observer对象
        def(value, "__ob__", this, false); // false 不希望这个属性能被枚举出来
        this.walk(value);
    }

    walk(value) {
        for (let key in value) {
            defineReactive(value, key)
        }
    }
}

工具方法,用于定义一个属性

utils.js

function def(data, key, value, enumerable) {
    Object.defineProperty(data, key, {
        value,
        enumerable,
        configurable: true,
        writable: true
    })
}

export {
    def
}

数组的响应式处理

数组响应式是通过改写7个会修改数组的方法来实现的。核心语句是 Object.setPrototypeOf(value, arrayMethods),用于替换Array实例身上的__proto__

array.js

import { def } from "./utils.js"

const arrayPrototype = Array.prototype;

// 以arrayPrototype为原型,创建对象
const arrayMethods = Object.create(arrayPrototype);

const arrayMethodNames = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse"
]

arrayMethodNames.forEach(methodName => {
    const originalMethod = arrayPrototype[methodName];
    def(arrayMethods, methodName, function() {
        console.log("数组操作中...")
        let ob = this.__ob__;
        // 有三种方法能插入新项目,要把新的项目也变成响应式
        let insertedArr;
        const args = [...arguments]
        switch (methodName) {
            case "push":
            case "unshift":
                insertedArr = args;
                break;
            case "splice":
                insertedArr = args.splice(2);
                break;
        }

        if (insertedArr.length > 0) {
            ob.observeArray(insertedArr)
        }

        return originalMethod.apply(this, arguments)
    }, false)
})

export { arrayMethods }

Observer类要新增相应的处理

observer.js

import { def } from './utils'
import defineReactive from "./defineReactive.js"
import { arrayMethods } from "./array.js"
import observe from './observe.js';

export default class Observer {
    constructor(value) {
        // 给value添加一个属性,值为创建出来的Observer对象
        def(value, "__ob__", this, false); // false 不希望这个属性能被枚举出来
        if (Array.isArray(value)) {
            Object.setPrototypeOf(value, arrayMethods);
            // 如果这个值为数组,那么要遍历它的每一项使之成为响应式
            this.observeArray(value)
        } else {
            this.walk(value);
        }
        console.log(value)
    }

    walk(value) {
        for (let key in value) {
            defineReactive(value, key)
        }
    }

    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            observe(arr[i])
        }
    }
}

依赖收集

核心概念:在getter中收集依赖,在setter中触发依赖.

每一个Observer实例身上都应该有个一个dep。

Reference

Vue