如何理解Vue源码中defineReactive、observe和set这三个有关响应式的函数

953 阅读3分钟

怎么给对象定义响应式属性,大家可能都会想到Object.defineProperty,如下:

let  obj = {};

Object.defineProperty(obj, 'name',{
    get(){
        console.log('get', 'name');
        return obj.name;
    },
    set(val){
        console.log('set', val)
    }
})

obj.name
obj.name = '李四'

我们在取值和赋值的时候,会有以下输出:

1650514188(1).png

也就是说我们在取值和赋值的时候,利用Object.defineProperty做了拦截,即分别调用了getset方法。

为了使用方便,可以把以上代码封装成为一个函数,如下:

function defineReactive(obj, key, val){
    Object.defineProperty(obj, key,{
        get(){
            console.log('get', key);
            return val;
        },
        set(newVal){
            if(newVal != val){
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

函数defineReactive很有意思,他是一个闭包结构,每调用一次就会形成一个闭包,使用时如下:

let  obj = {};

defineReactive(obj, 'name', '张三')
obj.name = '李四'

当我们有很多属性时,该怎么办呢?总不能调用很多次吧,如:

defineReactive(obj, 'name', '张三')
defineReactive(obj, 'age', '18')
defineReactive(obj, 'gender', '男')
...

显然是不合理的,我们可以定义一个observe函数,去遍历对象的所有属性,整体代码如下:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) return;

    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}

let obj = {
    name: '张三',
    age: '18'
};

observe(obj)

obj.name
obj.name = '李四'

通过以上代码可以发现,运行结果依然没有变,通过observe函数遍历obj所有属性并都定义成了相应式了,但这有个问题:

如果对象的一个属性值还是一个对象怎么办?

let obj = {
    name: '张三',
    age: '18',
    info:{
        content: '自我介绍',
        work: '工作经历'
    }
}

observe(obj)
obj.inf.content

这时按我们所想应该输出:get into 、get content,实际输出是什么样的呢?如下:

1650520684(1).png

这是为什么呢?原因也很简单,我们只监测了对象的第一层,若对象的值还是对象以及更深层的监测,我们并没有做,解决办法就是调用递归监测,即在defineReactive函数里再调用下oberve函数即可,如下:

function defineReactive(obj, key, val) {
    // 递归监测
    observe(val);

    // 定义响应式
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    };

    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}

let obj = {
    name: '张三',
    age: '18',
    info: {
        content: '自我介绍',
        work: '工作经历'
    }
}

observe(obj)
obj.info.content
obj.info.content = '自我介绍 很棒'

此时输出的结果就如我们所想了,如下:

1650520876(1).png

通过以上观察我们还得到一个结论:我们在对obj这个对象的属性做响应式的时候是obj已经创建了所有属性,那我们再调用observe函数后,再给对象添加一个新属性,这时新属性也会有响应式吗?

let obj = {
    name: '张三',
    age: '18',
    info: {
        content: '自我介绍',
        work: '工作经历'
    }
}

observe(obj)
obj.gender = '男'

发现并没有任何输出,即没有调用get函数,也没有调用set函数。这时是不是就想到了Vue里面的this.$set函数了,而这$set函数干了什么事呢,其实也很简单就是调用了一次defineReactive函数,如下:

// 相应响应式
function defineReactive(obj, key, val) {
    // 递归监测
    observe(val);

    // 定义响应式
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}
// 遍历属性
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    };

    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}
// 新增属性添加响应式
function set(obj, key, val) {
    defineReactive(obj, key, val)
}

let obj = {
    name: '张三',
    age: '18',
    info: {
        content: '自我介绍',
        work: '工作经历'
    }
}

observe(obj)

set(obj, 'gender', '男');
obj.gender
obj.gender = '女'

这时就解决了新添加的属性没有响应式的问题,输出如下:

1650521571(1).png

以上就是简单模拟了Vue中的defineReactive、observe和set的三个函数,希望对你理解有所帮助,nice ~~