2024年9月

论文提出了第一个端到端的半监督伪装目标检测模型
CamoTeacher
。为了解决半监督伪装目标检测中伪标签中存在的大量噪声问题,包括局部噪声和全局噪声,引入了一种名为双旋转一致性学习(
DRCL
)的新方法,包括像素级一致性学习(
PCL
)和实例级一致性学习(
ICL
)。
DRCL
帮助模型缓解噪音问题,有效利用伪标签信息,使模型在避免确认偏差的同时可以获得充分监督。广泛的实验验证了
CamoTeacher
的优越性能,同时显著降低了标注要求。

来源:晓飞的算法工程笔记 公众号

论文: CamoTeacher: Dual-Rotation Consistency Learning for Semi-Supervised Camouflaged Object Detection

Introduction


伪装物体检测(
COD
)旨在识别在其环境中完全融入的物体,包括动物或具有保护色彩并具有融入周围环境能力的人造实体,这一任务由于低对比度、相似纹理和模糊边界而变得复杂。与一般物体检测不同,
COD
受到这些因素的挑战,使得检测变得格外困难。现有的
COD
方法严重依赖于大规模的像素级注释数据集,其创建需要大量的人力和成本,从而限制了
COD
的进展。

为了缓解这一问题,半监督学习作为一种有希望的方法出现,利用标记和未标记数据。然而,由于复杂的背景和微妙的物体边界,其在
COD
中的应用并不直接。半监督学习在
COD
中的有效性受到伪标签中存在的大量噪声的严重影响,伪标签的噪声有两种主要类型:像素级噪声,表明在单个伪标签内的变化,以及实例级噪声,显示不同伪标签之间的变化。这种区分是至关重要的,因为它指导了如何改进伪标签质量以提高模型训练的方法。(
1
)像素级噪声的特点是在伪标签的各个部分内部的标注不一致。如图
1a
中所示,在第一行中,壁虎的尾部在视觉上比头部更难以识别。由
SINet
生成的伪标签在其尾部区域中的准确性较低(由红色框标出)。这一观察结果强调了对伪标签内的所有部分统一处理的不当性。(
2
)实例级噪声指的是不同伪标签之间噪声水平的变化。如图
1a
所示,第三行的伪标签与第二行相比不太准确,因为第三行中的伪装对象更难以检测。这些差异表明每个伪标签对模型训练的贡献是不同的,强调了需要对整合伪标签信息采取细致差异的方法。

为了解决在没有未标记
GT
的数据的情况下评估伪标签噪声的挑战,论文提出了基于两个旋转视图的像素级不一致性和实例级一致性的两种新策略。具体来说,对于像素级噪声,论文观察到通过比较两个旋转视图的伪标签计算出的像素级不一致性,可以反映相对于
GT
的实际误差,如图
2a
所示。这种关系显示了不同部分之间平均像素级不一致性与平均绝对误差(
MAE
)之间的正相关性,如图
2b
的折线所示。因此,具有较高像素级不一致性的区域更容易出现不准确性,表明在训练过程中需要减弱这些区域的重要性。

对于实例级噪声,跨旋转视图具有更大相似性的伪标签展示了更低的噪声水平,如图
3a
所示。伪标签和
GT
计算的
SSIM
之间的实例级一致性与正相关性进一步支持了这一观察结果,如图
3b
所示。因此,表现出更高实例级一致性的伪标签可能具有更高质量,并应在学习过程中优先考虑。

通过这些观察结果,论文提出了一种名为
CamoTeacher
的半监督伪装物体检测框架,该框架结合了一种名为
Dual-Rotation Consistency Learning

DRCL
)的新方法。具体而言,
DRCL
通过两个核心组件来实现其策略:像素级一致性学习(
PCL
)和实例级一致性学习(
ICL
)。
PCL
通过考虑不同旋转视图之间的像素级不一致性,创新地为伪标签中的不同部分分配可变权重。同时,
ICL
根据实例级一致性调整各个伪标签的重要性,实现细致、噪声感知的训练过程。

论文采用
SINet
作为基础模型来实现
CamoTeacher
,并将其应用于更经典的伪装物体检测(
COD
)模型,即基于
CNN

SINet-v2

SegMaR
,以及基于
Transforme

DTINet

FSPNet
。在四个
COD
基准数据集(即
CAMO

CHAMELEON

COD10K

NC4K
)上进行了大量实验,结果显示
CamoTeacher
不仅在与半监督学习方法相比方面达到了最先进的水平,而且与已建立的全监督学习方法相媲美。具体来说,如图
1b
所示,仅使用了
20%
的标记数据,它几乎达到了在
COD10K
上全监督模型的性能水平。

论文的贡献可以总结如下:

  1. 引入了第一个端到端的半监督伪装物体检测框架
    CamoTeacher
    ,为未来半监督伪装物体检测的研究提供了一个简单而有效的基准。

  2. 为解决半监督伪装物体检测中伪标签中大量噪声的问题,提出了
    Dual-Rotation Consistency Learning

    DRCL
    ),其中包括
    Pixel-wise Consistency Learning

    PCL
    )和
    Instance-wise Consistency Learning

    ICL
    ),允许自适应调整不同质量伪标签的贡献,从而有效利用伪标签信息。


  3. COD
    基准数据集上进行了大量实验,相较于完全监督设置,取得了显著的改进。

Methodology


Task Formulation

半监督伪装物体检测旨在利用有限的标记数据训练一个能够识别与周围环境无缝融合的物体的检测器。由于物体与背景之间的对比度较低,这个任务本身具有挑战性。给定一个用于训练的伪装物体检测数据集
\(D\)
,含
\(M\)
个标记样本的标记子集表示为
\(D_L=\{x_i^{l}, y_i\}_{i=1}^{M}\)
,含
\(N\)
个未标记样本的未标记子集表示为
\(D_U=\{x_i^{u}\}_{i=1}^{N}\)
,其中
\(x_i^{l}\)

\(x_i^{u}\)
表示输入图像,
\(y_i\)
表示标记数据的相应注释掩码。通常,
\(D_L\)
只占整个数据集
\(D\)
的很小一部分,这突出了
\(M \ll N\)
的半监督学习场景。对于
\(M \ll N\)
的强调,强调了半监督学习中的挑战和机遇:通过利用未标记数据
\(D_U\)
尚未发掘的潜力来提升检测能力,而这远远超过了标记子集
\(D_L\)

Overall Framework

如图
4
所示,采用
Mean Teacher
作为初步方案,以实现端到端的半监督伪装物体检测框架。该框架包含两个具有相同结构的
COD
模型,即教师模型和学生模型,分别由参数
\(\Theta_t\)

\(\Theta_s\)
参数化。教师模型生成伪标签,然后用于优化学生模型。整体损失函数
\(L\)
可以定义为:

\[\begin{equation}
L = L_s + \lambda_u L_u ,
\end{equation}
\]

其中,
\(L_s\)

\(L_u\)
分别表示有监督损失和无监督损失,
\(\lambda_u\)
是平衡损失项的无监督损失权重。按照经典的
COD
方法,使用二元交叉熵损失
\(L_{bce}\)
用于训练。

在训练过程中,采用弱数据增强
\(\mathcal{A}^w(\cdot)\)
和强数据增强
\(\mathcal{A}^s(\cdot)\)
策略的组合。弱数据增强应用于有标记数据以减轻过拟合,而无标记数据在强数据增强下经历各种数据扰动,以创造同一图像的不同视角。有监督损失
\(L_s\)
的定义如下:

\[\begin{equation}
L_s = \frac{1}{M} \sum\limits^{M}_{i=1} L_{bce}(F(\mathcal{A}^w(x_i^l);\Theta_s), y_i) ,
\end{equation}
\]

其中,
\(F(\mathcal{A}(x_i);\Theta)\)
表示模型
\(\Theta\)
对第
\(i\)
张图像在增强
\(\mathcal{A}(\cdot)\)
下的检测结果。对于无标记的图像,首先应用弱数据增强
\(\mathcal{A}^w(\cdot)\)
,然后将其传递给教师模型。这一初始步骤对于在不显著改变图像核心特征的变化下生成可靠的伪标签
\(\widehat{y_i}\)
至关重要。这些伪标签作为学生模型的一种软监督形式。接下来,相同的图像经过强数据增强
\(\mathcal{A}^s(\cdot)\)
后传递给学生模型。这个过程引入了更高层次的变异性和复杂性,模拟更具挑战性的条件,以适应学生模型。学生模型基于这些经过强增强的图像生成预测
\(p_i\)
,利用伪标签
\(\widehat{y_i}\)
作为无标记数据学习的指导。可以将其形式化为:

\[\begin{equation}
\widehat{y_i} = F(\mathcal{A}^w(x_i^u);\Theta_t), \ p_i = F(\mathcal{A}^s (\mathcal{A}^w(x_i^u));\Theta_s) .
\end{equation}
\]

因此,无监督损失
\(L_u\)
可以表示为:

\[\begin{equation}
L_u = \frac{1}{N} \sum\limits^{N}_{i=1} L_{bce}(p_i, \widehat{y_i}).
\end{equation}
\]

最后,学生模型通过总损失
\(L\)
进行密集训练,该损失包含了半监督框架中有监督和无监督学习的两个方面。这种方法确保学生模型从有标记和伪标记数据中受益,提高其检测能力。同时,教师模型通过指数移动平均(
EMA
)机制进行系统更新,有效地提取学生知识并防止噪音干扰,具体表述为:

\[\begin{equation}
\Theta_t \leftarrow \eta \Theta_t + (1 - \eta)\Theta_s ,
\end{equation}
\]

其中,
\(\eta\)
是一个超参数,表示保留的比例。

Dual-Rotation Consistency Learning

由于物体的伪装性质,伪标签中包含大量噪音,直接使用它们来优化学生模型可能会损害模型的性能。为解决这个问题,最直观的一个可能方法是设置一个固定的高阈值来过滤高质量的伪标签,但这会导致召回率较低,并使得难以充分利用伪标签的监督信息。为此,论文提出了双旋转一致性学习(
DRCL
),以动态调整伪标签的权重,减少噪音的影响。

对图像
\(x_i\)
进行两个独立的随机旋转,其中
\(x_i\)
在之前已进行了翻转和随机调整大小,得到两个不同的旋转视图
\(x_i^{r_1}\)

\(x_i^{r_2}\)

\[\begin{equation}
x_i^{r_1} = R(\mathcal{A}^w(x_i), \theta_1), \ x_i^{r_2} = R(\mathcal{A}^w(x_i), \theta_2),
\end{equation}
\]

其中,
\(x_i^{r} = R(x_i, \theta)\)
表示将输入图像
\(x_i\)
旋转
\(\theta\)
度。将获得的旋转视图输入到教师模型中,得到相应的预测值,即
\(\widehat y_i^{r} = F(x_i^{r}; \Theta_t)\)
。随后,对预测值进行
\(-\theta\)
的相反旋转,使其返回到原始的水平方向,得到
\(\widehat y_i^{h_1}\)

\(\widehat y_i^{h_2}\)
,以便在不同的旋转视图下计算预测不一致性。

\[\begin{equation}
\widehat y_i^{h_1} = R(\widehat y_i^{r_1}, -\theta_1), \ \widehat y_i^{h_2} = R(\widehat y_i^{r_2}, -\theta_2).
\end{equation}
\]

请注意,旋转会引入黑色的边界区域,这些区域不参与
DRCL
的计算过程。

由于伪标签的不同区域和不同伪标签之间的噪声水平不同,引入
PCL

ICL
动态调整不同像素在伪标签内部和各个伪标签之间的贡献。

  • Pixel-wise Consistency Learning

在像素级别上对水平预测
\(\widehat y_i^{h_1}\)

\(\widehat y_i^{h_2}\)
进行减法运算,得到像素级别的不一致性
\(\Delta_i\)

\[\begin{equation}
\Delta_i = | \widehat y_i^{h_1} - \widehat y_i^{h_2} |.
\end{equation}
\]

不同视图之间的像素级不一致性
\(\Delta_i\)
反映了伪标签的可靠性。然而,在两个旋转视图的预测值都接近
0.5
的情况下,
\(\Delta_i\)
无法有效区分它们。这些预测表现出高度的不确定性,意味着不能明确将它们分类为前景或背景,并且很可能代表嘈杂的标签。因此,有必要通过降低它们的权重来减弱它们的影响。因此,计算水平预测值
\(\widehat y_i^{h}\)
的平均值,

\[\begin{equation}
\widehat y_i^{h} = avg ( \widehat y_i^{h_1} , \widehat y_i^{h_2} ),
\end{equation}
\]

其中,
\(avg(\cdot, \cdot)\)
表示计算两个像素级别输入的平均值,并使用其与
0.5

L2
距离作为调整权重的一个组成部分。

因此,根据不同旋转视图之间的像素级别不一致性,推导出像素级别一致性权重
\(\omega_i^{pc}\)
,如下所示:

\[\begin{equation}
\omega_i^{pc} = (1 - \Delta_i^{\alpha})||\widehat y_i^{h} - \mu||_2^2 ,\label{wlc}
\end{equation}
\]

其中,
\(\alpha\)
是一个超参数,
\(\mu=0.5\)
。这个动态的像素级一致性权重
\(\omega_i^{pc}\)
会给与不同旋转视图间预测一致的区域分配更高的权重,而对于预测不一致的区域则分配较小的权重。

总而言之,将
PCL
损失函数
\(L_u^{PC}\)
表述为:

\[\begin{equation}
\label{unsup_loss}
\begin{split}
L_u^{PC} &= \frac{1}{N} \sum\limits^{N}_{i=1} \omega_{i}^{pc} L_{bce}(p_{i}, \widehat {y}_{i}^{r_1}) \\
&= - \frac{1}{NHW} \sum\limits^{N}_{i=1} \sum\limits^{H \times W}_{j=1} \omega_{i, j}^{pc} [\widehat {y}_{i, j}^{r_1}\log p_{i, j} \\
& \quad \quad \quad \quad \quad \quad + (1 - \widehat {y}_{i, j}^{r_1})\log (1-p_{i, j})] ,
\end{split}
\end{equation}
\]

自适应地调整每个像素的权重,以确保对学生模型进行全面监督,同时避免带来偏见。

  • Instance-wise Consistency Learning

不同图像之间的伪装程度会有所不同,导致伪标签质量在图像之间存在显著变化。平等地对待所有伪标签是不合理的。不幸的是,对于未标记的图像,评估伪标签质量是具有挑战性的,因为没有可用的
GT
标签。论文呢观察到两个旋转视图的实例一致性和伪标签质量之间存在正相关,由
SSIM
量化。基于此,引入
ICL
来调整具有不同质量的伪标签的贡献。将实例级一致性权重
\(\omega_i^{ic}\)
表示如下:

\[\begin{equation}
\omega_i^{ic} = (SSIM( \widehat y_i^{h_1} , \widehat y_i^{h_2} ))^{\beta},
\end{equation}
\]

其中,
\(\beta\)
是一个超参数,用于调整实例级一致性和伪标签质量之间的分布关系。

使用交并比(
IoU
)损失作为实例级限制,因此,
ICL
损失可以表示为:

\[\begin{equation}
\begin{split}
L_{u}^{IC} &= \frac{1}{N} \sum\limits^{N}_{i=1} \omega_i^{ic} L_{iou}( p_i , \widehat y_i^{r_1} ) \\
&= \frac{1}{NHW} \sum\limits^{N}_{i=1} \sum\limits^{H \times W}_{j=1} \omega_i^{ic} \Bigg ( 1 - \frac{ p_{i, j} \widehat {y}_{i,j}^{r_1} }{ p_{i,j} + \widehat {y}_{i, j}^{r_1} - p_{i,j} \widehat y_{i, j}^{r_1} } \Bigg ).
\end{split}
\end{equation}
\]

因此,最终的总损失
\(L\)
由三个部分组成:有监督损失
\(L_s\)

PCL
损失
\(L_u^{LC}\)

ICL
损失
\(L_u^{GC}\)
,可以表示为:

\[\begin{equation}
L = L_s + \lambda_u^{pc} L_u^{PC} + \lambda_{u}^{ic} L_u^{IC},
\end{equation}
\]

其中,
\(\lambda_u^{pc}\)

\(\lambda_{u}^{ic}\)
是超参数。

Experiment


Experiment Settings

  • Dataset

在四个基准数据集
CAMO

CHAMELEON

COD10K

NC4K
上评估了
CamoTeacher
模型。在
CAMO
数据集中,共有
2500
张图像,包括
1250
张伪装图像和
1250
张非伪装图像。
CHAMELEON
数据集包含
76
张手动注释图像。
COD10K
数据集由
5066
张伪装图像、
3000
张背景图像和
1934
张非伪装图像组成。
NC4K
是另一个包含
4121
张图像的大规模
COD
测试数据集。根据先前的工作中的数据划分,使用
COD10K

3040
张图像和
CAMO

1000
张图像作为实验的训练集。剩余的图像来自这两个数据集,被用作测试集。在训练过程中,采用了半监督分割的数据划分方法。我们从训练集中随机采样了
1%

5%

10%

20%

30%
的图像作为有标签的数据,剩余的部分作为无标签的数据。

  • Evaluation Metrics

参考先前的工作,在
COD
中使用了
6
个常见的评估指标来评估我们的
CamoTeacher
模型,包括
S-measure
(
\(S_{\alpha}\)
)、加权
F-measure
(
\(F_{\beta}^w\)
)、平均
E-measure
(
\(E_{\phi}^m\)
)、最大
E-measure
(
\(E_{\phi}^x\)
)、平均
F-measure
(
\(F_{\beta}^m\)
)和平均绝对误差(
\(M\)
)。

  • Implementation Details

提出的
CamoTeacher
模型使用
PyTorch
进行实现。采用
SINet
作为
COD
模型的基线。使用带有动量
0.9

SGD
优化器和多项式学习率衰减,初始学习率为
0.01
,来训练学生模型。训练周期设置为
40
个周期,其中前
10
个周期为
burn-in
阶段。批量大小为
20
,有标签数据和无标签数据的比例为
1
:
1
,即每个批次包含
10
个有标签和
10
个无标签的图像。在训练和推断过程中,每个图像被调整为
\(352 \times 352\)
的大小。通过
EMA
方法更新教师模型,动量
\(\eta\)

0.996
。弱数据增强包括随机翻转和随机缩放,而强数据增强涉及颜色空间转换,包括
Identity

Autocontrast

Equalize

Gaussian blur

Contrast

Sharpness

Color

Brightness

Hue

Posterize

Solarize
,从这个列表中随机选择最多
3
个。

Results



如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

前言

Vue3.5正式版
在这两天发布了,网上已经有了不少关于Vue3.5版本的解读文章。但是欧阳发现这些文章对3.5中新增的功能介绍都
不是很全
,所以导致不少同学有个
错觉
,觉得Vue3.5版本不过如此,选择跳过这个版本等下个大版本再去更新。所以欧阳写了这篇
超级详细
的Vue3.5版本解读文章,小伙伴们可以看看在3.5版本中有没有增加一些你期待的功能。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

版本号

这次的版本号是
天元突破红莲螺岩
,这是07年出的一个二次元动漫,欧阳是没看过的。在此之前我一直以为这次的版本号会叫
黑神话:悟空
,可能悟空不够二次元吧。

响应式

响应式相关的内容主要分为:重构响应式、响应式props支持解构、新增
onEffectCleanup
函数、新增
base watch
函数、新增
onWatcherCleanup
函数、新增
pause

resume
方法。

重构响应式

这次响应式的重构是属于Vue内部优化,对于普通开发者来说是无感的。重构后内存占用减少了56%,优化手段主要是通过
版本计数

双向链表数据结构
,灵感来源于
Preact signals
。后续欧阳会出一系列关于响应式相关的源码文章,大家可以关注一波欧阳。

响应式props支持解构

在3.5中响应式props支持解构终于正式稳定了,在没有这个功能之前我们想要在js中访问prop必须要这样写:
props.name
,否则
name
将会丢失响应式。

有了响应式props解构后,在js中我们就可以直接解构出
name
来使用,比如下面这样的代码:

<script setup lang="ts">
const { name } = defineProps({
  name: String,
});

console.log(name);
</script>


defineProps
搭配解构一起使用后,在编译时就可以将
name
处理成
props.name
。编译后简化的代码如下:

setup(__props) {
  console.log(__props.name);
  const __returned__ = {};
  return __returned__;
}

从上面的代码可以看到
console.log(name)
经过编译后变成了
console.log(__props.name)
,这样处理后
name
当然就不会丢失响应式了。

新增onEffectCleanup函数

在组件卸载之前或者下一次
watchEffect
回调执行之前会自动调用
onEffectCleanup
函数,有了这个函数后你就不需要在组件的
beforeUnmount
钩子函数去统一清理一些timer了。比如下面这个场景:

import { watchEffect, ref } from "vue";
import { onEffectCleanup } from "@vue/reactivity";

const flag = ref(true);
watchEffect(() => {
  if (flag.value) {
    const timer = setInterval(() => {
      // 做一些事情
      console.log("do something");
    }, 200);
    onEffectCleanup(() => {
      clearInterval(timer);
    });
  }
});

上面这个例子在
watchEffect
中会去注册一个循环调用的定时器,如果不使用
onEffectCleanup
,那么我们就需要在
beforeUnmount
钩子函数中去清理定时器。

但是有了
onEffectCleanup
后,将
clearInterval
放在他的回调中就可以了。当组件卸载时会自动执行
onEffectCleanup
传入的回调函数,也就是会执行
clearInterval
清除定时器。

还有一点值得注意的是
onEffectCleanup
函数目前没有在
vue
包中暴露出来,如果你想使用可以像我这样从
@vue/reactivity
包中导入
onEffectCleanup
函数。

新增base watch函数

我们之前使用的
watch
函数是和Vue组件以及生命周期一起实现的,他们是深度绑定的,所以
watch
函数代码的位置在vue源码中的
runtime-core
模块中。

但是有的场景中我们只想使用vue的响应式功能,也就是vue源码中的
reactivity
模块,比如小程序
vuemini
。为此我们不得不将
runtime-core
模块也导入到项目中,或者像
vuemini
一样去手写一个watch函数。

在3.5版本中重构了一个
base watch
函数,这个函数的实现和vue组件没有一毛钱关系,所以他是在
reactivity
模块中。详情可以查看我之前的文章:
Vue3.5新增的baseWatch让watch函数和Vue组件彻底分手

还有一点就是这个
base watch
函数对于普通开发者来说没有什么影响,但是对于一些下游项目,比如
vuemini
来说是和受益的。

新增onWatcherCleanup函数

和前面的
onEffectCleanup
函数类似,在组件卸载之前或者下一次
watch
回调执行之前会自动调用
onWatcherCleanup
函数,同样有了这个函数后你就不需要在组件的
beforeUnmount
钩子函数去统一清理一些timer了。比如下面这个场景:

import { watch, ref, onWatcherCleanup } from "vue";

watch(flag, () => {
  const timer = setInterval(() => {
    // 做一些事情
    console.log("do something");
  }, 200);
  onWatcherCleanup(() => {
    console.log("清理定时器");
    clearInterval(timer);
  });
});


onEffectCleanup
函数不同的是我们可以从vue中import导入
onWatcherCleanup
函数。

新增pause和resume方法

有的场景中我们可能想在“一段时间中暂停一下”,不去执行
watch
或者
watchEffect
中的回调。等业务条件满足后再去恢复执行
watch
或者
watchEffect
中的回调。在这种场景中
pause

resume
方法就能派上用场啦。

下面这个是
watchEffect
的例子,代码如下:

<template>
  <button @click="count++">count++</button>
  <button @click="runner2.pause()">暂停</button>
  <button @click="runner2.resume()">恢复</button>
</template>

<script setup lang="ts">
import { watchEffect } from "vue";

const count = ref(0);
const runner = watchEffect(() => {
  if (count.value > 0) {
    console.log(count.value);
  }
});
</script>

在上面的demo中,点击
count++
按钮后理论上每次都会执行一次
watchEffect
的回调。

但是当我们点击了暂停按钮后就会执行
pause
方法进行暂停,在暂停期间
watchEffect
的回调就不会执行了。

当我们再次点击了恢复按钮后就会执行
resume
方法进行恢复,此时
watchEffect
的回调就会重新执行。

console.log
的结果如下图:
console

从上图中可以看到
count
打印到4后就没接着打印了,因为我们执行了
pause
方法暂停了。当重新执行了
resume
方法恢复后可以看到
count
又重新开始打印了,此时从8开始打印了。

不光
watchEffect
可以执行
pause

resume
方法,
watch
一样也可以执行
pause

resume
方法。代码如下:

const runner = watch(count, () => {
  if (count.value > 0) {
    console.log(count.value);
  }
});

runner.pause()	// 暂停方法
runner.resume()	// 恢复方法

watch的deep选项支持传入数字

在以前
deep
选项的值要么是
false
,要么是
true
,表明是否深度监听一个对象。在3.5中
deep
选项支持传入数字了,表明监控对象的深度。

比如下面的这个demo:

const obj1 = ref({
  a: {
    b: 1,
    c: {
      d: 2,
      e: {
        f: 3,
      },
    },
  },
});

watch(
  obj1,
  () => {
    console.log("监听到obj1变化");
  },
  {
    deep: 3,
  }
);

function changeDeep3Obj() {
  obj1.value.a.c.d = 20;
}

function changeDeep4Obj() {
  obj1.value.a.c.e.f = 30;
}

在上面的例子
watch

deep
选项值是3,表明监听到对象的第3层。

changeDeep3Obj
函数中就是修改对象的第3层的
d
属性,所以能够触发
watch
的回调。


changeDeep4Obj
函数是修改对象的第4层的
f
属性,所以不能触发
watch
的回调。

SSR服务端渲染

服务端渲染SSR主要有这几个部分:新增
useId
函数、Lazy Hydration  懒加载水合、
data-allow-mismatch

新增
useId
函数

有时我们需要生成一个随机数塞到DOM元素上,比如下面这个场景:

<template>
  <label :htmlFor="id">Do you like Vue3.5?</label>
  <input type="checkbox" name="vue3.5" :id="id" />
</template>

<script setup lang="ts">
const id = Math.random();
</script>

在这个场景中我们需要生成一个随机数
id
,在普通的客户端渲染中这个代码是没问题的。

但是如果这个代码是在SSR服务端渲染中那么就会报警告了,如下图:
useId

上面报错的意思是服务端和客户端生成的
id
不一样,因为服务端和客户端都执行了一次
Math.random()
生成
id
。由于
Math.random()
每次执行的结果都不同,自然服务端和客户端生成的
id
也不同。

useId
函数的作用就是为了解决这个问题。

当然
useId
也可以用于客户端渲染的一些场景,比如在列表中我们需要一个唯一键,但是服务端又没有给我们,这时我们就可以使用
useId
给列表中的每一项生成一个唯一键。

Lazy Hydration  懒加载水合

异步组件现在可以通过 defineAsyncComponent() API 的 hydrate 选项来控制何时进行水合。(欧阳觉得这个普通开发者用不上,所以就不细讲了)

data-allow-mismatch

SSR中有的时候确实在服务端和客户端生成的html不一致,比如在DOM上面渲染当前时间,代码如下:

<template>
  <div>当前时间是:{{ new Date() }}</div>
</template>

这种情况是避免不了会出现前面
useId
例子中的那种警告,此时我们可以使用
data-allow-mismatch
属性来干掉警告,代码如下:

<template>
  <div data-allow-mismatch>当前时间是:{{ new Date() }}</div>
</template>

Custom Element 自定义元素改进

这个欧阳也觉得平时大家都用不上,所以就不细讲了。

Teleport组件新增defer延迟属性

Teleport
组件的作用是将children中的内容传送到指定的位置去,比如下面的代码:

<div id="target"></div>
<Teleport to="#target">被传送的内容</Teleport>

文案
被传送的内容
最终会渲染在
id="target"
的div元素中。

在之前有个限制,就是不能将
<div id="target">
放在
Teleport
组件的后面。

这个也很容易理解DOM是从上向下开始渲染的,如果先渲染到
Teleport
组件。然后就会去找id的值为
target
的元素,如果找不到当然就不能成功的将
Teleport
组件的子节点传送到
target
的位置。

在3.5中为了解决这个问题,在
Teleport
组件上新增了一个
defer
延迟属性。

加了
defer
延迟属性后就能将
target
写在
Teleport
组件的后面,代码如下:

<Teleport defer to="#target">被传送的内容</Teleport>
<div id="target"></div>

defer
延迟属性的实现也很简单,就是等这一轮渲染周期结束后再去渲染
Teleport
组件。所以就算是
target
写在
Teleport
组件的后面,等到渲染
Teleport
组件的时候
target
也已经渲染到页面上了。

useTemplateRef
函数

vue3中想要访问DOM和子组件可以使用ref进行模版引用,但是这个ref有一些让人迷惑的地方。

比如定义的ref变量到底是一个响应式数据还是DOM元素?

还有template中ref属性的值明明是一个字符串,比如
ref="inputEl"
,怎么就和script中同名的
inputEl
变量绑到一块了呢?

3.5中的
useTemplateRef
函数就可以完美的解决了这些问题。

这是3.5之前使用ref访问input输入框的例子:

<input type="text" ref="inputEl" />

const inputEl = ref<HTMLInputElement>();

这个写法很不符合编程直觉,不知道有多少同学和欧阳一样最开始用vue3时会给
ref
属性绑定一个响应式变量。比如这样:
:ref="inputEl"

更加要命的是这样写还不会报错,就是
inputEl
中的值一直是
undefined

最后一番排查后才发现
ref
属性应该是绑定的变量名称:
ref="inputEl"

使用
useTemplateRef
函数后就好多了,代码如下:

<input type="text" ref="inputRef" />

const inputEl = useTemplateRef<HTMLInputElement>("inputRef");

使用
useTemplateRef
函数后会返回一个ref变量,
useTemplateRef
函数传的参数是字符串
"inputRef"

在template中
ref
属性的值也是字符串
"inputRef"
,所以
useTemplateRef
函数的返回值就指向了DOM元素input输入框。这个比3.5之前的体验要好很多了,详情可以查看我之前的文章:
牛逼!Vue3.5的useTemplateRef让ref操作DOM更加丝滑

总结

对于开发者来说Vue3.5版本中还是新增了许多有趣的功能的,比如:
onEffectCleanup
函数、
onWatcherCleanup
函数、
pause

resume
方法、
watch

deep
选项支持传入数字、
useId
函数、
Teleport
组件新增
defer
延迟属性、
useTemplateRef
函数。

这些功能在一些特殊场景中还是很有用的,欧阳的个人看法还是得将Vue升到3.5。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

信仰是人们内心深处的信念,是推动人类前进的驱动力。AI从几十年前的缓慢探索,到如今的飞速发展,是什么信仰在驱动这一切呢?

摩尔定律

聊起信仰,我就会想起信息时代的摩尔定律。
摩尔定律
是由英特尔联合创始人戈登·摩尔在1965年提出的一个
观察性定律

摩尔定律指出,在大规模集成电路中,可容纳的晶体管数量大约每两年会增加一倍,而计算能力也会随之大幅提升。这一定律贯穿于整个芯片的发展历程。

如果说是真理、公理、定理,那毫无疑问具有很大的说服力。

但是,摩尔定律是一个基于观察和经验的预测规律,为什么全世界都相信它呢?它为什么能引起英特尔、AMD、高通等巨头纷纷投入大量资金、人才去发展芯片呢?

我认为这可能就是
信仰的力量
吧。这些芯片界的领袖,因为相信所以看见,才愿意重仓投入,才会持续推动芯片行业的发展。

一旦基于这样的信仰产生了持续的发展,那就会演变成一种默认的
确定性
。确定性就会持续吸引更多的资金和人才,会产生持续的变革。

规模效应

在回到AI的发展上看,AI的飞速发展源于神经网络的发展,神经网络借鉴了人脑的神经元机制。

人脑的是由百万亿的神经元连接组成的庞大网络,每个神经元都接受输入,然后经过简单计算,最后输出给下一个神经元。但是简单的神经元,被放大到了万亿级别的规模,则会产生更多的智能。

这一点,我们从生物的演化史也能看出来。

早起的地球只有单细胞,然后演化出了多细胞、低等生物、高等生物,再到人类。这个过程中,生物的神经系统也是越来越发达,智能程度也越来越高。

这就是规模效应。

AI时代的信仰

现在流行的大语言模型,是基于神经网络训练而来。早期的神经网络规模不够大,所以还没产生智能。

随着神经网络的参数被放大到一个阈值,模型会产生质的飞越,突然变得聪明,拥有智能,这就是量变引起质量。

这一切的发现都要归功于OpenAI,随后OpenAI还提出了类似摩尔定律这种信仰级别的理念,即规模越大越好。重点是3个:

  • 神经网络的模型参数越多越好
  • 训练数据集的越大越好
  • 算力越强越好

OpenAI自己也是吹起了冲锋号,OpenAI的CEO山姆·奥特曼正在寻求巨额投资,准备建立更多的AI基础设施和超大算力平台。

这就是AI时代的信仰吧,这种信仰推动这一个个AI巨头持续重仓在算力领域、大模型领域。

这一信仰将来应该也会与摩尔定律一样,持续推动科技的高速发展。

总结

每个时代和行业,总得有一些领袖存在信仰,正是这种信仰推动着一个又一个高科技的发展!

AI时代的信仰就是规模效应,无限去追求 模型参数、训练数据集、算力。

本篇完结!欢迎 关注、加V(yclxiao)交流、全网可搜(程序员半支烟)

原文链接:
https://mp.weixin.qq.com/s/AEn0VNXigqwCupKZZHfhzg

12天圣诞节程序怎样运行?
1988 年,一个令人印象深刻且令人敬畏的 C 代码,代号为 xmas.c,在国际混淆 C 代码竞赛中获胜。
该程序甚至比其输出的“压缩”类型还要小,代表了文本压缩标准的全新方式。评委们认为,这个程序像是随意敲击键盘所得到的。
但该程序神奇地打印出12天圣诞节的歌词,仅仅几句话的C代码!

下面来一步步搞清楚程序怎样运行的。

1 作者源代码,一些乱字符的感觉,好几个main~

#include <stdio.h>main(t,_,a)char *a;
{
return!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\ ;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \ q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw'i;# \
){nl]
!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw'iK{;[{nl]'/w#q#n'wk nw'\
iwk{KK{nl]
!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl
'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}
'+}##(!!/") :t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:
0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);
}

2 编译,运行,看看结果,嗯,小看这几个乱乱的代码了,这是12天圣诞节(The Twelve Days Of Christmas)的歌词,好神奇^@^

On the first day of Christmas my true love gave to me
a partridge in a pear tree.

On the second day of Christmas my true love gave to me
two turtle doves
and a partridge in a pear tree.

On the third day of Christmas my true love gave to me
three french hens, two turtle doves
and a partridge in a pear tree.

On the fourth day of Christmas my true love gave to me
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the fifth day of Christmas my true love gave to me
five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the sixth day of Christmas my true love gave to me
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the seventh day of Christmas my true love gave to me
seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the eighth day of Christmas my true love gave to me
eight maids a-milking, seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the ninth day of Christmas my true love gave to me
nine ladies dancing, eight maids a-milking, seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the tenth day of Christmas my true love gave to me
ten lords a-leaping,
nine ladies dancing, eight maids a-milking, seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the eleventh day of Christmas my true love gave to me
eleven pipers piping, ten lords a-leaping,
nine ladies dancing, eight maids a-milking, seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

On the twelfth day of Christmas my true love gave to me
twelve drummers drumming, eleven pipers piping, ten lords a-leaping,
nine ladies dancing, eight maids a-milking, seven swans -swimming,
six geese a-laying, five gold rings;
four calling birds, three french hens, two turtle doves
and a partridge in a pear tree.

“圣诞节的十二天”是一首英国圣诞特别颂歌,于 1780 年代左右出版,据说它是在英国女王伊丽莎白一世受迫害期间躲藏起来的天主教徒写的。它的创作是为了在不引起政府官员注意的情况下帮助教给孩子们关于天主教信仰的文章,使用形象化描述作为工具以帮助孩子们记忆。这首歌代表了在圣诞节十二天的每一天逐渐给予的盛大礼物。圣诞节的十二天是从圣诞节(12 月 25 日)开始的快乐节日。这也被也称为圣诞节节期(Christmastide and Twelvetide)。

4 查看下汇编代码

Address   Hex dump          Command                                  Comments00401000  /$  55            PUSH EBP                                 ;a.00401000(guessed Arg1,Arg2,Arg3)
00401001  |.  89E5          MOVEBP,ESP00401003  |.  81EC 04000000 SUB ESP,4
00401009  |.  90            NOP!0<t0040100A  |.  B8 01000000   MOV EAX,1
0040100F  |.  8B4D 08       MOV ECX,DWORD PTR SS:[ARG.1]00401012  |.  39C8          CMPEAX,ECX00401014  |.  B8 00000000   MOV EAX,0
00401019  |.  0F9CC0        SETLAL0040101C  |.  83F8 00       CMP EAX,0
0040101F  |.  0F84 51010000 JE 00401176t<3
00401025  |.  8B45 08       MOV EAX,DWORD PTR SS:[ARG.1]00401028  |.  83F8 03       CMP EAX,3
0040102B  |.  B8 00000000   MOV EAX,0
00401030  |.  0F9CC0        SETLAL00401033  |.  83F8 00       CMP EAX,0
00401036  |.  0F84 5D000000 JE 00401099

                            1-_0040103C  |.  B8 01000000   MOV EAX,1
00401041  |.  8B4D 0C       MOV ECX,DWORD PTR SS:[ARG.2]00401044  |.  29C8          SUBEAX,ECX

main(-
86,0,a+1)00401046 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]00401049 |. 41 INCECX0040104A |. 51 PUSH ECX ;/Arg3 0040104B |. B9 00000000 MOV ECX,0 ;| 00401050 |. 51 PUSH ECX ;|Arg2 => 0 00401051 |. B9 AAFFFFFF MOV ECX,-56 ;| 00401056 |. 51 PUSH ECX ;|Arg1 => -56 00401057 |. 8945 FC MOV DWORD PTR SS:[LOCAL.1],EAX ;| 0040105A |. E8 A1FFFFFF CALL 00401000 ;\a.00401000 main(-86,0,a+1) 0040105F |. 83C4 0C ADD ESP,0Cmain(-87,1-_, main(-86,0,a+1)+a)00401062 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]00401065 |. 01C1 ADDECX,EAX00401067 |. 51 PUSH ECX ;/Arg3 00401068 |. 8B45 FC MOV EAX,DWORD PTR SS:[LOCAL.1] ;| 0040106B |. 50 PUSH EAX ;|Arg2 => [LOCAL.1] 0040106C |. B8 A9FFFFFF MOV EAX,-57 ;| 00401071 |. 50 PUSH EAX ;|Arg1 => -57 00401072 |. E8 89FFFFFF CALL 00401000 ;\a.00401000 main(-87,1-_, main(-86,0,a+1)+a) 00401077 |. 83C4 0C ADD ESP,0Cmain(-79,-13,a+main(-87,1-_, main(-86,0,a+1)+a))0040107A |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]0040107D |. 01C1 ADDECX,EAX0040107F |. 51 PUSH ECX ;/Arg3 00401080 |. B8 F3FFFFFF MOV EAX,-0D ;| 00401085 |. 50 PUSH EAX ;|Arg2 => -0D 00401086 |. B8 B1FFFFFF MOV EAX,-4F ;| 0040108B |. 50 PUSH EAX ;|Arg1 => -4F 0040108C |. E8 6FFFFFFF CALL 00401000 ;\a.00401000 main(-79,-13,a+main(-87,1-_, main(-86,0,a+1)+a)) 00401091 |. 83C4 0C ADD ESP,0C 00401094 |. E9 0A000000 JMP 004010A3 00401099 |> B8 01000000 MOV EAX,1 0040109E |. E9 00000000 JMP 004010A3t<_004010A3 |> 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]004010A6 |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2]004010A9 |. 39C8 CMPEAX,ECX004010AB |. B8 00000000 MOV EAX,0 004010B0 |. 0F9CC0 SETLAL004010B3 |. 83F8 00 CMP EAX,0 004010B6 |. 0F84 1A000000 JE 004010D6main(t+1,_,a)004010BC |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]004010BF |. 40 INCEAX004010C0 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]004010C3 |. 51 PUSH ECX ;/Arg3 => [ARG.3] 004010C4 |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2] ;| 004010C7 |. 51 PUSH ECX ;|Arg2 => [ARG.2] 004010C8 |. 50 PUSH EAX ;|Arg1 004010C9 |. E8 32FFFFFF CALL 00401000 ;\a.00401000 004010CE |. 83C4 0C ADD ESP,0C 004010D1 |. E9 0A000000 JMP 004010E0 004010D6 |> B8 03000000 MOV EAX,3 004010DB |. E9 00000000 JMP 004010E0main(-94,-27+t,a)004010E0 |> 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]004010E3 |. 83C0 E5 ADDEAX,-1B004010E6 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]004010E9 |. 51 PUSH ECX ;/Arg3 => [ARG.3] 004010EA |. 50 PUSH EAX ;|Arg2 004010EB |. B8 A2FFFFFF MOV EAX,-5E ;| 004010F0 |. 50 PUSH EAX ;|Arg1 => -5E 004010F1 |. E8 0AFFFFFF CALL 00401000 ;\a.00401000 main(-94,-27+t,a)&&t==2 004010F6 |. 83C4 0C ADD ESP,0C 004010F9 |. 83F8 00 CMP EAX,0 004010FC |. 0F84 13000000 JE 00401115t==2 00401102 |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]00401105 |. 83F8 02 CMP EAX,2 00401108 |. 0F85 07000000 JNE 00401115 0040110E |. B8 01000000 MOV EAX,1 00401113 |. EB 05 JMP SHORT 0040111A 00401115 |> B8 00000000 MOV EAX,0 0040111A |> 83F8 00 CMP EAX,0 0040111D |. 0F84 44000000 JE 00401167_<13 00401123 |. 8B45 0C MOV EAX,DWORD PTR SS:[ARG.2]00401126 |. 83F8 0D CMP EAX,0D 00401129 |. B8 00000000 MOV EAX,0 0040112E |. 0F9CC0 SETLAL00401131 |. 83F8 00 CMP EAX,0 00401134 |. 0F84 1E000000 JE 00401158main(2,_+1,"%s %d %d\n")0040113A |. 8B45 0C MOV EAX,DWORD PTR SS:[ARG.2]0040113D |. 40 INCEAX0040113E |. B9 00204000 MOV ECX,OFFSET 00402000 ;ASCII "%s %d %d" 00401143 |. 51 PUSH ECX ;/Arg3 => ASCII "%s %d %d" 00401144 |. 50 PUSH EAX ;|Arg2 00401145 |. B8 02000000 MOV EAX,2 ;| 0040114A |. 50 PUSH EAX ;|Arg1 => 2 0040114B |. E8 B0FEFFFF CALL 00401000 ;\a.00401000 main(2,_+1,"%s %d %d\n") 00401150 |. 83C4 0C ADD ESP,0C 00401153 |. E9 0A000000 JMP 00401162 00401158 |> B8 09000000 MOV EAX,9 0040115D |. E9 00000000 JMP 00401162 00401162 |> E9 0A000000 JMP 00401171 00401167 |> B8 10000000 MOV EAX,10 0040116C |. E9 00000000 JMP 00401171 00401171 |> E9 87010000 JMP 004012FDt<0 00401176 |> 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]00401179 |. 83F8 00 CMP EAX,0 0040117C |. B8 00000000 MOV EAX,0 00401181 |. 0F9CC0 SETLAL00401184 |. 83F8 00 CMP EAX,0 00401187 |. 0F84 D4000000 JE 00401261t<-72 0040118D |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]00401190 |. 83F8 B8 CMP EAX,-48 00401193 |. B8 00000000 MOV EAX,0 00401198 |. 0F9CC0 SETLAL0040119B |. 83F8 00 CMP EAX,0 0040119E |. 0F84 1B000000 JE 004011BFmain(_,t,strText)004011A4 |. B8 0A204000 MOV EAX,OFFSET 0040200A ;ASCII "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw"... 004011A9 |. 50 PUSH EAX ;/Arg3 => ASCII "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw". 004011AA |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1] ;| 004011AD |. 50 PUSH EAX ;|Arg2 => [ARG.1] 004011AE |. 8B45 0C MOV EAX,DWORD PTR SS:[ARG.2] ;| 004011B1 |. 50 PUSH EAX ;|Arg1 => [ARG.2] 004011B2 |. E8 49FEFFFF CALL 00401000 ;\a.00401000 main(_,t, "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+ 004011B7 |. 83C4 0C ADD ESP,0C 004011BA |. E9 9D000000 JMP 0040125Ct<-50 004011BF |> 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]004011C2 |. 83F8 CE CMP EAX,-32 004011C5 |. B8 00000000 MOV EAX,0 004011CA |. 0F9CC0 SETLAL004011CD |. 83F8 00 CMP EAX,0 004011D0 |. 0F84 54000000 JE 0040122A_==*a?004011D6 |. 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]004011D9 |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2]004011DC |. 0FBE10 MOVSX EDX,BYTE PTR DS:[EAX]004011DF |. 39D1 CMPECX,EDX004011E1 |. B8 00000000 MOV EAX,0 004011E6 |. 0F94C0 SETEAL004011E9 |. 83F8 00 CMP EAX,0 004011EC |. 0F84 17000000 JE 00401209putchar(31[a])004011F2 |. 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]004011F5 |. 83C0 1F ADDEAX,1F004011F8 |. 0FBE08 MOVSX ECX,BYTE PTR DS:[EAX]004011FB |. 51 PUSH ECX ;/c 004011FC |. E8 AF020000 CALL <JMP.&msvcrt.putchar> ;\MSVCRT.putchar 00401201 |. 83C4 04 ADD ESP,4 00401204 |. E9 1C000000 JMP 00401225main(-65,_,a+1)00401209 |> 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]0040120C |. 40 INCEAX0040120D |. 50 PUSH EAX ;/Arg3 0040120E |. 8B45 0C MOV EAX,DWORD PTR SS:[ARG.2] ;| 00401211 |. 50 PUSH EAX ;|Arg2 => [ARG.2] 00401212 |. B8 BFFFFFFF MOV EAX,-41 ;| 00401217 |. 50 PUSH EAX ;|Arg1 => -41 00401218 |. E8 E3FDFFFF CALL 00401000 ;\a.00401000 main(-65,_,a+1) 0040121D |. 83C4 0C ADD ESP,0C 00401220 |. E9 00000000 JMP 00401225 00401225 |> E9 2D000000 JMP 00401257main((*a=='/')+t,_,a+1)0040122A |> 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]0040122D |. 0FBE08 MOVSX ECX,BYTE PTR DS:[EAX]00401230 |. 83F9 2F CMP ECX,2F (*a=='/')00401233 |. B8 00000000 MOV EAX,0 00401238 |. 0F94C0 SETEAL0040123B |. 8B4D 08 MOV ECX,DWORD PTR SS:[ARG.1]0040123E |. 01C8 ADD EAX,ECX (*a=='/')+t00401240 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]00401243 |. 41 INC ECX a+1 00401244 |. 51 PUSH ECX ;/Arg3 00401245 |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2] ;| 00401248 |. 51 PUSH ECX ;|Arg2 => [ARG.2] 00401249 |. 50 PUSH EAX ;|Arg1 0040124A |. E8 B1FDFFFF CALL 00401000 ;\a.00401000 0040124F |. 83C4 0C ADD ESP,0C 00401252 |. E9 00000000 JMP 00401257 00401257 |> E9 00000000 JMP 0040125C 0040125C |> E9 97000000 JMP 004012F8 0<t00401261 |> B8 00000000 MOV EAX,0 00401266 |. 8B4D 08 MOV ECX,DWORD PTR SS:[ARG.1]00401269 |. 39C8 CMPEAX,ECX0040126B |. B8 00000000 MOV EAX,0 00401270 |. 0F9CC0 SETLAL00401273 |. 83F8 00 CMP EAX,0 00401276 |. 0F84 1F000000 JE 0040129Bmain(2,2,"%s")0040127C |. B8 A2214000 MOV EAX,OFFSET 004021A2 ;ASCII "%s" 00401281 |. 50 PUSH EAX ;/Arg3 => ASCII "%s" 00401282 |. B8 02000000 MOV EAX,2 ;| 00401287 |. 50 PUSH EAX ;|Arg2 => 2 00401288 |. B8 02000000 MOV EAX,2 ;| 0040128D |. 50 PUSH EAX ;|Arg1 => 2 0040128E |. E8 6DFDFFFF CALL 00401000 ;\a.00401000 main(2,2,"%s") 00401293 |. 83C4 0C ADD ESP,0C 00401296 |. E9 58000000 JMP 004012F3*a=='/' 0040129B |> 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]0040129E |. 0FBE08 MOVSX ECX,BYTE PTR DS:[EAX]004012A1 |. 83F9 2F CMPECX,2F004012A4 |. 0F84 3F000000 JE 004012E9main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry")004012AA |. 8B45 10 MOV EAX,DWORD PTR SS:[ARG.3]004012AD |. B9 A5214000 MOV ECX,OFFSET 004021A5 ;ASCII "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}: \nuwloca-O;m .vpbks,fxntdCeghiry" 004012B2 |. 51 PUSH ECX ;/Arg3 => ASCII "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:uwloca-O;m .vpbks,fxntdCeghiry" 004012B3 |. 0FBE08 MOVSX ECX,BYTE PTR DS:[EAX] ;| 004012B6 |. 51 PUSH ECX ;|Arg2 004012B7 |. B8 C3FFFFFF MOV EAX,-3D ;| 004012BC |. 50 PUSH EAX ;|Arg1 => -3D 004012BD |. E8 3EFDFFFF CALL 00401000 ;\a.00401000 main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry" 004012C2 |. 83C4 0C ADD ESP,0Cmain(0,main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1)004012C5 |. 8B4D 10 MOV ECX,DWORD PTR SS:[ARG.3]004012C8 |. 41 INCECX004012C9 |. 51 PUSH ECX ;/Arg3 004012CA |. 50 PUSH EAX ;|Arg2 004012CB |. B8 00000000 MOV EAX,0 ;| 004012D0 |. 50 PUSH EAX ;|Arg1 => 0 004012D1 |. E8 2AFDFFFF CALL 00401000 ;\a.00401000 main(0,main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nu 004012D6 |. 83C4 0C ADD ESP,0C 004012D9 |. 83F8 00 CMP EAX,0 004012DC |. 0F85 07000000 JNE 004012E9 004012E2 |. B8 00000000 MOV EAX,0 004012E7 |. EB 05 JMP SHORT 004012EE 004012E9 |> B8 01000000 MOV EAX,1 004012EE |> \E9 00000000 JMP 004012F3 004012F3 |> E9 00000000 JMP 004012F8 004012F8 |> E9 00000000 JMP 004012FD 004012FD |> C9 LEAVE 004012FE \. C3 RETN


5 源代码断句
源码基于?/,/操作进行格式重排.用汇编代码辅助判断断句是否与原码执行一致。
为方便理解将二个字符串用宏替换。第一个是明文,第二个字符串是用来加密的密钥。

#include <stdio.h>
#define  strText  "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw'i;# \
){nl]
!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw'iK{;[{nl]'/w#q#n'wk nw'\
iwk{KK{nl]
!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl
'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}
'+}##(!!/" #define strEnc "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"main(t,_,a)char *a;
{
return !0<t?t<3 ?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a))
:
1,
t
<_?main(t+1,_,a)
:
3,
main(
-94,-27+t,a) && t==2 ?_<13 ?main(2,_+1,"%s %d %d\n")
:
9:16:
t
<0 ?t<-72 ?main(_,t,strText)
:
t
<-50 ?_==*a?putchar(31[a])
:
main(
-65,_,a+1)
:
main((
*a=='/')+t,_,a+1)
:
0<t?main(2,2,"%s")
:
*a=='/'||main(0,main(-61,*a,strEnc),a+1);
}


6 用C语言的if-then-else语句解析

#include <stdio.h>

#define  strText   "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw'i;# \
){nl]
!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw'iK{;[{nl]'/w#q#n'wk nw'\
iwk{KK{nl]
!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl
'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}
'+}##(!!/" #define strEnc "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"main(t,_,a)char *a;
{
if ((!0)<t)
{
if (t<3)
{
main(
-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a));
}
else{1;
}
if (t<_)
{
main(t
+1,_,a);
}
else{3;
}
if (main(-94,-27+t,a) && t==2)
{
if (_<13)
{
return main(2,_+1,"%s %d %d\n");
}
else{return 9;
}
}
else{return 16;
}
}
else{if (t<0)
{
if (t<-72)
{
returnmain(_,t,strText );
}
else{if (t<-50)
{
if (_==(*a))
{
return putchar(31[a]);
}
else{return main(-65,_,a+1);
}
}
else{return main((*a=='/')+t,_,a+1);
}
}
}
else{if (0<t)
{
return main(2,2,"%s");
}
else{return (*a=='/')||main(0,main(-61,*a,strEnc ),a+1);
}
}
}
}

7 源码分析
7.1)!0为常数1
7.2)main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a));
这语句为嵌套,可以分解为

int m1=main(-86,0,a+1);int m2=main(-87,1-_,m1+a);
main(
-79,-13,a+m2);

7.3)A,B语句中的逗号(,),表示执行完A,继续执行B

7.4)main(-94,-27+t,a) && t==2?A:B这语句可以分解成

int m3=main(-94,-27+t,a);
if(m3&& t==2)A;
else B;

7.5)return *a=='/'||main(0,main(-61,*a,strEnc),a+1);这语句可以分解成

if(*a=='/')
{
    return 1;
}else
{
    return main(0,main(-61,*a,strEnc),a+1);
}

因为运行到当前分支t=0,这其实是递归函数

7.6)putchar(31[a]),注意31[a],中括号[]代表C语言的数组,因为a[31]等同与*(a+31),31[a]等同与*(31+a),所以31[a]等同于a[31]。

8 整理代码
8.1)根据if-then-else源码,整理代码前面根据t的伪代码
if(t>1) Do2();
else if(t<0) DoN();
else if(t>0) Do1(); //满足t<=1&&t>=0&&t>0的t值只能为1
else Do0(); //以上都不满足的t只能为0
8.2)按t从大到小整理伪代码
if(t>1) Do2();
else if(t==1)Do1();
else if(t==0)Do0();
else DoN();
8.3)按t从大到小整理伪代码整理源码

#include <stdio.h>

#define  strText   "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/"
#define  strEnc  "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"

main(t,_,a)
char *a;
{
    if (t>1)
    {
        if (t<3) 
        {
            main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a));
        } 
        else 
        {
            1;
        }
        if (t<_) 
        {
            main(t+1,_,a);
        }
        else 
        {
            3;
        }
        if (main(-94,-27+t,a) && t==2)
        {
            if (_<13) 
            {
                return main(2,_+1,"%s %d %d\n");
            }
            else 
            {
                return 9;
            }
        }
        else 
        {
            return 16;
        }
    }
    else if(t==1)
	{
		return main(2,2,"%s");
	}else if(t==0)
	{
		return (*a=='/')||main(0,main(-61,*a,strEnc),a+1);
	}else if(t>=-50)
	{
		return main((*a=='/')+t,_,a+1);
	}else if(t>=-72)
	{
		if (_==(*a))
                    {
                        return putchar(31[a]);
                    }
                    else 
                    {
                        return main(-65,_,a+1);
                    }
		
	}else
	{
		return main(_,t,strText );
	}
    
}

9 输出分析
源码输出语句只有一句putchar(31[a]),此时t应满足-50>t>=-72。源码递归调用main(-65,_,a+1)直到(_==*a),然后打印解密后的31[a]字符。
用来解密的密钥如下

#define strEnc "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"

所以序号为1字符‘e'对应的原字符’u'序号为32=1+31,序号为2字符'k'的加密前原字符为'w',序号为33=2+31。注意'!'(序号为0)对应于加密前的换行‘\n'序号31。

用这种解密文本

"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/"

解密后的原文如下,

"On the /first/second/third/fourth/fifth/sixth/seventh/eigth/ninth/tenth/eleventh/twelfth/ day of Christmas my true love gave to me
/twelve drummers drumming, /eleven pipers piping, /ten lords a-leaping,
/nine ladies dancing, /eight maids a-milking, /seven swans a-swimming,
/six geese a-laying, /five gold rings;
/four calling birds, /three french hens, /two turtle doves
and /a partridge in a pear tree.

"

代码中字符'/'没有加密,用来分隔之子字符串,比加first,second。

改写不加密的源码,为避免换行'\n'中有字符'\',将换行符用'!',输出字符中对'!'当成换行处理。

#include <stdio.h>

#define strDeText "On the /first/second/third/fourth/fifth/sixth/seventh/eigth/ninth/tenth/eleventh/twelfth/ day of Christmas my true love gave to me!\
/twelve drummers drumming, /eleven pipers piping, /ten lords a-leaping,!\/nine ladies dancing, /eight maids a-milking, /seven swans a-swimming,!\/six geese a-laying, /five gold rings;!\/four calling birds, /three french hens, /two turtle doves!\
and
/a partridge in a pear tree.!!/"main(t,_,a)char *a;
{
if (t>1)
{
if (t<3)
{
main(
-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a));
}
else{1;
}
if (t<_)
{
main(t
+1,_,a);
}
else{3;
}
if (main(-94,-27+t,a) && t==2)
{
if (_<13)
{
return main(2,_+1,"%s %d %d\n");
}
else{return 9;
}
}
else{return 16;
}
}
else if(t==1)
{
return main(2,2,"%s");
}
else if(t==0)
{
return (*a=='/')||main(0,main(-61,*a,""),a+1);
}
else if(t>=-50)
{
return main((*a=='/')+t,_,a+1);
}
else if(t>=-72)
{
if(_=='!')_='\n';returnputchar(_);
}
else{returnmain(_,t,strDeText );
}

}

为了更好理解原代码,将'/'分隔的子字符用一个字母表示。

比如"first"用字符'a'代替,"second"用'b'代替,等等。简化代码如下

#include <stdio.h>
#define strDeText "On /a/b/c/d/e/f/g/h/i/j/k/l/ day /L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!/"
main(t,_,a)
char *a;
{
    if (t>1)
    {
        if (t<3) 
        {
            main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a));
        } 
        else 
        {
            1;
        }
        if (t<_) 
        {
            main(t+1,_,a);
        }
        else 
        {
            3;
        }
        if (main(-94,-27+t,a) && t==2)
        {
            if (_<13) 
            {
                return main(2,_+1,"%s %d %d\n");
            }
            else 
            {
                return 9;
            }
        }
        else 
        {
            return 16;
        }
    }
    else if(t==1)
	{
		return main(2,2,"%s");
	}else if(t==0)
	{
		return (*a=='/')||main(0,main(-61,*a,""),a+1);
	}else if(t>=-50)
	{
		return main((*a=='/')+t,_,a+1);
	}else if(t>=-72)
	{
		if(_=='!')_='\n';
		return putchar(_);		
	}else
	{//t<-72
		return main(_,t,strDeText );
	}
    
}

运行程序输出如下:

On a day A.
On b day B,A.
On c day C,B,A.
On d day D,C,B,A.
On e day E,D,C,B,A.
On f day F,E,D,C,B,A.
On g day G,F,E,D,C,B,A.
On h day H,G,F,E,D,C,B,A.
On i day I,H,G,F,E,D,C,B,A.
On j day J,I,H,G,F,E,D,C,B,A.
On k day K,J,I,H,G,F,E,D,C,B,A.
On l day L,K,J,I,H,G,F,E,D,C,B,A.

改写t==0时用递归方式输出字符串为正常调用函数,并注意到t<-72时,交换t和_且把a固定为strDeText递归调用main.

#include <stdio.h>

#define strDeText "On /a/b/c/d/e/f/g/h/i/j/k/l/ day /L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!/"

int funprint(t,_,a)
char *a;
{
	while(*a!='/')
	{
		char c=*a;
		if(c=='!')c='\n';
		putchar(c);
		a++;
	}
	return 1;
}

main(t,_,a)
char *a;
{	
    if (t>1)
    {
        if (t<3) 
        {
			int m1=main(0,-86,strDeText);
			int m2=main(1-_,-87,strDeText);
            main(-13,-79,strDeText);
        } 
        else 
        {
            1;
        }
        if (t<_) 
        {
            main(t+1,_,a);
        }
        else 
        {
            3;
        }
		
        if (main(-27+t,-94,strDeText) && t==2)
        {
            if (_<13) 
            {
                return main(2,_+1,"%s %d %d\n");
            }
            else 
            {
                return 9;
            }
        }
        else 
        {
            return 16;
        }
    }
    else if(t==1)
	{
		return main(2,2,"%s");
	}else if(t==0)
	{
		return funprint(t,_,a);		
	}else if(t>=-50)
	{//
		return main((*a=='/')+t,_,a+1);
	}else if(t>=-72)
	{
		if(_=='!')_='\n';
		return putchar(_);		
	}else
	{
		return main(_,t,strDeText );
	}
    
}

代码比较清晰了,可以注意到int m1=main(0,-86,strDeText)输出"On ",

int m2=main(1-_,-87,strDeText)输出'a'或者'b'或者’c'等等,
main(-13,-79,strDeText)输出' day ',可以明白对运行t>=-50这个分支递归调用main,此时t表示'/'的个数。
继续改写代码,将以上三个递归改成函数调用

1 #include <stdio.h>
2 
3 #define strDeText "On /a/b/c/d/e/f/g/h/i/j/k/l/ day /L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!/"
4 
5 intfunprint(t,_,a)6 char *a;7 {8     while(*a!='/')9 {10         char c=*a;11         if(c=='!')c='\n';12 putchar(c);13         a++;14 }15     return 1;16 }17 
18 intfunOut(t,_,a)19 char *a;20 {21     inti;22     for(i=t;i<0;i++)23 {24         while(*a!='/')25 {26             a++;27 }28         a++;29 }30     returnfunprint(t,_,a);31 }32 
33 main(t,_,a)34 char *a;35 {36     if (t>1)37 {38         if (t<3)39 {40             int m1=funOut(0,-86,strDeText);41             int m2=funOut(1-_,-87,strDeText);42             funOut(-13,-79,strDeText);43 }44         else 
45 {46             1;47 }48         if (t<_)49 {50             main(t+1,_,a);51 }52         else 
53 {54             3;55 }56         
57         if (funOut(-27+t,-94,strDeText) && t==2)58 {59             if (_<13)60 {61                 return main(2,_+1,"%s %d %d\n");62 }63             else 
64 {65                 return 9;66 }67 }68         else 
69 {70             return 16;71 }72 }73     else if(t==1)74 {75         return main(2,2,"%s");76     }else if(t==0)77 {78         returnfunprint(t,_,a);79     }else if(t>=-50)80 {81         return main((*a=='/')+t,_,a+1);82     }else if(t>=-72)83 {84         if(_=='!')_='\n';85         returnputchar(_);86     }else
87 {88         returnmain(_,t,strDeText );89 }90     
91 }

程序从t=1开始运行,递归调用t=2,_=2,打印完"On
a day A."第一句后main(2,_+1,"%s %d %d\n")递归调用main,将_值加1变成3,

运行

int m1=funOut(0,-86,strDeText);int m2=funOut(1-_,-87,strDeText);
funOut(
-13,-79,strDeText);

打印"On
b day
",因为t=2,_=3,递归调用main(t+1,_,a),此时t=3,_=3,返回后调用funOut(-27+t,-94,strDeText)打印出"B,A.",

继续调用main(2,_+1,"%s
%d
%d\n")将_值加1变成4,...,直到_=13完成输出"L,K,J,I,H,G,F,E,D,C,B,A."
明白这点后,将t>1的递归改成函数调用

#include <stdio.h>

#define strDeText "On /a/b/c/d/e/f/g/h/i/j/k/l/ day /L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!/"

intfunprint(t,_,a)char *a;
{
while(*a!='/')
{
char c=*a;if(c=='!')c='\n';
putchar(c);
a
++;
}
return 1;
}
intfunOut(t,_,a)char *a;
{
inti;for(i=t;i<0;i++)
{
while(*a!='/')
{
a
++;
}
a
++;
}
returnfunprint(t,_,a);
}

main(t,_,a)
char *a;
{
if (t>1)
{
inti,j;for(;_<13;_++)
{
int m1=funOut(0,-86,strDeText);int m2=funOut(1-_,-87,strDeText);
funOut(
-13,-79,strDeText);
i
=_;while(i>=t)
{
funOut(
-27+i,-94,strDeText);
i
--;
}
}
}
else if(t==1)
{
return main(2,2,"%s");
}
else if(t==0)
{
returnfunprint(t,_,a);
}
else if(t>=-50)
{
return main((*a=='/')+t,_,a+1);
}
else if(t>=-72)
{
if(_=='!')_='\n';returnputchar(_);
}
else{returnmain(_,t,strDeText );
}

}

最后去掉没用的代码,结构化改写原码,简单的逻辑打印出结果

#include <stdio.h>
//#define strDeText "On /a/b/c/d/e/f/g/h/i/j/k/l/ day /L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!/"
#define strDeText "On the /first/second/third/fourth/fifth/sixth/seventh/eigth/ninth/tenth/eleventh/twelfth/ day of Christmas my true love gave to me!\
/twelve drummers drumming, /eleven pipers piping, /ten lords a-leaping,!\/nine ladies dancing, /eight maids a-milking, /seven swans a-swimming,!\/six geese a-laying, /five gold rings;!\/four calling birds, /three french hens, /two turtle doves!\
and
/a partridge in a pear tree.!!/"int fun0Print(char *a)
{
while(*a!='/')
{
char c=*a;if(c=='!')c='\n';
putchar(c);
a
++;
}
return 1;
}
void funPrint(intk)
{
char *s=strDeText;inti;for(i=k;i<0;i++)
{
while(*s!='/')
{
s
++;
}
s
++;
}
fun0Print(s);
}
voidfunDisp()
{
int_,m;inti;for(_=2;_<=13;_++)
{
funPrint(
0); //输出"On " funPrint(1-_); //输出" a "or" b "or" c "or" d "or .... funPrint(-13); //输出" day " for(m=_;m>=2;m--)
{
funPrint(
-27+m); //输出" L,/K,/J,/I,/H,/G,/F,/E,/D,/C,/B,/A.!" }
}
}
main(
int t,int _,char*a)
{
funDisp();
}

前言

vue3中想要访问DOM和子组件可以使用ref进行模版引用,但是这个ref有一些让人迷惑的地方。比如定义的ref变量到底是一个响应式数据还是DOM元素?还有template中ref属性的值明明是一个字符串,比如
ref="inputEl"
,怎么就和script中同名的
inputEl
变量绑到一块了呢?所以Vue3.5推出了一个
useTemplateRef
函数,完美的解决了这些问题。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

ref模版引用的问题

我们先来看一个
react
中使用ref访问DOM元素的例子,代码如下:

const inputEl = useRef<HTMLInputElement>(null);
<input type="text" ref={inputEl} />

使用
useRef
函数定义了一个名为
inputEl
的变量,然后将input元素的ref属性值设置为
inputEl
变量,这样就可以通过
inputEl
变量访问到input输入框了。

inputEl
因为是一个
.current
属性的对象,由于
inputEl
变量赋值给了ref属性,所以他的
.current
属性的值被更新为了input DOM元素,这个做法很符合编程直觉。

再来看看
vue3
中的做法,相比之下就很不符合编程直觉了。

不知道有多少同学和欧阳一样,最开始接触vue3时总是在template中像
react
一样给ref属性绑定一个ref变量,而不是ref变量的名称。比如下面这样的代码:

<input type="text" :ref="inputEl" />

const inputEl = ref<HTMLInputElement>();

更加要命的是这样写还不会报错!!!!
当我们使用
inputEl
变量去访问input输入框时始终拿到的都是
undefined

经过多次排查发现原来ref属性接收的不是一个ref变量,而是ref变量的名称。正确的代码应该是这样的:

<input type="text" ref="inputEl" />

const inputEl = ref<HTMLInputElement>();

还有就是如果我们将ref模版引用相关的逻辑抽成hooks后,那么必须将在vue组件中也要将ref属性对应的ref变量也定义才可以。

hooks代码如下:

export default function useRef() {
  const inputEl = ref<HTMLInputElement>();
  function setInputValue() {
    if (inputEl.value) {
      inputEl.value.value = "Hello, world!";
    }
  }

  return {
    inputEl,
    setInputValue,
  };
}

在hooks中定义了一个名为
inputRef
的变量,并且在
setInputValue
函数中会通过
inputRef
变量对input输入框进行操作。

vue组件代码如下:

<template>
  <input type="text" ref="inputEl" />
  <button @click="setInputValue">给input赋值</button>
</template>

<script setup lang="ts">
import useInput from "./useInput";
const { setInputValue, inputEl } = useInput();
</script>

虽然在vue组件中我们不会使用
inputEl
变量,但是还是需要从hooks中导入
useInput
变量。大家不觉得这很奇怪吗?导入了一个变量,又没有显式的去使用这个变量。

如果在这里不去从hooks中导入
inputEl
变量,那么
inputEl
变量中就不能绑定上input输入框了。

useTemplateRef函数

为了解决上面说的ref模版引用的问题,在Vue3.5中新增了一个
useTemplateRef
函数。

useTemplateRef
函数的用法很简单:只接收一个参数
key
,是一个字符串。返回值是一个ref变量。

其中参数key字符串的值应该等于template中ref属性的值。

返回值是一个ref变量,变量的值指向模版引用的DOM元素或者子组件。

我们来看个例子,前面的demo改成
useTemplateRef
函数后代码如下:

<template>
  <input type="text" ref="inputRef" />
  <button @click="setInputValue">给input赋值</button>
</template>

<script setup lang="ts">
import { useTemplateRef } from "vue";

const inputEl = useTemplateRef<HTMLInputElement>("inputRef");
function setInputValue() {
  if (inputEl.value) {
    inputEl.value.value = "Hello, world!";
  }
}
</script>

在template中ref属性的值为字符串
"inputRef"

在script中使用
useTemplateRef
函数,传入的第一个参数也是字符串
"inputRef"

useTemplateRef
函数的返回值就是指向input输入框的ref变量。

由于
inputEl
是一个ref变量,所以在click事件中想要访问到DOM元素input输入框就需要使用
inputEl.value

我们这里是要给输入框中塞一个字符串"Hello, world!",所以使用
inputEl.value.value = "Hello, world!"

使用了
useTemplateRef
函数后和之前比起来就很符合编程直觉了。template中ref属性值是一个字符串
"inputRef"
,使用
useTemplateRef
函数时也传入字符串
"inputRef"
就能拿到对应的模版引用了。

hooks中使用useTemplateRef

回到前面讲的hooks的例子,使用
useTemplateRef
后hooks代码如下:

export default function useInput(key) {
  const inputEl = useTemplateRef<HTMLInputElement>(key);
  function setInputValue() {
    if (inputEl.value) {
      inputEl.value.value = "Hello, world!";
    }
  }
  return {
    setInputValue,
  };
}

现在我们在hooks中就不需要导出变量
inputEl
了,因为这个变量只需要在hooks内部使用。

vue组件代码如下:

<template>
  <input type="text" ref="inputRef" />
  <button @click="setInputValue">给input赋值</button>
</template>

<script setup lang="ts">
import useInput from "./useInput";
const { setInputValue } = useInput("inputRef");
</script>

由于在vue组件中我们不需要使用
inputEl
变量,所以在这里就不需要从
useInput
中引入变量
inputEl
了。而之前不使用
useTemplateRef
的方案中我们就不得不引入
inputEl
变量了。

动态切换ref绑定的变量

有的时候我们需要根据不同的场景去动态切换ref模版引用的变量,这时在template中ref属性的值就是动态的了,而不是一个写死的字符串。在这种场景中
useTemplateRef
也是支持的,代码如下:

<template>
  <input type="text" :ref="refKey" />
  <button @click="switchRef">切换ref绑定的变量</button>
  <button @click="setInputValue">给input赋值</button>
</template>

<script setup lang="ts">
import { useTemplateRef, ref } from "vue";

const refKey = ref("inputEl1");
const inputEl1 = useTemplateRef<HTMLInputElement>("inputEl1");
const inputEl2 = useTemplateRef<HTMLInputElement>("inputEl2");
function switchRef() {
  refKey.value = refKey.value === "inputEl1" ? "inputEl2" : "inputEl1";
}
function setInputValue() {
  const curEl = refKey.value === "inputEl1" ? inputEl1 : inputEl2;
  if (curEl.value) {
    curEl.value.value = "Hello, world!";
  }
}
</script>

在这个场景template中ref绑定的就是一个变量
refKey
,通过点击
切换ref绑定的变量
按钮可以切换
refKey
的值。相应的,绑定input输入框的变量也会从
inputEl1
变量切换成
inputEl2
变量。

useTemplateRef
是如何实现的?

我们来看看
useTemplateRef
的源码,其实很简单,简化后的代码如下:

function useTemplateRef(key) {
  const i = getCurrentInstance();
  const r = shallowRef(null);
  if (i) {
    const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs;
    Object.defineProperty(refs, key, {
      enumerable: true,
      get: () => r.value,
      set: (val) => (r.value = val),
    });
  }
  return r;
}

首先使用
getCurrentInstance
方法获取当前vue实例对象,赋值给变量
i

然后调用
shallowRef
函数生成一个浅层的ref对象,初始值为null。这个ref对象就是
useTemplateRef
函数返回的ref对象。

接着就是判断当前vue实例如果存在就读取实例上面的
refs
属性对象,如果实例对象上面没有
refs
属性,那么就初始化一个空对象到vue实例对象的
refs
属性。

vue实例对象上面的这个
refs
属性对象用过vue2的同学应该都很熟悉,里面存的是注册过ref属性的所有 DOM 元素和组件实例。

vue3虽然不像vue2一样将
refs
属性对象开放给开发者,但是他的内部依然还是用vue实例上面的
refs
属性对象来存储template中使用ref属性注册过的元素和组件实例。

这里使用了
Object.defineProperty
方法对
refs
属性对象进行拦截,拦截的字段是变量
key
的值,而这个
key
的值就是template中使用ref属性绑定的值。

以我们上面的demo举例,在template中的代码如下:

<input type="text" ref="inputRef" />

这里使用ref属性在vue实例的
refs
属性对象上面注册了一个input输入框,
refs.inputRef
的值就是指向DOM元素input输入框。

然后在script中是这样使用
useTemplateRef
的:

const inputEl = useTemplateRef<HTMLInputElement>("inputRef")

调用
useTemplateRef
函数时传入的是字符串
"inputRef"
,在
useTemplateRef
函数内部使用
Object.defineProperty
方法对
refs
属性对象进行拦截,拦截的字段为变量
key
的值,也就是调用
useTemplateRef
函数传入的字符串
"inputRef"

初始化时,vue处理input输入框上面的
ref="inputRef"
就会执行下面这样的代码:

refs[ref] = value

此时的
value
的值就是指向DOM元素input输入框,
ref
的值就是字符串
"inputRef"

那么这行代码就是将DOM元素input输入框赋值给
refs
对象上面的
inputRef
属性上。

由于这里对
refs
对象上面的
inputRef
属性进行写操作,所以会走到
useTemplateRef
函数中
Object.defineProperty
定义的
set
拦截。代码如下:

const r = shallowRef(null);

Object.defineProperty(refs, key, {
  enumerable: true,
  get: () => r.value,
  set: (val) => (r.value = val),
});


set
拦截中会将DOM元素input输入框赋值给ref变量
r
,而这个
r
就是
useTemplateRef
函数返回的ref变量。

同样的当对象
refs
对象的
inputRef
属性进行读操作时,也会走到这里的
get
拦截中,返回
useTemplateRef
函数中定义的ref变量
r
的值。

总结

Vue3.5中新增的
useTemplateRef
函数解决了ref属性中存在的几个问题:

  • 不符合编程直觉,template中ref属性的值是script中对应的ref变量的
    变量名

  • 在script中如果不使用ts,则不能直观的知道一个ref变量到底是响应式数据还是DOM元素?

  • 将定义和访问DOM元素相关的逻辑抽到hooks中后,虽然vue组件中不会使用到存放DOM元素的变量,但是也必须在组件中从hooks中导入。

接着我们讲了
useTemplateRef
函数的实现。在
useTemplateRef
函数中会定义一个ref对象,在
useTemplateRef
函数最后就是return返回这个ref对象。

接着使用
Object.defineProperty
对vue实例上面的
refs
属性对象进行get和set拦截。

初始化时,处理template中的ref属性,会对vue实例上面的
refs
属性对象进行写操作。

然后就会被set拦截,在set拦截中会将
useTemplateRef
函数中定义的ref对象的值赋值为绑定的DOM元素或者组件实例。


useTemplateRef
函数就是将这个ref对象进行return返回,所以我们可以通过
useTemplateRef
函数的返回值拿到template中ref属性绑定的DOM元素或者组件实例。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。