Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue响应式数据: Observer模块实现 #30

Open
MrErHu opened this issue Jul 1, 2018 · 2 comments
Open

Vue响应式数据: Observer模块实现 #30

MrErHu opened this issue Jul 1, 2018 · 2 comments
Labels

Comments

@MrErHu
Copy link
Owner

MrErHu commented Jul 1, 2018

前言

  首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。接下来的日子我应该会着力写一系列关于Vue与React内部原理的文章,感兴趣的同学点个关注或者Star。
  之前的两篇文章响应式数据与数据依赖基本原理从Vue数组响应化所引发的思考我们介绍了响应式数据相关的内容,没有看的同学可以点击上面的链接了解一下。如果大家都阅读过上面两篇文章的话,肯定对这方面内容有了足够的知识储备,想来是时候来看看Vue内部是如何实现数据响应化。目前Vue的代码非常庞大,但其中包含了例如:服务器渲染等我们不关心的内容,为了能集中于我们想学习的部分,我们这次阅读的是Vue的早期代码,大家可以checkout这里查看对应的代码。
  之前零零碎碎的看过React的部分源码,当我看到Vue的源码,觉得真的是非常优秀,各个模块之间解耦的非常好,可读性也很高。Vue响应式数据是在Observer模块中实现的,我们可以看看Observer是如何实现的。
  

发布-订阅模式  

  如果看过上两篇文章的同学应该会发现一个问题:数据响应化的代码与其他的代码耦合太强了,比如说:
  

//代码来源于文章:响应式数据与数据依赖基本原理
//定义对象的单个响应式属性
function defineReactive(obj, key, value){
  observify(value);
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    set: function(newValue){
      var oldValue = value;
      value = newValue;
      //可以在修改数据时触发其他的操作
      console.log("newValue: ", newValue, " oldValue: ", oldValue);
    },
    get: function(){
      return value;
    }
  });
}

  比如上面的代码,set内部的处理的代码就与整个数据响应化相耦合,如果下次我们想要在set中做其他的操作,就必须要修改set函数内部的内容,这是非常不友好的,不符合开闭原则(OCP: Open Close Principle)。当然Vue不会采用这种方式去设计,为了解决这个问题,Vue引入了发布-订阅模式。其实发布-订阅模式是前端工程师非常熟悉的一种模式,又叫做观察者模式,它是一种定义对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,其他观察它的对象都会得到通知。我们最常见的DOM事件就是一种发布-订阅模式。比如:
  

document.body.addEventListener("click", function(){
    console.log("click event");
});

  在上面的代码中我们监听了bodyclick事件,虽然我们不知道click事件什么时候会发生,但是我们一定能保证,如果发生了bodyclick事件,我们一定能得到通知,即回调函数被调用。在JavaScript中因为函数是一等公民,我们很少使用传统的发布-订阅模式,多采用的是事件模型的方式实现。在Vue中也实现了一个事件模型,我们可以看一下。因为Vue的模块之间解耦的非常好,因此在看代码之前,其实我们可以先来看看对应的单元测试文件,你就知道这个模块要实现什么功能,甚至如果你愿意的话,也可以自己实现一个类似的模块放进Vue的源码中运行。

  Vue早期代码使用是jasmine进行单元测试,emitter_spec.js是事件模型的单元测试文件。首先简单介绍一下jasmine用到的函数,可以对照下面的代码了解具体的功能:

  • describe是一个测试单元集合
  • it是一个测试用例
  • beforeEach会在每一个测试用例it执行前执行
  • expect期望函数,用作对期望值和实际值之间执行逻辑比较
  • createSpy用来创建spy,而spy的作用是监测函数的调用相关信息和函数执行参数

  

var Emitter = require('../../../src/emitter')
var u = undefined
// 代码有删减
describe('Emitter', function () {

  var e, spy
  beforeEach(function () {
    e = new Emitter()
    spy = jasmine.createSpy('emitter')
  })
  
  it('on', function () {
    e.on('test', spy)
    e.emit('test', 1, 2 ,3)
    expect(spy.calls.count()).toBe(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3)
  })

  it('once', function () {
    e.once('test', spy)
    e.emit('test', 1, 2 ,3)
    e.emit('test', 2, 3, 4)
    expect(spy.calls.count()).toBe(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3)
  })

  it('off', function () {
    e.on('test1', spy)
    e.on('test2', spy)
    e.off()
    e.emit('test1')
    e.emit('test2')
    expect(spy.calls.count()).toBe(0)
  })
  
  it('apply emit', function () {
    e.on('test', spy)
    e.applyEmit('test', 1)
    e.applyEmit('test', 1, 2, 3, 4, 5)
    expect(spy).toHaveBeenCalledWith(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3, 4, 5)
  })

})

  可以看出Emitter对象实例对外提供以下接口:

  • on: 注册监听接口,参数分别是事件名监听函数
  • emit: 触发事件函数,参数是事件名
  • off: 取消对应事件的注册函数,参数分别是事件名监听函数
  • once: 与on类似,仅会在第一次时通知监听函数,随后监听函数会被移除。

  看完了上面的单元测试代码,我们现在已经基本了解了这个模块要干什么,现在让我们看看对应的代码:

// 删去了注释并且对代码顺序有调整
// ctx是监听回调函数的执行作用域(this)
function Emitter (ctx) {
  this._ctx = ctx || this
}

var p = Emitter.prototype

p.on = function (event, fn) {
  this._cbs = this._cbs || {}
  ;(this._cbs[event] || (this._cbs[event] = []))
    .push(fn)
  return this
}
// 三种模式 
// 不传参情况清空所有监听函数 
// 仅传事件名则清除该事件的所有监听函数
// 传递事件名和回调函数,则对应仅删除对应的监听事件
p.off = function (event, fn) {
  this._cbs = this._cbs || {}

  // all
  if (!arguments.length) {
    this._cbs = {}
    return this
  }

  // specific event
  var callbacks = this._cbs[event]
  if (!callbacks) return this

  // remove all handlers
  if (arguments.length === 1) {
    delete this._cbs[event]
    return this
  }

  // remove specific handler
  var cb
  for (var i = 0; i < callbacks.length; i++) {
    cb = callbacks[i]
    // 这边的代码之所以会有cb.fn === fn要结合once函数去看
    // 给once传递的监听函数其实已经被wrapped过
    // 但是仍然可以通过原来的监听函数去off掉
    if (cb === fn || cb.fn === fn) {
      callbacks.splice(i, 1)
      break
    }
  }
  return this
}
// 触发对应事件的所有监听函数,注意最多只能用给监听函数传递三个参数(采用call)
p.emit = function (event, a, b, c) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event]

  if (callbacks) {
    callbacks = callbacks.slice(0)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].call(this._ctx, a, b, c)
    }
  }

  return this
}
// 触发对应事件的所有监听函数,传递参数个数不受限制(采用apply)
p.applyEmit = function (event) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event], args

  if (callbacks) {
    callbacks = callbacks.slice(0)
    args = callbacks.slice.call(arguments, 1)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].apply(this._ctx, args)
    }
  }

  return this
}
// 通过调用on与off事件事件,在第一次触发之后就`off`对应的监听事件
p.once = function (event, fn) {
  var self = this
  this._cbs = this._cbs || {}

  function on () {
    self.off(event, on)
    fn.apply(this, arguments)
  }

  on.fn = fn
  this.on(event, on)
  return this
}

  我们可以看到上面的代码采用了原型模式创建了一个Emitter类。配合Karma跑一下这个模块 ,测试用例全部通过,到现在我们已经阅读完Emitter了,这算是一个小小的热身吧,接下来让我们正式看一下Observer模块。
  

Observer

对外功能

  按照上面的思路我们先看看Observer对应的测试用例observer_spec.js,由于Observer的测试用例非常长,我会在代码注释中做解释,并尽量精简测试用例,能让我们了解模块对应功能即可,希望你能有耐心阅读下来。
 

//测试用例是精简版,否则太冗长
var Observer = require('../../../src/observe/observer')
var _ = require('../../../src/util') //Vue内部使用工具方法
var u = undefined
Observer.pathDelimiter = '.' //配置Observer路径分隔符

describe('Observer', function () {

  var spy
  beforeEach(function () {
    spy = jasmine.createSpy('observer')
  })
//我们可以看到我们通过Observer.create函数可以将数据变为可响应化,
//然后我们监听get事件可以在属性被读取时触发对应事件,注意对象嵌套的情况(例如b.c)
  it('get', function () {
    Observer.emitGet = true
    var obj = {
      a: 1,
      b: {
        c: 2
      }
    }
    var ob = Observer.create(obj)
    ob.on('get', spy)

    var t = obj.b.c
    expect(spy).toHaveBeenCalledWith('b', u, u)
    expect(spy).toHaveBeenCalledWith('b.c', u, u)
    
    Observer.emitGet = false
  })
//我们可以监听响应式数据的set事件,当响应式数据修改的时候,会触发对应的时间
  it('set', function () {
    var obj = {
      a: 1,
      b: {
        c: 2
      }
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)

    obj.b.c = 4
    expect(spy).toHaveBeenCalledWith('b.c', 4, u)
  })
//带有$与_开头的属性都不会被处理
  it('ignore prefix', function () {
    var obj = {
      _test: 123,
      $test: 234
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)
    obj._test = 234
    obj.$test = 345
    expect(spy.calls.count()).toBe(0)
  })
//访问器属性也不会被处理
  it('ignore accessors', function () {
    var obj = {
      a: 123,
      get b () {
        return this.a
      }
    }
    var ob = Observer.create(obj)
    obj.a = 234
    expect(obj.b).toBe(234)
  })
// 对数属性的get监听,注意嵌套的情况
  it('array get', function () {

    Observer.emitGet = true

    var obj = {
      arr: [{a:1}, {a:2}]
    }
    var ob = Observer.create(obj)
    ob.on('get', spy)

    var t = obj.arr[0].a
    expect(spy).toHaveBeenCalledWith('arr', u, u)
    expect(spy).toHaveBeenCalledWith('arr.0.a', u, u)
    expect(spy.calls.count()).toBe(2)

    Observer.emitGet = false
  })
// 对数属性的get监听,注意嵌套的情况
  it('array set', function () {
    var obj = {
      arr: [{a:1}, {a:2}]
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)

    obj.arr[0].a = 2
    expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u)
  })
// 我们看到可以通过监听mutate事件,在push调用的时候对应触发事件
// 触发事件第一个参数是"",代表的是路径名,具体源码可以看出,对于数组变异方法都是空字符串
// 触发事件第二个参数是数组本身
// 触发事件第三个参数比较复杂,其中:
// method属性: 代表触发的方法名称
// args属性: 代表触发方法传递参数
// result属性: 代表触发变异方法之后数组的结果
// index属性: 代表变异方法对数组发生变化的最开始元素
// inserted属性: 代表数组新增的元素
// remove属性: 代表数组删除的元素
// 其他的变异方法: pop、shift、unshift、splice、sort、reverse内容都是非常相似的
// 具体我们就不一一列举的了,如果有疑问可以自己看到全部的单元测试代码
  it('array push', function () {
    var arr = [{a:1}, {a:2}]
    var ob = Observer.create(arr)
    ob.on('mutate', spy)
    arr.push({a:3})
    expect(spy.calls.mostRecent().args[0]).toBe('')
    expect(spy.calls.mostRecent().args[1]).toBe(arr)
    var mutation = spy.calls.mostRecent().args[2]
    expect(mutation).toBeDefined()
    expect(mutation.method).toBe('push')
    expect(mutation.index).toBe(2)
    expect(mutation.removed.length).toBe(0)
    expect(mutation.inserted.length).toBe(1)
    expect(mutation.inserted[0]).toBe(arr[2])
  })
  
// 我们可以看到响应式数据中存在$add方法,类似于Vue.set,可以监听add事件
// 可以向响应式对象中添加新一个属性,如果之前存在该属性则操作会被忽略
// 并且新赋值的对象也必须被响应化
// 我们省略了对象数据$delete方法的单元测试,功能类似于Vue.delete,与$add方法相反,可以用于删除对象的属性
// 我们省略了数组的$set方法的单元测试,功能也类似与Vue.set,可以用于设置数组对应数字下标的值
// 我们省略了数组的$remove方法的单元测试,功能用于移除数组给定下标的值或者给定的值,例如:
// var arr = [{a:1}, {a:2}]
// var ob = Observer.create(arr)
// arr.$remove(0) => 移除对应下标的值 或者
// arr.$remove(arr[0]) => 移除给定的值

  it('object.$add', function () {
    var obj = {a:{b:1}}
    var ob = Observer.create(obj)
    ob.on('add', spy)

    // ignore existing keys
    obj.$add('a', 123)
    expect(spy.calls.count()).toBe(0)

    // add event
    var add = {d:2}
    obj.a.$add('c', add)
    expect(spy).toHaveBeenCalledWith('a.c', add, u)

    // check if add object is properly observed
    ob.on('set', spy)
    obj.a.c.d = 3
    expect(spy).toHaveBeenCalledWith('a.c.d', 3, u)
  })

// 下面的测试用例用来表示如果两个不同对象parentA、parentB的属性指向同一个对象obj,那么该对象obj改变时会分别parentA与parentB的监听事件

  it('shared observe', function () {
    var obj = { a: 1 }
    var parentA = { child1: obj }
    var parentB = { child2: obj }
    var obA = Observer.create(parentA)
    var obB = Observer.create(parentB)
    obA.on('set', spy)
    obB.on('set', spy)
    obj.a = 2
    expect(spy.calls.count()).toBe(2)
    expect(spy).toHaveBeenCalledWith('child1.a', 2, u)
    expect(spy).toHaveBeenCalledWith('child2.a', 2, u)
    // test unobserve
    parentA.child1 = null
    obj.a = 3
    expect(spy.calls.count()).toBe(4)
    expect(spy).toHaveBeenCalledWith('child1', null, u)
    expect(spy).toHaveBeenCalledWith('child2.a', 3, u)
  })

})

源码实现

数组

  能坚持看到这里,我们的长征路就走过了一半了,我们已经知道了Oberver对外提供的功能了,现在我们就来了解一下Oberver内部的实现原理。
  
  Oberver模块实际上采用采用组合继承(借用构造函数+原型继承)方式继承了Emitter,其目的就是继承Emitteron, offemit等方法。我们在上面的测试用例发现,我们并没有用new方法直接创建一个Oberver的对象实例,而是采用一个工厂方法Oberver.create方法来创建的,我们接下来看源码,由于代码比较多我会尽量去拆分成一个个小块来讲:
  

// 代码出自于observe.js
// 为了方便讲解我对代码顺序做了改变,要了解详细的情况可以查看具体的源码

var _ = require('../util')
var Emitter = require('../emitter')
var arrayAugmentations = require('./array-augmentations')
var objectAugmentations = require('./object-augmentations')

var uid = 0
/**
 * Type enums
 */

var ARRAY  = 0
var OBJECT = 1

function Observer (value, type, options) {
  Emitter.call(this, options && options.callbackContext)
  this.id = ++uid
  this.value = value
  this.type = type
  this.parents = null
  if (value) {
    _.define(value, '$observer', this)
    if (type === ARRAY) {
      _.augment(value, arrayAugmentations)
      this.link(value)
    } else if (type === OBJECT) {
      if (options && options.doNotAlterProto) {
        _.deepMixin(value, objectAugmentations)
      } else {
        _.augment(value, objectAugmentations)
      }
      this.walk(value)
    }
  }
}

var p = Observer.prototype = Object.create(Emitter.prototype)

Observer.pathDelimiter = '\b'

Observer.emitGet = false

Observer.create = function (value, options) {
  if (value &&
      value.hasOwnProperty('$observer') &&
      value.$observer instanceof Observer) {
    return value.$observer
  } if (_.isArray(value)) {
    return new Observer(value, ARRAY, options)
  } else if (
    _.isObject(value) &&
    !value.$scope // avoid Vue instance
  ) {
    return new Observer(value, OBJECT, options)
  }
}

  我们首先从Observer.create看起,如果value值没有响应化过(通过是否含有$observer属性去判断),则使用new操作符创建Obsever实例(区分对象OBJECT与数组ARRAY)。接下来我们看Observer的构造函数是怎么定义的,首先借用Emitter构造函数:
  

Emitter.call(this, options && options.callbackContext)

配合原型继承

var p = Observer.prototype = Object.create(Emitter.prototype)

从而实现了组合继承Emitter,因此Observer继承了Emitter的属性(ctx)和方法(on,emit等)。我们可以看到Observer有以下属性:

  • id: 响应式数据的唯一标识
  • value: 原始数据
  • type: 标识是数组还是对象
  • parents: 标识响应式数据的父级,可能存在多个,比如var obj = { a : { b: 1}},在处理{b: 1}的响应化过程中parents中某个属性指向的就是obj$observer

  我们接着看首先给该数据赋值$observer属性,指向的是实例对象本身。_.define内部是通过defineProperty实现的:

define = function (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value        : val,
    enumerable   : !!enumerable,
    writable     : true,
    configurable : true
  })
}

  下面我们首先看看是怎么处理数组类型的数据的

if (type === ARRAY) {
    _.augment(value, arrayAugmentations)
    this.link(value)
}

  如果看过我前两篇文章的同学,其实还记得我们对数组响应化当时还做了一个着重的原理讲解,大概原理就是我们通过给数组对象设置新的原型对象,从而遮蔽掉原生数组的变异方法,大概的原理可以是:
  

function observifyArray(array){
    var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
    var arrayAugmentations = Object.create(Array.prototype);
    aryMethods.forEach((method)=> {
        let original = Array.prototype[method];
        arrayAugmentations[method] = function () {
            // 调用对应的原生方法并返回结果
            // do everything you what do !
            return original.apply(this, arguments);
        };
    });
    array.__proto__ = arrayAugmentations;
}

  回到Vue的源码,虽然我们知道基本原理肯定是相同的,但是我们仍然需要看看arrayAugmentations是什么?下面arrayAugmentations代码比较长。我们会在注释里面解释基本原理:
  

// 代码来自于array-augmentations.js
var _ = require('../util')
var arrayAugmentations = Object.create(Array.prototype)
// 这边操作和我们之前的实现方式非常相似
// 创建arrayAugmentations原型继承`Array.prototype`从而可以调用数组的原生方法
// 然后通过arrayAugmentations覆盖数组的变异方法,基本逻辑大致相同
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method) {
  var original = Array.prototype[method]
  // 覆盖arrayAugmentations中的变异方法
  _.define(arrayAugmentations, method, function () {
    
    var args = _.toArray(arguments)
    // 这里调用了原生的数组变异方法,并获得结果
    var result = original.apply(this, args)
    var ob = this.$observer
    var inserted, removed, index
    // 下面switch这一部分代码看起来很长,其实目的就是针对于不同的变异方法生成:
    // insert removed inserted 具体的含义对照之前的解释,了解即可
    switch (method) {
      case 'push':
        inserted = args
        index = this.length - args.length
        break
      case 'unshift':
        inserted = args
        index = 0
        break
      case 'pop':
        removed = [result]
        index = this.length
        break
      case 'shift':
        removed = [result]
        index = 0
        break
      case 'splice':
        inserted = args.slice(2)
        removed = result
        index = args[0]
        break
    }

    // 如果给数组中插入新的数据,则需要调用ob.link
    // link函数其实在上面的_.augment(value, arrayAugmentations)之后也被调用了
    // 具体的实现我们可以先不管
    // 我们只要知道其目的就是分别对插入的数据执行响应化
    if (inserted) ob.link(inserted, index)
    // 其实从link我们就可以猜出unlink是干什么的
    // 主要就是对删除的数据解除响应化,具体实现逻辑后面解释
    if (removed) ob.unlink(removed)

    // updateIndices我们也先不讲是怎么实现的,
    // 目的就是更新子元素在parents的key
    // 因为push和pop是不会改变现有元素的位置,因此不需要调用
    // 而诸如splce shift unshift等变异方法会改变对应下标值,因此需要调用
    if (method !== 'push' && method !== 'pop') {
      ob.updateIndices()
    }

    // 同样我们先不考虑propagate内部实现,我们只要propagate函数的目的就是
    // 触发自身及其递归触发父级的事件
    // 如果数组中的数据有插入或者删除,则需要对外触发"length"被改变
    if (inserted || removed) {
      ob.propagate('set', 'length', this.length)
    }

    // 对外触发mutate事件
    // 可以对照我们之前讲的测试用例'array push',就是在这里触发的,回头看看吧
    ob.propagate('mutate', '', this, {
      method   : method,
      args     : args,
      result   : result,
      index    : index,
      inserted : inserted || [],
      removed  : removed || []
    })

    return result
  })
})

// 可以回看一下测试用例 array set,目的就是设置对应下标的值
// 其实就是调用了splice变异方法, 其实我们在Vue中国想要改变某个下标的值的时候
// 官网给出的建议无非是Vue.set或者就是splice,都是相同的原理
// 注意这里的代码忽略了超出下标范围的值
_.define(arrayAugmentations, '$set', function (index, val) {
  if (index >= this.length) {
    this.length = index + 1
  }
  return this.splice(index, 1, val)[0]
})
// $remove与$add都是一个道理,都是调用的是`splice`函数
_.define(arrayAugmentations, '$remove', function (index) {
  if (typeof index !== 'number') {
    index = this.indexOf(index)
  }
  if (index > -1) {
    return this.splice(index, 1)[0]
  }
})

module.exports = arrayAugmentations

  上面的代码相对比较长,具体的解释我们在代码中已经注释。到这里我们已经了解完arrayAugmentations了,我们接着看看_.augment做了什么。我们在文章从Vue数组响应化所引发的思考中讲过Vue是通过__proto__来实现数组响应化的,但是由于__proto__是个非标准属性,虽然广泛的浏览器厂商基本都实现了这个属性,但是还是存在部分的安卓版本并不支持该属性,Vue必须对此做相关的处理,_.augment就负责这个部分:
  

exports.augment = '__proto__' in {}
  ? function (target, proto) {
      target.__proto__ = proto
    }
  : exports.deepMixin
  
exports.deepMixin = function (to, from) {
  Object.getOwnPropertyNames(from).forEach(function (key) {
    var desc =Object.getOwnPropertyDescriptor(from, key)
    Object.defineProperty(to, key, desc)
  })
}  

  我们看到如果浏览器不支持__proto__话调用deepMixin函数。而deepMixin的实现也是非常的简单,就是使用Object.defineProperty将原对象的属性描述符赋值给目标对象。接着调用了函数:
  

this.link(value)

  关于link函数在上面的备注中我们已经见过了:

if (inserted) ob.link(inserted, index)

  当时我们的解释是将新插入的数据响应化,知道了功能我们看看代码的实现:
  

// p === Observer.prototype
p.link = function (items, index) {
  index = index || 0
  for (var i = 0, l = items.length; i < l; i++) {
    this.observe(i + index, items[i])
  }
}

p.observe = function (key, val) {
  var ob = Observer.create(val)
  if (ob) {
    // register self as a parent of the child observer.
    var parents = ob.parents
    if (!parents) {
      ob.parents = parents = Object.create(null)
    }
    if (parents[this.id]) {
      _.warn('Observing duplicate key: ' + key)
      return
    }
    parents[this.id] = {
      ob: this,
      key: key
    }
  }
}

  其实代码逻辑非常简单,link函数会对给定数组index(默认为0)之后的元素调用this.observe, 而observe其实也就是对给定的val值递归调用Observer.create,将数据响应化,并建立父级的Observer与当前实例的对应关系。前面其实我们发现Vue不仅仅会对插入的数据响应化,并且也会对删除的元素调用unlink,具体的调用代码是:

if (removed) ob.unlink(removed)

  之前我们大致讲过其用作就是对删除的数据解除响应化,我们来看看具体的实现:

p.unlink = function (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    this.unobserve(items[i])
  }
}
p.unobserve = function (val) {
  if (val && val.$observer) {
    val.$observer.parents[this.id] = null
  }
}

  代码非常简单,就是对数据调用unobserve,而unobserve函数的主要目的就是解除父级observer与当前数据的关系并且不再保留引用,让浏览器内核必要的时候能够回收内存空间。

  在arrayAugmentations中其实还调用过Observer的两个原型方法,一个是:

ob.updateIndices()

  另一个是:

ob.propagate('set', 'length', this.length)

  首先看看updateIndices函数,当时的函数的作用是更新子元素在parents的key,来看看具体实现:
  

p.updateIndices = function () {
  var arr = this.value
  var i = arr.length
  var ob
  while (i--) {
    ob = arr[i] && arr[i].$observer
    if (ob) {
      ob.parents[this.id].key = i
    }
  }
}

  接着看函数propagate:
  

p.propagate = function (event, path, val, mutation) {
  this.emit(event, path, val, mutation)
  if (!this.parents) return
  for (var id in this.parents) {
    var parent = this.parents[id]
    if (!parent) continue
    var key = parent.key
    var parentPath = path
      ? key + Observer.pathDelimiter + path
      : key
    parent.ob.propagate(event, parentPath, val, mutation)
  }
}

  我们之前说过propagate函数的作用的就是触发自身及其递归触发父级的事件,首先调用emit函数对外触发时间,其参数分别是:事件名、路径、值、mutatin对象。然后接着递归调用父级的事件,并且对应改变触发的path参数。parentPath等于parents[id].key + Observer.pathDelimiter + path

  到此为止我们已经学习完了Vue是如何处理数组的响应化的,现在需要来看看是如何处理对象的响应化的。
  

对象  

  
  在Observer的构造函数中关于对象处理的代码是:

if (type === OBJECT) {
    if (options && options.doNotAlterProto) {
        _.deepMixin(value, objectAugmentations)
    } else {
        _.augment(value, objectAugmentations)
    }
    this.walk(value)
}

  和数组一样,我们首先要了解一下objectAugmentations的内部实现:

var _ = require('../util')
var objectAgumentations = Object.create(Object.prototype)

_.define(objectAgumentations, '$add', function (key, val) {
  if (this.hasOwnProperty(key)) return
  _.define(this, key, val, true)
  var ob = this.$observer
  ob.observe(key, val)
  ob.convert(key, val)
  ob.emit('add:self', key, val)
  ob.propagate('add', key, val)
})

_.define(objectAgumentations, '$delete', function (key) {
  if (!this.hasOwnProperty(key)) return
  delete this[key]
  var ob = this.$observer
  ob.emit('delete:self', key)
  ob.propagate('delete', key)
})

  相比于arrayAugmentationsobjectAgumentations内部实现则简单的多,objectAgumentations添加了两个方法: $add$delete

  $add用于给对象添加新的属性,如果该对象之前就存在键值为key的属性则不做任何操作,否则首先使用_.define赋值该属性,然后调用ob.observe目的是递归调用使得val值响应化。而convert函数的作用是将该属性转换成访问器属性getter/setter使得属性被访问或者被改变的时候我们能够监听到,具体我可以看一下convert函数的内部实现:
  

p.convert = function (key, val) {
  var ob = this
  Object.defineProperty(ob.value, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Observer.emitGet) {
        ob.propagate('get', key)
      }
      return val
    },
    set: function (newVal) {
      if (newVal === val) return
      ob.unobserve(val)
      val = newVal
      ob.observe(key, newVal)
      ob.emit('set:self', key, newVal)
      ob.propagate('set', key, newVal)
    }
  })
}

  convert函数的内部实现也不复杂,在get函数中,如果开启了全局的Observer.emitGet开关,在该属性被访问的时候,会对调用propagate触发本身以及父级的对应get事件。在set函数中,首先调用unobserve对之间的值接触响应化,接着调用ob.observe使得新赋值的数据响应化。最后首先触发本身的set:self事件,接着调用propagate触发本身以及父级的对应set事件。

  $delete用于给删除对象的属性,如果不存在该属性则直接退出,否则先用delete操作符删除对象的属性,然后对外触发本身的delete:self事件,接着调用delete触发本身以及父级对应的delete事件。

  看完了objectAgumentations之后,我们在Observer构造函数中知道,如果传入的参数中存在op.doNotAlterProto意味着不要改变对象的原型,则采用deepMixin函数将$add$delete函数添加到对象中,否则采用函数arguments函数将$add$delete添加到对象的原型中。最后调用了walk函数,让我们看看walk是内部是怎么实现的:
  

p.walk = function (obj) {
  var key, val, descriptor, prefix
  for (key in obj) {
    prefix = key.charCodeAt(0)
    if (
      prefix === 0x24 || // $
      prefix === 0x5F    // _
    ) {
      continue
    }
    descriptor = Object.getOwnPropertyDescriptor(obj, key)
    // only process own non-accessor properties
    if (descriptor && !descriptor.get) {
      val = obj[key]
      this.observe(key, val)
      this.convert(key, val)
    }
  }
}

  首先遍历obj中的各个属性,如果是以$或者_开头的属性名,则不做处理。接着获取该属性的描述符,如果不存在get函数,则对该属性值调用observe函数,使得数据响应化,然后调用convert函数将该属性转换成访问器属性getter/setter使得属性被访问或者被改变的时候能被够监听。
  

总结

  到此为止,我们已经看完了整个Observer模块的所有代码,其实基本原理和我们之前设想都是差不多的,只不过Vue代码中各个函数分解粒度非常小,使得代码逻辑非常清晰。看到这里,我推荐你也clone一份Vue源码,checkout到对应的版本号,自己阅读一遍,跑跑测试用例,打个断点试着调试一下,应该会对你理解这个模块有所帮助。

  最后如果对这个系列的文章感兴趣欢迎大家关注我的Github博客算是对我鼓励,感谢大家的支持!
  
  

@MrErHu MrErHu added the Vue label Jul 1, 2018
@Mrcxt
Copy link

Mrcxt commented Jul 4, 2018

只能看懂一点点,不敢评论。。。跟大佬的差距太大了,膜拜膜拜

@MrErHu
Copy link
Owner Author

MrErHu commented Jul 5, 2018

@Mrcxt 我也是小白。共同学习。

This was referenced Jul 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants