Vue2 源码从零详解系列文章, 还没有看过的同学可能需要看一下之前的,vue.windliang.wang/ (opens new window)

# 数组set

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: [1, 2],
};
observe(data);
const updateComponent = () => {
    console.log(data.list);
};

new Watcher(updateComponent);

list[0] = 3;

list[0] 会触发 updateComponent 的重新执行吗?

可以先思考一下。

... ...

答案是否定的,数组我们只能通过重写的 pushsplice 等方法去触发更新,详见 Vue2剥丝抽茧-响应式系统之数组 (opens new window)

如果我们想要替换数组某个元素的话可以转一下弯,通过 splice 去实现。

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: [1, 2],
};
observe(data);
const updateComponent = () => {
    console.log(data.list);
};

new Watcher(updateComponent);

// list[0] = 3;
data.list.splice(0, 1, 3);

每次这样写太麻烦了,我们可以提供一个 set 方法供用户使用。

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set(target, key, val) {
    if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        return val;
    }

    // targe 是对象的情况
    // ...
}

然后我们直接使用 set 方法就可以了。

import { observe, set } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: [1, 2],
};
observe(data);
const updateComponent = () => {
    console.log(data.list);
};

new Watcher(updateComponent);

// list[0] = 3;
// data.list.splice(0, 1, 3);
set(data.list, 0, 3);

# 数组 del

同数组 set ,我们顺便提供一个 del 的方法,支持数组响应式的删除某个元素。

/**
 * Delete a property and trigger change if necessary.
 */
export function del(target, key) {
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1);
        return;
    }
    // targe 是对象的情况
    // ...
}

# 对象 set

import { observe, set, del } from "./reactive";
import Watcher from "./watcher";
const data = {
    obj: {
        a: 1,
        b: 2,
    },
};
observe(data);
const updateComponent = () => {
    const c = data.obj.c ? data.obj.c : 0;
    console.log(data.obj.a + data.obj.b + c);
};

new Watcher(updateComponent);

data.obj.c = 3;

updateComponent 方法中虽然使用了 objc 属性,但是在调用 observe 之前,data.obj 中并没有 c 属性,所以 c 属性不是响应式的。

当我们修改 data.obj.c 的值的时候,并不会触发 updateComponent 的执行。

如果想要变成响应式的话,一种方法就是在最开始就定义 c 属性。

const data = {
    obj: {
        a: 1,
        b: 2,
        c: null,
    },
};
observe(data);
const updateComponent = () => {
    const c = data.obj.c ? data.obj.c : 0;
    console.log(data.obj.a + data.obj.b + c);
};

new Watcher(updateComponent);

data.obj.c = 3;

另一种方法就是通过 set 去设置新的属性了,在 set 中我们可以将新添加的属性设置为响应式的。

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set(target, key, val) {
    if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        return val;
    }

    // targe 是对象的情况
    // key 在 target 中已经存在
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }

    const ob = target.__ob__;
    // target 不是响应式数据
    if (!ob) {
        target[key] = val;
        return val;
    }
  	// 将当前 key 变为响应式的
    defineReactive(target, key, val);
    return val;
}

回到我们之前的程序:

import { observe, set, del } from "./reactive";
import Watcher from "./watcher";
const data = {
    obj: {
        a: 1,
        b: 2,
    },
};
observe(data);
const updateComponent = () => {
    const c = data.obj.c ? data.obj.c : 0;
    console.log(data.obj.a + data.obj.b + c);
};

const ob = new Watcher(updateComponent);

set(data.obj, "c", 3);

虽然通过 set 增加了属性,但是此时 Watcher 并不会重新触发,原因的话我们看下依赖图。

image-20220409102100995

虽然属性 c 拥有了 Dep 对象,但由于没有调用过依赖属性 cWatcher ,所以它并没有收集到依赖。

当然我们可以 set 完手动调用一次相应的 Watcher

const data = {
    obj: {
        a: 1,
        b: 2,
    },
};
observe(data);
const updateComponent = () => {
    const c = data.obj.c ? data.obj.c : 0;
    console.log(data.obj.a + data.obj.b + c);
};

const ob = new Watcher(updateComponent);

set(data.obj, "c", 3);
ob.update(); // 手动调用 Watcher

data.obj.c = 4;

这样的话,当执行 data.obj.c = 4 的时候就会触发 Watcher 的执行了。

那么我们能将触发相应的 Watcher 的逻辑放到 set 函数中吗?

image-20220409102100995

可以看到 obj 里也有个 Dep,这个其实当时是为数组准备的,参考 Vue2剥丝抽茧-响应式系统之数组 (opens new window),但 objdep 什么都没收集。

我们修改一下代码让它也收集一下:

export function defineReactive(obj, key, val, shallow) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // 读取用户可能自己定义了的 get、set
    const getter = property && property.get;
    const setter = property && property.set;
    // val 没有传进来话进行手动赋值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }
    const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
    let childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                  	/******新位置 *************************/
                		childOb.dep.depend();
                  	/**********************************/
                    if (Array.isArray(value)) {
                       // childOb.dep.depend(); //原来的位置
                        dependArray(value);
                    }
                }
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            childOb = !shallow && observe(newVal);
            dep.notify();
        },
    });
}

function dependArray(value) {
    for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i];
      	/******新位置 *************************/
        e && e.__ob__ && e.__ob__.dep.depend();
      	/**********************************/
        if (Array.isArray(e)) {
           //  e && e.__ob__ && e.__ob__.dep.depend(); // 原位置
            dependArray(e);
        }
    }
}

因为读取 a 属性,一定先会读取 obj 属性,即 data.obj.ab 也同理。

所以通过上边的修改,objdep 会收集到它的所有属性的依赖,也就是这里的 ab 的依赖,但因为 ab 的依赖是相同的,所以收集到一个依赖。

image-20220409104705075

但其实我们并不知道 c 被哪些 Watcher 依赖,我们只知道和 c 同属于一个对象的 ab 被哪些 Watcher 依赖,但大概率 c 也会被其中的 Watcher 依赖。

所以我们可以在 set 中手动执行一下 objDep ,依赖 cWatcher 大概率会被执行,相应的 c 也会成功收集到依赖。

export function set(target, key, val) {
    if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        return val;
    }

    // targe 是对象的情况
    // key 在 target 中已经存在
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }

    const ob = target.__ob__;
    // target 不是响应式数据
    if (!ob) {
        target[key] = val;
        return val;
    }
    defineReactive(target, key, val);
   /******新增 *************************/
    ob.dep.notify() 
    /************************************/
    return val;
}

回到最开始的代码:

const data = {
    obj: {
        a: 1,
        b: 2,
    },
};
observe(data);
const updateComponent = () => {
    const c = data.obj.c ? data.obj.c : 0;
    console.log(data.obj.a + data.obj.b + c);
};

const ob = new Watcher(updateComponent);

set(data.obj, "c", 3);

执行完后 c 除了变为响应式的,也成功触发了 Watcher 执行,并且收集到了 Watcher

image-20220409105217629

此时如果修改 c 的值,也会成功触发 Watcher 的执行了。

# 对象 del

有了上边的了解,删除就很好解决了。

image-20220409105217629

如果要删除 a 属性,删除后执行下它相应的 Dep 就可以。但 aDep 是存在闭包中的,我们并不能拿到。

退而求其次,我们可以去执行 a 属性所在的对象 objDep 就可以了。

/**
 * Delete a property and trigger change if necessary.
 */
export function del(target, key) {
    if (Array.isArray(target)) {
        target.splice(key, 1);
        return;
    }
    // targe 是对象的情况
    const ob = target.__ob__;
    if (!hasOwn(target, key)) {
        return;
    }
    delete target[key];
    if (!ob) {
        return;
    }
    ob.dep.notify();
}

#

通过为对象收集依赖,将对象、数组的修改、删除也变成响应式的了,同时为用户提供了 setdel 方法。

Last Updated: 6/6/2022, 9:53:00 PM