Принципы адаптивного графического интерфейса Vue

Vue.js
Принципы адаптивного графического интерфейса Vue

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

  • Инициализация Vue
  • Рендеринг шаблона
  • Рендеринг компонентов

Версия исходного кода Vue в этой статье: 2.6.11, была удалена для простоты понимания.

В этой статье будут рассмотрены следующие два аспекта:

  • Процесс от инициализации Vue до первого рендеринга для создания DOM.

  • Процесс от модификации данных Vue до обновления страницы DOM.

Инициализация Vue

Начнем с самого простого фрагмента кода Vue:

<template>
  <div>
    {{ message }}
  </div>
</template>
<script>
new Vue({
  data() {
    return {
      message: "hello world",
    };
  },
});
</script>

Этот код очень прост и в конечном итоге напечатает на странице приветствие, мир. Как он реализован?

Начнем анализ с источника: новый Vue.

// 执行 new Vue 时会依次执行以下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)

// 5. 调用 walk 方法,遍历 data 中的每一个属性,监听数据的变化。
function walk(obj) {
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
}

// 6. 执行 defineProperty 监听数据读取和设置。
function defineReactive(obj, key, val) {
  // 为每个属性创建 Dep(依赖搜集的容器,后文会讲)
  const dep = new Dep();
  // 绑定 get、set
  Object.defineProperty(obj, key, {
    get() {
      const value = val;
      // 如果有 target 标识,则进行依赖搜集
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      val = newVal;
      // 修改数据时,通知页面重新渲染
      dep.notify();
    },
  });
}

После завершения привязки дескриптора данных мы можем получить следующую блок-схему:

Как мы видим на рисунке, когда Vue инициализируется, данные получаются и устанавливаются, и создается объект Dep.

Мы знакомы с привязкой данных get и set, но как насчет объекта Dep?

Объект Dep используется для сбора зависимостей.Он реализует модель публикации-подписки и завершает подписку данных Data и рендеринг Viewer.Давайте проанализируем это вместе.

class Dep {
  // 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型。
  static target: ?Watcher;
  // subs 存放搜集到的 Watcher 对象集合
  subs: Array<Watcher>;
  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    // 搜集所有使用到这个 data 的 Watcher 对象。
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 搜集依赖,最终会调用上面的 addSub 方法
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用对应的 Watcher,更新视图
      subs[i].update();
    }
  }
}

Согласно анализу исходного кода Dep, мы получили следующую логическую схему:

Теперь, когда мы разобрались с Data и Dep, давайте перейдем к раскрытию Watcher.

class Watcher {
  constructor(vm: Component, expOrFn: string | Function) {
    // 将 vm._render 方法赋值给 getter。
    // 这里的 expOrFn 其实就是 vm._render,后文会讲到。
    this.getter = expOrFn;
    this.value = this.get();
  }
  get() {
    // 给 Dep.target 赋值为当前 Watcher 对象
    Dep.target = this;
    // this.getter 其实就是 vm._render
    // vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
    const value = this.getter.call(this.vm, this.vm);
    return value;
  }
  addDep(dep: Dep) {
    // 将当前的 Watcher 添加到 Dep 收集池中
    dep.addSub(this);
  }
  update() {
    // 开启异步队列,批量更新 Watcher
    queueWatcher(this);
  }
  run() {
    // 和初始化一样,会调用 get 方法,更新视图
    const value = this.get();
  }
}

В исходном коде мы видим, что Watcher реализует метод рендеринга_renderДля ассоциации с Dep при инициализации Watcher отметьте логотип Dep.target, а затем вызовите метод get для рендеринга страницы. В дополнение к вышеуказанным данным, текущие отношения между данными, депо и наблюдателем следующие:

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

Текущий общий процесс выглядит следующим образом:

В приведенном выше процессе и Data, и Dep создаются при инициализации Vue, но теперь мы не знаем, откуда создается Wacher.С этой проблемой мы продолжим исследовать.

Рендеринг шаблона

Выше мы проанализировали часть обработки данных в процессе инициализации Vue, далее проанализировали часть рендеринга данных.

Фактически, в конце выполнения new Vue будет вызван метод mount для рендеринга экземпляра Vue в dom.

// new Vue 执行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
// 5. mountComponent

// 6. 创建 Watcher 实例
const updateComponent = () => {
  vm._update(vm._render());
};
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);

// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._render())

// 10. 调用 vm._render 生成虚拟 dom
Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const { render } = vm.$options;
  let vnode = render.call(vm._renderProxy, vm.$createElement);
  return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode: VNode) {
  const vm: Component = this;
  if (!prevVnode) {
    // 初次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom,这里就不展开了。

Прочитав процесс рендеринга шаблона Vue, мы можем получить следующую блок-схему:

На данный момент мы знаем, что Watcher фактически создается на этапе инициализации Vue, который принадлежит позиции beforeMount в жизненном цикле.Когда Watcher будет создан, будет выполнен метод рендеринга, и, наконец, код Vue будет отрендерен. в настоящий ДОМ.

Когда мы интегрируем предыдущий процесс, мы можем получить следующий процесс:

На приведенном выше рисунке анализируется весь процесс от инициализации Vue до рендеринга DOM Наконец, давайте проанализируем, как Vue обновляется при изменении данных?

На самом деле, как видно на рисунке выше, при изменении данных будет вызываться метод Dep.notify, а затем метод update внутри Watcher, который добавит все Watchers, которые используют эти данные, в очередь и открыть асинхронную очередь, сделать обновление и, наконец, выполнить_renderметод для завершения обновления страницы.

Общий процесс выглядит следующим образом:

Что ж, мы изучили принцип отзывчивости Vue, который был нами тщательно проанализирован.

Рендеринг компонентов

Первоначально изучение приведенной выше блок-схемы было закончено, но мне стало любопытно, и я задумался над другим вопросом 😂.

Как визуализируются компоненты Vue?

С этим вопросом я продолжил обращаться к исходному коду.

// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement

// 6. createComponent
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
  const baseCtor = context.$options._base;
  // Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
  if (isObject(Ctor)) {
    // 将组件中 export 出的对象,继承自 Vue,得到一个构造函数
    // 相当于 Vue.extend(YourComponent)
    Ctor = baseCtor.extend(Ctor);
  }
  const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
  return vnode;
}

// 7. 实现组件继承 Vue,并调用 Vue._init 方法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
  const Super = this;
  const Sub = function VueComponent(options) {
    // 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
    this._init(options);
  };
  // 原型继承,相当于:Component extends Vue
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  return Sub;
};

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

Что ж, на самом деле все закончилось, и последняя блок-схема приведена выше.

Вопрос, теперь вы понимаете реактивные принципы Vue?

Если вам все еще сложно понять, я также подготовил упрощенную схему с аннотациями 😂

Думать и подводить итоги

В этой статье представлен принцип отзывчивости Vue с точки зрения исходного кода, давайте кратко рассмотрим его.

  1. Начиная с нового Vue, сначала отслеживайте изменения данных в Data с помощью get и set, а затем создайте Dep для сбора Watcher, который использует Data.
  2. Скомпилируйте шаблон, создайте Наблюдателя и определите Dep.target как текущего Наблюдателя.
  3. При компиляции шаблона, если используются данные в Data, будет запущен метод get Data, а затем будет вызван Dep.addSub для сбора Watcher.
  4. Когда данные будут обновлены, будет запущен метод set Data, а затем будет вызван Dep.notify, чтобы уведомить всех наблюдателей, которые используют данные для обновления DOM.

Наконец, если у вас есть какие-либо мысли по этому поводу, пожалуйста, оставьте комментарий!