vue 响应式原理

借助es5的Object.defineProperty实现vue数据劫持原理
首先画一个html页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script src="./watcher.js"></script>
  <script src="./observer.js"></script>
  <script src="./mvvm.js"></script>
  <script>
    const vm = new MVVM({
      el: '#app',
      data: {
        country: 'china',
        content: 'family',
        family: {
          count: 4
        }
      },
      computed: {
        ageTotal() {
          return this.family.father.age + this.family.mother.age + this.family.children.age
        }
      },
      watch: {
        "family.count": function(nV, oV){
          console.log(nV);
        }
      }
    });
    vm.family.count = 100;
  </script>
</body>
</html>

看一下mvvm.js 的内容

function MVVM (options){
  this.$options = options;
  var data = this._data = this.$options.data;
  let me = this;
  // 将this.aaa代理到this._data.aaa
  Object.keys(data).forEach(key => {
    me._proxyData(key);
  });
  // 初始化计算属性
  this._initComputed();
  // 利用递归进行数据劫持
  observe(data, this);
  // 初始化watcher
  this._initWatcher();
}
MVVM.prototype = {
  _proxyData: function(key ,setter, getter){
    let me = this;
    setter = setter || 
    Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function proxyGetter(){
        return me._data[key];
      },
      set: function proxySetter(newVal){
        me._data[key] = newVal;
      }
    });
  },
  _initComputed: function () {
    let me = this;
    let computed = this.$options.computed;
    if (typeof computed === 'object') {
      Object.keys(computed).forEach(key => {
        Object.defineProperty(me, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function (){}
        });
      });
    }
  },
  _initWatcher: function () {
    let me = this;
    let watcher = this.$options.watch;
    if (typeof watcher === 'object') {
      Object.keys(watcher).forEach(key => {
        new Watcher(me, key, watcher[key]);
      });
    }
  }
}

看一下observer的内容

function Observer(data){
  this.data = data;
  this.walk(data);
}
Observer.prototype = {
  walk: function(data){
    let me = this;
    Object.keys(data).forEach(key => {
      me.convert(key, data[key]);
    });
  },
  convert: function (key, val) {
    this.defineReactive(this.data, key, val);
  },
  defineReactive: function (data, key, val) {
    let dep = new Dep();
    let childObj = observe(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false,
      get: function() {
        // 使用watch aaa.bbb时候,会触发两次get,且两次实例的dep是不同的,watch队列会被push进两个dep,如果继续使用this.add.bbb也会持续触发get,但是wather队列不会继续添加该dep
        if (Dep.target) {
          dep.depend();
        }
        return val;
      },
      set: function(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        childObj = observe(newVal);
        dep.notify();
      }
    });
  }
};

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

var uid = 0;
Dep.target = null;

function Dep () {
  this.id = uid++;
  this.subs = [];
};
Dep.prototype = {
  depend: function () {
    Dep.target.addDep(this);
  },
  addSub: function (sub) {
    // 当使用watch a.b时候getter会触发两次,watcher会被push到当前sub中,sub是当前的watcher
    this.subs.push(sub);
  },
  notify: function(){
    this.subs.forEach(sub => {
      sub.update();
    });
  }
};

看一下watcher的内容

function Watcher(vm, expOrFn, cb){
  this.cb = cb;
  this.vm = vm;
  this.expOrFn = expOrFn;
  this.depIds = {};
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  }
  this.value = this.get();
};

Watcher.prototype = {
  parseGetter: function(exp) {
    if (/[^\w.$]/.test(exp)) return; 
    var exps = exp.split('.');
    return function(obj) {
      for (var i = 0, len = exps.length; i < len; i++) {
          if (!obj) return;
          obj = obj[exps[i]];
      }
      return obj;
    }
  },
  get: function () {
    Dep.target = this;
    let value = this.getter.call(this.vm, this.vm);
    Dep.target = null
    return value;;
  },
  addDep: function (dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this); // 当使用watch aaa.bbb时候,会触发两次get,因此该函数会执行两次,dep.id是不同的,然后不同的dep会执行addSub,把当前的watcher放到当前dep的subs队列中
      this.depIds[dep.id] = dep;
    }
  },
  update: function () {
    this.run();
  },
  run: function () {
    let value = this.get();
    let oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  }
};

大功告成!!!

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

推荐阅读更多精彩内容

  • 几种双向绑定的做法目前几种主流的mvc(vm)框架都实现了单向数据绑定,我认为的双向数据绑定其实就是在单向绑定的基...
    Picidae阅读 5,632评论 2 4
  • 检测数据变化 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对...
    Mr无愧于心阅读 117评论 0 0
  • vue框架 中最核心的就是 vue的响应式 ,通过对vue中data数据的变更实现页面效果的重新渲染。但在实际开发...
    郝小淞阅读 4,342评论 1 1
  • 在js对象传给data时,Vue将遍历这个对象中的属性,并用Object.defineProperty()将属性转...
    二货豆子阅读 179评论 0 0
  • 今天我们快要放学的时候,我们做牙签肉,牙签肉特别香,也特别好吃。今天爸爸给我了一个特别大的一个硬币,我特别喜欢那个...
    小狐狸的麻麻阅读 310评论 0 0