浅谈一种你或许知道, 但我绝对不知道的设计模式

659 阅读4分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

背景

最近对unocss非常感兴趣,于是进去翻了翻源码,仔细却又初略的阅读了一下。由于本人才学尚浅,未能解读到其中代码深刻含义,但其中源码的书写结构非常优雅,不仅感叹到:偶像就是偶像。

水了这么多文字,让我们来看看其中的代码。

项目地址: github.com/antfu/unocs…

阅读unocss源码的好处:

  • 可以明确的知道书写的规范和api,因为unocss中,多部分使用正则匹配的,知道正确的匹配规则可以减少我们在项目中的试错
  • 可以更深一步的理解unocss的原理。了解其本质,也是对自己的能力和开发也是一种有价值的帮助。

目录结构

image.png

我们可以看到其中core为主要的核心包,vite是作为 Vite Plugin使用的,另外的scope因为还没有写,盲猜可能是关于css scopecss 作用域相关的问题。可能与SFC中的scope有点类似。另外的就是关于unocss的预设,这可能是我们开发用的最多的地方了。

preset-uno

我主要是学习了一下preset-uno这个包。

image.png

分析一下包的目录结构,主要是

  • rules:主要是对各种css的规则校验,通过什么的正则匹配什么样式的css,返回什么样的样式。 例如: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' }],
]
  • theme:这是存放一些基础主体样式的配置。如color,font和基础的视口宽度断点的前缀
  • variants:这是关于一些伪类/伪元素,断点,亮暗模式,权重的一些处理
  • utils:项目中用到的一些工具类方法,主要有对正则匹配到的值进行解析处理等

handlers

本文主要解析handlers的工作机制。

我们先看一下其中的一些工具方法的定义

image.png

它们的输入值都是一个字符串,然后进行相应的规则处理,返回处理结果。 比如bracket函数,就是一个匹配到[ ]处理函数,返回括号中间的值。

<div z-[10]></div>(这是一个z-index = 10的例子),这样的一个字符串就会进如这个函数进行解析,返回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) })],
]

当时看到这段代码,我震惊了,为了多个相互隔离的功能函数,可以同时链式调用呢?

于是我们来看看antfu是如何处理的吧

源码处理

我们需要定位到shorthand.ts文件

import * as handlers from './handlers'

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

首先导入了开始准备的所有的功能函数, 对他的key值进行划分.

然后我们定义了一个handler对象, 他是一个函数. 我们先来看看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 Handler

Hanlder本质是一个函数, 他也拥有自己的一个__opions, 里面是个sequence, 其实就是刚才的处理工具函数的key的一个集合, 稍后会解释道.

我们来看一下这个 handlerName的工具函数是如何绑定到handler上面的.

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

原理主要是通过循环handlersNames, 然后对handler添加属性描述符. 这里注意到有一个addProcessor.

function addProcessor(that: Handler, name: HandlerName) {
  if (!that.__options) { // 非空判定, 没有就创建一个
    that.__options = {
      sequence: [],
    }
  }
  that.__options.sequence.push(name)
  return that
}

主要是对我们的handle添加一些即将要处理的Processor(处理器). 将我们Processor存放到sequence里面.

例如: handler.bracket.fraction.rem(str), 这里我们收集到处理器 便有bracket fraction rem, 它们将依次的存放入__options.sequence里面为 ['bracket', 'fraction', 'rem']

那处理器何时使用呢?

我们可以看到handle是一个函数, 所以在调用它是, 它便会进行调度.

const s = this.__options?.sequence || []
this.__options.sequence = []

我们定位到这一段代码, 每次__options.sequence进行处理时,他都会进行一个清空的操作. 目的时为了不影响到下一轮的处理调度.

例如: handler.bracket.fraction.rem(str) 并不会影响到 handler.number.percent.px(str)的处理. 因为每一次的处理, 都会将sequence进行清空.

为什么会有链式调用呢?

可能这篇文章废话了这么多, 文章的内核却是一个普通for of循环. 可却是这个for of循环才是精髓所在.

const s = this.__options?.sequence || []

for (const n of s) {
const res = handlers[n](str)
if (res)
  return res
}

我们看到每一次的循环都是去调用我们的处理器函数, 还记得处理器函数是啥吗? 他就是上面讲到得 导入 得handlersNames的函数. 通过调用处理器函数返回我们真正的处理结果.

当我们的处理结果有值时(也存在undefined的情况, 因为可能解析器并没有匹配到需要解析的内容), 就会返回结果, 并会跳出for of循环体. 如果没有则会交给下一个处理器进行处理, 这就是真正的链式调用的原理.

总结原理

如何做到分离开的函数进行统一的链式调用

原理: 把分离开的函数进行统一绑定, 绑定到一个处理器中心, 然后通过循环, 去循环的处理这个结果, 将函数结果可以转接到下一个处理器进行执行.

大概就是这么个意思, 才疏学浅的我不知道这是什么设计模式, 但是我已经明白了其中的原理. 欢迎大家给我指点指点.

具体源码地址: 源文件地址