这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
背景
最近对unocss非常感兴趣,于是进去翻了翻源码,仔细却又初略的阅读了一下。由于本人才学尚浅,未能解读到其中代码深刻含义,但其中源码的书写结构非常优雅,不仅感叹到:偶像就是偶像。
水了这么多文字,让我们来看看其中的代码。
项目地址: github.com/antfu/unocs…
阅读unocss源码的好处:
- 可以明确的知道书写的规范和
api,因为unocss中,多部分使用正则匹配的,知道正确的匹配规则可以减少我们在项目中的试错 - 可以更深一步的理解
unocss的原理。了解其本质,也是对自己的能力和开发也是一种有价值的帮助。
目录结构
我们可以看到其中core为主要的核心包,vite是作为 Vite Plugin使用的,另外的scope因为还没有写,盲猜可能是关于css scopecss 作用域相关的问题。可能与SFC中的scope有点类似。另外的就是关于unocss的预设,这可能是我们开发用的最多的地方了。
preset-uno
我主要是学习了一下preset-uno这个包。
分析一下包的目录结构,主要是
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的工作机制。
我们先看一下其中的一些工具方法的定义
它们的输入值都是一个字符串,然后进行相应的规则处理,返回处理结果。
比如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循环体. 如果没有则会交给下一个处理器进行处理, 这就是真正的链式调用的原理.
总结原理
如何做到分离开的函数进行统一的链式调用
原理: 把分离开的函数进行统一绑定, 绑定到一个处理器中心, 然后通过循环, 去循环的处理这个结果, 将函数结果可以转接到下一个处理器进行执行.
大概就是这么个意思, 才疏学浅的我不知道这是什么设计模式, 但是我已经明白了其中的原理. 欢迎大家给我指点指点.
具体源码地址: 源文件地址