Анализ исходного кода ядра компилятора компилятора Vue.js 3.0

Vue.js

Автор: Глубокий горный муравей

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

В настоящее время это все еще предварительная альфа-версия, и с оптимизмом можно предположить, что существуют альфа-, бета-версии и, наконец, официальная версия.

Не говори много, смотриPre-Alpha. вуаляcompiler-core

Популярная реактивность снова и снова изучалась большими парнями, поэтому я буду интерпретировать «непопулярный» компилятор со всеми! 😄😄😄😄

Если вы не знакомы с AST или как реализовать простой парсер AST, можете ткнуть:Научить вас писать парсер AST

Разбор шаблона в vue3.0 сильно отличается от парсинга в vue2.0, но как бы он ни менялся, основной принцип тот же.Различные html-коды, которые мы пишем, на самом деле представляют собой строку, когда используется js.Данные, преобразованные в структурированный AST, мы используем мощные регулярные выражения и indexOf для оценки.
Одной из основных функций ядра компилятора является преобразование строк в синтаксические деревья абстрактных объектов AST.

Let's do IT !

Структура каталогов

  • _tests_прецедент
  • Определения типов важных элементов синтаксиса src/ast ts, таких как тип, перечисление, интерфейс и т. д.
  • src/codegen преобразует сгенерированный ast в строку рендеринга
  • src/errors определяет типы ошибок компилятора
  • Файл записи src/index в основном имеет baseCompile, который используется для компиляции файлов шаблонов.
  • src/parse преобразует строки шаблона в AST
  • src/runtimeHelper определяет соответствующую связь констант при генерации кода
  • src/transform обрабатывает специфичный для vue синтаксис в AST, например v-if , v-on parsing

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

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

test('simple text', () => {
    const ast = parse('some text')
    const text = ast.children[0] as TextNode
    expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        isEmpty: false,
        loc: {
            start: { offset: 0, line: 1, column: 1 },
            end: { offset: 9, line: 1, column: 10 },
            source: 'some text'
        }
    })
})

Сначала посмотрите на картинку, основное внимание уделяется четырем частям:

  • начальный тег
  • конечный тег
  • динамический контент
  • нормальный контент

Начальный тег будет использовать рекурсию для обработки дочерних узлов.

alt

Далее, давайте начнем читать вместе с исходным кодом~~~~~~

parse: преобразовать строковый шаблон в абстрактное синтаксическое дерево AST

Это основной метод внешнего воздействия. Давайте сначала проверим результаты:

const source = `
    <div id="test" :class="cls">
        <span>{{ name }}</span>
        <MyCom></MyCom>
    </div>
`.trim()
import { parse } from './compiler-core.cjs'
const result = parse(source)

output:

output

Представлен простой результат преобразования.С точки зрения сгенерированной структуры есть несколько важных изменений по сравнению с vue2.x:

  • Добавлено свойство местоположения Каждый узел записывает начало и конец узла в исходном коде и определяет подробное расположение кода, столбца, строки, смещения.
    vu3.0 также основана на подробном выводе журнала для проблем, возникших в процессе разработки, и поддерживает исходную карту.
  • Добавлено свойство tagType
    Атрибут tagType определяет тип узла. Мы знаем, что vue2.x оценивает тип узла только во время выполнения, а vu3.0 продвигает оценку на этапе компиляции, что повышает производительность.
    В настоящее время существует три типа tagType: 0 элемент, 1 компонент, 2 слота, 3 шаблона.
  • Добавлено Isstatic Property
    Заранее скомпилируйте шаблон и определите, является ли он динамическим, например динамические инструкции.
  • ...

Новая версия AST явно сложнее, чем vue2.x.Видно, что vue3.0 определит много вещей, которые можно определить на этапе компиляции, и идентифицирует результаты компиляции.Не нужно ждать, пока время выполнения, экономя память и производительность. Это также ключевой момент, о котором вы сказали, оптимизация компиляции и повышение производительности.

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

  • основная запись parse & parseChildren
  • parseTag обрабатывает теги
  • parseAttribute обрабатывает атрибуты тегов
  • parseElement обрабатывает начальные теги
  • parseInterpolation обрабатывает динамический текстовый контент
  • parseText обрабатывает статический текстовый контент

основная запись parse & parseChildren

Здесь создается основная запись parse, parseContext, что удобно для получения содержимого, параметров и т. д. непосредственно из контекста в будущем.
getCursor Получает текущую обрабатываемую позицию указателя, пользователь генерирует loc, и начальное значение равно 1.

export function parse(content: string, options: ParserOptions = {}): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return {
    type: NodeTypes.ROOT,
    children: parseChildren(context, TextModes.DATA, []),
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    codegenNode: undefined,
    loc: getSelection(context, start)
  }
}

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

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = []
  while (!isEnd(context, mode, ancestors)) {
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
    if (startsWith(s, context.options.delimiters[0])) {
      // '{{'
      node = parseInterpolation(context, mode)
    } else if (mode === TextModes.DATA && s[0] === '<') {
      // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
      if (s.length === 1) {
        emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
      } else if (s[1] === '!') {
          // <!DOCTYPE <![CDATA[ 等非节点元素 暂不讨论
      } else if (s[1] === '/') {
        if (s.length === 2) {
        } else if (s[2] === '>') {
          advanceBy(context, 3)
          continue
        } else if (/[a-z]/i.test(s[2])) {
          parseTag(context, TagType.End, parent)
          continue
        } else {
        }
      } else if (/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors)
      } else if (s[1] === '?') {
      } else {
      }
    }
    if (!node) {
      node = parseText(context, mode)
    }
    if (Array.isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(context, nodes, node[i])
      }
    } else {
      pushNode(context, nodes, node)
    }
  }
  return nodes
}

предки используются для хранения несопоставленных начальных узлов и представляют собой стек LIFO.

Исходник обрабатывается в цикле.Условием отсечки цикла является то, что метод isEnd возвращает true, то есть обработка завершена.Условий окончания два:

  1. context.source пустой, то есть обрабатывается весь шаблон
  2. Обнаружен тег конечного узла (), и соответствующий тег можно найти среди несовпадающих начальных тегов (предков). Обрабатывается дочерний узел, соответствующий parseChildren.

Если матч еще не закончился, введите повторный матч. Есть три случая:

  1. if (startsWith(s, context.options.delimiters[0]))
    разделители — это разделенные совпадения, vue — это {{ и }}. Начните сопоставлять содержимое текстового вывода vue {{ , это означает, что вам нужно обработать вставку текстового содержимого,
  2. else if (mode === TextModes.DATA && s[0] === '<')
    Если содержимое уже имеет
  3. Ни одно из вышеперечисленных условий, или матч был неудачным
    Тогда это динамический текстовый контент.

Если это третья динамическая вставка текста, выполните parseInterpolation для сборки текстового узла, где флаг isStatic=false является относительно простой переменной, и метод не будет опубликован.

return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      content,
      loc: getSelection(context, innerStart, innerEnd)
    },
    loc: getSelection(context, start)
  }

Давайте посмотрим на эти два метода обработки исходного контента в обратном порядке:

advanceBy(context,number) : переместите исходный код шаблона для обработки числовыми символами и перезапишите местоположение.
advanceSpaces() : продвиньте существующие последовательные пробелы

Возвращаясь к приведенному выше условию соответствия, если оно начинается с

  1. Второй символ "/"
    Соответствующее
    Если это > , то он считается недопустимым тегом, просто переместите его на 3 символа назад.
    Если это , то он считается обрезающим тегом и выполняется метод parseTag.
  2. второй символ это буква
    Соответствующий начальный текст тега, например
, выполняет метод parseElement для обработки начального тега.

parseTag обрабатывает теги

Если это обрезной тег: parseTag, обработка завершается напрямую.
Если это начальный тег: выполняется parseElement, вызывается parseTag для обработки тега, а затем рекурсивно обрабатывает дочерние узлы и т. д.

Обычный: /^?([a-z][^\t\r\n\f />]*)/i Об этом особо нечего сказать, он соответствует тегам типа

.
Тестовый матч:
/^<\/?([a-z][^\t\r\n\f />]*)/i.exec("<div class='abc'>")
(2) ["<div", "div", index: 0, input: "<div class='abc'>", groups: undefined]

Очевидно, что mathch[1] является соответствующим элементом тега. Рассмотрим основной метод:

function parseTag(
  context: ParserContext,
  type: TagType,
  parent: ElementNode | undefined
): ElementNode {
  // Tag open.
  const start = getCursor(context)
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  const tag = match[1]
  const props = []
  const ns = context.options.getNamespace(tag, parent)
  let tagType = ElementTypes.ELEMENT
  if (tag === 'slot') tagType = ElementTypes.SLOT
  else if (tag === 'template') tagType = ElementTypes.TEMPLATE
  else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
  advanceBy(context, match[0].length)
  advanceSpaces(context)
  // Attributes.
  const attributeNames = new Set<string>()
  while (
    context.source.length > 0 &&
    !startsWith(context.source, '>') &&
    !startsWith(context.source, '/>')
  ) {
    const attr = parseAttribute(context, attributeNames)
    if (type === TagType.Start) {
      props.push(attr)
    }
    advanceSpaces(context)
  }
  // Tag close.
  let isSelfClosing = false
  if (context.source.length === 0) {
  } else {
    isSelfClosing = startsWith(context.source, '/>')
    advanceBy(context, isSelfClosing ? 2 : 1)
  }
  return {
    type: NodeTypes.ELEMENT,
    ns,
    tag,
    tagType,
    props,
    isSelfClosing,
    children: [],
    loc: getSelection(context, start),
    codegenNode: undefined // to be created during transform phase
  }
}

Здесь определены четыре типа tagType: 0 элемент, 1 компонент, 2 слота, 3 шаблона.

Давайте посмотрим на цикл while, advanceBy после удаления открывающего Если за ним следует > или /> , то обработка тега завершается и цикл завершается.
В противном случае это элемент метки, мы выполняем parseAttribute для обработки атрибута метки, добавления реквизита к узлу и сохранения атрибутов начального узла;

После метода казни! , это синтаксис ts, который эквивалентен сообщению ts, что здесь должно быть значение, нет необходимости делать короткие суждения, такие как const match = /^?([az][^\t\r\n\f />]*)/i .exec(context.source)!

parseAttribute обрабатывает атрибуты тегов

Регулярно получать имя по атрибуту

/^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec('class='abc'>')
["class", index: 0, input: "class='abc'>", groups: undefined]

Если это не изолированный атрибут, он имеет значение (/[1]*=/.test(context.source)), затем получите значение атрибута.

function parseAttribute(
  context: ParserContext,
  nameSet: Set<string>
): AttributeNode | DirectiveNode {
  // Name.
  const start = getCursor(context)
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  const name = match[0]
  nameSet.add(name)
  advanceBy(context, name.length)
  // Value
  let value:
    | {
        content: string
        isQuoted: boolean
        loc: SourceLocation
      }
    | undefined = undefined
  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    advanceSpaces(context)
    advanceBy(context, 1)
    advanceSpaces(context)
    value = parseAttributeValue(context)
  }
  const loc = getSelection(context, start)
  if (/^(v-|:|@|#)/.test(name)) {
    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
      name
    )!
    let arg: ExpressionNode | undefined
    if (match[2]) {
      const startOffset = name.split(match[2], 2)!.shift()!.length
      const loc = getSelection(
        context,
        getNewPosition(context, start, startOffset),
        getNewPosition(context, start, startOffset + match[2].length)
      )
      let content = match[2]
      let isStatic = true

      if (content.startsWith('[')) {
        isStatic = false
        content = content.substr(1, content.length - 2)
      }
      arg = {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        isStatic,
        loc
      }
    }
    if (value && value.isQuoted) {
      const valueLoc = value.loc
      valueLoc.start.offset++
      valueLoc.start.column++
      valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
      valueLoc.source = valueLoc.source.slice(1, -1)
    }
    return {
      type: NodeTypes.DIRECTIVE,
      name:
        match[1] ||
        (startsWith(name, ':')
          ? 'bind'
          : startsWith(name, '@')
            ? 'on'
            : 'slot'),
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        loc: value.loc
      },
      arg,
      modifiers: match[3] ? match[3].substr(1).split('.') : [],
      loc
    }
  }
  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      isEmpty: value.content.trim().length === 0,
      loc: value.loc
    },
    loc
  }
}

Метод parseAttributeValue для получения значения атрибута проще:

  • Если значение значения начинается с кавычки, найдите следующую кавычку, которая не заканчивается значением значения (class="aaa" class='aaa')
  • Если значение не имеет кавычек, найдите следующий пробел, чтобы закончить значение значения (class=aaa)

Среди них есть грамматические особенности для обработки vue.Если имя атрибута начинается с v-,:,@,#, требуется специальная обработка.Взгляните на эту закономерность:

/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec("v-name")
(4) ["v-name", "name", undefined, undefined, index: 0, input: "v-name", groups: undefined]

/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(":name")
(4) [":name", undefined, "name", undefined, index: 0, input: ":name", groups: undefined]

Если mathch[2] имеет значение, это означает, что оно соответствует, указывая на то, что это не v-имя.Если имя заключено в [], оноДинамические инструкции, установить для isStatic значение false

parseElement обрабатывает начальные теги

parseElement обрабатывает начальный тег, мы сначала выполняем parseTag для анализа тега и получаем элемент тега и атрибуты начального узла, если текущий также является конечным тегом (например,
), метка возвращается напрямую.
В противном случае поместите начальную метку в стек несопоставленных начальных предков.
Затем продолжите обработку дочерних элементов parseChildren , обратите внимание, что передаются несопоставленные предки, для parseChildren есть два условия отсечки:

  1. context.source пуст, то есть обработка завершена
  2. Обнаружен тег конечного узла (), и соответствующий тег можно найти среди несовпадающих начальных тегов (предков).

Таким образом, если цикл достигает соответствующей метки отсечки, для добавления узла к текущему дочернему элементу требуется предки.pop().

Конечно, для обработки текущего начального узла этот узел также может быть конечным узлом, например: , затем продолжить обработку конечного узла.
Методы, как показано ниже:

function parseElement(
  context: ParserContext,
  ancestors: ElementNode[]
): ElementNode | undefined {
  // Start tag.
  const parent = last(ancestors)
  const element = parseTag(context, TagType.Start, parent)

  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element
  }
  // Children.
  ancestors.push(element)
  const mode = (context.options.getTextMode(
    element.tag,
    element.ns
  ) as unknown) as TextModes
  const children = parseChildren(context, mode, ancestors)
  ancestors.pop()
  element.children = children
  // End tag.
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End, parent)
  } else {
  }
  element.loc = getSelection(context, element.loc.start)
  return element
}

На данный момент основной процесс преобразования файлов шаблонов в AST в vue3.0 в основном завершен.
Дождитесь следующей части, преобразуйте обработку AST.


Если вы считаете, что этот контент ценен для вас, поставьте лайк и подпишитесь на нас.Официальный сайтА на нашем официальном аккаунте WeChat (WecTeam) каждую неделю публикуются качественные статьи:

WecTeam


  1. \t\r\n\f ↩︎