Реактивные данные Vue: реализация модуля Observer

внешний интерфейс модульный тест Vue.js React.js
Реактивные данные Vue: реализация модуля Observer

предисловие

   Прежде всего, приглашаю всех подписаться на меняБлог на гитхабе, что можно рассматривать как небольшое поощрение для меня, ведь у меня нет денег, чтобы писать вещи, и я могу продолжать на своем собственном энтузиазме и всеобщем поощрении. В ближайшие дни я должен сосредоточиться на написании серии статей о внутренних принципах 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Контент внутри функции очень недружественный и не соответствует принципу Open Close (OCP: Open Close Principle). Конечно, Vue не будет разработан таким образом.Чтобы решить эту проблему, Vue представилмодель публикации-подписки. На самом деле модель публикации-подписки — это модель, с которой хорошо знакомы фронтенд-инженеры, также известная какШаблон наблюдателя, который является способом определенияодин ко многимКогда состояние объекта изменяется, другие объекты, наблюдающие за ним, будут уведомлены. Наше самое распространенное событие DOM — этомодель публикации-подписки. Например:   

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

   В приведенном выше коде мы слушаемbodyизclickсобытия, хотя мы не знаемclickКогда произойдет событие, но мы можем гарантировать, что если это произойдетbodyизclickСобытия, мы должны иметь возможность получать уведомления, то есть вызывается функция обратного вызова. В JavaScript, поскольку функции являются гражданами первого класса, мы редко используем традиционную модель публикации-подписки и в основном используеммодель событияспособ достижения. Существует также модель событий, реализованная в Vue, на которую мы можем взглянуть. Поскольку разделение между модулями Vue очень хорошее, прежде чем смотреть код, мы можем на самом деле взглянуть на соответствующий файл модульного теста, вы будете знать, какую функцию будет выполнять этот модуль, и даже если вы хотите, вы можете сделать Внедрите аналогичный модуль в исходный код Vue для запуска.

  Раннее использование кода Vuejasmineвыполнять модульные тесты,emitter_spec.js— это файл модульного теста для модели событий. Сначала краткое введениеjasmineИспользуемые функции, вы можете обратиться к следующему коду, чтобы понять конкретные функции:

  • describeпредставляет собой набор тестовых единиц
  • itэто тестовый случай
  • beforeEachв каждом тестовом примереitвыполнить перед выполнением
  • expectФункция ожидания, используемая для логического сравнения ожидаемых и фактических значений.
  • createSpyИспользуется для создания шпиона, а роль шпиона заключается в мониторинге функцийпередачасоответствующую информацию ипараметры выполнения функции

  

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Добрый. Запустите этот модуль с Кармой, все кейсы пройдены, теперь мы его прочитали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, целью которого является наследованиеEmitterизon, off,emitи другие методы. В приведенном выше тестовом примере мы обнаружили, что не использовали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 для создания экземпляра наблюдателя (различайте объект 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__Это нестандартный атрибут. Хотя многие производители браузеров в основном реализовали этот атрибут, все еще есть некоторые версии Android, которые не поддерживают этот атрибут. 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Функция будет вызываться для элементов после заданного индекса массива (по умолчанию 0)this.observe, а такжеobserveВ самом деле, для данногоvalрекурсивный вызов значенияObserver.create, Сделайте данные отзывчивыми и установите соответствующую связь между наблюдателем родителя и текущим экземпляром. На самом деле мы обнаружили, что 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Функция, функцией функции на тот момент было обновление ключа дочернего элемента у родителей, давайте посмотрим на конкретную реализацию:   

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Код для обработки объекта в конструкторе 's:

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)
})

в сравнении сarrayAugmentations,objectAgumentationsВнутренняя реализация намного проще,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.emitGetswitch, при доступе к свойству он вызовет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 очень мала, что делает логику кода очень понятной. Увидев это, я рекомендую вам также клонировать исходный код Vue, перейти к соответствующему номеру версии, прочитать его самостоятельно, запустить тестовый пример, нажать точку останова и попытаться отладить его, это должно помочь вам понять этот модуль.

   Наконец, если вам интересна эта серия статей, подписывайтесь на меняБлог на гитхабеЭто воодушевление для меня, спасибо всем за вашу поддержку!