[Официальные заметки к учебнику Vue] - Рукописное мини-вью You Yuxi

внешний интерфейс Vue.js
[Официальные заметки к учебнику Vue] - Рукописное мини-вью You Yuxi

опубликовано на прошлой неделе【Официальное руководство по Vue3】🎄 4D Notes | Синхронизированное обучающее видео1050 лайков

🔥На этой неделе я смотрел мини-версию Vue3, написанную от руки You Dashen.Примечания следующие, пожалуйста, поправьте меня.

【Оригинальное видео】

image-20201230111207968

⚡️Подписаться на официальный аккаунт [Front-end big bus] Ответить [mini-vue] Запросить полный код

1. Общий рабочий процесс

Kapture 2020-12-10 at 16.13.53.gif

  1. Компилятор компилирует шаблоны представлений в функции рендеринга.
  2. Модуль ответа данных инициализирует объект данных как реактивный объект данных.
  3. просмотр рендеринга
    1. RenderPhase: модуль рендеринга использует функцию рендеринга для создания виртуального дома на основе данных инициализации.
    2. MountPhase: создать HTML-страницу просмотра с помощью виртуального Dom
    3. PatchPhase: после изменения модели данных функция рендеринга будет вызываться снова для создания нового виртуального Dom, а затем выполнять Dom Diff для обновления представления Html.

Во-вторых, разделение труда трех модулей

image.png

  • модуль обработки данных
  • переводчик
  • функция рендеринга

1. Модули, реагирующие на данные

Предоставляет методы для создания реактивных объектов, которые можно прослушивать для всех изменений данных.Kapture 2020-12-10 at 11.47.59.gif

2. Скомпилируйте модуль

image.pngСкомпилируйте HTML-шаблон в функцию рендеринга

Этот процесс компиляции может быть выполнен в следующие два раза

  • время выполнения браузера (время выполнения)
  • Время компиляции пакета проекта Vue (время компиляции)

3. Функция рендеринга

Функция рендеринга отображает представление на странице в течение следующих трех циклов.image.png

  • Render Phase
  • Mount Phase
  • Patch Phase

3. Прототип MVVM (Mock-версия)

MVVM原理

Инфраструктура MVVM фактически добавляет слой виртуальной машины между исходным представлением и моделью для выполнения следующих задач. Завершите мониторинг данных и представлений. Давайте сначала напишем Mock-версию на этом шаге. Фактически, он впервые реализует мониторинг фиксированных представлений и моделей данных.

1. Определение интерфейса

Интерфейс нашего фреймворка MVVM точно такой же, как у Vue3.

Необходимо определить инициализацию

  • шаблон просмотра
  • модель данных
  • Поведение модели. Например, мы хотим, чтобы сообщения модели данных сортировались в обратном порядке при нажатии.
const App = {
  // 视图
  template: `
<input v-model="message"/>
<button @click='click'>{{message}}</button>
`,
  setup() {
    // 数据劫持
    const state = new Proxy(
      {
        message: "Hello Vue 3!!",
      },
      {
        set(target, key, value, receiver) {
          const ret = Reflect.set(target, key, value, receiver);
          // 触发函数响应
          effective();
          return ret;
        },
      }
    );

    const click = () => {
      state.message = state.message.split("").reverse().join("");
    };
    return { state, click };
  },
};
const { createApp } = Vue;
createApp(App).mount("#app");

2. Каркас программы

Процесс выполнения программы примерно такой:

render-proxy

const Vue = {
  createApp(config) {
    // 编译过程
    const compile = (template) => (content, dom) => {
      
    };

    // 生成渲染函数
    const render = compile(config.template);

    return {
      mount: function (container) {
        const dom = document.querySelector(container);
        
				// 实现setup函数
        const setupResult = config.setup();
				
        // 数据响应更新视图
        effective = () => render(setupResult, dom);
        render(setupResult, dom);
      },
    };
  },
};

3. Скомпилируйте функцию рендеринга

Функция рендеринга в инфраструктуре MVVM создается путем компиляции шаблона представления.

// 编译函数
// 输入值为视图模板
const compile = (template) => {
  //渲染函数
  return (observed, dom) => {
  	// 渲染过程
	}
}

Проще говоря, он анализирует шаблон представления и генерирует функцию рендеринга.

Есть примерно три вещи, чтобы сделать

  • Определите, какие значения необходимо отобразить в соответствии с моделью данных

    // <button>{{message}}</button>
    // 将数据渲染到视图
    button = document.createElement('button')
    button.innerText = observed.message
    dom.appendChild(button)
    
  • Привязать события модели

    // <button @click='click'>{{message}}</button>
    // 绑定模型事件
    button.addEventListener('click', () => {
      return config.methods.click.apply(observed)
    })
    
  • Определите, какие входы требуют двусторонней привязки

// <input v-model="message"/>
// 创建keyup事件监听输入项修改
input.addEventListener('keyup', function () {
  observed.message = this.value
})

полный код

const compile = (template) => (observed, dom) => {

    // 重新渲染
    let input = dom.querySelector('input')
    if (!input) {
        input = document.createElement('input')
        input.setAttribute('value', observed.message)
      	
        input.addEventListener('keyup', function () {
            observed.message = this.value
        })
        dom.appendChild(input)
    }
    let button = dom.querySelector('button')
    if (!button) {
        console.log('create button')
        button = document.createElement('button')
        button.addEventListener('click', () => {
            return config.methods.click.apply(observed)
        })
        dom.appendChild(button)
    }
    button.innerText = observed.message
}

В-четвертых, реализация ответа данных

Vue обычно использует метод захвата данных. Разница заключается в том, использовать ли DefineProperty или Proxy. То есть захватывать ли одно свойство за раз или один объект за раз. Конечно, последний имеет очевидные преимущества перед первым. Это адаптивный принцип Vue3.

Proxy/Reflect был добавлен в спецификацию ES 2015. Proxy может лучше перехватывать поведение объекта, а Reflect может более элегантно манипулировать объектами. Преимущество

  • Он настраивается для всего объекта, а не для свойства объекта, поэтому нет необходимости проходить ключи.
  • Массивы поддерживаются, а это DefineProperty — нет. Это избавляет от хакерского процесса перегрузки методов массива.
  • Второй параметр Proxy может иметь 13 методов перехвата, что богаче, чем Object.defineProperty().
  • Proxy, как новый стандарт, привлек внимание и оптимизацию производительности производителей браузеров, в то время как Object.defineProperty() является существующим старым методом.
  • Вложение объектов может быть удобно выполнено с помощью рекурсии.

Сказав так много, давайте начнем с небольшого примера

var obj = new Proxy({}, {
    get: function (target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
        console.log(`setting ${key}!`);
        return Reflect.set(target, key, value, receiver);
    }
})
obj.abc = 132

Таким образом, если вы измените значение в obj, оно будет напечатано.

То есть, если объект будет изменен, он получит ответ.

image-20200713122621925

Конечно, ответ, который нам нужен, — повторно обновить представление и повторно запустить метод рендеринга.

Сначала создайте абстрактную функцию ответа данных

// 定义响应函数
let effective
observed = new Proxy(config.data(), {
  set(target, key, value, receiver) {
    const ret = Reflect.set(target, key, value, receiver)
    // 触发函数响应
    effective()
    return ret
  },
})

Во время инициализации мы устанавливаем ответное действие для рендеринга представления.

const dom = document.querySelector(container)
// 设置响应动作为渲染视图
effective = () => render(observed, dom)
render(observed, dom)

1. Слушатель для просмотра изменений

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

document.querySelector('input').addEventListener('keyup', function () {
  data.message = this.value
})

2. Полный код

<html lang="en">
  <body>
    <div id="app"></div>
    <script>
      const Vue = {
        createApp(config) {
          // 编译过程
          const compile = (template) => (content, dom) => {
            // 重新渲染
            dom.innerText = "";
            input = document.createElement("input");
            input.addEventListener("keyup", function () {
              content.state.message = this.value;
            });
            input.setAttribute("value", content.state.message);
            dom.appendChild(input);

            let button = dom.querySelector("button");
            button = document.createElement("button");
            button.addEventListener("click", () => {
              return content.click.apply(content.state);
            });
            button.innerText = content.state.message;
            dom.appendChild(button);
          };
          
          // 生成渲染函数
          const render = compile(config.template);

          return {
            mount: function (container) {
              const dom = document.querySelector(container);
              const setupResult = config.setup();
              effective = () => render(setupResult, dom);
              render(setupResult, dom);
            },
          };
        },
      };
      // 定义响应函数
      let effective;
      const App = {
        // 视图
        template: `
                <input v-model="message"/>
                <button @click='click'>{{message}}</button>
            `,
        setup() {
          // 数据劫持
          const state = new Proxy(
            {
              message: "Hello Vue 3!!",
            },
            {
              set(target, key, value, receiver) {
                const ret = Reflect.set(target, key, value, receiver);
                // 触发函数响应
                effective();
                return ret;
              },
            }
          );

          const click = () => {
            state.message = state.message.split("").reverse().join("");
          };
          return { state, click };
        },
      };
      const { createApp } = Vue;
      createApp(App).mount("#app");
    </script>
  </body>
</html>

5. Посмотреть процесс рендеринга

Dom => virtual DOM => render functions

1. Что такое Dom, объектная модель документа

image.png

HTML сопоставляется с рядом узлов в браузере, который нам удобно вызывать.

image.png

2. Что такое виртуальный дом

В Доме много узлов, и производительность прямого запроса и обновления Дома низкая.

Способ представления реального DOM с помощью объектов JavaScript. Представляйте фактический Дом с помощью объекта JS.

image.png

3. Что такое функция рендеринга

Во Vue мы компилируем шаблон представления (template) в функцию рендеринга (render function), а затем конвертируем его в виртуальный дом.image.png

4. Эффективно обновлять представления через DomDiff

image.png

5. Резюме

Возьмите каштан 🌰 Виртуальный Дом и Дом - это как связь между зданием и планом здания.image.pngДопустим, вы хотите добавить кухню на 29-й этаж. ❌ Снести весь 29-й этаж и отстроить заново ✅Сначала нарисуйте дизайн, узнайте разницу между старой и новой структурой, а затем стройте

6. Реализуйте функцию рендеринга

Во Vue мы компилируем шаблон представления (template) в функцию рендеринга (render function), а затем конвертируем его в виртуальный дом.image.png

Процесс рендеринга обычно делится на три части:

vue-next-template-explorer.netlify.app/

  • RenderPhase: модуль рендеринга использует функцию рендеринга для создания виртуального дома на основе данных инициализации.
  • MountPhase: создать HTML-страницу просмотра с помощью виртуального Dom
  • PatchPhase: после изменения модели данных функция рендеринга будет вызываться снова для создания нового виртуального Dom, а затем выполнять Dom Diff для обновления представления Html.
mount: function (container) {
    const dom = document.querySelector(container);
    const setupResult = config.setup();
    const render = config.render(setupResult);

    let isMounted = false;
    let prevSubTree;
    watchEffect(() => {
      if (!isMounted) {
        dom.innerHTML = "";
        // mount
        isMounted = true;
        const subTree = config.render(setupResult);
        prevSubTree = subTree;
        mountElement(subTree, dom);
      } else {
        // update
        const subTree = config.render(setupResult);
        diff(prevSubTree, subTree);
        prevSubTree = subTree;
      }
    });
  },

1.Render Phase

Модуль рендеринга использует функцию рендеринга для создания виртуального дома на основе данных инициализации.

render(content) {
  return h("div", null, [
    h("div", null, String(content.state.message)),
    h(
      "button",
      {
        onClick: content.click,
      },
      "click"
    ),
  ]);
},

2. Mount Phase

Используйте виртуальный дом для создания страницы просмотра Html

function mountElement(vnode, container) {
  // 渲染成真实的 dom 节点
  const el = (vnode.el = createElement(vnode.type));

  // 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const val = vnode.props[key];
      patchProp(vnode.el, key, null, val);
    }
  }

  // 要处理 children
  if (Array.isArray(vnode.children)) {
    vnode.children.forEach((v) => {
      mountElement(v, el);
    });
  } else {
    insert(createText(vnode.children), el);
  }

  // 插入到视图内
  insert(el, container);
}

3. Patch Phase(Dom diff)

Как только модель данных изменится, функция рендеринга будет вызвана снова для создания нового виртуального Dom, а затем выполните Dom Diff для обновления представления Html.

function patchProp(el, key, prevValue, nextValue) {
  // onClick
  // 1. 如果前面2个值是 on 的话
  // 2. 就认为它是一个事件
  // 3. on 后面的就是对应的事件名
  if (key.startsWith("on")) {
    const eventName = key.slice(2).toLocaleLowerCase();
    el.addEventListener(eventName, nextValue);
  } else {
    if (nextValue === null) {
      el.removeAttribute(key, nextValue);
    } else {
      el.setAttribute(key, nextValue);
    }
  }
}

Через DomDiff — эффективное обновление представлений

image.png

image-20201230104838657

function diff(v1, v2) {
  // 1. 如果 tag 都不一样的话,直接替换
  // 2. 如果 tag 一样的话
  //    1. 要检测 props 哪些有变化
  //    2. 要检测 children  -》 特别复杂的
  const { props: oldProps, children: oldChildren = [] } = v1;
  const { props: newProps, children: newChildren = [] } = v2;
  if (v1.tag !== v2.tag) {
    v1.replaceWith(createElement(v2.tag));
  } else {
    const el = (v2.el = v1.el);
    // 对比 props
    // 1. 新的节点不等于老节点的值 -> 直接赋值
    // 2. 把老节点里面新节点不存在的 key 都删除掉
    if (newProps) {
      Object.keys(newProps).forEach((key) => {
        if (newProps[key] !== oldProps[key]) {
          patchProp(el, key, oldProps[key], newProps[key]);
        }
      });

      // 遍历老节点 -》 新节点里面没有的话,那么都删除掉
      Object.keys(oldProps).forEach((key) => {
        if (!newProps[key]) {
          patchProp(el, key, oldProps[key], null);
        }
      });
    }
    // 对比 children

    // newChildren -> string
    // oldChildren -> string   oldChildren -> array

    // newChildren -> array
    // oldChildren -> string   oldChildren -> array
    if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          setText(el, newChildren);
        }
      } else if (Array.isArray(oldChildren)) {
        // 把之前的元素都替换掉
        v1.el.textContent = newChildren;
      }
    } else if (Array.isArray(newChildren)) {
      if (typeof oldChildren === "string") {
        // 清空之前的数据
        n1.el.innerHTML = "";
        // 把所有的 children mount 出来
        newChildren.forEach((vnode) => {
          mountElement(vnode, el);
        });
      } else if (Array.isArray(oldChildren)) {
        // a, b, c, d, e -> new
        // a1,b1,c1,d1 -> old
        // 如果 new 的多的话,那么创建一个新的

        // a, b, c -> new
        // a1,b1,c1,d1 -> old
        // 如果 old 的多的话,那么把多的都删除掉
        const length = Math.min(newChildren.length, oldChildren.length);
        for (let i = 0; i < length; i++) {
          const oldVnode = oldChildren[i];
          const newVnode = newChildren[i];
          // 可以十分复杂
          diff(oldVnode, newVnode);
        }

        if (oldChildren.length > length) {
          // 说明老的节点多
          // 都删除掉
          for (let i = length; i < oldChildren.length; i++) {
            remove(oldChildren[i], el);
          }
        } else if (newChildren.length > length) {
          // 说明 new 的节点多
          // 那么需要创建对应的节点
          for (let i = length; i < newChildren.length; i++) {
            mountElement(newChildren[i], el);
          }
        }
      }
    }
  }
}

Семь, принцип компиляции

Это место не было реализовано You Dashen, и тогда дядя предоставит вам суперкраткую версию В этой главе мы в основном рассмотрим функцию компиляции.

compiler

Функция скомпилированной функции была упомянута выше

// 编译函数
// 输入值为视图模板
const compile = (template) => {
  //渲染函数
  return (observed, dom) => {
  	// 渲染过程
	}
}

Проще говоря

  • Ввод: шаблон просмотра
  • вывод: функция рендеринга

Его можно разделить на три небольших шага

Snip20200713_17

  • Разобрать строку шаблона -> абстрактное синтаксическое дерево AST (Abstract Syntax Treee)

  • Маркеры преобразования преобразования, такие как v-bind v-if v-для преобразования

  • Генерация AST -> функция рендеринга

    //  模板字符串 -> AST(Abstract Syntax Treee)抽象语法树
    let ast = parse(template)
    // 转换处理 譬如 v-bind v-if v-for的转换
    ast = transfer(ast)
    // AST -> 渲染函数
    return generator(ast)
    

    Мы можем почувствовать это через онлайн-версию VueTemplateExplorer.

    vue-next-template-explorer.netlify.com/

image-20200713150630150

Анализ функций компиляции

1. Разбирать парсер

Принцип работы парсера на самом деле представляет собой серию обычных совпадений.

Например:

Соответствие атрибутов тега

  • class="title"

  • class='title'

  • class=title

const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)=("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)/

"class=abc".match(attr);
// output
(6) ["class=abc", "class", "abc", undefined, undefined, "abc", index: 0, input: "class=abc", groups: undefined]

"class='abc'".match(attr);
// output
(6) ["class='abc'", "class", "'abc'", undefined, "abc", undefined, index: 0, input: "class='abc'", groups: undefined]

Подробно об этом будет рассказано при реализации. Вы можете обратиться к статье.

Парсер AST в действии

Итак, для нашего проекта это можно написать так

// <input v-model="message"/>
// <button @click='click'>{{message}}</button>
// 转换后的AST语法树
const parse = template => ({
    children: [{
            tag: 'input',
            props: {
                name: 'v-model',
                exp: {
                    content: 'message'
                },
            },
        },
        {
            tag: 'button',
            props: {
                name: '@click',
                exp: {
                    content: 'message'
                },
            },
            content:'{{message}}'
        }
    ],
})

2. Преобразование обработки преобразования

Предыдущий абзац знаний — это абстрактное синтаксическое дерево, а здесь выполняется специальное преобразование для шаблонов Vue3.

Например: vFor, vOn

В Vue три типа также будут разделены на два уровня обработки.

  • основная логика компиляции compile-core

    • AST-Parser

    • Разрешение базового типа v-for , v-on

      image-20200713183256931

  • compile-dom Логика компиляции для браузеров

    • v-html

    • v-model

    • v-clock

      image-20200713183210079

const transfer = ast => ({
    children: [{
            tag: 'input',
            props: {
                name: 'model',
                exp: {
                    content: 'message'
                },
            },
        },
        {
            tag: 'button',
            props: {
                name: 'click',
                exp: {
                    content: 'message'
                },
            },
            children: [{
                content: {
                    content: 'message'
                },
            }]
        }
    ],
})

3. Сгенерируйте визуализатор

Генератор фактически генерирует функции рендеринга на основе преобразованного синтаксического дерева AST. Конечно, вы можете отображать разные результаты для одного и того же синтаксического дерева. Например, если вы хотите, чтобы кнопка отображалась как кнопка или блок svg, это зависит от ваших предпочтений. Это называется пользовательским рендерером. Здесь мы просто пишем фиксированный плейсхолдер Dom renderer. Я расширяю обработку, когда она будет реализована позже.

const generator = ast => (observed, dom) => {
    // 重新渲染
    let input = dom.querySelector('input')
    if (!input) {
        input = document.createElement('input')
        input.setAttribute('value', observed.message)
        input.addEventListener('keyup', function () {
            observed.message = this.value
        })
        dom.appendChild(input)
    }
    let button = dom.querySelector('button')
    if (!button) {
        console.log('create button')
        button = document.createElement('button')
        button.addEventListener('click', () => {
            return config.methods.click.apply(observed)
        })
        dom.appendChild(button)
    }
    button.innerText = observed.message
}

🔥Подпишитесь на официальный аккаунт [front-end big bus] Ответьте на [mini-vue], чтобы запросить полный код

Подписаться на Full Stack Uncle Ran

Последние статьи (Спасибо за вашу поддержку и поддержку 🌹🌹🌹)

Добро пожаловать в Paizhuan, давайте вместе обсудим более элегантную реализацию