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 DOMstyleThrough theCSS var()) is used inline in CSSstyleThe definition ofCustom attribute, the corresponding HTML section:



    The CSS part:

  • throughDynamic update colorVariable to implement inlinestyleAttribute 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

descriptorRefers to the inclusion obtained after parsing the SFC
script,
style,
templateAttribute, each containing information about each Block in the SFC, for example
<style>The properties of the
scopedAnd 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 ofuseCssVars()Method, which is mainly listeningwatchEffectDynamically 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 instanceinstance, the VNode Tree for the subsequent operation of the component instance, i.einstance.subTree
  • definesetVars()Method, which callssetVarsOnVNode()Methods, andinstance.subTreeAnd receivedgetter()Methods the incoming
  • inonMounted()Life cyclewatchEffectIs called every time a component is mountedsetVars()methods
  • inonUpdated()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!