vue源码——浅析响应式原理

又到了学习源码的时间⌚️。

我们都知道vue是一个mvvm框架,数据与视图双向绑定,所有入门vue的同学,实现的第一个demo应该都是👇

<div id="app">
  <span>{{ msg }}</span>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      msg: 'hello, world!'
    }
  })                    // 此时浏览器打印 hello,world!
  vm.msg = 'change msg' // 添加这句之后, 浏览器打印'change msg'
</script>

这种数据响应式绑定是如何实现的呢? 稍有经验的同学知道,是发布订阅的设计模式实现的,还知道实现这个模式的关键代码是Object.defineProperty()。再往下问,可能有人就不知道了。 不知道咱们就学,学无止境好伐~😆。开始正文 💪

准备工作
核心代码
  • Observer类, 代码注释中的序号我会一一解释,如果解释不对,请指正!🤝
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates. 
 */
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();  // 1. 依赖对象
  this.vmCount = 0;
  def(value, '__ob__', this); // 2.def方法
  if (Array.isArray(value)) {
    if (hasProto) {   // 3. hasProto变量
      protoAugment(value, arrayMethods);  // 4.arrayMethods变量,5. protoAugment方法
    } else {
      copyAugment(value, arrayMethods, arrayKeys);  //6.copyAugment方法
    }
    this.observeArray(value);  // 7.observeArray方法
  } else {
    this.walk(value);  // 8.walk方法
  }
};

首先我们看一下代码开始的官方解释,我理解的意思是:

当目标对象被追踪,观察者这个方法会将目标对象的属性key转换为getter和setter方法,用来收集依赖和捕获更新

一个截图你就明白了~


我在data中随便写了一个对象,打印出来之后可以看到,这个对象中多了get xxx set xxx这种方法。这些方法就是观察者给目标对象添加的

  • 依次解释注释部分
  1. Dep() 依赖类
var uid = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};

官方解释我觉得非常晦涩,我理解的意思就是 dep对象上有多个方法。这个对象的作用是为了去检测数组的变化,因为Array类型的变量没有getter setter方法,只能通过__ob__属性中的dep来收集依赖捕获更新。

  1. def方法
/**
 * Define a property.
 */
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

对原生Object.defineProperty进行了封装

  1. hasProto
var hasProto = '__proto__' in {};

判断一个空对象中是否有__proto__属性。这段代码我最初以为是判断当前环境,但是我尝试了在node环境下打印空对象,发现里面也有__proto__属性。在网上查了也没有结果,如果你知道的话,请麻烦在评论区帮我解惑!

  1. arrayMethods
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

arrayMethodsArray的子类。这样做是为了获取Array的方法,比如push,slice 等等所有方法。

  1. protoAugment()
/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src) {
  /* eslint-disable no-proto */
  target.__proto__ = src;
  /* eslint-enable no-proto */
}

arrayMethods赋值给目标对象。 __proto__这个属性是所有对象都有的,是浏览器实现的,方便我们查看原型链,MDN上建议这个属性作为可读属性,最好不要直接使用。

  1. copyAugment
/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}

这段代码是把arrayMethods中的方法依次添加到目标对象中

  1. observeArray()
/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

如果目标是数组对象,遍历这个数组,给每个对象注册观察者对象(也就是watcher)。

  1. walk()
/**
 * Walk through all properties and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

如果目标是纯对象, 就给其中的每个属性添加getter/setter方法

  1. defineReactive$$1()
/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep();

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

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childOb = !shallow && 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();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();  // 通知watcher改变, 响应式原理
    }
  });
}

走到了这一步,才是真正实现了响应式。核心是dep.notify()

整体解析

有一部分代码我没有贴出来,如果感兴趣可以去vue源码中查看。

我理解的vue响应式的思路大致是

  1. 首先给目标加上__ob__属性,其值是目标本身的值以及dep依赖对象和vmcount
  2. 判断目标是否为数组,因为数组变化是无法检测到的,所以特例一个情况。
  3. 如果目标是数组的话,先把数组中会改变原数组的方法取出来,赋给目标,如果目标触发了这些方法,说明原数组改变了,这样能侧面反应出数据是否改变。源码中不仅仅是如此,还给数组中的每个值注册了watcher,如果这些值改变了,也会通知watcher
  4. 如果目标是对象,给目标绑定getter/setter。对象的值改变会触发notify(),通知watcher改变,引起视图改变
总结

目前写下这篇博客,仅仅是我在阅读源码之后写的,其中肯定有很多理解不正确的地方,后续在网上学习之后,我会改正。其实我觉得vue实现响应式最关键的是Dep对象, 其中的notify()通知watcher,才实现了响应式,但是由于我对Dep对象的理解不深,所以暂时没有写下相关的代码,继续学习,等我深刻理解之后再回来补充🧐

路漫漫其修远兮,吾将上下而求索。共勉。

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

推荐阅读更多精彩内容