Принцип отзывчивости Vue, о котором вы не знаете

внешний интерфейс Vue.js внешний фреймворк
Принцип отзывчивости Vue, о котором вы не знаете

Статья впервые опубликована вgithub Blog.

Эта статья основана наИсходный код Vuev2.x для анализа. Здесь отсортированы только самые важные части исходного кода, а некоторые неосновные части пропущены. Реактивные обновления в основном включаютWatcher,Dep,Observerэти основные категории.

watcher-dep-observer

В этой статье в основном разъясняются следующие вопросы, которые легко спутать:

  • Watcher,Dep,ObserverОтношения между этими классами?
  • DepсерединаsubsЧто хранится?
  • WatcherсерединаdepsЧто хранится?
  • Dep.targetЧто это такое и где присваивается значение?

Эта статья начинается непосредственно с нового экземпляра Vue и шаг за шагом раскрывает принцип адаптивности Vue, предполагая следующий простой код Vue:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    watch: {
        counter: function(val, oldVal) {
            console.log('counter changed...')
        }
    }
})

1. Инициализация экземпляра Vue

из ВьюЖизненный циклИзвестно, что первыйinitОперация инициализации, эта часть кода находится вinstance/init.jsсередина.

src/core/instance/init.js

initLifecycle(vm) // vm生命周期相关变量初始化操作
initEvents(vm) // vm事件相关初始化
initRender(vm) // 模板解析相关初始化
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子函数
initInjections(vm) // resolve injections before data/props 
initState(vm) // vm状态初始化(重点在这里)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子函数

Вышеупомянутый источникinitState(vm)Это фокус исследования, который реализуетсяprops,methods,data,computed,watchоперация инициализации. Здесь, основываясь на приведенном выше примере, сосредоточьтесь наdataа такжеwatch, Местоположение источникаinstance/state.js

src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  // 对添加的watch进行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

2. initData

Экземпляр Vue реализует для каждого из своих данныхgetter/setterметод, который является основой для реализации отзывчивости. оgetter/setterвидимыйMDN web docs. Проще говоря, он принимает значениеthis.counterПри можно настроить некоторые операции, а затем вернуть значение счетчика; при изменении значенияthis.counter = 10При установке значения вы также можете настроить некоторые операции при установке значения.initData(vm)Реализация находится в исходном кодеinstance/state.js.

src/core/instance/state.js

while (i--) {
	...
    // 这里将data,props,methods上的数据全部代理到vue实例上
	// 使得vm.counter可以直接访问
}
// 这里略过上面的代码,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)

здесьobserve()Метод превращает данные в наблюдаемые, почему они наблюдаемые? в основном реализованоgetter/setterметод, пустьWatcherМожно наблюдать изменения этих данных. Посмотрите нижеobserveреализация.

src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // 重点在这里,响应式的核心所在
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Просто сосредоточься здесьnew Observer(value), который является ядром метода, путемObserverкласс будет вьюdataстать отзывчивым. Согласно нашему примеру, в это время параметрvalueЗначение{ counter: 1 }. Рассмотрим подробно нижеObserverДобрый.

3. Observer

Сначала посмотрите на конструктор этого класса,new Observer(value)Конструктор выполняется первым. В примечании автора говорится, что класс Observer преобразует значение ключа каждого целевого объекта (то есть данные в данных) вgetter/setterформа для сбора и обновления зависимостей через уведомление о зависимостях.

src/core/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.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value) // 遍历data对象中{counter : 1, ..} 中的每个键值(如counter),设置其setter/getter方法。
    }
  }

  ...
}

Самое главное здесьthis.walk(value)метод,this.observeArray(value)Это обработка данных массива для достижения соответствующегоМетод мутации, здесь не рассматривается.

Продолжай читатьwalk()метод, описанный в комментарияхwalk()Что он делает, так это проходит данные каждой настройки в объекте данных и преобразует их вsetter/getter.

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

Затем соответствующие данные окончательно преобразуются вgetter/setterпутьdefineReactive()метод. Также из названия метода легко понять, что метод определен как отзывчивый.В сочетании с первоначальным примером вызов здесьdefineReactive(...)как показано на рисунке:

defineReactive

Исходный код выглядит следующим образом:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep 为当前数据的依赖实例
  // dep 维护着一个subs列表,保存依赖与当前数据(此时是当前数据是counter)的观察者(或者叫订阅者)。观察者即是Watcher实例。
  const dep = new Dep() ---------------(1)

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  
  // 定义getter与setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      // 这里在获取值之前先进行依赖收集,如果Dep.target有值的话。
      if (Dep.target) {    -----------------(2)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      
      // 依赖收集完后返回值
      return value
    },
    
    ...
}

Первый взглядgetterметод, есть два наиболее важных аспекта этого метода.

  1. объявить по одному для каждых данныхdepобъект экземпляра, затемdepНа замыкание ссылаются соответствующие данные. Например, каждый разcounterКогда значение извлекается или изменяется, к его экземпляру dep можно получить доступ, и он не исчезнет.
  2. согласно сDep.targetЧтобы определить, следует ли собирать зависимости или общие значения. здесьDep.targetЗадание последует позже, здесь мы впервые узнаем, что есть такое.

тогда посмотри еще разsetterметод, исходный код выглядит следующим образом:

set: function reactiveSetter (newVal) {
  const 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()
  }
  // 这里对数据的值进行修改
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  
  // 最重要的是这一步,即通过dep实例通知观察者我的数据更新了
  dep.notify()
}

На этом инициализация данных экземпляра Vue в основном завершена.Давайте рассмотрим следующий рисунок.initDataпроцесс:

initData flow

Далее следуетwatchинициализация:

src/core/instance/state.js

export function initState (vm: Component) {
  ...
  
  if (opts.data) {
    initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
  } 
  
  // initData(vm) 完成后进行 initWatch(..)
  ...
  
  // 对添加的watch进行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

4. initWatch

здесьinitWatch(vm, opts.watch)Это соответствует нашему примеру следующим образом:

initWatch

initWatchИсходный код выглядит следующим образом:

src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    // handler 是观察对象的回调函数
    // 如例子中counter的回调函数
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher(vm, key, handler)основывается на входных параметрахWatcherПример информации, исходный код выглядит следующим образом:

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 判断是否是对象,是的话提取对象里面的handler方法
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 判断handler是否是字符串,是的话说明是vm实例上的一个方法
  // 通过vm[handler]获取该方法
  // 如 handler='sayHello', 那么handler = vm.sayHello
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  
  // 最后调用vm原型链上的$watch(...)方法创建Watcher实例
  return vm.$watch(keyOrFn, handler, options)
}

$watchЭто метод, определенный в цепочке прототипов Vue.Исходный код выглядит следующим образом:

core/instance/state.js

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 创建Watcher实例对象
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    
    // 该方法返回一个函数的引用,直接调用该函数就会调用watcher对象的teardown()方法,从它注册的列表中(subs)删除自己。
    return function unwatchFn () {
      watcher.teardown()
    }
  }

После серии инкапсуляций мы, наконец, видим создание экземпляра объекта Watcher. Это будет подробно объяснено нижеWatcherДобрый.

5. Watcher

Согласно нашему примеру,new Watcher(...)Как показано ниже:

newWatcher

сначала выполнитьWatcherСтруктура класса, исходный код выглядит следующим образом, а какой-то код опущен:

core/observer/watcher.js

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
  	 ...
    this.cb = cb // 保存传入的回调函数
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = [] // 保存观察数据当前的dep实例对象
    this.newDeps = []  // 保存观察数据最新的dep实例对象
    this.depIds = new Set()
    this.newDepIds = new Set()

    // parse expression for getter
    // 获取观察对象的get方法
    // 对于计算属性, expOrFn为函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
    // 通过parsePath方法获取观察对象expOrFn的get方法
      this.getter = parsePath(expOrFn)
      ...
    }
    
    // 最后通过调用watcher实例的get()方法,
    // 该方法是watcher实例关联观察对象的关键之处
    this.value = this.lazy
      ? undefined
      : this.get()
  }

parsePath(expOrFn)Конкретный метод реализации заключается в следующем:

core/util/lang.js

/**
 * Parse simple path.
 */
const bailRE = /[^\w.$]/ // 匹配不符合包含下划线的任意单词数字组合的字符串
export function parsePath (path: string): any {
  // 非法字符串直接返回
  if (bailRE.test(path)) {
    return
  }
  // 举例子如 'counter'.split('.') --> ['counter']
  const segments = path.split('.')
  // 这里返回一个函数给this.getter
  // 那么this.getter.call(vm, vm),这里vm就是返回函数的入参obj
  // 实际上就是调用vm实例的数据,如 vm.counter,这样就触发了counter的getter方法。
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

Это ловко возвращает метод вthis.getter, который:

this.getter = function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
}

this.getterбудетthis.get()Вызывается в методе для получения значения наблюдаемого объекта и запуска его коллекции зависимостей, вот получениеcounterценность .

Последний шаг конструктора Watcher, вызовthis.get()метод, методисходный кодследующим образом:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 该方法实际上是设置Dep.target = this
    // 把Dep.target设置为该Watcher实例
    // Dep.target是个全局变量,一旦设置了在观察数据中的getter方法就可使用了
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用观察数据的getter方法
      // 进行依赖收集和取得观察数据的值
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 此时观察数据的依赖已经收集完
      // 重置Dep.target=null
      popTarget()
      // 清除旧的deps
      this.cleanupDeps()
    }
    return value
  }

Ключевые шаги были аннотированы в приведенном выше коде.Ниже показано отношение между классами Observer и Watcher.Рисунок по-прежнему описывается нашим примером:

Observer-Watcher-rel

  • Красная стрелка: создается экземпляр класса Watcher, вызывающий экземпляр Watcher.get()метод и наборDep.targetДля текущего экземпляра наблюдателя инициировать наблюдение за объектомgetterметод.
  • синяя стрелка:counterобъектgetterметод срабатывает, вызовdep.depend()Выполнить сбор зависимостей и возвратcounterценность . Зависит от собранных результатов:1.counterэкземпляр dep закрытияsubsДобавьте экземпляр наблюдателя w1, который наблюдает за ним.;2. п1depsдобавить объект наблюденияcounterотдел закрытия.
  • Оранжевая стрелка: когдаcounterПосле изменения значения срабатываетsubsНаблюдайте за его выполнением w1 вupdate()метод и, наконец, вызывает функцию обратного вызова cb для w1.

Другие родственные методы в классе Watcher относительно интуитивно понятны и здесь опущены.Подробности см. в исходном коде класса Watcher.

6. Dep

На рисунке выше классы Observer и Watcher связаны с Dep, так что же такое Dep?

Депа можно сравнить с издателем, Наблюдателя с читателями и Наблюдателя с книгами, связанными с Кэйго Хигасино. Например, читатель w1 интересуется «Прогулкой белой ночи» Кейго Хигасино (в нашем примере счетчик). Как только читатель w1 купит книгу Кейго Хигасино, он автоматически зарегистрируется и заполнит информацию w1 в издателе книги (пример отдела). есть последние новости о книге Кейго Хигашино (например, о скидках), w1 будет уведомлен.

Теперь посмотрим на исходный код Dep:

core/observer/dep.js

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 保存观察者watcher实例的数组
    this.subs = []
  }

  // 添加观察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // 移除观察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 进行依赖收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知观察者数据有变化
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Класс Dep относительно прост, и соответствующий метод также очень интуитивно понятен.Самое главное здесь — поддерживать массив, содержащий экземпляр watcher.subs.

7. Резюме

На этом основные три категории изучены, и теперь вы можете в основном ответить на вопросы в начале статьи.

Q1:Watcher,Dep,ObserverОтношения между этими классами?

А1:Watcherнаблюдатель наблюдаетObserverупакованные данные,DepдаWatcherСвязь между данными наблюдения и основной функцией состоит в том, чтобы полагаться на сбор и уведомление об обновлениях.

Q2:DepсерединаsubsЧто хранится?

A2: subsСохраняется экземпляр Watcher-наблюдателя.

Q3:WatcherсерединаdepsЧто хранится?

А3:depsЧто хранится, так это данные наблюдения в закрытииdepпример.

Q4:Dep.targetЧто это такое и где присваивается значение?

А4:Dep.target— это глобальная переменная, которая сохраняет текущий экземпляр наблюдателя, вnew Watcher()Когда назначение сделано, назначением является текущий экземпляр Watcher.

8. Расширение

Вот пример вычисляемого свойства:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    computed: {
        result: function() {
            return 'The result is :' + this.counter + 1;
        }
    }
})

здесьresultЗначение зависит отcounterзначение, черезresultЭто может лучше отражать адаптивные вычисления Vue. Вычисляемые свойства передаются черезinitComputed(vm, opts.computed)После инициализации, следуя отслеживанию исходного кода, вы обнаружите, что также создается экземпляр Watcher:

core/instance/state.js

  watchers[key] = new Watcher(
    vm,  // 当前vue实例
    getter || noop,  // result对应的方法 function(){ return 'The result is :' + this.counter + 1;}
    noop, // noop是定义的一个空方法,这里没有回调函数用noop代替
    computedWatcherOptions // { lazy: true }
  )

Схема показана ниже:

computed-watcher

Это вычисляемое свойствоresultпотому что это зависит отthis.counter, поэтому настройте наблюдателя для наблюденияresultценность . затем черезdefinedComputed(vm, key, userDef)для определения вычисляемых свойств. попасть в расчетresult, он снова сработаетthis.counterизgetterМетод, это делаетresultЗначение зависит отthis.counterценность .

definedComputed

Наконец, это будет вычисляемое свойствоresultопределить этоsetter/getterАтрибуты:Object.defineProperty(target, key, sharedPropertyDefinition). См. исходный код для более подробной информации.

9. Ссылка

  1. vue официальная документация
  2. исходный код
  3. Анализ исходного кода Vue