Preface
SFC Style CSS Variable Injection , that is, <style>
dynamic variable injection when introducing the optimization of Single File Component (SFC) in the compilation stage. Simply put, it allows you to use the variables defined <style>
in v-bind
<script>
It sounds like CSS In JS? Indeed, from the perspective of use, it is very similar to CSS In JS. However, everyone knows that CSS In JS has certain performance problems in some scenarios, while <style>
dynamic variable injection does not have similar problems.
So, <style>
dynamic variable injection realized? I think this is a question that many students will have, so today let us thoroughly understand <style>
dynamic variable injection is and what is done behind its implementation.
1 What is <style>
dynamic variable injection
<style>
dynamic variable injection, according to the big summary on 16141979f41ad7 SFC , it mainly has the following 5 capabilities:
- There is no need to explicitly declare that a property is injected as a CSS variable (will be based on)
- Responsive variables
- Different performance in Scoped/Non-scoped mode
- Will not pollute sub-components
- The use of ordinary CSS variables will not be affected
Below, let's take a look at a simple example of dynamic variable injection <style>
<template>
<p class="word">{{ msg }}</p>
<button @click="changeColor">
click me
</button>
</template>
<script setup>
import { ref } from "vue"
const msg = 'Hello World!'
let color = ref("red")
const changeColor = () => {
if (color.value === 'black') {
color.value = "red"
} else {
color.value = "black"
}
}
</script>
<style scoped>
.word {
background: v-bind(color)
}
</style>
The corresponding rendering on the page:
From the above code snippet, it is easy to know that when we click the click me
button, the background color of the text will change:
And this is <style>
dynamic variable injection gives us the ability to let us very easily by <script>
to manipulate the variables <template>
HTML elements dynamically change the style of .
So, what happened in this process? How did it happen? It is a good thing to have questions, and then let us step by step to uncover the realization principle behind the scenes.
2 The principle of <style>
At the beginning of the article, we talked about the <style>
dynamic variable injection is derived from the optimization of the single file (SFC) in the compilation stage. However, does not explain all compiled by SFC here. Students who don't know can read the article I wrote before [From the compilation process to understand the Vue3 static node promotion process]().
So, let us focus on the SFC <style>
during the compilation process. First of all, there are two key points in this process.
2.1 SFC compile <style>
dynamic variable injection
<style>
dynamic variable injection of 06141979f41d23 during the compilation process, which is mainly based on the 2 key points . Here, we take the above example as an example analysis:
- Bound to the corresponding DOM inline
style
, byCSS var()
) provided with a row in the CSSstyle
defined above custom properties , corresponding HTML part:
CSS part: - Through dynamically update
color
variable to realizestyle
attribute value in line, and then change the style of HTML element that uses the CSS custom attribute
So, apparently to complete the entire process, unlike in the absence of <style>
SFC compiled before the injection of dynamic variables, where the need for <style>
, <script>
corresponding increase in special treatment . Below, we will explain in 2 points:
SFC compilation <style>
related processing
Everyone knows that the <style>
part of Vue SFC is compiled mainly by postcss
. This corresponds to the doCompileStyle()
method packages/compiler-sfc/sfc/compileStyle.ts
in the Vue source code.
Here, let's take a look at its <style>
dynamic variable injection, and the corresponding code (pseudo code):
export function doCompileStyle(
options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
const {
...
id,
...
} = options
...
const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
...
}
It can be seen in the use of postcss
compile <style>
will be added before cssVarsPlugin
plug-ins, and to cssVarsPlugin
incoming shortId
(ie scopedId
replace data-v
results within) and isProd
(whether in a production environment).
cssVarsPlugin
is used postcss
plug provided Declaration
method , to access <style>
values of all CSS properties declared, each visit to match a regular v-bind
content of the command, and then use replace()
the property value replacement method is var(--xxxx-xx)
, Behaved like this in the above example:
cssVarsPlugin
plug-in:
const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g
const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
const { id, isProd } = opts!
return {
postcssPlugin: 'vue-sfc-vars',
Declaration(decl) {
// rewrite CSS variables
if (cssVarRE.test(decl.value)) {
decl.value = decl.value.replace(cssVarRE, (_, $1, $2, $3) => {
return `var(--${genVarName(id, $1 || $2 || $3, isProd)})`
})
}
}
}
}
Here the CSS var()
variable names, i.e. --
(after the contents) by genVarName()
generation method, it will be based isProd
is true
or false
generate different values:
function genVarName(id: string, raw: string, isProd: boolean): string {
if (isProd) {
return hash(id + raw)
} else {
return `${id}-${raw.replace(/([^\w-])/g, '_')}`
}
}
2.SFC compilation <script>
related processing
If you just stand <script>
, it is obvious that 16141979f462b7 cannot perceive current SFC of <style>
uses 06141979f462b1 dynamic variable injection. Therefore, it is necessary to start from the SFC to identify whether the <style>
dynamic variable injection is currently used.
In packages/compiler-sfc/parse.ts
in parse
method will resolve SFC obtained descriptor
object calls parseCssVars()
method to get <style>
used to v-bind
all variables.
descriptor
refers parsed SFC obtained comprisingscript
,style
,template
object attributes, each attribute comprising an SFC in each block (Block) information, e.g.<style>
propertiesscoped
and content.
Part of the code (pseudo code) in the corresponding parse()
function parse(
source: string,
{
sourceMap = true,
filename = 'anonymous.vue',
sourceRoot = '',
pad = false,
compiler = CompilerDOM
}: SFCParseOptions = {}
): SFCParseResult {
//...
descriptor.cssVars = parseCssVars(descriptor)
if (descriptor.cssVars.length) {
warnExperimental(`v-bind() CSS variable injection`, 231)
}
//...
}
As you can see, the result (array) returned by the parseCssVars()
descriptor.cssVars
. Then, when compiling script
, judge whether to inject <style>
dynamic variable into related code according to descriptor.cssVars.length
<style>
dynamic variable injection is used in the project, and you will see a prompt in the terminal to inform us that this feature is still in the experiment.
The compiler script
by package/compile-sfc/src/compileScript.ts
in compileScript
completion method, where we look at it for <style>
handle dynamic variable injection:
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions
): SFCScriptBlock {
//...
const cssVars = sfc.cssVars
//...
const needRewrite = cssVars.length || hasInheritAttrsFlag
let content = script.content
if (needRewrite) {
//...
if (cssVars.length) {
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
scopeId,
!!options.isProd
)
}
}
//...
}
For the example we gave earlier (using <style>
dynamic variable injection), obviously cssVars.length
exists, so here we will call the genNormalScriptCssVarsCode()
method to generate the corresponding code.
genNormalScriptCssVarsCode()
:
// package/compile-sfc/src/cssVars.ts
const CSS_VARS_HELPER = `useCssVars`
function genNormalScriptCssVarsCode(
cssVars: string[],
bindings: BindingMetadata,
id: string,
isProd: boolean
): string {
return (
`\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
`const __injectCSSVars__ = () => {\n${genCssVarsCode(
cssVars,
bindings,
id,
isProd
)}}\n` +
`const __setup__ = __default__.setup\n` +
`__default__.setup = __setup__\n` +
` ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
` : __injectCSSVars__\n`
)
}
genNormalScriptCssVarsCode()
method mainly does these 3 things:
- Introduce the
useCssVars()
method, which is mainly to monitor the variables injected dynamically bywatchEffect
, and then update the value of theVars()
- Define the
__injectCSSVars__
method, which mainly calls thegenCssVarsCode()
method to generate the code related<style>
- Compatible with the combined API usage in the case of
<script setup>
__setup__
here), if it exists, rewrite__default__.setup
to(props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
So, at this point, we have roughly analyzed <style>
dynamic variable injection by SFC compilation. Some of the logic has not been explained too much (to avoid falling into the situation of a doll), and interested students can understand it by themselves. Next, let's take a look at the previous example and see what will be the result of SFC compilation?
3 From the results of SFC compilation, understand the implementation details of <style>
Here, we directly use Vue's official SFC Playground to view the code output of the above example after SFC compiled
import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from "vue"
const _withId = /*#__PURE__*/_withScopeId("data-v-f13b4d11")
import { ref } from "vue"
const __sfc__ = {
expose: [],
setup(__props) {
_useCssVars(_ctx => ({
"f13b4d11-color": (_unref(color))
}))
const msg = 'Hello World!'
let color = ref("red")
const changeColor = () => {
if (color.value === 'black') {
color.value = "red"
} else {
color.value = "black"
}
}
return (_ctx, _cache) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("p", { class: "word" }, _toDisplayString(msg)),
_createVNode("button", { onClick: changeColor }, " click me ")
], 64 /* STABLE_FRAGMENT */))
}
}
}
__sfc__.__scopeId = "data-v-f13b4d11"
__sfc__.__file = "App.vue"
export default __sfc__
You can see the result of SFC compilation, and output the __sfc__
, render
function, <style>
dynamic variable injection. So putting aside the first two, we directly look at <style>
dynamic variable injection:
_useCssVars(_ctx => ({
"f13b4d11-color": (_unref(color))
}))
Here called _useCssVars()
method in which the source of the middle finger is useCssVars()
method, and then introduced to a function that returns an object { "f13b4d11-color": (_unref(color)) }
. So, let's take a look at the useCssVars()
method.
3.1 useCssVars() method
useCssVars()
method is defined in runtime-dom/src/helpers/useCssVars.ts
:
// runtime-dom/src/helpers/useCssVars.ts
function useCssVars(getter: (ctx: any) => Record<string, string>) {
if (!__BROWSER__ && !__TEST__) return
const instance = getCurrentInstance()
if (!instance) {
__DEV__ &&
warn(`useCssVars is called without current active component instance.`)
return
}
const setVars = () =>
setVarsOnVNode(instance.subTree, getter(instance.proxy!))
onMounted(() => watchEffect(setVars, { flush: 'post' }))
onUpdated(setVars)
}
useCssVars
mainly did these 4 things:
- Get the current component instance
instance
, which is used for the subsequent operation of the VNode Tree of the component instance, that is,instance.subTree
- Define the
setVars()
method, it will call thesetVarsOnVNode()
method, and theinstance.subTree
and the receivedgetter()
method will be passed in onMounted()
life cycle ofwatchEffect
, the methodsetVars()
will be called every time a component is mounted- In
onUpdated()
add life cyclesetVars()
method, when each component updates will callsetVars()
method
As you can see, whether it is onMounted()
or onUpdated()
life cycle, they will call the setVars()
method, which is essentially the setVarsOnVNode()
method. Let’s take a look at its definition first:
function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
if (suspense.pendingBranch && !suspense.isHydrating) {
suspense.effects.push(() => {
setVarsOnVNode(suspense.activeBranch!, vars)
})
}
}
while (vnode.component) {
vnode = vnode.component.subTree
}
if (vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el) {
const style = vnode.el.style
for (const key in vars) {
style.setProperty(`--${key}`, vars[key])
}
} else if (vnode.type === Fragment) {
;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars))
}
}
For our chestnut, since the initial incoming is instance.subtree
, its type
is Fragment
. So, in setVarsOnVNode()
method will hit vnode.type === Fragment
logic will traverse vnode.children
, then continue to the recursive call setVarsOnVNode()
.
FEATURE_SUSPENSE and vnode.component are not analyzed here. Those who are interested can find out by themselves
In the subsequent setVarsOnVNode()
method, if the logic of vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el
style.setProperty()
method will be called to add the VNode
to the DOM ( vnode.el
) style
, where key
is the value of the previous processing <style>
when the CSS var()
value
Corresponds to the value of the variable defined in <script>
In this way, the entire <script>
from the variable change in <style>
to the style change in 06141979f46ac7 is completed. Here we use the legend to briefly review this process:
Concluding remarks
If you simply summarize the <style>
dynamic variable injection, you may be able to express it in a few sentences. But how does it do it at the source code level? This is worthy of in-depth understanding, through which we can understand how to write the postcss
plug-in, vars()
and other technical points.
And, originally planned to leave a section to introduce how to write a Vite plug-in vite-plugin-vue2-css-vars so that Vue 2.x can also support <style>
dynamic variable injection. However, considering that the length of the article is too long, it may cause reading obstacles for everyone. So, this will be introduced in the next article, but the plug-in has already been posted to NPM, and interested students can also learn about it by themselves.
Finally, if there are improper or wrong expressions in the text, you are welcome to mention Issue~
like
By reading this article, if you have any gains, you can , this will be 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 (Vue 3, 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) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。