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

# 场景1

Vue2剥丝抽茧-响应式系统之watch (opens new window) 中,我们实现了 initWatch ,对于下边的代码

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {
    data: {
        title: "liang",
    },
    watch: {
        title(newVal, oldVal) {
            console.log("收到变化", newVal, oldVal);
        },
    },
};
observe(options.data);
initWatch(options.data, options.watch);

options.data.title = "changeTitle";

我们可以感知到 title 的变化,但 title 赋初始值的时候并不会感知到,Vue 中我们可以通过添加 immediate 属性来达到。

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {
    data: {
        title: "liang",
    },
    watch: {
        title: {
            handler(newVal, oldVal) {
                console.log("收到变化", newVal, oldVal);
            },
            immediate: true,
        },
    },
};
observe(options.data);
initWatch(options.data, options.watch);

options.data.title = "changeTitle";

回调函数是传递一个 handler 方法。

接下来我们来实现一下。

# 实现思路

其实思路非常简单,实现两点就可以:

  • 解析 handler ,将传入的 handleroptions 分开
  • 如果 immediatetrue ,立即执行一次回调函数

可以直接看代码了:

/**
 * Get the raw type string of a value, e.g., [object Object].
 */
const _toString = Object.prototype.toString;

/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
export function isPlainObject(obj) {
    return _toString.call(obj) === "[object Object]";
}

function createWatcher(data, expOrFn, handler, options) {
    // 如果是对象,就将 handler 和 options 分离
    if (isPlainObject(handler)) {
        options = handler;
        handler = handler.handler;
    }
    return $watch(data, expOrFn, handler, options);
}

立即执行回调函数:

function $watch(data, expOrFn, handler, options) {
    const watcher = new Watcher(data, expOrFn, handler);
  /******新增 *************************/
    if (options.immediate) {
        handler.call(data, watcher.value);
    }
  /************************************/
}

# 测试

回到开头的代码:

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {
    data: {
        title: "liang",
    },
    watch: {
        title(newVal, oldVal) {
            console.log("收到变化", newVal, oldVal);
        },
    },
};
observe(options.data);
initWatch(options.data, options.watch);

options.data.title = "changeTitle";

第一次得到初值的时候也会触发回调函数,只不过 oldValundefined

image-20220417132654191

# 场景2

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {
    data: {
        info: {
            name: {
                firstName: "wind",
                secondName: "liang",
            },
        },
    },
    watch: {
        "info.name": {
            handler(newVal, oldVal) {
                console.log("收到变化", newVal, oldVal);
            },
        },
    },
};
observe(options.data);
initWatch(options.data, options.watch);
options.data.info.name = { // 整体赋值
    firstName: "wind2",
    secondName: "liang2",
};
setTimeout(() => {
    options.data.info.name.firstName = "wind3"; // 单独赋值
}, 0);

当监听对象的时候,如果是对象 options.data.info.name 整体赋值,会调用回调函数。

但如果给对象中的属性单独赋值 options.data.info.name.firstName 就不会触发回调函数了。

(至于第二次赋值为什么放到了 setTimeout 中,可以回顾一下 Vue2剥丝抽茧-响应式系统之nextTick (opens new window)。)

为了监听到对象内部的变化,Vue 提供了 deep 属性供我们使用。

const options = {
    data: {
        info: {
            name: {
                firstName: "wind",
                secondName: "liang",
            },
        },
    },
    watch: {
        "info.name": {
            handler(newVal, oldVal) {
                console.log("收到变化", newVal, oldVal);
            },
            deep: true,
        },
    },
};

接下来我们来实现 deep 的功能。

# 实现思路

我们只需要在收集 Watcher 的过程中,深度遍历一遍当前对象,触发所有属性的 get ,然后每一个属性就会收集到当前 Watcher ,这样改变对象内部的值的时候,就会触发该 Watcher ,从而执行回调函数。

遍历对象的话,首先就需要一个 travel 函数。

/* @flow */

import { isObject } from "./util";

const seenObjects = new Set();

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse(val) {
    _traverse(val, seenObjects);
    seenObjects.clear();
}

function _traverse(val, seen) {
    let i, keys;
    const isA = Array.isArray(val);
    if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
        return;
    }
    if (val.__ob__) {
        const depId = val.__ob__.dep.id;
        if (seen.has(depId)) {
            return;
        }
        seen.add(depId);
    }
    // 判断是数组还是对象
    if (isA) {
        i = val.length;
        while (i--) _traverse(val[i], seen);
    } else {
        keys = Object.keys(val);
        i = keys.length;
       // 遍历对象的每一个 key
        while (i--) _traverse(val[keys[i]], seen);
    }
}

两个循环比较好理解,增加了seen 变量来去重是为了防止对象之间的循环引用,造成死循环。

const obj1 = {}
const obj2 = {}
obj1.data = obj2
obj2.data = obj1

const data = {
  obj1,
  obj2,
}

当我们遍历 obj1 的时候会遍历 obj2 ,遍历 obj2 的时候又会遍历 obj1 ,从而造成死循环。

有了 travel 函数以后,剩下的就水到渠成了。

首先在 Watcher 的构造函数中保存 deep 的值。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
        if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
        this.deps = [];
        this.newDeps = []; // 记录新一次的依赖
        this.newDepIds = new Set();
        this.id = ++uid; // uid for batching
        this.cb = cb;
        // options
        if (options) {
          /******新增 *************************/
            this.deep = !!options.deep;
           /************************************/
            this.sync = !!options.sync;
        }
        this.value = this.get();
    }
  ...
}

然后在执行当前 Watcher 的时候深度遍历对象的所有属性。

/**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this); // 保存包装了当前正在执行的函数的 Watcher
        let value;
        try {
            value = this.getter.call(this.data, this.data);
        } catch (e) {
            throw e;
        } finally {
          /******新增 *************************/
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value);
            }
            /************************************/
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }

$watch 方法中把 options 传递给 Watcher

function $watch(data, expOrFn, handler, options) {
    /******新增 options*************************/
    const watcher = new Watcher(data, expOrFn, handler, options);
  /************************************/
    if (options.immediate) {
        handler.call(data, watcher.value);
    }
    return function unwatchFn() {
        watcher.teardown();
    };
}

# 测试

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {
    data: {
        info: {
            name: {
                firstName: "wind",
                secondName: "liang",
            },
        },
    },
    watch: {
        "info.name": {
            handler(newVal, oldVal) {
                console.log("收到变化", newVal, oldVal);
            },
            deep: true,
        },
    },
};
observe(options.data);
initWatch(options.data, options.watch);

options.data.info.name = {
  firstName: "wind2",
  secondName: "liang2",
};

setTimeout(() => {
    options.data.info.name.firstName = "wind3";
}, 0);

这样的话两次修改就都会触发 Watcher 的更新了。

image-20220417172959487

# 总结

实现了 watch 中常用的 immediatedeep

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