Preface
let count = ref(1)
function add() {
count.value++
}
Therefore, some users will prefer to use reactive
only, so that they do not have to deal with the refs
of .value
using 061414caba7e18. ref
syntactic sugar is to allow us to directly obtain and change the variable itself when ref
.value
to obtain and change the corresponding value. Simply put, standing use level , we can say goodbye to use refs
when .value
question:
let count = $ref(1)
function add() {
count++
}
So, ref
syntactic sugar be used in the project? How is it achieved? This is the first question I saw about the establishment of this RFC. I believe this is also a question held by many students. So, let us reveal one by one below.
1 The use of Ref syntactic sugar in the project
Since ref
syntactic sugar is currently in the experimental (Experimental) stage, ref
syntactic sugar will not be supported by default in Vue3. So, here we take the use of Vite + Vue3 project development as an example to see how to enable support for ref
syntactic sugar.
When using the Vite + Vue3 project development, the @vitejs/plugin-vue
plug-in is used to implement the code conversion (Transform) and hot update (HMR) .vue
Therefore, we need to vite.config.js
in @vitejs/plugin-vue
the options of the refTransform: true
:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue({
refTransform: true
})]
})
Then, in this way, the @vitejs/plugin-vue
plug-in will judge whether it is necessary to perform a specific code conversion ref
refTransform
Because, here we set true
, obviously it will perform a specific code conversion ref
Then, we can use the ref
.vue
file. Here we look at a simple example:
<template>
<div>{{count}}</div>
<button @click="add">click me</button>
</template>
<script setup>
let count = $ref(1)
function add() {
count++
}
</script>
Correspondingly rendered on the page:
As you can see, we can use the ref
to create responsive variables, instead of thinking about adding .value
when using it. In addition, the ref
syntax sugar also supports other writing $ref
method introduced here. If you are interested, you can go to the RFC to learn about other writing methods.
Then, after understanding the use of ref
syntactic sugar in the project, we can be regarded as answering the first question (how to use it in the project). Next, let's answer the second question, how is it implemented, that is, what processing is done in the source code?
2 Implementation of Ref syntax sugar
First, we through Vue Playground intuitively feel, previously used ref
example of the syntax of sugars <script setup>
block (Block) in the compiled results:
import { ref as _ref } from 'vue'
const __sfc__ = {
setup(__props) {
let count = _ref(1)
function add() {
count.value++
}
}
It can be seen that although we do not need to deal with .value
ref
is still used .value
after being compiled. So, this process is certainly not inevitable to do a lot of compilation related code conversion processing. Because we need to find the declaration statement and variables that $ref
_ref
, and add .value
to the latter.
In the previous section, we also mentioned that the @vitejs/plugin-vue
plug-in will .vue
code of the 061414caba802e file. This process uses the @vue/compiler-sfc
package provided by Vue3, which provides compilation related blocks of <script>
, <template>
, <style>
function.
Well, obviously we need to focus on here is <script>
block function is compiled relevant, which corresponds to @vue/compiler-sfc
in compileScript()
function.
2.1 compileScript() function
compileScript()
function is defined in vue-next
of packages/compiler-sfc/src/compileScript.ts
file, which is mainly responsible for <script>
or <script setup>
compile processing block content, it receives two parameters:
sfc
contains the.vue
content of the code of the 061414caba80cb file, including attributes such asscript
,scriptSetup
,source
options
contains some optional and required attributes. For example, thescopeId
corresponding to the component will be used asoptions.id
, the aforementionedrefTransform
etc.
compileScript()
function definition (pseudo code):
// packages/compiler-sfc/src/compileScript.ts
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions
): SFCScriptBlock {
// ...
return {
...script,
content,
map,
bindings,
scriptAst: scriptAst.body
}
}
For ref
syntactic sugar, the compileScript()
function will first get refTransform
in the option (Option) and assign it to enableRefTransform
:
const enableRefTransform = !!options.refTransform
enableRefTransform
will be used to determine whether to call the conversion function related to the syntactic sugar of ref
So, we also mentioned earlier to use ref
syntactic sugar, you need to give @vite/plugin-vue
plug-in options refTransform
property to true
, it will be passed compileScript()
function options
, is here options.refTransform
.
Then, from sfc
, scriptSetup
, source
, filename
and other attributes will be deconstructed. Among them, the code string source
source file will be used to create a MagicString
instance s
, which is mainly used for the subsequent code conversion to replace and add the source code string , and then call the parse()
function to parse the <script setup>
The content is scriptSetup.content
, which generates the corresponding abstract syntax tree 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
)
The parse()
function internally uses the @babel/parser
method provided by parser
to analyze the code and generate the corresponding AST. For our example above, the generated AST would look like this:
{
body: [ {...}, {...} ],
directives: [],
end: 50,
interpreter: null,
loc: {
start: {...},
end: {...},
filename: undefined,
identifierName: undefined
},
sourceType: 'module',
start: 0,
type: 'Program'
}
Note that the contentbody
,start
,end
is omitted here
Then, according to the previously defined enableRefTransform
and the return value of the shouldTransformRef()
true
or false
), it is judged whether to perform the code conversion of the ref
If the corresponding conversion is required, the transformRefAST()
function will be called to perform the corresponding code conversion operation according to the AST:
if (enableRefTransform && shouldTransformRef(scriptSetup.content)) {
const { rootVars, importedHelpers } = transformRefAST(
scriptSetupAst,
s,
startOffset,
refBindings
)
}
In the previous, we have introduced enableRefTransform
. Here we take a look at the shouldTransformRef()
function, which mainly uses the regular matching code content scriptSetup.content
to determine whether the ref
syntactic sugar is used:
// packages/ref-transform/src/refTransform.ts
const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\(/
export function shouldTransform(src: string): boolean {
return transformCheckRE.test(src)
}
Therefore, when you specify refTransform
as true
, but you do not actually use the ref
syntactic sugar in your code, during the compilation of <script>
or <script setup>
, will not perform and ref
syntactic sugar, which is also Vue3 considers more detailed aspects and avoids the performance overhead caused by unnecessary code conversion operations.
So, for our example (using ref
syntactic sugar), it will hit the above transformRefAST()
function. And transformRefAST()
function is the corresponding packages/ref-transform/src/refTransform.ts
the transformAST()
function.
So, let's take a look at how the transformAST()
function converts the code related ref
according to the AST.
2.2 transformAST() function
In the transformAST()
function, it mainly traverses the AST corresponding to the incoming source code, and then performs a specific conversion on the source code MagicString
instance s
$ref
to _ref
, adding .value
etc.
transformAST()
function definition (pseudo code):
// 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]
}
}
It can be seen that transformAST()
will first call walkScope()
to process the root scope ( root scope
), and then call the walk()
function to process the AST node layer by layer, and the walk()
estree-walker
written by Rich Haris.
Next, let's take a look at what the walkScope()
and walk()
functions do.
walkScope() function
First, let's look at where previously used ref
syntactic sugar declaration let count = $ref(1)
corresponding AST structure:
You can see let
the AST node type type
will be VariableDeclaration
, and the AST nodes corresponding to the rest of the code part will be placed in declarations
. Wherein the variables count
the AST node is used as declarations.id
, and $ref(1)
the AST node is used as declarations.init
.
Then, back walkScope()
function, which depending on the type of node AST type
for specific processing, for our example let
corresponding AST node type
is VariableDeclaration
will hit this kind of logic:
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
)
}
}
}
}
}
Here stmt
is the AST node corresponding to let
stmt.declarations
, where decl.init.callee.name
refers to $ref
, and then calls the isToVarCall()
function and assigns it to toVarCall
.
isToVarCall()
function definition:
// 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
}
In front of us also mention ref
syntactic sugar can support other wording, because we are using the $ref
way, so there will be a hit callee[0] === TO_VAR_SYMBOL && shorthands.includes(callee.slice(1))
logic, that toVarCall
will be assigned $ref
.
Then, it calls processRefDeclaration()
function, which according to the incoming decl.init
location information provided to source code corresponding MagicString
example s
operation, i.e. $ref
rewritten as 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))
)
}
// ...
}
Refers to the location information of the position of the node in the AST source code, typically usingstart
,end
represents, for example, wherelet count = $ref(1)
, thencount
AST node correspondingstart
be. 4,end
be 9.
id
passed in at this time corresponds to count
, it will look like this:
{
type: "Identifier",
start: 4,
end: 9,
name: "count"
}
So, this will hit the logic id.type === 'Identifier'
First of all, we will call registerRefBinding()
function, it is actually called is registerBinding()
, and registerBinding
will current scope currentScope
bind the variables id.name
and set true
, it indicates that this is a use ref
variable syntactic sugar created, This will be used to subsequently determine whether to add .value
to a variable:
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
)
}
}
It can be seen that in registerBinding()
, the AST node will be excludedIds
excludeIds
is a WeekMap
, which will be used to skip the AST node of type Identifier
ref
Then, the s.overwrite()
function will be called to rewrite $ref
_ref
, it will receive 3 parameters, which are the starting position, ending position and the string to be rewritten to. And call
corresponds to $ref(1)
, it will look like this:
{
type: "Identifier",
start: 12,
end: 19,
callee: {...}
arguments: {...},
optional: false
}
And I think we should note that in the calculation of the starting position when rewriting uses offset
, it represents a string operation at this time in the source string offset , for example, the string of characters in the source The beginning of the string, then the offset will be 0
.
And helper()
function will return a string _ref
, and in the process will ref
added to importedHelpers
, which will compileScript()
for generating a corresponding time import
statement:
function helper(msg: string) {
importedHelpers.add(msg)
return `_${msg}`
}
So, here we have completed the $ref
to _ref
, that is, at this time our code will look like this:
let count = _ref(1)
function add() {
count++
}
Then, it walk()
function 061414caba88ae to convert count++
to count.value++
. Next, let's take a look at the walk()
function.
walk() function
Earlier, we mentioned that the walk()
estree-walker written by Rich Haris, which is an AST package (Package) ESTree
walk()
function will be used like this:
import { walk } from 'estree-walker'
walk(ast, {
enter(node, parent, prop, index) {
// ...
},
leave(node, parent, prop, index) {
// ...
}
});
It can be seen that walk()
can be passed in the function options
, among which enter()
will be called every time the AST node is visited, and leave()
is called when leaving the AST node.
So, going back to the example mentioned earlier, the walk()
function mainly does two things:
1. Maintain scopeStack, parentStack and currentScope
scopeStack
used to store the scope chain where the AST node is located at this time. Initially, the top of the stack is the root scope rootScope
; parentStack
used to store the ancestor AST node in the process of traversing the AST node (the AST node at the top of the stack is the current AST node Father AST node); currentScope
points to the current scope, which is equal to the root scope rootScope
initial case:
const scopeStack: Scope[] = [rootScope]
const parentStack: Node[] = []
let currentScope: Scope = rootScope
Therefore, at enter()
, it will be judged whether the AST node type is a function or block at this time. If yes, then pushed into the stack scopeStack
:
parent && parentStack.push(parent)
if (isFunctionType(node)) {
scopeStack.push((currentScope = {}))
// ...
return
}
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
scopeStack.push((currentScope = {}))
// ...
return
}
Then, at leave()
, judge whether the AST node type is a function or block at this time. If yes, then pops scopeStack
, and updates currentScope
to the stack top element scopeStack
parent && parentStack.pop()
if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
isFunctionType(node)
) {
scopeStack.pop()
currentScope = scopeStack[scopeStack.length - 1] || null
}
2. Process AST nodes of Identifier type
Since, in our example ref
create syntactic sugar count
variable AST node type is Identifier
, so it will enter()
hit this logic stages:
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
}
}
}
In if
judgment, for excludedIds
we have already introduced, and isReferencedIdentifier()
is through parenStack
to determine the current type Identifier
AST node node
whether it is a reference to a certain AST node before that.
Then, by accessing scopeStack
to follow the scope chain to determine whether there is a id.name
(variable name count
) attribute in a scope and the attribute value is true
, which means it is a variable created with the syntax sugar of ref
.value
to the variable by operating s
( s.appendLeft
):
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
}
Concluding remarks
By understanding ref
syntactic sugar, I think everyone should have a different understanding of the term syntactic sugar. Its essence is to operate specific code conversion operations by traversing the AST during the compilation phase. In addition, the use of some packages of this implementation process is also very clever, such as MagicString
manipulate source code strings, estree-walker
traverse AST nodes and scope related processing.
Finally, if there are improper or wrong expressions in the text, you are welcome to mention Issue~
like
After reading this article, if you have any gains, you can , this will become my motivation to continue to share, thank you~
I’m Wuliu, I like to innovate and tinker with the source code, focusing on the source code (Vue3, Vite), front-end engineering, cross-end technology learning and sharing. In addition, all my articles will be included in https://github.com/WJCHumble/Blog , welcome to Watch Or Star!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。