2024年9月

信仰是人们内心深处的信念,是推动人类前进的驱动力。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。

一、查询参数编码问题

我们在日常开发中,有时候会遇到拼接参数特别多的情况,那么就会导致一行代码特别长。那么为了美观呢,有的同学会进行换行处理,如下代码:


可以看到我红色框出来的地方就是经过了手动的回车导致产生的回车换行符。这么做乍一看也挺正常是吧,但其实对于JavaScript来说,这是会被保留的。
我们知道,当使用uni.request或其他HTTP客户端发送请求时,浏览器或客户端会对URL后面的查询参数进行编码,也就是问号后面那些东西,于是我们可以自己将编码后的东西打印出来看看,如下:

可以看到我们编码后多了很多
%0A%09
的东西,而这个其实就是我们的回车换行符,大家可以去
在线编解码网站
看看解码后的东西。

我把这种情况分别运行在浏览器端、Android端以及ios端,如下:

H5

iPhone

Android

可以看到,即使是这种带入了回车换行符的情况下,在H5以及Android端都是可以正常发送请求的,而ios就没那么幸运了,ios处理比较严格,也可以说是反人类,它不会去处理这种东西,一起给到后端,导致参数错误。

那么解决办法呢有很多种,比如:

通过设置打开Hbuilder X的自动换行,这样的话就不会改变代码并且也不影响代码阅读;
另外也可以通过 ‘+’ 号手动拼接各个参数的写法,比较麻烦;
再不济也可以手动将换行符换成空格也可以。

我这边选择打开自动换行,如下:

在编辑器中看的效果就会根据你的视口宽度自动换行,如下:

二、日期使用问题

在日期的使用上,如果传入的字符串非标准格式(主要就是得用‘-’分割),iso情况下会出现错误。如下代码:

const date = '2024/8'
let year = new Date(date).getFullYear(),
	month = new Date(date).getMonth() + 1
console.log('获取到的年份:', year)
console.log('获取到的月份:', month)

打印结果:

Android

H5

IOS

可以看到当在ios情况下时,获取到的年月都是NaN,那么对后续用到这两个变量的地方都会是意想之外的结果。
所以在使用时要确保传入的值是标准日期格式,如果确保不了就做下格式化处理,如下:

const getMonthStr = (date) => {
	let year = date.getFullYear();
	let month = (date.getMonth() + 1).toString().padStart(2, '0');//这里做下补0操作,避免个位数月份的情况
	return `${year}-${month}`;
};

前言

最近在给
opentelemetry-java-instrumentation
提交了一个
PR
,是关于给 gRPC 新增四个 metrics:

  • rpc.client.request.size
    : 客户端请求包大小
  • rpc.client.response.size
    :客户端收到的响应包大小
  • rpc.server.request.size
    :服务端收到的请求包大小
  • rpc.server.response.size
    :服务端响应的请求包大小

这个 PR 的主要目的就是能够在指标监控中拿到
RPC
请求的包大小,而这里的关键就是如何才能拿到这些包的大小。

首先支持的是
gRPC
(目前在云原生领域使用的最多),其余的 RPC 理论上也是可以支持的:

在实现的过程中我也比较好奇
OpenTelemetry
框架是如何给
gRPC
请求创建
span
调用链的,如下图所示:
image.png
image.png

这是一个 gRPC 远程调用,java-demo 是 gRPC 的客户端,k8s-combat 是 gRPC 的服务端

在开始之前我们可以根据
OpenTelemetry
的运行原理大概猜测下它的实现过程。

首先我们应用可以创建这些链路信息的前提是:使用了
OpenTelemetry
提供的
javaagent
,这个 agent 的原理是在运行时使用了
byte-buddy
增强了我们应用的字节码,在这些字节码中代理业务逻辑,从而可以在不影响业务的前提下增强我们的代码(只要就是创建 span、metrics 等数据)

Spring 的一些代理逻辑也是这样实现的

gRPC 增强原理

而在工程实现上,我们最好是不能对业务代码进行增强,而是要找到这些框架提供的扩展接口。


gRPC
来说,我们可以使用它所提供的
io.grpc.ClientInterceptor

io.grpc.ServerInterceptor
接口来增强代码。

打开
io.opentelemetry.instrumentation.grpc.v1_6.TracingClientInterceptor
类我们可以看到它就是实现了
io.grpc.ClientInterceptor

而其中最关键的就是要实现
io.grpc.ClientInterceptor#interceptCall
函数:

@Override  
public <REQUEST, RESPONSE> ClientCall<REQUEST, RESPONSE> interceptCall(  
    MethodDescriptor<REQUEST, RESPONSE> method, CallOptions callOptions, Channel next) {  
  GrpcRequest request = new GrpcRequest(method, null, null, next.authority());  
  Context parentContext = Context.current();  
  if (!instrumenter.shouldStart(parentContext, request)) {  
    return next.newCall(method, callOptions);  
  }  
  Context context = instrumenter.start(parentContext, request);  
  ClientCall<REQUEST, RESPONSE> result;  
  try (Scope ignored = context.makeCurrent()) {  
    try {  
      // call other interceptors  
      result = next.newCall(method, callOptions);  
    } catch (Throwable e) {  
      instrumenter.end(context, request, Status.UNKNOWN, e);  
      throw e;  
    }  }  
  return new TracingClientCall<>(result, parentContext, context, request);  
}

这个接口是
gRPC
提供的拦截器接口,对于
gRPC
客户端来说就是在发起真正的网络调用前后会执行的方法。

所以在这个接口中我们就可以实现创建 span 获取包大小等逻辑。

使用 byte-buddy 增强代码

不过有一个问题是我们实现的
io.grpc.ClientInterceptor
类需要加入到拦截器中才可以使用:

var managedChannel = ManagedChannelBuilder.forAddress(host, port) .intercept(new TracingClientInterceptor()) // 加入拦截器
.usePlaintext()
.build();

但在
javaagent
中是没法给业务代码中加上这样的代码的。

此时就需要
byte-buddy
登场了,它可以动态修改字节码从而实现类似于修改源码的效果。


io.opentelemetry.javaagent.instrumentation.grpc.v1_6.GrpcClientBuilderBuildInstr umentation
类里可以看到
OpenTelemetry
是如何使用
byte-buddy
的。

  @Override
  public ElementMatcher<TypeDescription> typeMatcher() {
    return extendsClass(named("io.grpc.ManagedChannelBuilder"))
        .and(declaresField(named("interceptors")));
  }

  @Override
  public void transform(TypeTransformer transformer) {
    transformer.applyAdviceToMethod(
        isMethod().and(named("build")),
        GrpcClientBuilderBuildInstrumentation.class.getName() + "$AddInterceptorAdvice");
  }

  @SuppressWarnings("unused")
  public static class AddInterceptorAdvice {

    @Advice.OnMethodEnter(suppress = Throwable.class)
    public static void addInterceptor(
        @Advice.This ManagedChannelBuilder<?> builder,
        @Advice.FieldValue("interceptors") List<ClientInterceptor> interceptors) {
      VirtualField<ManagedChannelBuilder<?>, Boolean> instrumented =
          VirtualField.find(ManagedChannelBuilder.class, Boolean.class);
      if (!Boolean.TRUE.equals(instrumented.get(builder))) {
        interceptors.add(0, GrpcSingletons.CLIENT_INTERCEPTOR);
        instrumented.set(builder, true);
      }
    }
  }

从这里的源码可以看出,使用了
byte-buddy
拦截了
io.grpc.ManagedChannelBuilder#intercept(java.util.List<io.grpc.ClientInterceptor>)
函数。

io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers#extendsClass/ isMethod 等函数都是 byte-buddy 库提供的函数。

而这个函数正好就是我们需要在业务代码里加入拦截器的地方。

interceptors.add(0, GrpcSingletons.CLIENT_INTERCEPTOR);
GrpcSingletons.CLIENT_INTERCEPTOR = new TracingClientInterceptor(clientInstrumenter, propagators);

通过这行代码可以手动将
OpenTelemetry
里的
TracingClientInterceptor
加入到拦截器列表中,并且作为第一个拦截器。

而这里的:

extendsClass(named("io.grpc.ManagedChannelBuilder"))
        .and(declaresField(named("interceptors")))

通过函数的名称也可以看出是为了找到 继承了
io.grpc.ManagedChannelBuilder
类中存在成员变量
interceptors
的类。

transformer.applyAdviceToMethod(  
    isMethod().and(named("build")),  
    GrpcClientBuilderBuildInstrumentation.class.getName() + "$AddInterceptorAdvice");

然后在调用
build
函数后就会进入自定义的
AddInterceptorAdvice
类,从而就可以拦截到添加拦截器的逻辑,然后把自定义的拦截器加入其中。

获取 span 的 attribute

我们在 gRPC 的链路中还可以看到这个请求的具体属性,比如:

  • gRPC 服务提供的 IP 端口。
  • 请求的响应码
  • 请求的 service 和 method
  • 线程等信息。

这些信息在问题排查过程中都是至关重要的。

可以看到这里新的
attribute
主要是分为了三类:

  • net.*
    是网络相关的属性
  • rpc.*
    是和 grpc 相关的属性
  • thread.*
    是线程相关的属性

所以理论上我们在设计 API 时最好可以将这些不同分组的属性解耦开,如果是 MQ 相关的可能还有一些 topic 等数据,所以各个属性之间是互不影响的。

带着这个思路我们来看看 gRPC 这里是如何实现的。

clientInstrumenterBuilder
	.setSpanStatusExtractor(GrpcSpanStatusExtractor.CLIENT)
	.addAttributesExtractors(additionalExtractors)
        .addAttributesExtractor(RpcClientAttributesExtractor.create(rpcAttributesGetter))
        .addAttributesExtractor(ServerAttributesExtractor.create(netClientAttributesGetter))
        .addAttributesExtractor(NetworkAttributesExtractor.create(netClientAttributesGetter))

OpenTelemetry
会提供一个
io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder#addAttributesExtractor
构建器函数,用于存放自定义的属性解析器。

从这里的源码可以看出分别传入了网络相关、RPC 相关的解析器;正好也就对应了图中的那些属性,也满足了我们刚才提到的解耦特性。

而每一个自定义属性解析器都需要实现接口
io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor

public interface AttributesExtractor<REQUEST, RESPONSE> {
}

这里我们以
GrpcRpcAttributesGetter
为例。

enum GrpcRpcAttributesGetter implements RpcAttributesGetter<GrpcRequest> {
  INSTANCE;

  @Override
  public String getSystem(GrpcRequest request) {
    return "grpc";
  }

  @Override
  @Nullable
  public String getService(GrpcRequest request) {
    String fullMethodName = request.getMethod().getFullMethodName();
    int slashIndex = fullMethodName.lastIndexOf('/');
    if (slashIndex == -1) {
      return null;
    }
    return fullMethodName.substring(0, slashIndex);
  }

可以看到 system 是写死的
grpc
,也就是对于到页面上的
rpc.system
属性。

而这里的
getService
函数则是拿来获取
rpc.service
属性的,可以看到它是通过
gRPC
的method
信息来获取
service
的。


public interface RpcAttributesGetter<REQUEST> {  
  
  @Nullable  
  String getService(REQUEST request);
}

而这里
REQUEST
其实是一个泛型,在 gRPC 里是
GrpcRequest
,在其他 RPC 里这是对应的 RPC 的数据。

这个
GrpcRequest
是在我们自定义的拦截器中创建并传递的。

而我这里需要的请求包大小也是在拦截中获取到数据然后写入进 GrpcRequest。

static <T> Long getBodySize(T message) {  
  if (message instanceof MessageLite) {  
    return (long) ((MessageLite) message).getSerializedSize();  
  } else {  
    // Message is not a protobuf message  
    return null;  
  }}

这样就可以实现不同的 RPC 中获取自己的
attribute
,同时每一组
attribute
也都是隔离的,互相解耦。

自定义 metrics

每个插件自定义 Metrics 的逻辑也是类似的,需要由框架层面提供 API 接口:

public InstrumenterBuilder<REQUEST, RESPONSE> addOperationMetrics(OperationMetrics factory) {  
  operationMetrics.add(requireNonNull(factory, "operationMetrics"));  
  return this;  
}
// 客户端的 metrics
.addOperationMetrics(RpcClientMetrics.get());

// 服务端的 metrics
.addOperationMetrics(RpcServerMetrics.get());

之后也会在框架层面回调这些自定义的
OperationMetrics
:

    if (operationListeners.length != 0) {
      // operation listeners run after span start, so that they have access to the current span
      // for capturing exemplars
      long startNanos = getNanos(startTime);
      for (int i = 0; i < operationListeners.length; i++) {
        context = operationListeners[i].onStart(context, attributes, startNanos);
      }
    }

	if (operationListeners.length != 0) {  
	  long endNanos = getNanos(endTime);  
	  for (int i = operationListeners.length - 1; i >= 0; i--) {  
	    operationListeners[i].onEnd(context, attributes, endNanos);  
	  }
	}

这其中最关键的就是两个函数 onStart 和 onEnd,分别会在当前这个 span 的开始和结束时进行回调。

所以通常的做法是在
onStart
函数中初始化数据,然后在
onEnd
结束时统计结果,最终可以拿到 metrics 所需要的数据。

以这个
rpc.client.duration
客户端的请求耗时指标为例:

@Override  
public Context onStart(Context context, Attributes startAttributes, long startNanos) {  
  return context.with(  
      RPC_CLIENT_REQUEST_METRICS_STATE,  
      new AutoValue_RpcClientMetrics_State(startAttributes, startNanos));  
}

@Override  
public void onEnd(Context context, Attributes endAttributes, long endNanos) {  
  State state = context.get(RPC_CLIENT_REQUEST_METRICS_STATE);
	Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build();  
	clientDurationHistogram.record(  
	    (endNanos - state.startTimeNanos()) / NANOS_PER_MS, attributes, context);
}

在开始时记录下当前的时间,结束时获取当前时间和结束时间的差值正好就是这个 span 的执行时间,也就是 rpc client 的处理时间。


OpenTelemetry
中绝大多数的请求时间都是这么记录的。

Golang 增强

而在
Golang
中因为没有
byte-buddy
这种魔法库的存在,不可以直接修改源码,所以通常的做法还是得硬编码才行。

还是以
gRPC
为例,我们在创建 gRPC server 时就得指定一个
OpenTelemetry
提供的函数。

s := grpc.NewServer(  
    grpc.StatsHandler(otelgrpc.NewServerHandler()),  
)

在这个 SDK 中也会实现刚才在 Java 里类似的逻辑,限于篇幅具体逻辑就不细讲了。

总结

以上就是
gRPC

OpenTelemetry
中的具体实现,主要就是在找到需要增强框架是否有提供扩展的接口,如果有就直接使用该接口进行埋点。

如果没有那就需要查看源码,找到核心逻辑,再使用
byte-buddy
进行埋点。

比如 Pulsar 并没有在客户端提供一些扩展接口,只能找到它的核心函数进行埋点。

而在具体埋点过程中
OpenTelemetry
提供了许多解耦的 API,方便我们实现埋点所需要的业务逻辑,也会在后续的文章继续分析
OpenTelemetry
的一些设计原理和核心 API 的使用。

这部分 API 的设计我觉得是
OpenTelemetry
中最值得学习的地方。

参考链接: