This is the 7th day of my participation in the November Gwen Challenge. Check out the details: The last Gwen Challenge 2021

background

Recently, I was very interested in UNOCSS, so I went in and turned over the source code, carefully but initially read it. Because my talent is still shallow, failed to interpret the code profound meaning, but the source code writing structure is very elegant, not only sigh: idol is idol.

With all this text, let’s take a look at the code.

Project address: github.com/antfu/unocs…

Benefits of reading unoCSS source code:

  • Can clearly know the specification of writing andapiBecause unoCSS uses regular matching for multiple parts, knowing the correct matching rules can reduce trial and error in the project
  • You can take it a step furtherunocssThe principle of. Understanding its essence is also a valuable aid to one’s ability and development.

The directory structure

We can see that core is the main core package, vite is used as a Vite Plugin, and the other scope is not written yet. Blind guess may be related to CSS scopecss scope. It may be somewhat similar to scope in SFC. The other thing is the default for UNOCSS, which is probably the one we use the most.

preset-uno

I mainly learned preset-uno.

Analyze the package directory structure, mainly

  • rules: is mainly for a variety of CSS rule verification, through what the re match what style CSS, what style to return.

For example: flex. Ts

// flex.ts
export const flex: Rule[] = [
  ['flex-col', { 'flex-direction': 'column' }],
  ['flex-col-reverse', { 'flex-direction': 'column-reverse' }],
  ['flex-row', { 'flex-direction': 'row' }],
  ['flex-row-reverse', { 'flex-direction': 'row-reverse' }],
  ['flex-wrap', { 'flex-wrap': 'wrap' }],
  ['flex-wrap-reverse', { 'flex-wrap': 'wrap-reverse' }],
  ['flex-nowrap', { 'flex-wrap': 'nowrap' }],
  ['flex-1', { flex: '1 1 0%' }],
  ['flex-auto', { flex: '1 1 auto' }],
  ['flex-initial', { flex: '0 1 auto' }],
  ['flex-none', { flex: 'none' }],
  [/^flex-\[(.+)\]$/, ([, d]) => ({ flex: d })],
  ['flex-grow', { 'flex-grow': 1 }],
  ['flex-grow-0', { 'flex-grow': 0 }],
  ['flex-shrink', { 'flex-shrink': 1 }],
  ['flex-shrink-0', { 'flex-shrink': 0 }],
  ['flex', { display: 'flex' }],
  ['inline-flex', { display: 'inline-flex' }],
]
Copy the code
  • themeThis is the configuration that stores some basic body styles. Such ascolor.fontAnd the prefix of the base viewport width breakpoint
  • variants: This is some handling of pseudo-classes/pseudo-elements, breakpoints, light and dark modes, weights
  • utils: some tool class methods used in the project, mainly to analyze the value of the re match

handlers

This paper mainly analyzes the working mechanism of handlers.

Let’s first look at the definitions of some of these utility methods

The input value is a string, and the corresponding rule processing is performed, and the processing result is returned. The BRACKET function, for example, is a bracket that matches the [] handler and returns the value between the brackets.

(this is an example of z-index = 10), such a string is parsed into the function and returns 10


// size.ts
export const sizes: Rule[] = [
  ['w-full', { width: '100%' }],
  ['h-full', { height: '100%' }],
  ['w-screen', { width: '100vw' }],
  ['h-screen', { height: '100vh' }],
  [/^w-([^-]+)$/, ([, s]) => ({ width: h.bracket.fraction.rem(s) })],
  [/^h-([^-]+)$/, ([, s]) => ({ height: h.bracket.fraction.rem(s) })],
  [/^max-w-([^-]+)$/, ([, s]) => ({ 'max-width': h.bracket.fraction.rem(s) })],
  [/^max-h-([^-]+)$/, ([, s]) => ({ 'max-height': h.bracket.fraction.rem(s) })],
]
Copy the code

At that time, I was shocked to see this code, for multiple isolated function functions, can be called chain at the same time?

So let’s see how ANTFU works, okay

The source code to deal with

We need to locate the sabot.ts file

import * as handlers from './handlers'

export const handlersNames = Object.keys(handlers) as HandlerName[]
Copy the code

First, import all the function functions prepared at the beginning, and divide its key value.

Then we define a handler object, which is a function. Let’s first look at the structure type of handle

export type Handler = {[K in HandlerName]: Handler} & { (str: string): string | undefined __options: { sequence: HandlerName[] } } const handler = function( this: Handler, str: string, ): string | number | undefined { const s = this.__options? .sequence || [] this.__options.sequence = [] for (const n of s) { const res = handlers[n](str) if (res) return res } return undefined } as unknown as HandlerCopy the code

Hanlder is essentially a function that has its own __opions containing a sequence, which is a set of keys from the processing tool function, as explained later.

Let’s take a look at how the handlerName utility function is bound to handler.

handlersNames.forEach((i) => {
  Object.defineProperty(handler, i, {
    enumerable: true,
    get() {
      return addProcessor(this, i)
    },
  })
})
Copy the code

The idea is to loop through handlersNames and then add a property descriptor to the handler. Notice there’s an addProcessor.

function addProcessor(that: Handler, name: HandlerName) { if (! __options = {sequence: [],}} that.__options.sequence. Push (name) return that}Copy the code

The main thing is to add some processors to our handle that we want to process. Store our Processor in the sequence.

Such as: Handler. Bracket. Fraction. Rem (STR), here we collect to the processor Have bracket fraction rem, they will in turn in __options. Sequence for [‘ bracket, ‘fraction’, ‘rem’]

So when is the processor used?

We can see that Handle is a function, so when it is called, it will be scheduled.

const s = this.__options? .sequence || [] this.__options.sequence = []Copy the code

When we get to this code, every time __options.sequence is processed, it does a cleanup operation. In order not to affect the next round of processing scheduling.

For example: handler. Bracket. Fraction. Rem (STR) will not affect to the handler. Number. The percent. The processing of px (STR). Because each time, the sequence will be cleared.

Why do we have chain calls?

Perhaps the core of this article is a common for of loop. But it’s the “for of” loop that’s the essence.

const s = this.__options? .sequence || [] for (const n of s) { const res = handlers[n](str) if (res) return res }Copy the code

We see that every time we loop we’re going to call our handler function, remember what the handler function is? This is the function that imports handlersNames as mentioned above. Return the result of our actual processing by calling the handler function.

When our result has a value (and undefined, because the parser might not have matched what needs to be parsed), we return the result and break out of the for of body. If it doesn’t, it gets passed on to the next handler, which is how true chain calls work.

Summarizing the principle

How do separate functions make unified chain calls

Principle: Unify the separated function binding, binding to a processor center, and then through the loop, to loop the result, the function result can be transferred to the next processor for execution.

I don’t know what the design pattern is, but I already understand how it works. Everyone is welcome to give me advice.

Specific source address: source file address