Vue源码学习之一:监听数据对象变化

监听数据对象变化,最容易想到的是建立一个需要监视对象的表,定时扫描其值,有变化,则执行相应操作,不过这种实现方式,性能是个问题,如果需要监视的数据量大的话,每扫描一次全部的对象,需要的时间很长。当然,有些框架是采用的这种方式,不过他们用非常巧妙的算法提升性能,这不在我们的讨论范围之类。

Vue 中数据对象的监视,是通过设置 ES5 的新特性(ES7 都快出来了,ES5 的东西倒也真称不得新)Object.defineProperty() 中的 set、get 来实现的。

目标

与官方文档第一个例子相似,不过也有简化,因为这篇只是介绍下数据对象的监听,不涉及文本解析,所以文本解析相关的直接舍弃了:

<div id="app"></div>
var app = new Vue({
  el: 'app',
  data: {
    message: 'Hello Vue!'
  }
});

浏览器显示:

Hello Vue!

在控制台输入诸如:

app.message = 'Changed!'

之类的命令,浏览器显示内容会跟着修改。

Object.defineProperty

引用 MDN 上的定义:

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

与此相生相伴的还有一个 Object.getOwnPropertyDescriptor()

**Object.getOwnPropertyDescriptor()
** 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

下面的例子用一种比较简单、直观的方式来设置 setter、getter:

var dep = [];

function defineReactive(obj, key, val) {
  // 有自定义的 property,则用自定义的 property
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if(property && property.configurable === false) {
    return;
  }
  
  var getter = property && property.get;
  var setter = property && property.set;

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      var value = getter ? getter.call(obj) : val;
      dep.push(value);
      return value;
    },
    set: function(newVal) {
      var value = getter ? getter.call(obj) : val;
      // set 值与原值相同,则不更新
      if(newVal === value) {
        return;
      }
      if(setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      console.log(dep);
    }
  });
}
var a = {};
defineReactive(a, 'a', 12);
// 调用 getter,12 被压入 dep,此时 dep 值为 [12]
a.a;
// 调用 setter,输出 dep ([12])
a.a = 24;
// 调用 getter,24 被压入 dep,此时 dep 值为 [12, 24]
a.a;

Observer

简单说过 Object.defineProperty 之后,就要开始扯 Observer 了。observer,中文解释为“观察者”,观察什么东西呢?观察对象属性值的变化。故此,所谓 observer,就是给对象的所有属性加上 getter、setter,如果对象的属性还有属性,比如说 {a: {a: {a: 'a'}}},则通过递归给其属性的属性也加上 getter、setter:

function Observer(value) {
  this.value = value;
  this.walk(value);
}
Observer.prototype.walk = function(obj) {
  var keys = Object.keys(obj);
  for(var i = 0; i < keys.length; i++) {
    // 给所有属性添加 getter、setter
    defineReactive(obj, keys[i], obj[keys[i]]);
  }
};

var dep = [];

function defineReactive(obj, key, val) {
  // 有自定义的 property,则用自定义的 property
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if(property && property.configurable === false) {
    return;
  }
  
  var getter = property && property.get;
  var setter = property && property.set;

  // 递归的方式实现给属性的属性添加 getter、setter
  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      var value = getter ? getter.call(obj) : val;
      dep.push(value);
      return value;
    },
    set: function(newVal) {
      var value = getter ? getter.call(obj) : val;
      // set 值与原值相同,则不更新
      if(newVal === value) {
        return;
      }
      if(setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 给新赋值的属性值的属性添加 getter、setter
      childOb = observe(newVal);
      console.log(dep);
    }
  });
}

function observe(value) {
  if(!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
}

Watcher

Observer 通过设置数据对象的 getter、setter 来达到监听数据变化的目的。数据被获取,被设置、被修改,都能监听到,且能做出相应的动作。

现在还有一个问题就是,谁让你监听的?

这个发出指令的就是 Watcher,只有 Watcher 获取数据才触发相应的操作;同样,修改数据时,也只执行 Watcher 相关操作。

那如何讲 Observer、Watcher 两者关联起来呢?全局变量!这个全局变量,只有 Watcher 才做修改,Observer 只是读取判断,根据这个全局变量的值不同而判断是否 Watcher 对数据进行读取,这个全局变量可以附加在 dep 上:

dep.target = null;

根据以上所述,简单整理下,代码如下:

function Watcher(data, exp, cb) {
  this.data = data;
  this.exp = exp;
  this.cb = cb;
  this.value = this.get();
}
Watcher.prototype.get = function() {
  // 给 dep.target 置值,告诉 Observer 这是 Watcher 调用的 getter
  dep.target = this;
  // 调用 getter,触发相应响应
  var value = this.data[this.exp];
  // dep.target 还原
  dep.target = null;
  return value;
};
Watcher.prototype.update = function() {
  this.cb();
};
function Observer(value) {
  this.value = value;
  this.walk(value);
}
Observer.prototype.walk = function(obj) {
  var keys = Object.keys(obj);
  for(var i = 0; i < keys.length; i++) {
    // 给所有属性添加 getter、setter
    defineReactive(obj, keys[i], obj[keys[i]]);
  }
};

var dep = [];
dep.target = null;

function defineReactive(obj, key, val) {
  // 有自定义的 property,则用自定义的 property
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if(property && property.configurable === false) {
    return;
  }
  
  var getter = property && property.get;
  var setter = property && property.set;

  // 递归的方式实现给属性的属性添加 getter、setter
  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      var value = getter ? getter.call(obj) : val;
      // 如果是 Watcher 监听的,就把 Watcher 对象压入 dep
      if(dep.target) {
        dep.push(dep.target);
      }
      return value;
    },
    set: function(newVal) {
      var value = getter ? getter.call(obj) : val;
      // set 值与原值相同,则不更新
      if(newVal === value) {
        return;
      }
      if(setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 给新赋值的属性值的属性添加 getter、setter
      childOb = observe(newVal);
      // 按序执行 dep 中元素的 update 方法
      for(var i = 0; i < dep.length; i++) {
        dep[i].update(); 
      }
    }
  });
}

function observe(value) {
  if(!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
}
var data = {a: 1};
new Observer(data);
new Watcher(data, 'a', function(){console.log('it works')});
data.a =12;
data.a =14;

上面基本实现了数据的监听,bug 肯定有不少,不过只是一个粗糙的 demo,只是想展示一个大概的流程,没有扣到非常细致。

Dep

上面几个例子,dep 是个全局的数组,但凡 new 一个 Watcher,dep 中就要多一个 Watcher 实例,这时候不管哪个 data 更新,所有的 Watcher 实例的 update 都会执行,这是不可接受的。

Dep 抽象出来,单独搞一个构造函数,不放在全局,就能解决了:

function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
};
Dep.prototype.notify = function() {
  var subs = this.subs.slice();
  for(var i = 0; i < subs.length; i++) {
    subs[i].update();
  }
}

利用 Dep 将上面的代码改写下就好了(当然,此处的 Dep 代码也不完全,只是一个大概的意思罢了)。

Vue 实例代理 data 对象

官方文档中有这么一句话:

每个 Vue 实例都会代理其 data 对象里所有的属性。

var data = { a: 1 };
var vm = new Vue({data: data});

vm.a === data.a // -> true

// 设置属性也会影响到原始数据
vm.a = 2
data.a // -> 2

// ... 反之亦然
data.a = 3
vm.a // -> 3

这种代理看起来很麻烦,其实也是可以通过 Object.defineProperty 来实现的:

function Vue(options) {
  var data = this.data = options.data;
  
  var keys = Object.keys(data);
  var i = keys.length;
  while(i--) {
    proxy(this, keys[i];
  }
}
function proxy(vm, key) {
  Object.defineProperty(vm, key, {
    configurable: true,
    enumerable: true,
    // 直接获取 vm.data[key] 的值
    get: function() {
      return vm.data[key];
    },
    // 设置值的时候直接设置 vm.data[key] 的值
    set: function(val) {
      vm.data[key] = val;
    }
  };
}

捏出一个 Vue,实现最初目标

var Vue = (function() {
    var Watcher = function Watcher(vm, exp, cb) {
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        this.value = this.get();
    };
    Watcher.prototype.get = function get() {
        Dep.target = this;
        var value = this.vm._data[this.exp];
        Dep.target = null;
        return value;
    };
    Watcher.prototype.addDep = function addDep(dep) {
        dep.addSub(this);
    };
    Watcher.prototype.update = function update() {
        this.run();
    };
    Watcher.prototype.run = function run() {
        this.cb.call(this.vm);
    }

    var Dep = function Dep() {
        this.subs = [];
    };
    Dep.prototype.addSub = function addSub(sub) {
        this.subs.push(sub);
    };
    Dep.prototype.depend = function depend() {
        if(Dep.target) {
            Dep.target.addDep(this);
        }
    };
    Dep.prototype.notify = function notify() {
        var subs = this.subs.slice();
        for(var i = 0; i < subs.length; i++) {
            subs[i].update();
        }
    };

    Dep.target = null;

    var Observer = function Observer(value) {
        this.value = value;
        this.dep = new Dep();

        this.walk(value);
    };
    Observer.prototype.walk = function walk(obj) {
        var keys = Object.keys(obj);

        for(var i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    };

    function defineReactive(obj, key, val) {
        var dep = new Dep();

        var property = Object.getOwnPropertyDescriptor(obj, key);
        if(property && property.configurable === false) {
            return;
        }

        var getter = property && property.get;
        var setter = property && property.set;

        var childOb = observe(val);
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter() {
                var value = getter ? getter.call(obj) : val;

                if(Dep.target) {
                    dep.depend();
                    if(childOb) {
                        childOb.dep.depend();
                    }
                }
                return value;
            },
            set: function reactiveSetter(newVal) {
                var value = getter ? getter.call(obj) : val;
                if(newVal === value) {
                    return;
                }
                if(setter) {
                    setter.call(obj, newVal);
                } else {
                    val = newVal;
                }
                childOb = observe(newVal);
                dep.notify();
            }
        });
    }
    function observe(value) {
        if(!value || typeof value !== 'object') {
            return;
        }
        return new Observer(value);
    }

    function Vue(options) {
        var vm = this;
        this._el = options.el;
        var data = this._data = options.data;

        var keys = Object.keys(data);
        var i = keys.length;
        while(i--) {
            proxy(this, keys[i]);
        }
        observe(data);

        var elem = document.getElementById(this._el);
        elem.innerHTML = vm.message;
        
        new Watcher(this, 'message', function() {
            elem.innerHTML = vm.message;
        });

    }
    function proxy(vm, key) {
        Object.defineProperty(vm, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter() {
                return vm._data[key];
            },
            set: function proxySetter(val) {
                vm._data[key] = val;
            }
        });
    }
    return Vue;
})();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script type="text/javascript" src="vue.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript">
        var app = new Vue({
            el: 'app',
            data: {
                message: 'aaaaaaaaaaaaa'
            }
        });
    </script>
</body>
</html>

参考资料:
vue 源码分析之如何实现 observer 和 watcher
vue早期源码学习系列之一:如何监听一个对象的变化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,349评论 8 64
  • 我的github: vue双向绑定原理 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们...
    KlausXu阅读 44,725评论 7 91
  • 最近因为公司要用到Vue.js开发,想着看一看源码,知己知彼。 从Vue对象的构造开始说起 本节目标,是能够理解下...
    星期五__阅读 4,262评论 3 31
  • 数据绑定模块 最核心的三个类: Observer,Watcher,Dep observer是Vue核心中最重要的一...
    Yang152412阅读 1,072评论 0 48
  • 今年七月份毕业 我是宿舍里最后一个走的人 打扫,检查,交钥匙,盖章 自己一个人走完所有的流程 那个时候我并不难过,...
    婷纸大人阅读 164评论 0 0