предисловие
Недавно 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.declarations
,вdecl.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()
функция может быть передана вoptions
,вenter()
будет вызываться каждый раз при посещении узла 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.