preface
In Vue Conf 21 on May 22, we introduced the SFC Style CSS Variable Injection (SFC) proposal, which is a < Style > dynamic Variable Injection. Simply put, it lets you use variables defined in
Sounds a lot like CSS In JS, right? Indeed, from a usage point of view it is very similar to CSS In JS. However, it is well known that CSS In JS has some performance problems In some scenarios, while
How does
1 what is<style>
Dynamic variable injection
- You don’t need to explicitly declare that a property is injected as a CSS variable.
- Responsive variables
- It has different performance in Scoped/ Non-Scoped mode
- No contamination of child components
- The use of normal CSS variables will not be affected
Let’s look at a simple example using
<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 render to the page:
From the code snippet above, it’s easy to see that when we click on the Click Me button, the background color of the text changes:
This is what
So what happens to this process? How does that work? It’s good to be in doubt, but let’s take a step by step to unravel how it works.
2 <style>
The principle of dynamic variable injection
At the beginning of the article, we explained that the implementation of
So, let’s focus on the SFC’s handling of
2.1 SFC compilation pair<style>
Dynamic variable injection handling
SFC in the compilation process of
- Bind inline on the corresponding DOM
style
Through theCSS var()
) is used inline in CSSstyle
The definition ofCustom attribute, the corresponding HTML section:
The CSS part: - throughDynamic update
color
Variable to implement inlinestyle
Attribute value, which in turn changes the style of the HTML element that uses the CSS custom attribute
Obviously, then, to finish the whole process, different from without the < style > dynamic variables into the SFC before compilation, here need to < style >, < script > increase corresponding special treatment. Next, we will explain at 2 points:
1. The SFC compilation<style>
Related processing
It is well known that the compilation of the
Here we have a look at the compilation handling for
export function doCompileStyle(
options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
const {
...
id,
...
} = options
...
const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
...
}
As you can see, the CSSVarSplugin plugin is added before compiling
CssVarsPlugin is used postcss plug-in provides Declaration method, to access the < style > declared in all CSS attribute’s value, each access through regular to match v – bind instruction content, Then replace() with var(–xxxx-xx), which would look like this in the example above:
The definition of the 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 variable name of CSS var() is generated by the genvarName () method, which generates a different value depending on whether isProd is true or false:
function genVarName(id: string, raw: string, isProd: boolean): string {
if (isProd) {
return hash(id + raw)
} else {
return `${id}-${raw.replace(/([^\w-])/g, '_')}`
}
}
2. The SFC compilation<script>
Related processing
If you only stand in the perspective of
The parse method in packages/compiler-sfc/parse.ts calls parsecsVars () to fetch
descriptor
Refers to the inclusion obtained after parsing the SFC
script
,
style
,
template
Attribute, each containing information about each Block in the SFC, for example
<style>
The properties of the
scoped
And content, etc.
Partial code in the corresponding parse() method (pseudocode) :
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 parsecsVars () is assigned to descriptor.cssVars. Then, in the build script, according to descriptor. CssVars. Length determine whether injection < style > dynamic variables into the relevant code.
Used in the project
<style>
Dynamic variable injection, we’ll see messages on the end that tell us this feature is still experimental and so on.
The compilation script is done by the compileScript method in package/compile-sfc/ SRC /compileScript.ts. Here’s a look at its handling of
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 ) } } //... }
Front for our example (using the < style > dynamic variable injection), apparently cssVars. The length is there, so there will be calls genNormalScriptCssVarsCode () method to generate the corresponding code.
The definition of 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 is mainly to do these three things:
- The introduction of
useCssVars()
Method, which is mainly listeningwatchEffect
Dynamically injected variables, and then updated the corresponding CSSVars()
The value of the - define
__injectCSSVars__
Method, which is mainly calledgenCssVarsCode()
Method to generate<style>
Dynamic style related code - Compatible with the
<script setup>
In case of combination API use (corresponding here__setup__
), and override it if it exists__default__.setup
为(props, ctx) => { __injectCSSVars__(); return __setup__(props, ctx) }
So, here we have roughly analyzed the SFC compilation of
3 Compile results from SFC, recognize<style>
Dynamic variable injection implementation details
Here, let’s take a look at the output code of the above example compiled by SFC directly through the official SFC Playground of Vue:
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 results of the SFC compilation, output single-file object __sfc__, render function,
_useCssVars(_ctx => ({
"f13b4d11-color": (_unref(color))
}))
This calls the _useCssVars() method, which in the source code refers to the useCssVars() method, and passes in a function that returns an object {“f13b4d11-color”: (_unref(color))}. So, let’s look at the usecssVars () method.
3.1 useCssVars () method
UseCssVars () method is defined in the 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 does these four things:
- Gets the current component instance
instance
, the VNode Tree for the subsequent operation of the component instance, i.einstance.subTree
- define
setVars()
Method, which callssetVarsOnVNode()
Methods, andinstance.subTree
And receivedgetter()
Methods the incoming - in
onMounted()
Life cyclewatchEffect
Is called every time a component is mountedsetVars()
methods - in
onUpdated()
Life cyclesetVars()
Method that is called every time a component is updatedsetVars()
methods
The onMounted() or onUpdated() lifecycles.setVars () is called by the onMounted() or onUpdated() lifecycles.setVarsonVNode () is called by the onMounted() method.
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 previous chestnut, since it was passed in instance.subtree, its type is Fragment. So, in the setvarsonVNode () method, the logic of vnode.type === Fragment will be hit, vnode.children will be traversed, and setvarsonVNode () will be recursively called.
Here is wrong
FEATURE_SUSPENSEAnd vnode.ponent situation to carry out analysis, interested students can understand by themselves
In the subsequent execution of the setvarsonVNode () method, if the logic of vnode.shapeFlag & shapeFlags.element && vnode.el is satisfied, Then call the style.setProperty() method to add an inline style to the DOM (vnode.el) corresponding to each VNode, where the key is the value of the CSS var() when
This completes the whole linkage from the variable change in
conclusion
Also, I intended to have a section on how to write a Vite plugin vite-plugin-vue2-css-vars so that Vue 2.x can also support
Last but not least, if there is something wrong or wrong in the text, please mention it
give a like
If you get anything from reading this article, you can like it. It will be my motivation to keep sharing. Thank you
I am Wu Liu, like innovation, tinkering with source code, focus on source code (VUE 3, VET), front-end engineering, cross-end technology learning and sharing. In addition, all my articles will be included in
https://github.com/WJCHumble/Blog, welcome Watch Or Star!