Синтаксический сахар Vue3 Ref, попрощайтесь с написанием .value

исходный код Vue.js
Синтаксический сахар Vue3 Ref, попрощайтесь с написанием .value

предисловие

Недавно Vue3 предложилRFC для RefSugar,Прямо сейчасrefСинтаксический сахар, который в настоящее время также обрабатывает экспериментальную фазу. В Мотивации RFC Эван Ю сообщил, что после введения Composition API основной нерешенной проблемой былоrefsа такжеreactiveиспользование предметов. и использовать везде.valueМожет быть громоздким и его легко пропустить, если система типов не используется:

let count = ref(1)

function add() {
	count.value++
}

Поэтому некоторые пользователи предпочтут использовать толькоreactive, так что вам не придется иметь дело с использованиемrefsиз.valueвопрос. а такжеrefРоль синтаксического сахара состоит в том, чтобы позволить нам использоватьrefПри создании реактивных переменных вы можете получить и изменить саму переменную напрямую, а не использовать.valueчтобы получить и изменить соответствующее значение. Проще говоря,на уровне использования, мы можем попрощаться с использованиемrefsвремя.valueвопрос:

let count = $ref(1)

function add() {
	count++
}

Так,refКак сейчас в проекте используется синтаксический сахар? Как это достигается? Это первый раз, когда я вижу вопрос об учреждении этого RFC, и я думаю, что этот вопрос также волнует многих студентов. Итак, давайте раскрывать их один за другим.

1 Использование синтаксического сахара Ref в проекте

из-заrefСинтаксический сахар все еще находится на экспериментальной стадии, поэтому по умолчанию он не будет поддерживаться в Vue3.refСинтаксический сахар. Итак, здесь мы используем разработку проекта Vite + Vue3 в качестве примера, чтобы увидеть, как включитьrefПоддержка синтаксического сахара.

При использовании разработки проекта Vite + Vue3 он разрабатывается@vitejs/plugin-vueплагин для реализации.vueТранскодирование файлов (Transform), горячее обновление (HMR) и т. д. Итак, нам нужноvite.config.jsсередина, чтобы дать@vitejs/plugin-vueОпции плагина (Options) входящиеrefTransform: true:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue({
    refTransform: true
  })]
})

Затем, чтобы@vitejs/plugin-vueВнутри плагина он будет основываться на входящих опцияхrefTransformзначение, чтобы определить, является лиrefСинтаксический сахар для конкретных преобразований кода. Так как здесь мы устанавливаемtrue, очевидно будетrefСинтаксический сахар выполняет определенные преобразования кода.

Тогда мы можем.vueиспользуется в файлеrefСинтаксический сахар, здесь мы рассмотрим простой пример:

<template>
	<div>{{count}}</div>
	<button @click="add">click me</button>
</template>

<script setup>
let count = $ref(1)

function add() {
	count++
}
</script>

Соответственно отображается на странице:

Как видите, мы можем использоватьrefСинтаксический сахар для создания адаптивных переменных, не думая о добавлении.valueЭта проблема. также,refСинтаксический сахар также поддерживает другие методы письма, и здесь представлена ​​личная рекомендация.$refметод, заинтересованные студенты могут обратиться в RFC, чтобы узнать о других методах письма.

Затем, после пониманияrefПосле использования синтаксического сахара в проекте мы ответили на первый вопрос (как его использовать в проекте). Далее ответим на второй вопрос, как это реализовано, то есть какая обработка производится в исходном коде?

2 Реализация синтаксического сахара Ref

Во-первых, мы проходимVue PlaygroundЧтобы почувствовать это интуитивно, используйте предыдущийrefпример синтаксического сахара<script setup>Результат блока (Block) после компиляции:

import { ref as _ref } from 'vue'

const __sfc__ = {
  setup(__props) {
  let count = _ref(1)

  function add() {
    count.value++
  }
}

Видно, что хотя мы используемrefНе нужно иметь дело с синтаксическим сахаром.value, но компилируетсявсе еще используется.value. Ну, этот процесс должен сделать многоСвязанные с компиляциейобработка преобразования кода. Потому что нам нужно найти с помощью$refоператоры объявления и переменные, перепишите первый как_ref, добавить к последнему.value.

Ранее мы также упоминали@vitejs/plugin-vueплагин будет.vueФайл преобразуется в код, и этот процесс обеспечивается Vue3.@vue/compiler-sfcПакет (Пакет), предоставляющий<script>,<template>,<style>Функции, связанные с компиляцией, такие как блоки.

Итак, очевидно, что здесь нам нужно сосредоточиться на<script>Функции, связанные с компиляцией блоков, которые соответствуют@vue/compiler-sfcсерединаcompileScript()функция.

2.1 Функция compileScript()

compileScript()функция определена вvue-nextизpackages/compiler-sfc/src/compileScript.tsфайл, он в основном отвечает за<script>или<script setup>Процесс компиляции содержимого блока, он получит 2 параметра:

  • sfcВключать.vueПроанализированное содержимое кода файла, в том числеscript,scriptSetup,sourceРавные атрибуты
  • optionsСодержит некоторые необязательные и обязательные свойства, такие как соответствующиеscopeIdбудет действовать какoptions.id, вышеупомянутоеrefTransformЖдать

compileScript()Определение функции (псевдокод):

// packages/compiler-sfc/src/compileScript.ts
export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions
): SFCScriptBlock {
  // ...
  return {
    ...script,
    content,
    map,
    bindings,
    scriptAst: scriptAst.body
  }
} 

дляrefС точки зрения синтаксического сахара,compileScript()Функция сначала получит опцию (Option)refTransformзначение и присвоить егоenableRefTransform:

const enableRefTransform = !!options.refTransform

enableRefTransformОн будет использоваться позже, чтобы определить, следует ли вызыватьrefФункции преобразования, связанные с синтаксическим сахаром. Ну, мы также упоминали ранее, что нам нужно использоватьrefСинтаксический сахар, вам нужно дать@vite/plugin-vueпараметры плагинаrefTransformсвойство установлено наtrue, он будет передан вcompileScript()функциональныйoptions, который здесьoptions.refTransform.

Затем изsfcдеконструированныйscriptSetup,source,filenameи другие свойства. Среди них строка кода исходного файла будет использоваться первой.sourceСоздаватьMagicStringпримерs, который в основном используется для последующих преобразований кодаЗамена, добавление и т. д. в строки исходного кода, который затем вызоветparse()функция для разбора<script setup>содержание, то естьscriptSetup.content, чтобы сгенерировать соответствующее абстрактное синтаксическое деревоscriptSetupAst:

let { script, scriptSetup, source, filename } = sfc
const s = new MagicString(source)
const startOffset = scriptSetup.loc.start.offset
const scriptSetupAst = parse(
  scriptSetup.content,
  {
    plugins: [
      ...plugins,
      'topLevelAwait'
    ],
    sourceType: 'module'
  },
  startOffset
)

а такжеparse()Внутри используется функция@babel/parserкоторый предоставилparserМетод анализирует код и генерирует соответствующий AST. Для нашего примера выше сгенерированный AST будет выглядеть так:

{
  body: [ {...}, {...} ],
  directives: [],
  end: 50,
  interpreter: null,
  loc: {
    start: {...}, 
    end: {...}, 
    filename: undefined, 
    identifierName: undefined
  },
  sourceType: 'module',
  start: 0,
  type: 'Program'
}

Обратите внимание, что здесь он опущенbody,start,endсодержание в

Затем, в соответствии с ранее определеннымenableRefTransformи позвониshouldTransformRef()Возвращаемое значение функции (trueилиfalse), чтобы определить, следует ли продолжатьrefСинтаксический сахар для преобразований кода. Если требуется соответствующее преобразование, он вызоветtransformRefAST()функция для выполнения соответствующей операции преобразования кода в соответствии с AST:

if (enableRefTransform && shouldTransformRef(scriptSetup.content)) {
  const { rootVars, importedHelpers } = transformRefAST(
    scriptSetupAst,
    s,
    startOffset,
    refBindings
  )
}

Ранее мы представилиenableRefTransform. Здесь мы посмотримshouldTransformRef()функция, которая в основном соответствует содержанию кода через обычныеscriptSetup.contentопределить, следует ли использоватьrefСинтаксический сахар:

// packages/ref-transform/src/refTransform.ts
const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\(/

export function shouldTransform(src: string): boolean {
  return transformCheckRE.test(src)
}

Итак, когда вы указываетеrefTransformдляtrue, но на самом деле вы не используете его в своем кодеrefсинтаксический сахар при компиляции<script>или<script setup>тоже в процессене будет выполнятьсяа такжеrefОперации преобразования кода, связанные с синтаксическим сахаром, который также более подробно рассматривается в Vue3, позволяет избежать снижения производительности, вызванного ненужными операциями преобразования кода.

Так, для нашего примера (используяrefсинтаксический сахар), он ударит по вышеуказанномуtransformRefAST()функция. а такжеtransformRefAST()Функция соответствуетpackages/ref-transform/src/refTransform.tsсерединаtransformAST()функция.

Итак, давайте посмотримtransformAST()Как функция анализируется в соответствии с ASTrefСинтаксический код, связанный с сахаром, для операций преобразования.

2.2 функция transformAST()

существуетtransformAST()Функция в основном проходит через AST, соответствующий входящему исходному коду, а затем генерирует его, манипулируя строкой исходного кода.MagicStringпримерsдля выполнения определенных преобразований в исходном коде, таких как переписывание$refдля_ref,Добавить к.valueЖдать.

transformAST()Определение функции (псевдокод):

// packages/ref-transform/src/refTransform.ts
export function transformAST(
  ast: Program,
  s: MagicString,
  offset: number = 0,
  knownRootVars?: string[]
): {
  // ...
  walkScope(ast)
  (walk as any)(ast, {
    enter(node: Node, parent?: Node) {
      if (
        node.type === 'Identifier' &&
        isReferencedIdentifier(node, parent!, parentStack) &&
        !excludedIds.has(node)
      ) {
        let i = scopeStack.length
        while (i--) {
          if (checkRefId(scopeStack[i], node, parent!, parentStack)) {
            return
          }
        }
      }
    }
  })
  
  return {
    rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
    importedHelpers: [...importedHelpers]
  }
}

можно увидетьtransformAST()позвонит первымwalkScope()для обработки корневой области (root scope), то звонитеwalk()Функция обрабатывает узлы AST слой за слоем, и здесьwalk()Функция написана Ричем Харисом с использованиемestree-walker.

Далее рассмотрим каждыйwalkScope()а такжеwalk()что делает функция.

функция WalkScope()

Прежде всего, давайте посмотрим на предыдущее использованиеrefСинтаксический сахар для операторов объявленияlet count = $ref(1)Соответствующая структура AST:

можно увидетьletТип узла ASTtypeбудетVariableDeclaration, узлы AST, соответствующие остальной части кода, будут помещены вdeclarationsсередина. Среди них переменнаяcountУзлы AST будут использоваться какdeclarations.id,а также$ref(1)Узлы AST будут использоваться какdeclarations.init.

Итак, вернемся кwalkScope()функция, которая будет основываться на типе узла ASTtypeВыполните конкретную обработку, для нашего примераletСоответствующий узел ASTtypeдляVariableDeclarationпопадет в эту логику:

function walkScope(node: Program | BlockStatement) {
  for (const stmt of node.body) {
    if (stmt.type === 'VariableDeclaration') {
      for (const decl of stmt.declarations) {
        let toVarCall
        if (
          decl.init &&
          decl.init.type === 'CallExpression' &&
          decl.init.callee.type === 'Identifier' &&
          (toVarCall = isToVarCall(decl.init.callee.name))
        ) {
          processRefDeclaration(
            toVarCall,
            decl.init as CallExpression,
            decl.id,
            stmt
          )
        }
      }
    }
  }
}

здесьstmtявляетсяletСоответствующий узел AST, а затем пройденныйstmt.declarationsdecl.init.callee.nameОтносится$ref, с последующим вызовомisToVarCall()функцию и назначитьtoVarCall.

isToVarCall()Определение функции:

// packages/ref-transform/src/refTransform.ts
const TO_VAR_SYMBOL = '$'
const shorthands = ['ref', 'computed', 'shallowRef']
function isToVarCall(callee: string): string | false {
  if (callee === TO_VAR_SYMBOL) {
    return TO_VAR_SYMBOL
  }
  if (callee[0] === TO_VAR_SYMBOL && shorthands.includes(callee.slice(1))) {
    return callee
  }
  return false
}

Мы также упоминали ранееrefСинтаксический сахар может поддерживать другие обозначения, так как мы используем$refКстати, так что здесь будет хитcallee[0] === TO_VAR_SYMBOL && shorthands.includes(callee.slice(1))логика, то естьtoVarCallбудет назначен как$ref.

Затем он вызоветprocessRefDeclaration()функция, которая будетdecl.init Предоставленная информация о местоположениисоответствует исходному кодуMagicStringпримерsоперация, собирается$refпереписать какref:

// packages/ref-transform/src/refTransform.ts
function processRefDeclaration(
    method: string,
    call: CallExpression,
    id: VariableDeclarator['id'],
    statement: VariableDeclaration
) {
  // ...
  if (id.type === 'Identifier') {
    registerRefBinding(id)
    s.overwrite(
      call.start! + offset,
      call.start! + method.length + offset,
      helper(method.slice(1))
    )
  } 
  // ...
}

Информация о местоположении относится к обычно используемому местоположению узла AST в исходном коде.start,endзначит, например здесьlet count = $ref(1),Такcountсоответствующего узла ASTstartбыло бы 4.endбудет 9.

Потому что входящийidсоответствуетcountузел AST, это будет выглядеть так:

{
  type: "Identifier",
  start: 4,
  end: 9,
  name: "count"
}

Итак, это ударит по вышеуказанномуid.type === 'Identifier'логика. Во-первых, он вызоветregisterRefBinding()функция, которая фактически вызываетregisterBinding(),а такжеregisterBindingБудет втекущий объем currentScopeпривязать переменную кid.nameи установить наtrue, что указывает на то, что этоrefПеременная, созданная синтаксическим сахаром, которая позже будет использоваться для определения, добавлять ли переменную к переменной..value:

const registerRefBinding = (id: Identifier) => registerBinding(id, true)
function registerBinding(id: Identifier, isRef = false) {
  excludedIds.add(id)
  if (currentScope) {
    currentScope[id.name] = isRef
  } else {
    error(
      'registerBinding called without active scope, something is wrong.',
      id
    )
  }
}

Видно, что вregisterBinding()сдастсяexcludedIdsдобавить узел AST вexcludeIdsэтоWeekMap, он будет использоваться для последующих пропусков, которые не нужно выполнятьrefТипы обработки синтаксического сахара:Identifierузел АСТ.

Затем он вызоветs.overwrite()функционировать, чтобы$refпереписать как_ref, он получит 3 параметра: начальную позицию перезаписи, конечную позицию и строку, которую нужно перезаписать. а такжеcallсоответствует$ref(1)узел AST, это будет выглядеть так:

{
  type: "Identifier",
  start: 12,
  end: 19,
  callee: {...}
  arguments: {...},
  optional: false
}

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

а такжеhelper()Функция возвращает строку_ref, и в этом процессе будетrefдобавить вimportedHelpers, это будет вcompileScript()используется для создания соответствующихimportЗаявление:

function helper(msg: string) {
  importedHelpers.add(msg)
  return `_${msg}`
}

Ну вот и свершилось$refприбыть_refРерайт, то есть наш код будет таким:

let count = _ref(1)

function add() {
	count++
}

Затем, черезwalk()функционировать, чтобыcount++Перевести вcount.value++. Далее давайте посмотримwalk()функция.

функция ходьбы()

Ранее мы упоминалиwalk()Функция написана Rich Harisestree-walker, который являетсяESTreeКанонический пакет AST (Пакет).

walk()Функция будет выглядеть так:

import { walk } from 'estree-walker'

walk(ast, {
  enter(node, parent, prop, index) {
    // ...
  },
  leave(node, parent, prop, index) {
    // ...
  }
});

можно увидеть,walk()функция может быть передана вoptionsenter()будет вызываться каждый раз при посещении узла AST,leave()Вызывается при выходе из узла AST.

Итак, возвращаясь к упомянутому ранее примеру,walk()Функция в основном делает две вещи:

1. Поддерживать scopeStack, parentStack и currentScope

scopeStackОн используется для хранения цепочки областей, в которой в данный момент находится узел AST.Первоначально вершина стека является корневой областью.rootScope;parentStackИспользуется для хранения узлов AST-предков в процессе обхода узлов AST (узел AST наверху стека является родительским узлом AST текущего узла AST);currentScopeУказывает на текущую область, изначально совпадающую с корневой областьюrootScope:

const scopeStack: Scope[] = [rootScope]
const parentStack: Node[] = []
let currentScope: Scope = rootScope

Итак, вenter()Этап определит, является ли тип узла AST функцией или блоком в это время.вставить в стек scopeStack:

parent && parentStack.push(parent)
if (isFunctionType(node)) {
  scopeStack.push((currentScope = {}))
  // ...
  return
}
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
  scopeStack.push((currentScope = {}))
  // ...
  return
}

Затем вleave()На этапе оценки того, является ли тип узла AST функцией или блоком в данный момент, если да, топоп scopeStackи обновитьcurrentScopeдля выскочившихscopeStackВерхний элемент стека:

parent && parentStack.pop()
if (
  (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
  isFunctionType(node)
) {
  scopeStack.pop()
  currentScope = scopeStack[scopeStack.length - 1] || null
}

2. Обработка узла AST типа идентификатора

Так как в нашем случаеrefСинтаксический сахар для созданияcountТип узла AST переменной:Identifier, так что это будет вenter()Сцена попадает в логику следующим образом:

if (
    node.type === 'Identifier' &&
    isReferencedIdentifier(node, parent!, parentStack) &&
    !excludedIds.has(node)
  ) {
    let i = scopeStack.length
    while (i--) {
      if (checkRefId(scopeStack[i], node, parent!, parentStack)) {
        return
      }
    }
  }

существуетifв приговоре, дляexcludedIdsМы уже представили его ранее, иisReferencedIdentifier()черезparenStackчтобы определить, что текущий типIdentifierузел АСТnodeЯвляется ссылкой на узел AST перед этим.

Затем, обратившисьscopeStackследовать цепочке областей действия, чтобы определить, имеет ли областьid.name(имя переменнойcount) свойство и значение свойстваtrue, что означает, что это использованиеrefПеременные, созданные синтаксическим сахаром, наконец будут переданы через операциюs(s.appendLeft), чтобы добавить к переменной.value:

function checkRefId(
    scope: Scope,
    id: Identifier,
    parent: Node,
    parentStack: Node[]
): boolean {
  if (id.name in scope) {
    if (scope[id.name]) {
      // ...
      s.appendLeft(id.end! + offset, '.value')
    }
    return true
  }
  return false
}

Эпилог

понимаяrefРеализация синтаксического сахара, я думаю, у каждого должно быть разное понимание термина синтаксический сахар, Суть которого заключается в выполнении определенных операций преобразования кода путем обхода AST на этапе компиляции. Кроме того, использование некоторых наборов инструментов (Пакетов) в этом процессе реализации также очень умно, например,MagicStringМанипулировать строками исходного кода,estree-walkerОбход узлов AST и обработки, связанной с областью действия, и т. д.

Наконец, если в тексте есть неуместные выражения или ошибки, вы можете поднять вопрос ~

подобно

Прочитав эту статью, в случае сбора урожая вы сможетепоставить лайк, это будет для меня движущей силой, чтобы продолжать делиться, спасибо~

Меня зовут Вулиу. Мне нравится вводить новшества и возиться с исходным кодом. Я сосредоточен на изучении и обмене технологиями, такими как исходный код (Vue 3, Vite), проектирование интерфейса и кросс-энд. Добро пожаловать, чтобы следовать за мнойПубличный аккаунт WeChat:Code center.