preface

In Vue Conf 21 on May 22, uVU introduced the SFC Style CSS Variable Injection proposal, which is called < Style > dynamic Variable Injection, during the compilation phase optimization of single-file components (SFC). In a nutshell, it lets you use variables defined in

Sounds like CSS In JS? Indeed, it is very similar to CSS In JS In terms of usage. However, it is well known that CSS In JS has some performance problems In some scenarios, while

1 what is<style>Dynamic variable injection

  • There is no need to explicitly declare that a property is injected as a CSS variablev-bind()Inference)
  • Reactive variable
  • Different performance in Scoped/ non-scoped mode
  • Does not contaminate child components
  • The use of normal CSS variables is not affected

Let’s look at a simple example of dynamic variable injection 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>
Copy the code

Corresponding render to the page:

From the code snippet above, it’s easy to see that the background color of the text changes when we click the Click Me button:

This is where

So, what happens to this process? How do you do that? It’s good to have questions, so let’s take a step behind the scenes look at how this works.

2 <style>Principle of dynamic variable injection

At the beginning of this article, we explained that

Note: The following single-file components are expressed by the abbreviation SFC

So, let’s focus on SFC’s handling of

2.1 SFC Compilation pair<style>Handling of dynamic variable injection

SFC’s handling of

  • Bind lines in the corresponding DOMstyleThrough theCSS var()Use inline in CSSstyleThe definition ofCustom attributes, corresponding HTML section:The CSS part:
  • throughDynamic update colorVariable to implement inlinestyleProperty value changes, which in turn changes the style of the HTML element that uses the CSS custom property

So, obviously, to complete this whole process, unlike SFC compilation without

1. The SFC compilation<style>Related processing

Everyone knows that the

Here, we look at the compiler handling for dynamic variable injection

// packages/compiler-sfc/sfc/compileStyle.ts
export function doCompileStyle(
  options: SFCAsyncStyleCompileOptions
) :SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  const{... id, ... } = options ...const plugins = (postcssPlugins || []).slice()
  plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
  ...
}
Copy the code

As you can see, cssVarsPlugin is added before compiling

CssVarsPlugin uses the Declaration method provided by the postCSS plugin to access the values of all CSS attributes declared in the

CssVarsPlugin plugin definition:

// packages/compiler-sfc/sfc/cssVars.ts
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)}) `; }); }}}; };Copy the code

Here the CSS var() variable name — (later) is generated by the genVarName() method, which generates different values depending on whether isProd is true or false:

// packages/compiler-sfc/sfc/cssVars.ts
function genVarName(id: string, raw: string, isProd: boolean) :string {
  if (isProd) {
    return hash(id + raw);
  } else {
    return `${id}-${raw.replace(/([^\w-])/g."_")}`; }}Copy the code

2. The SFC compilation<script>Related processing

If, from the point of view of

The parse method in Packages/Compiler-sfc /parse.ts calls parseCssVars() on descriptor objects parsed from SFC to get the V-bind used in

Descriptor refers to objects containing script, style, and template attributes after parsing the SFC. Each attribute contains information about each Block in the SFC, such as

Part of the corresponding parse() method (pseudocode) :

// packages/compiler-sfc/parse.ts
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);
  }
  / /...
}
Copy the code

As you can see, the result (array) returned by the parseCssVars() method is assigned to Description.cssVars. Then, in the build script, according to descriptor. CssVars. Length determine whether injection < style > dynamic variables into the relevant code.

Compilingscript is done by the compileScript method in package/compile-sfc/ SRC/compilescript. ts. Here’s how it handles dynamic variable injection

// package/compile-sfc/src/compileScript.ts
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
      );
    }
  }
  / /...
}
Copy the code

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`
  );
}
Copy the code

GenNormalScriptCssVarsCode () method is mainly to do these three things:

  • The introduction ofuseCssVars()Method, its main is to monitorwatchEffectDynamically 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 this case, the combined API is used (corresponding here__setup__), overridden if it exists__default__.setup(props, ctx) => { __injectCSSVars__(); return __setup__(props, ctx) }

3 Learn from the SFC compilation result<style>Dynamic variable injection implementation details

Here, we directly view the output code of the above example after SFC compilation through Vue’s official SFC Playground:

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__;
Copy the code

You can see the result of the SFC compilation, which outputs the single-file object __sfc__, the render function, the

_useCssVars((_ctx) = > ({
  "f13b4d11-color": _unref(color),
}));
Copy the code

Here we call the _useCssVars() method, which in the source code is useCssVars(), and pass 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)
}
Copy the code

UseCssVars do four main things:

  • Gets the current component instanceinstanceVNode Tree for subsequent operations on component instances, i.einstance.subTree
  • definesetVars()Method, it callssetVarsOnVNode()Method, and willinstance.subTree, receivedgetter()Methods the incoming
  • inonMounted()Added in the life cyclewatchEffectIs called each time a component is mountedsetVars()methods
  • inonUpdated()Added in the life cyclesetVars()Method, called every time a component is updatedsetVars()methods

OnMounted () and onUpdated() call setVars(), which is essentially setVarsOnVNode().

// packages/runtime-dom/src/helpers/useCssVars.ts
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.childrenas VNode[]).forEach(c= > setVarsOnVNode(c, vars))
  }
}
Copy the code

For our example, the type of subtree is Fragment because it was originally passed in as instance.subtree. So, the setVarsOnVNode() method hits the logic of vnode.type === Fragment and recursively calls the setVarsOnVNode() method through vnode.children.

__FEATURE_SUSPENSE__ and vnode.componentare not discussed here, but those who are interested can find out for themselves

In the subsequent setVarsOnVNode() method, if the logic of vnode.shapeFlags.element && vnode.el is met, The style.setProperty() method is called to add inline style to the DOM (vnode.el) corresponding to each VNode, where key is the value of CSS var() when

This completes the entire linkage from variable changes in

conclusion

Also, we planned to leave a section on how to write a Vite plugin, vite-plugin-vue2-CSS-vars, so that Vue 2.x can also support

Finally, if there is any improper expression or mistake in the article, please make an Issue

give a like

If you get something from this post, please give me a “like”. This will be my motivation to keep sharing. Thank you

My name is Wu Liu. I like innovation and source Code manipulation, and I focus on source Code (Vue 3, Vite), front-end engineering, cross-end and other technology learning and sharing. Welcome to follow my wechat official account: Code Center.