vue早期源码学习系列之一:如何监听一个对象的变化

@youngwind 2016-08-20 12:26:50发表于 youngwind/blog Vue

前言

我们都知道,要想精通前端领域,研究分析成熟的框架是必不可少的一步。很多人可能都有这样的体会:“很努力地去阅读一些热门框架的源码,但是发现难度太高,花了很多时间却得不到什么,最终不得不放弃。”
我也一直被这个问题困扰,直到我想到了这样的一个方法。
从成熟框架的早期源码开始看起,从作者的第一个commit开始看起,然后逐个的往前翻。这样一开始的代码量不多,多看几遍还是可以理解的。而且在这个过程中,就像电影回放一样,我们可以看到作者先写什么,后写什么,在哪些地方进行了什么样的改良,其中又不小心引入了什么bug,等等。
这真的是一个很好的办法。所以,我就用这个方法来研究vue的源码。

目标

我checkout到的版本是这个位置,在这个版本中,我们可以发现:代码中主要是observer和emitter这些东西,这些是以后实现数据绑定以及$watch的关键基础。所以我们把本篇的学习目标定位为:“如何监听一个对象的变化” 具体要实现效果如下图所示。
demo

注:这个版本build出来的vue代码运行是会报错了,根据错误提示,很容易定位到错误原因是src/internal/init.js文件头部少引了observer和util,添加上就好了。

思路

我们有两个难点需要解决。

第一:当对象的某个属性变化的时候,如何触发自定义的回调函数?
答案:ES5中新添加了一个方法:Object.defineProperty,通过这个方法,可以自定义getter和setter函数,从而在获取对象属性和设置对象属性的时候能够执行自定义的回调函数。

第二:对象往往是一个深层次的结构,对象的某个属性可能仍然是一个对象,这种情况怎么处理?
比如说

let data = {
    user: {
        name: "liangshaofeng",
        age: "24"
    },
    address: {
        city: "beijing"
    }
};

答案:递归算法,也就是下面代码中的walk函数。如果对象的属性仍然是一个对象的话,那么继续new一个Observer,直到到达最底层的属性位置。

下面是实现的具体代码。

代码

// 观察者构造函数
function Observer(data) {
    this.data = data;
    this.walk(data)
}

let p = Observer.prototype;

// 此函数用于深层次遍历对象的各个属性
// 采用的是递归的思路
// 因为我们要为对象的每一个属性绑定getter和setter
p.walk = function (obj) {
    let val;
    for (let key in obj) {
        // 这里为什么要用hasOwnProperty进行过滤呢?
        // 因为for...in 循环会把对象原型链上的所有可枚举属性都循环出来
        // 而我们想要的仅仅是这个对象本身拥有的属性,所以要这么做。
        if (obj.hasOwnProperty(key)) {
            val = obj[key];

            // 这里进行判断,如果还没有遍历到最底层,继续new Observer
            if (typeof val === 'object') {
                new Observer(val);
            }

            this.convert(key, val);
        }
    }
};

p.convert = function (key, val) {
    Object.defineProperty(this.data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            console.log('你访问了' + key);
            return val
        },
        set: function (newVal) {
            console.log('你设置了' + key);
            console.log('新的' + key + ' = ' + newVal)
            if (newVal === val) return;
            val = newVal
        }
    })
};

let data = {
    user: {
        name: "liangshaofeng",
        age: "24"
    },
    address: {
        city: "beijing"
    }
};

let app = new Observer(data);

遗留问题

上面实现的代码还有很多问题。
比如:

  1. 只监听的对象的变化,没有处理数组的变化。
  2. 当你重新set的属性是对象的话,那么新set的对象里面的属性不能调用getter和setter。比如像下图所示,重新设置的job属性就不在带有自定义的getter和setter了,不再提示“你访问了job"这些字样。
    bug

参考资料:

  1. https://segmentfault.com/a/1190000004384515
  2. http://jiongks.name/blog/vue-code-review/