Углубленный анализ исходного кода Vue — построение адаптивной системы (включено)

Vue.js
Углубленный анализ исходного кода Vue — построение адаптивной системы (включено)

Из этого раздела официально войтиVueЯдро исходного кода также является одной из трудностей построения отзывчивой системы. Этот раздел послужит введением в анализ исходного кода процесса адаптивной сборки. Он в основном разделен на две части. Первая часть предназначена для адаптивных данных.props,methods,data,computed,watherАнализ процесса инициализации, другая часть заключается в том, чтобы попытаться вручную построить базовую отзывчивую систему, исходя из сохранения концепции дизайна исходного кода. Предвосхищая эти два основных содержания, анализ конкретных деталей исходного кода будет более удобен в следующей статье.

7.1 Инициализация данных

Оглядываясь назад на предыдущий контент, мы имеемVueАнализ исходного кода начинается с инициализации, инициализация_initБудет выполнен ряд процессов, включая слияние параметров конфигурации, агент мониторинга данных и, наконец, монтирование экземпляра. И до того, как инстанс будет смонтирован, важный процесс намеренно игнорируется.инициализация данных(которыйinitState(vm)).initStateПроцесс адаптивного дизайна данных, процесс будет нацеленprops,methods,data,computedиwatchВыполните инициализационную обработку данных и преобразуйте их в реактивные объекты, а затем мы разберем каждый процесс шаг за шагом.

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  // 初始化props
  if (opts.props) { initProps(vm, opts.props); }
  // 初始化methods
  if (opts.methods) { initMethods(vm, opts.methods); }
  // 初始化data
  if (opts.data) {
    initData(vm);
  } else {
    // 如果没有定义data,则创建一个空对象,并设置为响应式
    observe(vm._data = {}, true /* asRootData */);
  }
  // 初始化computed
  if (opts.computed) { initComputed(vm, opts.computed); }
  // 初始化watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

7.2 initProps

Краткий обзорpropsиспользования родительский компонент передает данные дочернему компоненту в виде атрибутов, а дочерний компонент передаетpropsСвойства получают значения, переданные родительским компонентом.

// 父组件
<child :test="test"></child>
var vm = new Vue({
  el: '#app',
  data() {
    return {
      test: 'child'
    }
  }
})
// 子组件
Vue.component('child', {
  template: '<div>{{test}}</div>',
  props: ['test']
})

Так проанализируйpropsНам нужно проанализировать два процесса родительского компонента и дочернего компонента.Давайте сначала посмотрим на обработку переданного значения родительским компонентом. Как описано в предыдущих статьях, родительский компонент сначала выполняет компиляцию шаблона, чтобы получитьrenderфункция, которая встречает свойства дочерних компонентов во время разбора,:test=testбудет проанализирован как{ attrs: {test: test}}и как подкомпонентrenderФункция существует следующим образом:

with(){..._c('child',{attrs:{"test":test}})}

renderРазобратьVnodeпроцесс знакомстваchildТаким образом, этот дочерний узел-заполнитель используется для создания дочерних компонентов.Vnodeпроцесс, который создает подVnodeпроцедура называетсяcreateComponent, На этом этапе мы проанализировали его в главе о компонентах, а также проанализировали расширенное использование компонента и в конечном итоге вызовемnew Vnodeсоздать подVnode. И дляpropsобработка,extractPropsFromVNodeDataбудетattrsПосле того, как атрибут проверен спецификацией, результат проверки, наконец, отображается какpropsDataВходящие в виде атрибутовVnodeв конструкторе. заключить,propsОбозначение, переданное компоненту-заполнителю, будет начинаться сpropsDataформа как подкомпонентVnodeсвойства существуют. Конкретные детали будут проанализированы ниже.

// 创建子组件过程
function createComponent() {
  // props校验
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);
  ···
  // 创建子组件vnode
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
}

7.2.1 Соглашения об именах для реквизита

Сначала проверьтеpropsнормативный процесс. **propsЕсть два вида результатов после компиляции, среди которыхattrsКак анализировалось ранее, он компилируется и генерируетсяrenderфункция для обработки свойств, в то время какpropsнаписано для пользователяrenderЗначение свойства функции. **Поэтому оба метода должны быть проверены одновременно.

function extractPropsFromVNodeData (data,Ctor,tag) {
  // Ctor为子类构造器
  ···
  var res = {};
  // 子组件props选项
  var propOptions = Ctor.options.props;
  // data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数
  var attrs = data.attrs;
  var props = data.props;
  if (isDef(attrs) || isDef(props)) {
    for (var key in propOptions) {
      // aB 形式转成 a-b
      var altKey = hyphenate(key);
      {
          var keyInLowerCase = key.toLowerCase();
          if (
            key !== keyInLowerCase &&
            attrs && hasOwn(attrs, keyInLowerCase)
          ) {
            // 警告
          }
        }
    }
  }
}

Сосредоточьтесь на обработке исходного кода в этой части,HTML нечувствителен к регистру, все браузеры интерпретируют прописные буквы как строчные, поэтому мы используемDOMВерблюжий чехол (верблюжий чехол)propsимя должно использовать его эквивалентkebab-case(имена, разделенные тире) команда вместо.который:<child :aB="test"></child>нужно записать как<child :a-b="test"></child>

7.2.2 Реквизиты адаптивных данных

просто про анализыpropsТребуются два процесса, и родительский компонент был сопряжен ранееpropsописывается, а для подкомпонентов мы передаемpropsВозможность получить значение, переданное родительским компонентом. Давайте снова посмотрим на пару подкомпонентовpropsобработка:

обработка дочерних компонентовpropsПроцесс, происходящий в родительском компоненте_updateэтап, этот этапVnodeПроцесс генерации реальных узлов, во время которого будут встречаться детиVnode, то вызоветcreateComponentДля создания дочерних компонентов. И процесс создания дочерних компонентов восходит к_initИнициализация, в это время произойдет слияние опций, дляpropsварианты, в конечном итоге будут объединены в{props: { test: { type: null }}}письма. затем позвонитinitProps, initPropsЧто делать, в двух словах, так это поместить компонентpropsДанные устанавливаются как реагирующие данные.

function initProps (vm, propsOptions) {
  var propsData = vm.$options.propsData || {};
  var loop = function(key) {
    ···
    defineReactive(props,key,value,cb);
    if (!(key in vm)) {
      proxy(vm, "_props", key);
    }
  }
  // 遍历props,执行loop设置为响应式数据。
  for (var key in propsOptions) loop( key );
}

вproxy(vm, "_props", key);заpropsСделайте слой прокси, пользователи черезvm.XXXможно получить через проксиvm._propsзначение на . противdefineReactive, по существу используяObject.definePropertyдля данныхgetter,setterМетод переписан. Конкретный принцип см. в содержании главы о прокси данных. Во второй половине этого раздела также будет базовая реализация.

7.3 initMethods

initMethodЭтот метод не имеет ничего общего с реакцией, представленной в этом разделе, и его реализация относительно проста, в основном для обеспеченияmethodsОпределение метода должно быть функцией, а имя не может совпадать сpropsНеоднократно определенные методы в конечном итоге будут смонтированы на корневом экземпляре.

function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        // method必须为函数形式
        if (typeof methods[key] !== 'function') {
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        // methods方法名不能和props重复
        if (props && hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        //  不能以_ or $.这些Vue保留标志开头
        if ((key in vm) && isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      // 直接挂载到实例的属性上,可以通过vm[method]访问。
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
  }

7.4 initData

dataФункция генерируется, когда параметры инициализации объединяются, и при выполнении функции возвращаются только реальные данные, поэтомуinitDataМетод будет выполнен первым, чтобы получить компонентdataданные, и имя каждого атрибута объекта будет проверено, чтобы гарантировать, что его нельзя комбинировать сprops,methodsповторение. Последний основной методobserve,observeметод заключается вОбъекты данных отмечены как реактивные объектыи выполнять реактивную обработку для каждого свойства объекта. В то же время иpropsПрокси обрабатывается таким же образом,proxyбудетdataСделайте слой прокси, напрямую черезvm.XXXможно получить через проксиvm._dataСвойства объекта смонтированы на нем.

function initData(vm) {
  var data = vm.$options.data;
  // 根实例时,data是一个对象,子组件的data是一个函数,其中getData会调用函数返回data对象
  data = vm._data = typeof data === 'function'? getData(data, vm): data || {};
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    {
      // 命名不能和方法重复
      if (methods && hasOwn(methods, key)) {
        warn(("Method \"" + key + "\" has already been defined as a data property."),vm);
      }
    }
    // 命名不能和props重复
    if (props && hasOwn(props, key)) {
      warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm);
    } else if (!isReserved(key)) {
      // 数据代理,用户可直接通过vm实例返回data数据
      proxy(vm, "_data", key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

последний разговорobserve,observeКонкретное поведение заключается в добавлении неперечислимого свойства к объекту данных.__ob__, объект флага является реагирующим объектом, и получить значение атрибута каждого объекта, переписатьgetter,setterметод, чтобы каждое значение свойства было реактивными данными. Подробный код мы проанализируем позже.

7.5 initComputed

Как и в случае с методом анализа, описанным выше,initComputedдаcomputedИнициализация данных, разница заключается в следующих моментах:

  1. computedМожет быть объектом или функцией, но объект должен иметьgetterметод, поэтому, еслиcomputedПроверка требуется, когда значением свойства является объект.
  2. противcomputedДля каждого свойства создайте прослушивающую зависимость, т. е. создайте экземплярwatcher,watcherОпределение , можно временно понимать как зависимость самого использования данных,watcherЭкземпляр представляет собой еще одну зависимость данных, которую необходимо отслеживать.

За исключением разницы,initComputedОн также установит для каждого свойства адаптивные данные, а также будет реагировать наcomputedИменование делается для обнаружения и предотвращенияprops,dataконфликт.

function initComputed (vm, computed) {
  ···
  for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      // computed属性为对象时,要保证有getter方法
      if (getter == null) {
        warn(("Getter is missing for computed property \"" + key + "\"."),vm);
      }
      if (!isSSR) {
        // 创建computed watcher
        watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);
      }
      if (!(key in vm)) {
        // 设置为响应式数据
        defineComputed(vm, key, userDef);
      } else {
        // 不能和props,data命名冲突
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
}

очевидноVueДля использования разработчиками предоставляется множество видов данных, но после анализа выясняется, что ядром каждой обработки является преобразование данных в реагирующие данные.Как построить реагирующую систему с реагирующими данными? упоминалось ранееwatcherЧто это? Вам нужно что-нибудь еще, чтобы построить отзывчивую систему? Далее мы пытаемся реализовать минималистскую отзывчивую систему.

7.6 Минималистская адаптивная система

VueКонструкция отзывчивой системы относительно сложна, и будет сложно понять каждый процесс, непосредственно входящий в анализ и построение исходного кода.Поэтому я считаю, что, максимально сохраняя логику проектирования исходного кода, используйте наименьшие код для создания самой простой отзывчивой системы.система необходима. правильноDep,Watcher,ObserverНачальное понимание концепции также полезно для анализа деталей дизайна адаптивной системы в следующей статье.

7.6.1 Конструкция каркаса

мы начинаем сMyVueПоскольку это адаптивная к классам структура, ее конструкция подробно описываться не будет. мы моделируемVueИдеи реализации исходного кода, создание экземпляровMyVueПри передаче конфигурации опции упрощенный код имеет только одинidэлемент монтирования и объект данныхdata. Идея моделирования исходного кода, мы сначала инициализируем данные при создании экземпляра, этот шаг является адаптивной конструкцией, мы проанализируем ее позже. После инициализации данных реальныйDOMустанавливать.

var vm = new MyVue({
  id: '#app',
  data: {
    test: 12
  }
})
// myVue.js
(function(global) {
  class MyVue {
      constructor(options) {
        this.options = options;
        // 数据的初始化
        this.initData(options);
        let el = this.options.id;
        // 实例的挂载
        this.$mount(el);
      }
      initData(options) {
      }
      $mount(el) {
      }
    }
}(window))

7.6.2 Настройка реактивных объектов — Наблюдатель

Сначала введите классObserver, цель этого класса — превратить данные в реагирующие объекты, используяObject.definePropertyдля данныхgetter,setterметод переписан. чтение данныхgetterэтап, который мы проведемколлекция зависимостей, в модификации данныхsetterэтап, мы будемЗависимое обновление(Введение в эти два понятия следует позже). Поэтому на этапе инициализации данных мы будем использоватьObserverЭтот класс изменяет объекты данных в соответствующие объекты, которые являются основой всех процессов.

class MyVue {
  initData(options) {
    if(!options.data) return;
    this.data = options.data;
    // 将数据重置getter,setter方法
    new Observer(options.data);
  }
}
// Observer类的定义
class Observer {
  constructor(data) {
    // 实例化时执行walk方法对每个数据属性重写getter,setter方法
    this.walk(data)
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for(let i = 0;i< keys.length; i++) {
      // Object.defineProperty的处理逻辑
      defineReactive(obj, keys[i])
    }
  }
}

7.6.3 Сама зависимость — Наблюдатель

Мы можем понять это так, т.WatcherЭкземпляр — это зависимость.Используются ли данные при рендеринге шаблона или когда пользователь выполняет расчеты, их можно считать зависимостью, которую необходимо отслеживать.watcherОн записывает состояние этого мониторинга зависимостей и способы обновления метода работы.

// 监听的依赖
class Watcher {
  constructor(expOrFn, isRenderWatcher) {
    this.getter = expOrFn;
    // Watcher.prototype.get的调用会进行状态的更新。
    this.get();
  }

  get() {}
}

Итак, в какой момент времени он будет созданwatcherА как насчет обновления состояния данных? Очевидно, что рендеринг данных в реальномDOMможет быть создан, когдаwatcher.$mountЭтот процесс был представлен в предыдущей главе, и он будет проходить через генерацию шаблона.renderфункция иrenderфункция делает реальнымDOMпроцесс. Мы упростили код,updateViewсконденсировал процесс.

class MyVue {
  $mount(el) {
    // 直接改写innerHTML
    const updateView = _ => {
      let innerHtml = document.querySelector(el).innerHTML;
      let key = innerHtml.match(/{(\w+)}/)[1];
      document.querySelector(el).innerHTML = this.options.data[key]
    }
    // 创建一个渲染的依赖。
    new Watcher(updateView, true)
  }
}

7.6.4 Управление зависимостями — отд.

watcherЕсли понимать это как зависимость, которую нужно отслеживать для каждых данных, тоDepЭто можно понимать как управление зависимостями. Данные можно использовать как при рендеринге, так и в вычисляемых свойствах. соответствующие каждому даннымwatcherЕсть также много. И когда мы обновляем данные, как уведомить каждую зависимость, связанную с данными, которая требуетDepУправление уведомлениями сделано. И браузер может обновлять только по одномуwatcher, поэтому вам также нужен атрибут для записи текущего обновленияwatcher. иDepЭтот класс должен делать только две вещи: собирать зависимости и отправлять зависимости для обновлений.

let uid = 0;
class Dep {
  constructor() {
    this.id = uid++;
    this.subs = []
  }
  // 依赖收集
  depend() {
    if(Dep.target) {
      // Dep.target是当前的watcher,将当前的依赖推到subs中
      this.subs.push(Dep.target)
    }
  }
  // 派发更新
  notify() {
    const subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) { 
      // 遍历dep中的依赖,对每个依赖执行更新操作
      subs[i].update();
    }
  }
}

Dep.target = null;

7.6.5 Процесс управления зависимостями — defineReactive

Давайте посмотрим на процесс перехвата данных. фронтObserverсоздание экземпляра в конечном итоге вызоветdefineReactiveпереписатьgetter,setterметод. Этот метод начинается с создания экземпляраDep, то есть для создания диспетчера зависимостей данных. переписываниеgetterСбор зависимостей выполняется в методе, то есть вызовdep.dependМетоды. существуетsetterФаза, после сравнения двух чисел будет вызвано зависимое обновление отправки. которыйdep.notify

const defineReactive = (obj, key) => {
  const dep = new Dep();
  const property = Object.getOwnPropertyDescriptor(obj);
  let val = obj[key]
  if(property && property.configurable === false) return;
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 做依赖的收集
      if(Dep.target) {
        dep.depend()
      }
      return val
    },
    set(nval) {
      if(nval === val) return
      // 派发更新
      val = nval
      dep.notify();
    }
  })
}

оглядыватьсяwatcher, создать экземплярwatcherбудетDep.targetустановить на текущийwatcher, после выполнения функции обновления состояния,Dep.targetЗаглушка. Таким образом, при сборе зависимостей толькоDep.targetТекущийwatcher pushприбытьDepизsubsМассив подойдет. На этапе обновления дистрибутива необходимо снова обновить только состояние.

class Watcher {
  constructor(expOrFn, isRenderWatcher) {
    this.getter = expOrFn;
    // Watcher.prototype.get的调用会进行状态的更新。
    this.get();
  }

  get() {
    // 当前执行的watcher
    Dep.target = this
    this.getter()
    Dep.target = null;
  }
  update() {
    this.get()
  }
}

7.6.6 Результаты

Построена минималистичная отзывчивая система. При упрощении кода сохраняется идея и логика дизайна исходного кода. На основе этого шага будет легче подробно проанализировать детали реализации каждой ссылки в исходном коде.

7.7 Резюме

В этом разделе мы официально вводим введение адаптивной системы. В предыдущей главе о агентстве данных мы узналиObject.definePropertyЭто метод выполнения перехвата данных, а основой построения отзывчивой системы является перехват данных. Давайте представим его первым.VueВ процессе внутренней инициализации данных окончательный вывод заключается в том, чтоdata,computed, или другие пользовательские данные, в конечном итоге вызываяObject.definePropertyПерехват данных. В конце статьи мы построили упрощенную версию адаптивной системы, исходя из идеи и логики дизайна исходного кода. Полная функция помогает нам проанализировать и обдумать конкретные детали реализации исходного кода в следующем разделе.