Skip to content

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

Open
@youngwind

Description

@youngwind
Owner

前言

我们都知道,要想精通前端领域,研究分析成熟的框架是必不可少的一步。很多人可能都有这样的体会:“很努力地去阅读一些热门框架的源码,但是发现难度太高,花了很多时间却得不到什么,最终不得不放弃。”
我也一直被这个问题困扰,直到我想到了这样的一个方法。
从成熟框架的早期源码开始看起,从作者的第一个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/

Activity

physihan

physihan commented on Oct 21, 2016

@physihan

怎么去用chrome浏览器的控制台查看以及修改js数据啊,好神奇,还有这个动画是怎么做的

youngwind

youngwind commented on Oct 22, 2016

@youngwind
OwnerAuthor

gif录制软件licecap @physihan

zhangguixu

zhangguixu commented on Oct 28, 2016

@zhangguixu

你好。我想问一下如何查看一个开源项目的commit记录,之前一直以为在github上面就有。结果找半天没有找到

caiyongmin

caiyongmin commented on Oct 28, 2016

@caiyongmin

@zhangguixu 点击此处
image

zhangguixu

zhangguixu commented on Oct 28, 2016

@zhangguixu

thk~ 看到了。好蠢。。这么明显都没有看到。﹌○﹋ @caiyongmin

leoDreamer

leoDreamer commented on Oct 28, 2016

@leoDreamer

你好,在vue里提供了vm.$set()和Vue.set()两个方法去在更新对象属性后监听到改变,但是在单文件组件的开发方式下如何使用呢?貌似在控制台打印出this没有$set()方法,Vue根本没有

609519669

609519669 commented on Dec 21, 2016

@609519669

我想用你的方法checkout react最初的版本 发现commit 有8000多 这怎么找到最初的那个版本额 要是一页页翻简直不可能完成啊 求解~~~

youngwind

youngwind commented on Dec 22, 2016

@youngwind
OwnerAuthor

@zhouxiaoyan
放在以前,这是一件很容易的事情。因为原先github的commits页面是按照?page=100这样的参数组织分页的,所以我只需要通过url直接访问https://github.com/facebook/react/commits/master?page=100
, 就能知道第100页有没有commits。然后通过二分法便能快速定位到最后一个有commit的page是多少。

然而,现在就不容易了。因为github改版了,现在的commits分页页面是这样的https://github.com/facebook/react/commits/master?after=Y3Vyc29yOstm9cRAV8BHxA0tkrQuPluXwVcZKzM0 且这个after参数并非commit的id,这就很烦人了。

================分割线===============
我找了一个临时性的解决方案,那就是直接调用github提供的公共api: https://api.github.com/repos/facebook/react/commits?page=261
这个api依然支持page参数,所以,我们又可以愉快地使用二分法了!

缺点:由于是公共api,所以会有调用频次的限制。(可以通过注册应用来解决,不过很麻烦)
至此,你就可以使用这个方法找到react的第一个commit了:75897c2dcd1dd3a6ca46284dd37e13d22b4b16b4

然而,当你去查看这个commit的时候,你会发现有点懵逼。。因为即便是第一个commit,那代码也至少有成千上万行。究其原因,是因为react一开始就是facebook的内部产物,人家在内部开发得差不多了,才开源到github上,所以,之前的commit都丢失了,无法追溯。

另外,要提一点,那就是分支的选择。因为commit是可以通过rebase来重置合并的,比如像vue现在的master分支,你追溯master分支最早的commit,会发现是2016年的,这显然是合并过的。要想找到最最最初的版本,有时候你还得合理地选择分支,比如选择branch 0.11,这是vue的早期版本,其commit并没有被合并,所以可以通过公共api查询到。
还有其他人发现的一些方法:

  1. http://webapps.stackexchange.com/a/59893
  2. http://webapps.stackexchange.com/a/99526

不过,这些方法并不完善,请灵活使用。

总的来说,自github改版之后,我就没有找到非常快捷又方便的方法。如果你找到了,欢迎交流。

xlelou

xlelou commented on Feb 28, 2017

@xlelou

haonan

Larixs

Larixs commented on Mar 10, 2017

@Larixs

关于遗留问题2,提出一个方案:
在setter里重新追踪一下新值即可,即
if(typeof newVal =="object"){
new Observer(newVal);
}
有问题还望指正。

Yangfan2016

Yangfan2016 commented on Mar 10, 2017

@Yangfan2016

还是没懂

RebornL

RebornL commented on Mar 12, 2017

@RebornL

@Larixs 我也是这样去修复的,不知道这样有没有什么弊端呢???希望楼主可以解答一下,谢谢

34 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @yrl@muzea@zhangguixu@youngwind@caiyongmin

        Issue actions

          vue早期源码学习系列之一:如何监听一个对象的变化 · Issue #84 · youngwind/blog