关于vue源码中发布订阅模式的探索

数据响应

开始

首先,我找到了实例instance文件夹下的vue.js。

// src/instance/vue.js
function Vue (options) {
  //初始化函数 跳转至./internal/init.js
  this._init(options)
}

显然vue的构造函数用一个this._init就解决了。接下来找到./internal/init.js,前面进行了一系列的参数初始化,之后有以下代码

// src/instance/internal/init.js
...
    // set ref
    this._updateRef()

    // initialize data as empty object.
    // it will be filled up in _initData().
    this._data = {}

    // call init hook
    this._callHook('init')

    // initialize data observation and scope inheritance.
    //初始化观察者模式数据 跳转至./state.js
    this._initState()

    // setup event system and option events.
    this._initEvents()

    // call created hook
    this._callHook('created')

    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
...

我在this._initState()这个函数中找到了数据响应初始化的相关内容。跳转至./state.js

// src/instance/internal/state.js

  /**
   * Initialize the data.
   */

  Vue.prototype._initData = function () {
    // this.$options.data: 实例中的data() {return ...};
    var dataFn = this.$options.data
    var data = this._data = dataFn ? dataFn() : {}
    // isPlainObject: 来自../../util/lang;
    //判断是否为对象
    if (!isPlainObject(data)) {
      data = {}
      //这个就是data声明出错的警告了
      process.env.NODE_ENV !== 'production' && warn(
        'data functions should return an object.',
        this
      )
    }
    var props = this._props
    // proxy data on instance
    var keys = Object.keys(data)
    var i, key
    i = keys.length
    while (i--) {
      key = keys[i]
      // there are two scenarios where we can proxy a data key:
      // 1. it's not already defined as a prop
      // 2. it's provided via a instantiation option AND there are no
      //    template prop present
      //1.将data属性直接代理到vm上去,这样就可以直接访问属性了。
      //2.比如访问实例化vm对象data下的a属性,直接vm.a即可。
      if (!props || !hasOwn(props, key)) {
        //若不是props里的属性,则直接代理。
        //_proxy方法源码在下面,作用是vm直接访问_data里面的数据
        this._proxy(key)
      } else if (process.env.NODE_ENV !== 'production') {
        //发出警告。
        warn(
          'Data field "' + key + '" is already defined ' +
          'as a prop. To provide default value for a prop, use the "default" ' +
          'prop option; if you want to pass prop values to an instantiation ' +
          'call, use the "propsData" option.',
          this
        )
      }
    }
    // observe data
    //生成观察者模式的对象,跳转至../../observer/index
    observe(data, this)
  }

这个函数解决了我平时使用vue的几个疑问:

(1).无论是手抖还是什么原因。经常出现的几个警告和报错原来来自这里。
(2).为什么我明明写在vue data属性下的数据,可以直接通过vm.xxx来访问了。

第二个问题,主要依靠_proxy这个函数,将_data里的数据直接挂在到了vm下面。


/** * Proxy a property, so that * vm.prop === vm._data.prop * * @param {String} key */ Vue.prototype._proxy = function (key) { if (!isReserved(key)) { // need to store ref to self here // because these getter/setters might // be called by child scopes via // prototype inheritance. var self = this Object.defineProperty(self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] }, set: function proxySetter (val) { self._data[key] = val } }) } }

然后_initData最后一句,observe(data, this)进入正菜

observe

// src/observer/index.js

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 *
 * @param {*} value
 * @param {Vue} [vm]
 * @return {Observer|undefined}
 * @static
 */

export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    //判断是否为对象
    return
  }
  var ob
  if (
    //__ob__这个属性若存在,证明已经observe过。直接赋值ob
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    //shouldConvert:开关,暂不知道其作用
    //Object.isExtensible:判断该对象是否可以添加新属性
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //构造函数Observer,看返回值。
    ob = new Observer(value)
  }
  if (ob && vm) {
    //vm: 当前实例
    //addVm:将vm添加到ob的vms属性中
    ob.addVm(vm)
  }
  //最后输出的ob应是一个可以get和set的对象
  return ob
}

很明显,observe这个函数,就是用来生成一个经过处理的,可以get和set的对象,所以有如下问题:

(1)get和set是什么?
(2)这么做的思路是什么?

问题1:js有个很神奇的方法,Object.defineProperty,用来设置对象的一些属性。其中就有get和set,照英文表面意思,一个‘获得’,一个’设置‘。
问题2:通过get,用来收集依赖于此条数据的订阅者。何为订阅者,比如此条数据为{ss: 1},某个dom展示{{ss}}即为它的订阅者,而通过set,当数据发生改变的时候,通知对应的订阅者。
如此就可以实现vue传说中的订阅功能。它的具体实现在Dep(dependence简写)这个构造函数中(src/observer/dep.js)

Observer

在observe这个函数中有一个构造函数Observer,是用来生成这种对象的核心函数。

// src/observer/index.js

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 *
 * @param {Array|Object} value
 * @constructor
 */

export function Observer (value) {
  //将传参value挂载到this上
  this.value = value
  //将依赖收集对象实例化,挂载到this上
  //构造函数Dep跳转至./dep
  this.dep = new Dep()
  //Object.definePropert封装../util/lang
  //将Observer实例化后的对象挂载到value.__ob__下
  def(value, '__ob__', this)
  if (isArray(value)) {
    //由于数组的特殊性,若用Object.defineProperty,存在以下问题:
    //1.将数字作为属性存在性能问题
    //2.无法解决push pop等数组方法
    //解决方案:
    //重写数组方法,由于es5继承数组方法是返回新数组,所以得用特别的继承方式
    //(1):利用大部分高级浏览器的__proto__属性,指向Array.prototype里的方法
    //(2):遍历,将方法def到数组实例上
    //hasProto:from ../util/env
    //protoAugment:方案(1)
    //copyAugment:方案(2)
    //arrayMethods:重写方法 from ./array
    var augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    //walk:遍历
    this.walk(value)
  }
}

从代码逻辑上看,这个函数解决了数组这个特殊的Object所产生的问题。数组使用Object.defineProperty存在以下问题:

1.将数字作为属性存在性能问题
2.无法准确响应push pop等数组方法

重写数组方法,由于es5继承数组方法是返回新数组,所以得用特别的继承方式

(1):利用大部分高级浏览器的proto属性,指向Array.prototype里的方法
(2):遍历,将方法def到数组实例上(def是作者封装的一个方法,作用是把方法挂载到相应对象下)

所以,原来我们用的push和pop等,都是变异过的。具体改写在src/observer/array.js。
接下来,无论数组还是对象,最后都要对其设置getter和setter,其核心函数为这个:

/**
 * Define a reactive property on an Object.
 *
 * @param {Object} obj
 * @param {String} key
 * @param {*} val
 */

export function defineReactive (obj, key, val) {
  //dep:阅读完本段代码跳至./dep,是一个收集watcher的构造函数。很多资料中称之为依赖收集
  var dep = new Dep()
  //Object.getOwnPropertyDescriptor:获取属性描述符,如writable等
  var property = Object.getOwnPropertyDescriptor(obj, key)
  //configurable:false不可更改与扩展
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  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 () {
      //判断value值是否有getter
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        //当watcher发生变化时,触发所有依赖的getter
        //然后存储依赖于此的订阅者dep.depend()相当于dep.addSub(Dep.target)
        //这样watcher成功的分发到了相关依赖中。
        //可以理解为解决getter不能传参但需要传参的情况
        dep.depend()
        if (childOb) {
          //Observer构造函数中已经声明this.dep = new Dep();
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.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()
    }
  })
}

其中的getter,主要依靠Dep这个依赖收集器,来收集依赖于此条字段的那些订阅者。并且存储到dep.subs这个数组中。个人认为如果getter能传参,就不需要如此大费周章了,可惜现实是不能。但这也是此段代码的神奇之处,真的很神奇!
然后setter,来判断数据是否发生了改变,并且通知更新,dep.notify()。
所以,所有的问题都指向了Dep这个构造函数。

Dep(src/observer/dep.js)

Dep这个类其实很简单,基本上就是实现watcher实例的增删改查。然后将其存储在subs这个数组中。其中Dep.target指向的是当前watcher,在afterwatch后,会重置为null。
至于watcher,是我接下来学习的对象。

 

发表评论