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

简介:

前言

  首先欢迎大家关注我的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博客算是对我鼓励,感谢大家的支持!      



原文发布时间:2018年07月01日

原文作者:请叫我王磊同学

本文来源掘金,如需转载请紧急联系作者


相关文章
|
8天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
8天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
8天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
14天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
14天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
14天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
14天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
13天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
15天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
13天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
下一篇
无影云桌面