wenmo8 发布的文章

[!note]

本来主要是想找一下「」和『』,然后便找到了以下方法,可以实现输出大部分的特殊字符

通过输入法输出

  1. 『Ctr + shift + Z』进入搜狗输入法的『符号大全』

  2. 在『标点符号』项可以找到「」和『』


使用
AutoHotkey
自定义替换

[!note]

每次想要用到这两个符号的时候都要进入输入法的『符号大全』里面找的话,操作还是没那么方便,所以就想着找到别的更高效的方法,于是就查到了可以使用『
AutoHotkey
』这个工具进行自定义一些热键。

AutoHotkey 简介

AutoHotkey (AHK)
是一个功能强大的开源脚本语言和自动化工具,主要用于 Windows 操作系统。它通过简洁的语法和强大的功能,使用户能够快速定义快捷键、宏、文本替换规则,甚至开发功能复杂的应用程序。


主要特点

  1. 快捷键绑定

    • 定义自定义快捷键,如组合键(
      Ctrl+Alt+T
      )、鼠标按键,甚至设备专用键。
    • 适用于提高工作效率,如快速启动程序、操作窗口等。
  2. 文本替换

    • 设置特定的输入触发特定的输出,例如将
      btw
      自动替换为
      by the way
  3. 脚本自动化

    • 模拟鼠标点击、键盘输入等操作,自动完成繁琐重复的任务。
  4. 窗口管理

    • 自动调整窗口大小、位置,切换窗口,隐藏窗口等。
  5. 硬件扩展支持

    • 自定义处理特殊硬件设备(如自定义游戏键盘和鼠标按键的功能)。
  6. 轻量级与开源

    • 脚本体积小巧,运行效率高,完全免费,拥有活跃的社区支持。


常见用途

  1. 文本和脚本自动化

    • 录入经常使用的短语。
    • 快速批量修改文件名。
  2. 提高工作效率

    • 快捷打开文件、文件夹或网站。
    • 一键填写表单。
  3. 游戏辅助

    • 编写简单的辅助脚本,如自动点击、宏操作等。
  4. 窗口管理

    • 在多显示器之间快速移动窗口。
    • 根据需求排列窗口位置和大小。
  5. 开发工具

    • 定制化工具和简单程序,如定时器、弹窗工具。


基本语法

AutoHotkey 的语法简单易学。以下是一些基本示例:

  1. 快捷键绑定

    ^j::Send, Hello, AutoHotkey!
    

    解释


    • ^
      表示
      Ctrl
      键,
      ^j
      表示
      Ctrl+J
    • 按下
      Ctrl+J
      时,发送文本
      Hello, AutoHotkey!
  2. 文本替换

    ::btw::by the way
    

    解释


    • 输入
      btw
      后按空格,自动替换为
      by the way
  3. 自动化任务

    F1::
        Run, notepad.exe
        Sleep, 1000
        Send, This is an automated script.{Enter}
    Return
    

    解释



    • F1
      键时,打开记事本,等待 1 秒后输入一行文本。
  4. 窗口管理

    #z::WinMaximize, A
    

    解释


    • #
      表示
      Win
      键,
      #z
      表示
      Win+Z
    • 按下组合键时,最大化当前活动窗口。


安装与使用

  1. 下载
    : 从
    AutoHotkey 官方网站
    下载并安装。

  2. 创建脚本

  • 新建一个
    .ahk
    文件(如
    MyScript.ahk
    )。

  • 使用记事本或任意代码编辑器打开,编写脚本内容。

  • 示例:
    |-
    替换为


    -|
    替换为

    #Requires AutoHotkey v2.0
    
    ::|-::「
    ::-|::」
    
  1. 运行脚本


    • 双击
      .ahk
      文件即可运行脚本。

    • 使用托盘图标可以进行暂停、退出脚本或重新加载操作。


      [!tip]

      不知道啥是托盘图标?看下面指示

      找到对应的「图标」,点击鼠标右键就会出现「操作选项」了。

  2. 编译为可执行文件


    • 进入AutoHotKey,选择「Complie」进行编译
      .ahk
      文件


    • 选择之前写好的
      脚本
      ,生成
      .exe
      文件以「独立运行」即之后的每次运行不再需要AutoHotKey,方便设置「
      开机自启动
      」。

设置脚本开机自启动

[!tip]

为了使脚本每次开机之后都自动生效,不需要再手动开启,可以直接设置「开机自启动」,只需要将上面编辑好的.exe可执行文件放到「startup」目录下。

  1. 快速进入「startup」目录:点击Ctr+R快捷键,输入
    shell:startup
    ,回车进入


    当然也可以通过「资源管理器」一步步进入目录

  2. 将.exe文件复制到该目录下,即可


优势与局限

优势

  • 易学易用
    :简单语法,适合快速上手。
  • 功能丰富
    :从快捷键到窗口管理,几乎覆盖所有桌面操作需求。
  • 社区支持
    :拥有丰富的教程和脚本资源。

局限

  • 仅限 Windows 平台
    :无法在 macOS(可使用
    textexpander
    ) 或 Linux 上使用。
  • 高级功能需要学习
    :复杂脚本的编写可能需要掌握更深入的编程知识。
  • 与防作弊软件冲突
    :用于游戏时,可能被误判为作弊工具。


学习资源

  1. 官方文档:
  2. 社区论坛:
  3. 教程网站:


AutoHotKey介绍总结

AutoHotkey 是一款小巧但功能强大的工具,适合想要提升工作效率、自动化任务或定制系统功能的用户。


实现自定义输出特殊字符

在简单了解之后,AutoHotKey的功能完全可以实现我的需求,于是便开始根据自己目前的需要,自定义输出特殊字符。

  1. 明确替换规则

    |-
    替换成

    -|
    替换成

    |=
    替换成

    =|
    替换成

  2. 编写脚本

    #Requires AutoHotkey v2.0
    
    :*:|-::「
    :*:-|::」
    :*:|=::『
    :*:=|::』
    

    [!important]

    与一般AutoHotKey的「文本替换」脚本的小小改进:

    关键的修改是在每个替换规则前添加
    :*:
    ,这个星号的作用是告诉 AutoHotkey 在检测到触发文本时立即执行替换,而不需要等待终止符(比如空格)。

    这样做可以避免每次都要多输出一个「空格」才能进行替换。


    1. 编译成.exe


      如前面所述

    2. 设置开机自启动


      如前面所述


[!important]

「」

『』
一般都是成对出现的,所以可以进一步优化脚本:

  1. 将‘|-’替换成‘「」’,并使得替换之后,光标停留在‘「’和‘」’之间,方便输入内容;

  2. 将‘|=’替换成‘『』’,并使得替换之后,光标停留在‘『’和‘』’之间,方便输入内容。

改进之后的脚本:

#Requires AutoHotkey v2.0

:*:|-::
{
    SendInput "「」"
    SendInput "{Left}"  ; 移动光标到括号中间
}

:*:|=::
{
    SendInput "『』"
    SendInput "{Left}"  ; 移动光标到括号中间
}

; 保留单独的右括号输入
:*:-|::」
:*:=|::』

这个新版本脚本可以:

  1. 输入
    |-
    时,自动输入一对
    「」
    并将光标置于中间
  2. 输入
    |=
    时,自动输入一对
    『』
    并将光标置于中间
  3. 仍然保留了单独输入右括号的功能(使用
    -|

    =|

使用效果:

  • 输入
    |-
    → 得到
    「|」
    (|表示光标位置)
  • 输入
    |=
    → 得到
    『|』
    (|表示光标位置)
  • 输入
    -|
    → 得到
  • 输入
    =|
    → 得到


当然,我这么用似乎有点大材小用了,AutoHotKey的强大功能可以实现更多功能,帮助提高效率,更多有趣用法等待被探索。

附:「
AutoHotkey 效率提升脚本集

小感慨

没想到会因为先前的一个「特殊字符输出」问题,而最后学到了使用「AutoHotKey」这个工具,感觉很有收获。
似乎是对现状的不满足,推动自己去探索,去思考,最后有所收获。

着色器语言编程比较重要,后面的几个章节都会围绕这个来做特效

一.初识着色器语言

首先什么叫做着色器,他是一种语言,首先需要设置为着色器材质,然后在材质里面书写一些语言,可以告诉他顶点,然后去自定义一些东西,比如我想要这一面为红色等等

image-20241225223903619

比如用一个基础材质做了一个平面

image-20241225223947852

image-20241225223935800

现在改为着色器材质

着色器里面一个顶点着色器告诉顶点位置,gl只能支持四维向量,vec4表示四维向量,position是有material自带的position直接拿过来的,最后一个1是第四个顶点

下面一个事片源着色器,负责告诉这个顶点要干嘛,比如我要修改颜色,也是一个四维向量rgba并且是浮点数,这个颜色就表示红色

image-20241225224452718

image-20241225224819496

此时呢颜色就出来了,但是并不会跟着坐标轴转换

image-20241225224839921

此时需要将顶点位置进行转换,这个公式也是固定的

image-20241225225018167

image-20241225225036810

此时就能随便动了

image-20241225225054818

1.1 插件安装与文件导入开发

当然这个语言有时候可能会写很多,在一个模板字符串里面就比较麻烦,没有任何代码提示,而且也不会报错

所以一般我们都会这么来

image-20241225230556176

image-20241225230607593

这边导入并使用

image-20241225230743065

这里解决了些东西弄了半天,反正vite不支持直接这样分包导入

如果一定要这样需要下载并配置

image-20241225233419320

image-20241225233429249

可以安装的插件,第一个对glsl语言的支持不再是字符串,第二个是格式化并代码提示的

QQ_1735180948347

1.2 原始着色器材质(RawShaderMaterial)

原始着色器和之前的着色器材质用法基本类似,唯一的区别就是,着色器材质很多都是可以默认直接拿mesh里面的比如position,但是原始着色器材质,所有的矩阵的属性都需要声明

image-20241226134211431

QQ_1735191745820

到这里就可以实现之前着色器一样的效果,初次之外还有一些别的设置

还有一个属性uv,他表示的是顶点,二维属性那就是二维平面的顶点

QQ_1735192410314

image-20241226140049591

QQ_1735193116068

QQ_1735193132990

1.3 控制顶点类型改变波浪形状

到目前为止好像也没有看出这个材质特殊性在哪

我们可以把之前顶点着色器代表位置的参数领出来作为一个变量,可以去单独设置他的x轴z轴等

需要注意的是:这里的position每个轴的范围是-1到1,参考之前的uv顶点,也就是x轴是越来越大的,所以如果你用z轴去加x轴,就会变成这个效果,细品

QQ_1735195001880

QQ_1735195015282

这里提供一个sin函数

GIF 2024-12-26 14-46-48

当然这是横着的波浪如果要竖着那就是对应还是z轴加y轴

image-20241226145323166

image-20241226145335831

然后还可以来一个例子可以根据不同的凸出起来的高度设置暗度不同的颜色,所以首先我要拿到z轴

顶点这边声明可以交换的属性并且把z轴给上去

image-20241226150436920

顶点拿过来的值不支持修改,如果要改,那就拿过来在片段着色器里面改,声明一个变量需要声明是什么类型的值,这里只是为了让范围在0-1

一定要注意分号每一行都要加分号

image-20241226152249705

image-20241226152326834

1.4 uniform传递变量

我们可以让这个波浪动起来,既然要动起来就可以让z轴加一些动态的数据,其实就是时间就可以了

首先我们材质这里是可以传值进来的

image-20241226153319996

然后在动画函数里面,之前拿到过时间的函数,这个函数就是从进入3D一直以来的秒数

QQ_1735198778424

QQ_1735198828492

GIF 2024-12-26 15-40-51

1.5 uv纹理

如果有一张图,比如冬奥会的旗帜是否可以让他实现彩旗飘飘的效果,这里需要用到uv纹理贴图

大概步骤是,加载贴图,然后跟刚才一样通过变量传进来

QQ_1735200983415

这边接收注意有专门的类型,根据顶点去除对应的颜色,刚好是四个点,图片也是四个点

QQ_1735201429309

GIF 2024-12-26 16-24-08

二.着色器编写各类型图案

2.1 上部分

首先回到最开始的状态,用uv作为四个顶点的颜色的状态

image-20241226215305833

对于之前uv的应用

image-20241226215543190

利用uv实现渐变效果

image-20241226220355777

image-20241226220402770

mod取余函数

GIF

step函数

实现斑马效果,注意上面mod的结果0-1之间

当然如果是对vUv.x那就是垂直条纹

image-20241226221559868

image-20241226221617113

条纹还可以相加,x轴弄完弄y轴

image-20241226221913449

image-20241226221922462

当然加减乘除其实都是可以的,分别可以完成不同的图案

乘的话就是类似于一个正方形

只有1的位置才是白色,因为值设置的很大0.8,所以1的很少

image-20241226223542851

image-20241226223620628

如果设置的很小大多数都可以为1

image-20241226223835056

image-20241226223843140

相减

image-20241226223719237

如果相乘,值有些偏差此时可以形成一个条状效果

image-20241226223942543

image-20241226223957783

那我如果来两个条带相加呢

image-20241226224024252

image-20241226224033551

T型图,也就是设置偏移量,乘了10之后,减一些值

image-20241226224404855

image-20241226224421073

利用绝对值
abs函数

首先这里可以理解下

此时表示0-0.5,为什么以为vUv的值是0-1,减去0.5后,那就是-0.5-0.5,但是颜色rgba不支持负数,所以x轴就只能从0-0.5开始变,小于0的部分就都是黑色

image-20241226224710347

image-20241226224845318

那如果我想实现一个从白到黑再到白的效果,那就要用到绝对值

image-20241226224923369

image-20241226224936658

2.2 下部分

min函数

实现十字交叉效果

image-20241226225208839

image-20241226225222850

同理把min改为max

image-20241226225251105

利用step函数

外围是1,里面是0

想反过来前面加个1.0-即可

image-20241226225435481

image-20241226225417500

floor函数向下取整

同方法可以用向上取整ceil就是现在的图案白的更多一点

也可以实现之前的条纹渐变

x*10范围在0-9.9999向下取整后那就是0-9再除以10那就是0-1左右

image-20241226225814496

image-20241226225931417

既然说floor可以实现渐变,之前也说乘可以实现格子效果,那么

QQ_1735265346083

QQ_1735265368197

随机值

glsl本身没有随机函数,这里推荐一个网站

https://thebookofshaders.com/

类似于glsl的文档,里面有一些关于随机怎么生成的使用,直接拿过来

QQ_1735265999923

QQ_1735266077787

QQ_1735266086429

同理改造下,让每个网格随机

QQ_1735266186919

QQ_1735266304481

length函数

通过length函数得到uv变量长度给到color可以实现一个半径的扩展渐变效果

image-20241228125345301

image-20241228125358635

distance函数,两个向量之间的距离

比如上面那个图案我不想以圆弧向外扩进,想以中心远点扩展,那我就可以计算顶点到0.5 0.5的距离,0.5 0.5就是中心原点

要反过来颜色,就1-

image-20241228130720259

image-20241228130728307

除法

前面加减乘都是有了对应的效果,由刚才的效果可知,越中间,和原点的距离是越短的值肯定就越小,那么一个值除以小值结果反而越大,除以大值结果反而越小,所以除法之后,中间越大,就越亮接近白色

image-20241228135225333

image-20241228135237936

那如果此时减去一个1.0,周围可能会使负数,之前也看过负数也是黑色,然后光圈周围会越来越胆,最后只剩下比1大的还保留颜色

image-20241228135426185

image-20241228135437038

设置uVu

我们可以对uVu进行直接设置,乘就是拉伸

image-20241228135712551

image-20241228135721140

加减就是偏移

image-20241228135756624

image-20241228135804383

套用十字交叉效果

image-20241228135959173

image-20241228140009333

当然也可以直接相加

image-20241228140029918

image-20241228140041867

如果你想要这个图案旋转起来

也是一样的需要去找一些能实现的函数

image-20241228140409393

三个参数一个是uv,一个是旋转角度,一个是中心原点位置

旋转角度,也没有pi可以使用这里就用3.14代替,*0.25就表示180度,向量如果都是0.5可以只写一个

image-20241228140627878

image-20241228140634115

如果要动起来传一个时间属性就可以了

image-20241228140658811

GIF

三.shader着色器编写高级图案

3.1 上部分

实现圆形

顶点到原点的距离扩散,但是同时给个限定要么黑要么白

image-20241228142825202

image-20241228142832500

如果你想让黑色再小点,也就是偏移往回走

image-20241228142916330

image-20241228142921522

实现圆环效果,也就是里面再来个圆,一个偏移量大一个小,然后乘起来

image-20241228143301333

image-20241228143318595

当然实现光圈的效果不止这一种比如还有

image-20241228145551435

实现波浪环效果

就是把顶点的x和y进行改造,加上sin角度,放入刚才环形里面

image-20241228150308837

image-20241228150314489

image-20241228150332109

image-20241228150337452

实现雷达效果

需要用到一个
atan
反正切函数

先简单设置一个角度

image-20241228151442258

然后就可以得到这么一个图案,从左下顶点开始沿着一个顺时针,颜色变淡

image-20241228153020331

调整一下从中心原点开始,不从左下角开始

image-20241228153523283

image-20241228153530157

但是现在角度有点问题,左边部分是小于1减去0.5还是负数所以是黑色,这里有个公式

image-20241228154800692

image-20241228154806994

然后用刚才实现圆环的效果,给到透明度

image-20241228154859489

image-20241228154907112

或者也可以用刚才实现圆的效果

image-20241228154939964

image-20241228154950874

image-20241228155008519

实现动态效果,就用刚才的旋转加时间方法拿来给到uv

image-20241228155155327

GIF

万花筒效果

角度不变但是使用角度乘以一个数(决定到时候万花筒有多少壁),再对6.28取余,6.28就是2π,就是一圈

image-20241228155924306

image-20241228160045041

或者也可以先把角度除了6.28

image-20241228160129215

image-20241228160136266

当然也可以在最上面定义好一个π,
注意定义的方式

image-20241228160213840

3.2 下部分

分享一个网站https://www.shadertoy.com/,国内外大佬分享着色器成果的地方

噪声效果

可以在文档里面看到,一种随机效果,可以实现云,沙滩等效果

image-20241228160636320

image-20241228160651117

实现波纹效果

文档直接复制其函数

image-20241228163336472

直接用上来还不行还需要配合分段函数来

image-20241228163712126

image-20241228163717891

sin实现波纹效果

本身直接加sin是很模糊的

image-20241228165849092

image-20241228165855212

image-20241228165914593

image-20241228165920279

现在想实现一个效果除了这些颜色在混合一点颜色进来,还有一个函数叫做混合mix

黑加黄,第三个参数就是第三种颜色,就是黑

image-20241228170140830

image-20241228170144742

image-20241228170343901

image-20241228170356423

融入波纹给到比例即可

image-20241228170502466

image-20241228170506535

剩下漫天孔明灯效果

怎么在 Linux 下运行 smart_rtmpd

操作系统的准备

我们知道比较流行的 Linux 操作系统基本上分为两类,一类是以 Redhat 为基线的 Redhat, CentOS;另一类是 Debian 为基线的 Debian,Ubuntu。当然现在还有一些新兴势力 Arch Linux,但大家都是基于 Linux 内核进行封装运作的。主要表现是包管理器不同,面向的用户场景不同,有的往桌面方面发展,有的往嵌入式方向发展。这些系统大家可以根据自己的喜好进行选择,本文不做过多阐述,差异不大。建议大家采用操作系统最新版本,这样系统性能更好,功能更强大,兼容性更好,性能也是最佳。

smart_rtmpd 服务器软件的准备

  1. 软件的下载
    访问
    https://github.com/superconvert/smart_rtmpd
    , 下载
    https://github.com/superconvert/smart_rtmpd/blob/master/rtmpd.zip

    https://github.com/superconvert/smart_rtmpd/releases
    访问
    https://gitee.com/superconvert/smart_rtmpd
    , 下载
    https://gitee.com/superconvert/smart_rtmpd/blob/master/rtmpd.zip

  2. 软件包的介绍
    下载的软件包 rtmpd.zip 里面包含不同平台的安装运行包

    smart_rtmpd.coroutines.centos7.7.1908.x64.tar.gz        CentOS 系统的协程版本 (x64)
    smart_rtmpd.multithread.centos7.7.1908.x64.tar.gz       CentOS 系统的多线程版本 (x64) --- 推荐
    smart_rtmpd.coroutines.ubuntu16.04LTS.x64.tar.gz        Ubuntu 系统的协程版本 (x64)
    smart_rtmpd.multithread.ubuntu16.04LTS.x64.tar.gz       Ubuntu 系统的多线程版本 (x64)
    smart_rtmpd.multithread.generic.aarch64.tar.gz          ARM-v8 架构的多线程版本 (arm)
    
  3. 软件包的准备
    我们下载 rtmpd.zip 后进行解压,把 smart_rtmpd.multithread.centos7.7.1908.x64.tar.gz 上传到我们的 Linux 操作系统内,进行解压

    [root@localhost ~]# mkdir smart_rtmpd
    [root@localhost ~]# tar zxvf smart_rtmpd.multithread.centos7.7.1908.x64.tar.gz -C smart_rtmpd
    [root@localhost ~]# cd smart_rtmpd
    [root@localhost smart_rtmpd]# ls -alh
    total 8.0M
    drwxr-xr-x. 3 root root  167 Aug 15 05:34 .
    drwxr-xr-x. 4 root root   28 Aug 15 05:34 ..
    -rw-r--r--. 1 root root 9.7K Aug 14 18:12 config.xml
    -rw-r--r--. 1 root root  378 Jul 23 12:24 gb28181.xml
    drwxr-xr-x. 4 root root  100 Aug 15 03:43 html
    -rw-r--r--. 1 root root  234 Aug 17  2021 ice_server.json
    -rw-r--r--. 1 root root  173 Feb 17  2023 mime.xml
    -rw-r--r--. 1 root root 2.8K Jul  4  2021 policy.xml
    -rw-r--r--. 1 root root 1.6K Aug  9  2021 server.crt
    -rw-r--r--. 1 root root 1.7K Aug  9  2021 server.key
    -rwxr-xr-x. 1 root root 8.0M Aug 15 04:08 smart_rtmpd    // 确保 smart_rtmpd 是可执行的
    

    为什么选用 smart_rtmpd.multithread.centos7.7.1908.x64.tar.gz 版本,这个应该是通用版本,应该适用用大部分 Linux 系统,无论是 CentOS, Ubuntun, Arch Linux

  4. 运行 smart_rtmpd
    我们准备运行 smart_rtmpd 流媒体服务器

    问题1 :smart_rtmpd 没有可执行权限,通过 chmod 赋权
    [root@localhost smart_rtmpd]# ./smart_rtmpd
    -bash: ./smart_rtmpd: Permission denied                 // 大部分这种情况是 smart_rtmpd 拷贝复制过程中丢失了可执行权限
    [root@localhost smart_rtmpd]# chmod +x ./smart_rtmpd
    
    问题2 :端口被其它程序占用,需要通过 config.xml 更改端口,或者停止其它的程序
    [root@localhost bin]# ./smart_rtmpd 
    EAB7D740 [24-12-31 14:15:02.161] I: smart_rtmpd --- build time : 2024-08-15 04:06:58
    EAB7D740 [24-12-31 14:15:02.161] I: website url : http://www.qiyicc.com/download/rtmpd.zip
    EAB7D740 [24-12-31 14:15:02.161] I: gitee url : https://gitee.com/mirrors/smart-rtmpd
    EAB7D740 [24-12-31 14:15:02.161] I: github url : https://github.com/superconvert/smart_rtmpd
    EAB7D740 [24-12-31 14:15:02.161] I: liveshow url : https://github.com/superconvert/smart_rtmpd/blob/master/liveshow.md
    EAB7D740 [24-12-31 14:15:02.161] I: development url : https://github.com/superconvert/smart_rtmpd/blob/master/web_dev.md
    EAB7D740 [24-12-31 14:15:02.161] I: examples url : https://github.com/superconvert/smart_rtmpd/tree/master/example
    EAB7D740 [24-12-31 14:15:02.161] I: email : cwf12345@sina.com
    EAB7D740 [24-12-31 14:15:02.161] I: webchat : 99766553, qq : 99766553
    EAB7D740 [24-12-31 14:15:02.161] I: 192.168.161.136 ens33
    EAB7D740 [24-12-31 14:15:02.221] I: ssl client no config.
    EAB7D740 [24-12-31 14:15:02.221] I: ssl server no config.
    EAB7D740 [24-12-31 14:15:02.306] I: the rtmp server ip: 0.0.0.0, port is: 1935
    EAB7D740 [24-12-31 14:15:04.101] I: the http server ip: 0.0.0.0, port is: 8080
    generating new self-signed cert for smart_rtmpd@qiyicc.com
    EAB7D740 [24-12-31 14:15:04.125] I: rtc-manager build self-signed certificate.
    EAB7D740 [24-12-31 14:15:04.125] I: the rtsp server ip: 0.0.0.0, port is: 8554
    EAB7D740 [24-12-31 14:15:04.345] I: the srt server ip: 0.0.0.0, port is: 9000
    EAB7D740 [24-12-31 14:15:04.346] I: srt bind socket failed(Connection setup failure: unable to create/configure SRT socket)  // 端口被别的程序占用了
    EAB7D740 [24-12-31 14:15:04.346] I: srt bind socket failed.
    EAB7D740 [24-12-31 14:15:04.449] I: the rtmp server stop
    EAB7D740 [24-12-31 14:15:04.597] I: the http server stop
    EAB7D740 [24-12-31 14:15:04.597] I: the rtsp server stop
    
    正常运行日志打印
    [root@localhost bin]# ./smart_rtmpd 
    48AEE740 [24-12-31 13:40:10.361] I: smart_rtmpd --- build time : 2024-08-15 04:06:58
    48AEE740 [24-12-31 13:40:10.361] I: website url : http://www.qiyicc.com/download/rtmpd.zip
    48AEE740 [24-12-31 13:40:10.361] I: gitee url : https://gitee.com/mirrors/smart-rtmpd
    48AEE740 [24-12-31 13:40:10.361] I: github url : https://github.com/superconvert/smart_rtmpd
    48AEE740 [24-12-31 13:40:10.361] I: liveshow url : https://github.com/superconvert/smart_rtmpd/blob/master/liveshow.md
    48AEE740 [24-12-31 13:40:10.361] I: development url : https://github.com/superconvert/smart_rtmpd/blob/master/web_dev.md
    48AEE740 [24-12-31 13:40:10.361] I: examples url : https://github.com/superconvert/smart_rtmpd/tree/master/example
    48AEE740 [24-12-31 13:40:10.361] I: email : cwf12345@sina.com
    48AEE740 [24-12-31 13:40:10.361] I: webchat : 99766553, qq : 99766553
    48AEE740 [24-12-31 13:40:10.361] I: 192.168.1.102 ens33                               // 我们的服务器地址
    48AEE740 [24-12-31 13:40:10.404] I: ssl client no config.
    48AEE740 [24-12-31 13:40:10.404] I: ssl server no config.
    48AEE740 [24-12-31 13:40:10.528] I: the rtmp server ip: 0.0.0.0, port is: 1935
    48AEE740 [24-12-31 13:40:11.097] I: the http server ip: 0.0.0.0, port is: 8080
    generating new self-signed cert for smart_rtmpd@qiyicc.com
    48AEE740 [24-12-31 13:40:11.366] I: rtc-manager build self-signed certificate.
    48AEE740 [24-12-31 13:40:11.594] I: the rtsp server ip: 0.0.0.0, port is: 8554
    48AEE740 [24-12-31 13:40:11.972] I: the srt server ip: 0.0.0.0, port is: 9000
    48AEE740 [24-12-31 13:40:12.004] I: the sip server ip: 0.0.0.0, port is: 5060
    48AEE740 [24-12-31 13:40:12.244] I: the ims server ip: 0.0.0.0, port is: 6666
    

    这样 smart_rtmpd 就表示正确运行起来了!!!

推流拉流的验证

  1. 推流例子
    RTMP 推流

    ffmpeg -re -stream_loop -1 -i 33.mp4 -vcodec libx264 -acodec aac -f flv rtmp://192.168.1.102:1935/rec/stream
    

    RTMP 推流 ( HEVC )

    ffmpeg -re -i 1.mp4 -c:a copy -c:v libx265 -f flv rtmp://192.168.1.102:1935/live/hevc
    

    怎么让 ffmpeg 支持 hevc 推流 怎么编译 ffmpeg 让其支持 hevc(h265) - superconvert的个人空间 - OSCHINA - 中文开源技术交流社区

    RTSP 推流

    ffmpeg -re -stream_loop -1 -i 1.mp4 -vcodec libx264 -acodec aac -f rtsp rtsp://192.168.1.102:8554/live/stream
    

    RTSP 推流 ( HEVC )

    ffmpeg -re -stream_loop -1 -i video-h265.mkv -vcodec libx265 -acodec aac -f rtsp rtsp://192.168.1.102:8554/live/stream
    

    SRT 推流

    ffmpeg -re -i 22.mp4 -vcodec libx264 -acodec aac -f mpegts srt://192.168.1.102:9000?streamid=192.168.1.102:9000/live/stream,role=publisher
    

    SRT 推流 ( HEVC )

    ffmpeg -stream_loop -1 -re -i video-h265.mkv -vcodec libx265 -acodec aac -f mpegts srt://192.168.1.102:9000?streamid=192.168.1.102:9000/live/stream,role=publisher
    
  2. 拉流例子
    RTMP 拉流

    ffplay rtmp://192.168.1.102:1935/live/stream
    

    HTTP-HLS 拉流

    ffplay http://192.168.1.102:8080/live/stream.m3u8
    

    HTTP-DASH 拉流

    ffplay http://192.168.1.102:8080/live/stream.mpd
    

    RTSP 拉流

    ffplay rtsp://192.168.1.102:8554/live/stream
    

    SRT 拉流 ( ffplay )

    ffplay srt://192.168.1.102:9000?streamid=192.168.1.102:9000/live/stream,role=player
    

更多的推拉流例子请参阅博客
https://blog.csdn.net/freeabc/article/details/117403471?spm=1001.2014.3001.5501
对于支持 Enhanced-rtmp 的 HEVC 推流,需要使用 smart_rtmpd 的收费版本,目前 ffmpeg 从版本 6.1 开始就支持 Enhanced-rtmp 的 HEVC 推流和拉流!!!

  1. smart_rtmpd 的日志查看
    [root@localhost smart_rtmpd]# ./smart_rtmpd 
    4F136740 [24-12-31 13:58:36.864] I: smart_rtmpd --- build time : 2024-08-15 04:06:58
    4F136740 [24-12-31 13:58:36.864] I: website url : http://www.qiyicc.com/download/rtmpd.zip
    4F136740 [24-12-31 13:58:36.864] I: gitee url : https://gitee.com/mirrors/smart-rtmpd
    4F136740 [24-12-31 13:58:36.864] I: github url : https://github.com/superconvert/smart_rtmpd
    4F136740 [24-12-31 13:58:36.864] I: liveshow url : https://github.com/superconvert/smart_rtmpd/blob/master/liveshow.md
    4F136740 [24-12-31 13:58:36.864] I: development url : https://github.com/superconvert/smart_rtmpd/blob/master/web_dev.md
    4F136740 [24-12-31 13:58:36.864] I: examples url : https://github.com/superconvert/smart_rtmpd/tree/master/example
    4F136740 [24-12-31 13:58:36.864] I: email : cwf12345@sina.com
    4F136740 [24-12-31 13:58:36.864] I: webchat : 99766553, qq : 99766553
    4F136740 [24-12-31 13:58:36.864] I: 192.168.161.136 ens33
    4F136740 [24-12-31 13:58:36.892] I: ssl client no config.
    4F136740 [24-12-31 13:58:36.893] I: ssl server no config.
    4F136740 [24-12-31 13:58:37.027] I: the rtmp server ip: 0.0.0.0, port is: 1935
    4F136740 [24-12-31 13:58:37.589] I: the http server ip: 0.0.0.0, port is: 8080
    generating new self-signed cert for smart_rtmpd@qiyicc.com
    4F136740 [24-12-31 13:58:37.601] I: rtc-manager build self-signed certificate.
    4F136740 [24-12-31 13:58:37.601] I: the rtsp server ip: 0.0.0.0, port is: 8554
    4F136740 [24-12-31 13:58:37.852] I: the srt server ip: 0.0.0.0, port is: 9000
    4F136740 [24-12-31 13:58:37.882] I: the sip server ip: 0.0.0.0, port is: 5060
    4F136740 [24-12-31 13:58:38.169] I: the ims server ip: 0.0.0.0, port is: 6666
    1FFFF700 [24-12-31 13:59:34.490] I: rtmp-none(obj: 0x7f8534000900, ip: 192.168.161.1:60252) set in_chunk_size to 60000
    1FFFF700 [24-12-31 13:59:34.491] I: rtmp-publisher(obj: 0x7f8534000900, ip: 192.168.161.1:60252) stream connected.
    1FFFF700 [24-12-31 13:59:34.491] I: rtmp-manager add application(obj: 0x7f8518001e40, url: rtmp://192.168.161.136/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) add http-hls(0x7f8518004de0)
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) add http-dash(0x7f8518005f30)
    1FFFF700 [24-12-31 13:59:34.507] I: rtsp-manager add application(obj: 0x7f851800bd60, url: rtsp://192.168.161.136:8554/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: rtsp-manager add publisher(obj: 0x7f851800b480, url: rtsp://192.168.161.136:8554/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) add rtsp(0x7f851800b480)
    1FFFF700 [24-12-31 13:59:34.507] I: srt-manager add application(obj: 0x7f851800f950, url: srt://192.168.161.136:9000/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: srt-manager add publisher(obj: 0x7f851800e610, url: srt://192.168.161.136:9000/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) add srt(0x7f851800e610)
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-manager add publisher(obj: 0x7f8534000900, url: rtmp://192.168.161.136/live/stream)!
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-publisher(obj: 0x7f8534000900, ip: 192.168.161.1:60252) build elapse 142 ms.
    1FFFF700 [24-12-31 13:59:34.507] I: rtmp-publisher(obj: 0x7f8534000900, ip: 192.168.161.1:60252) amf_cmd unsupport _checkbw
    1FFFF700 [24-12-31 13:59:34.548] I: rtmp-publisher(obj: 0x7f8534000900, ip: 192.168.161.1:60252) publish stream
    4D4F8700 [24-12-31 13:59:34.616] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) inital video(h264 size:1000x562 fps:15)
    4D4F8700 [24-12-31 13:59:34.673] I: rtmp-app(obj:0x7f8518001e40, url: rtmp://192.168.161.136/live/stream) inital audio(aac sample:44100 channel:2 bit:16)     // 推流成功
    3D7FA700 [24-12-31 13:59:51.374] I: rtmp-none(obj: 0x7f85340211d0, ip: 192.168.161.1:60259) amf_cmd unsupport _checkbw
    3D7FA700 [24-12-31 13:59:51.414] I: rtmp-manager add player(obj: 0x7f85340211d0, url: rtmp://192.168.161.136/live/stream)!
    3D7FA700 [24-12-31 13:59:51.414] I: rtmp-player(obj: 0x7f85340211d0, ip: 192.168.161.1:60259) build elapse 84 ms
    3D7FA700 [24-12-31 13:59:51.414] I: rtmp-player(obj: 0x7f85340211d0, ip: 192.168.161.1:60259) play stream
    4D4F8700 [24-12-31 13:59:52.158] I: rtmp-player(obj: 0x7f85340211d0, ip: 192.168.161.1:60259) inital video meta(sps)                                          // 拉流成功
    

收尾工作

通过上述工作,我们的 smart_rtmpd 流媒体服务器能正常工作了,让我们来真正运行它吧,是不是操作特别简单,下载,解压,运行即可!

[root@localhost smart_rtmpd]# ./smart_rtmpd -d                                     // 参数 -d 表示以后台模式运行
[root@localhost smart_rtmpd]# tail -f log/20241231.log                             // 可以查看日志

Good luck!

-- 题图:苏州周庄古镇双桥

2024 年的最后一天,照旧写个年终总结。今年工作上稳步发挥,但是在生活上收获了一个新的爱好,大家可能知道,痞子衡比较爱运动,一直有在打篮球羽毛球桌球。有感于公司乒乓球文化浓厚,也为了挑战一下自己,所以今年从零开始发展了乒乓球爱好,人到中年,想学好学精一个新技能真没那么容易。在公司活动室购置了发球机之后,痞子衡一整个夏天的中午几乎都会去小练一会,来培养手感球感。明年要继续努力,希望球技能上一台阶,这种水平正处于上升期的爱好确实挺让人入迷,好久没有这种感觉了。

说回正题,今年一共写了 61 篇原创(有两篇还没同步到下图 CSDN 创作历程上),产量相比去年稍有回升,但是离巅峰 2020/2021 仍然有较大差距,相信明年文章产量会进一步提升,因为痞子衡找到了新方向(先卖个关子,文末再揭秘)。

从文章组成来看,
嵌入式半月刊
最多,今年出了 27 期(今年份额已经达标,但是要填去年亏空那还少几期,目前累计已出到 115 期);其次是
i.MXRT微控制器知识
有 20 篇,再底下分别是 5 篇
嵌入式基础知识
、 4 篇
职场感悟
、3 篇
MCUXpresso软件教程
、2 篇
友商微控制器知识

2024 年,是痞子衡开通微信公众号的第八个年头,今年新增读者比去年增长稍多(超过6000人),主要得益于 11、12 月的两次互推。以前都讲公众号流量属于私域,现在看来这句话不一定对了,如果文章能得到官方平台推荐,那阅读量大部分是来自于公共流量池,那么小号也能斩获单篇 1W/5W/10W+ 阅读量的机会,所以互推也不再是简单的读者群体互换,更主要是去博官方推荐机会。其他平台阅读统计数据如下:

博客园总阅读量 84 万,读者数 571,排名 898
CSDN总阅读量 58.9 万,读者数 2306,排名 4531
知乎总阅读量 16.3 万,读者数 2110,排名 N/A
51CTO总阅读量 363 万,读者数 38,排名 N/A
掘金总阅读量 3.3 万,读者数 15,排名 N/A
电子技术应用AET总阅读量 18.6 万,读者数 N/A,排名 N/A
电源网/星球号总阅读量 3.6 万,读者数 41,排名 N/A

2024 年,痞子衡在全网没有新入驻平台,甚至都没有新的平台运营和痞子衡接洽,看起来经济形势一般,大家都节衣缩食了,活下去才是最重要的。不过这里有个例外,就是 21IC 平台想在论坛文章质量这一块重点发力,他们据说找了一批高质量原创作者试水,至于效果拭目以待了。除此以外,1 月下旬拿到了
2023年度电子星球(eestar)黑马作者
;2 月初得知再次入选
2023年度与非网(eefocus)最佳创作者Top15
;11 月下旬继续在
华邦电子&恩智浦2024联合技术论坛担任演讲嘉宾

展望 2025 年,除了已有的一些文章主题外,痞子衡会重点在如下两个主题方向上多发力,尤其是 Zephyr 这一块。单片机领域发展这么多年,一直都是各自为战,甚至 RTOS 都是百花齐放。痞子衡其实一直期待能出现一个类似 Linux 那样的终结者,将所有智慧全部凝聚在一起,从 Zephyr 本身架构设计以及社区活跃度角度来看,它很有希望。

VS Code之嵌入式开发
Zephyr物联网操作系统

前几天痞子衡随便写了一篇关于 VS Code 做嵌入式开发的主题文章,阅读量竟然出奇地高,这说明时代变了,软件生态也不再是以前那样由某几个头部原厂来主导了,大家都希望有更开源、更通用的工具/技术出现,这就是痞子衡在文章开头提到的新的创作方向。雷总说了,风口之上猪都会飞,咱们创作者也要紧跟热点不是!

最后还是那句话,在你的嵌入式世界里给痞子衡留个位置,让痞子衡陪你度过这平凡而又精彩的技术生涯。

欢迎订阅

文章会同时发布到我的
博客园

CSDN

微信公众号

知乎

与非网

电子技术应用AET

电子星球

51CTO
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

我们是
袋鼠云数栈 UED 团队
,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

前言

对于 ref 的理解,我们一部人还停留在用 ref 获取真实 dom 元素和获取组件层面上,但实际 ref 除了这两项功能之外,在使用上还有很多小技巧。本章我们就一起深入探讨研究一下 React ref 的用法和原理;本章中所有的源码节选来自 16.8 版本

基本概念和使用

此部分将分成两个部分去分析,第一部分是 ref 对象的创建,第二部分是 React 本身对 ref 的处理;两者不要混为一谈,所谓 ref 对象的创建,就是通过 React.createRef 或者 React.useRef 来创建一个 ref 原始对象。而 React 对 ref 处理,主要指的是对于标签中 ref 属性,React 是如何处理以及 React 转发 ref 。下面来仔细介绍一下。

ref 对象的创建

什么是 ref ?

所谓 ref 对象就是用 createRef 或者 useRef 创建出来的对象,一个标准的 ref 对象应该是如下的样子:

{
  current: null, // current指向ref对象获取到的实际内容,可以是dom元素,组件实例。
}

React 提供两种方法创建 ref 对象

类组件 React.createRef

class Index extends React.Component{
    constructor(props){
       super(props)
       this.currentDom = React.createRef(null)
    }
    componentDidMount(){
        console.log(this.currentDom)
    }
    render= () => <div ref={ this.currentDom } >ref对象模式获取元素或组件</div>
}

打印

file

React.createRef 的底层逻辑很简单。下面一起来看一下:

react/src/ReactCreateRef.js

export function createRef() {
  const refObject = {
    current: null,
  }
  return refObject;
}

createRef 一般用于类组件创建 Ref 对象,可以将 Ref 对象绑定在类组件实例上,这样更方便后续操作 Ref。
注意:不要在函数组件中使用 createRef,否则会造成 Ref 对象内容丢失等情况。

函数组件

函数组件创建 ref ,可以用 hooks 中的 useRef 来达到同样的效果。

export default function Index(){
    const currentDom = React.useRef(null)
    React.useEffect(()=>{
        console.log( currentDom.current ) // div
    },[])
    return  <div ref={ currentDom } >ref对象模式获取元素或组件</div>
}

react-reconciler/ReactFiberHooks.js

function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
这个 ref 对象只有一个current属性,你把一个东西保存在内,它的地址一直不会变。

拓展和总结

useRef 底层逻辑是和 createRef 差不多,就是 ref 保存位置不相同,类组件有一个实例 instance 能够维护像 ref 这种信息,但是由于函数组件每次更新都是一次新的开始,所有变量重新声明,所以 useRef 不能像 createRef 把 ref 对象直接暴露出去,如果这样每一次函数组件执行就会重新声明 ref,此时 ref 就会随着函数组件执行被重置,这就解释了在函数组件中为什么不能用 createRef 的原因。
为了解决这个问题,hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来。

react 对 ref 属性的处理-标记 ref

首先我们先明确一个问题就是 DOM 元素和组件实例必须用 ref 来获取吗?答案肯定是否定的,比如 react 还提供了一个 findDOMNode 方法可以获取 dom 元素,有兴趣的可以私下去了解一下。不过通过 ref 的方式来获取还是最常用的一种方式。

类组件获取 ref 二种方式

因为 ref 属性是字符串的这种方式,react 高版本已经舍弃掉,这里就不再介绍了。

ref 属性是个函数

class Children extends React.Component{  
    render=()=><div>hello,world</div>
}
/* TODO: Ref属性是一个函数 */
export default class Index extends React.Component{
    currentDom = null
    currentComponentInstance = null
    componentDidMount(){
        console.log(this.currentDom)
        console.log(this.currentComponentInstance)
    }
    render=()=> <div>
        <div ref={(node)=> this.currentDom = node }  >Ref属性是个函数</div>
        <Children ref={(node) => this.currentComponentInstance = node  }  />
    </div>
}

打印

file

当用一个函数来标记 ref 的时候,将作为 callback 形式,等到真实 DOM 创建阶段,执行 callback ,获取的 DOM 元素或组件实例,将以回调函数第一个参数形式传入,所以可以像上述代码片段中,用组件实例下的属性 currentDom 和 currentComponentInstance 来接收真实 DOM 和组件实例。

ref 属性是一个 ref 对象

class Children extends React.Component{  
    render=()=><div>hello,world</div>
}
export default class Index extends React.Component{
    currentDom = React.createRef(null)
    currentComponentInstance = React.createRef(null)
    componentDidMount(){
        console.log(this.currentDom)
        console.log(this.currentComponentInstance)
    }
    render=()=> <div>
         <div ref={ this.currentDom }  >Ref对象模式获取元素或组件</div>
        <Children ref={ this.currentComponentInstance }  />
   </div>
}

file

函数组件获取 ref 的方式(useRef)

const Children = () => {  
  return <div>hello,world</div>
}
const Index = () => {
  const currentDom = React.useRef(null)
  const currentComponentInstance = React.useRef(null)

  React.useEffect(()=>{
    console.log( currentDom ) 
    console.log( currentComponentInstance ) 
  },[])

  return (
    <div>
      <div ref={ currentDom }  >通过useRef获取元素或者组件</div>
      <Children ref={ currentComponentInstance }  />
   </div>
  )
}

file

ref 的拓展用法

不得不说的 forwardRef

forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。 forwardRef 接受了父级元素标记的 ref 信息,并把它转发下去,使得子组件可以通过 props 来接受到上一层级或者是更上层级的 ref 。

跨层级获取

我们把上面的例子改一下

获取子元素的dom

const Children = React.forwardRef((props, ref) => {
  return <div ref={ref}>hello,world</div>
}) 
const Index = () => {
  const currentDom = React.useRef(null)
  const currentComponentInstance = React.useRef(null)

  React.useEffect(()=>{
    console.log( currentDom ) 
    console.log( currentComponentInstance ) 
  },[])

  return (
    <div>
      <div ref={ currentDom }  >通过useRef获取元素或者组件</div>
      <Children ref={ currentComponentInstance }  />
   </div>
  )

file

想要在 GrandFather 组件通过标记 ref ,来获取孙组件 Son 的组件实例。

// 孙组件
function Son (props){
  const { grandRef } = props
  return <div>
      <div> i am alien </div>
      <span ref={grandRef} >这个是想要获取元素</span>
  </div>
}
// 父组件
class Father extends React.Component{
  render(){
      return <div>
          <Son grandRef={this.props.grandRef}  />
      </div>
  }
}
const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref}  {...props} />)
// 爷组件
class GrandFather extends React.Component{
  node = null 
  componentDidMount(){
      console.log(this.node) // span #text 这个是想要获取元素
  }
  render(){
      return <div>
          <NewFather ref={(node)=> this.node = node } />
      </div>
  }
}

file

合并转发ref

通过 forwardRef 转发的 ref 不要理解为只能用来直接获取组件实例,DOM 元素,也可以用来传递合并之后的自定义的 ref

场景:想通过 Home 绑定 ref ,来获取子组件 Index 的实例 index ,dom 元素 button ,以及孙组件 Form 的实例

// 表单组件
class Form extends React.Component{
  render(){
     return <div>{...}</div>
  }
}
// index 组件
class Index extends React.Component{ 
  componentDidMount(){
      const { forwardRef } = this.props
      forwardRef.current={
          form:this.form,      // 给form组件实例 ,绑定给 ref form属性 
          index:this,          // 给index组件实例 ,绑定给 ref index属性 
          button:this.button,  // 给button dom 元素,绑定给 ref button属性 
      }
  }
  form = null
  button = null
  render(){
      return <div   > 
        <button ref={(button)=> this.button = button }  >点击</button>
        <Form  ref={(form) => this.form = form }  />  
    </div>
  }
}
const ForwardRefIndex = React.forwardRef(( props,ref )=><Index  {...props} forwardRef={ref}  />)
// home 组件
const Home = () => {
  const ref = useRef(null)
   useEffect(()=>{
       console.log(ref.current)
   },[])
  return <ForwardRefIndex ref={ref} />
}

file

高阶组件转发

如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件 HOC 没有处理 ref ,那么由于高阶组件本身会返回一个新组件,所以当使用 HOC 包装后组件的时候,标记的 ref 会指向 HOC 返回的组件,而并不是 HOC 包裹的原始类组件,为了解决这个问题,forwardRef 可以对 HOC 做一层处理。

function HOC(Component){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
  return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) 
}

class Index1 extends React.Component{
  state={
    name: '222'
  }
  render(){
    return <div>hello,world</div>
  }
}

const HocIndex =  HOC(Index1)

const AppIndex = ()=>{
  const node = useRef(null)
  useEffect(()=>{
    console.log(node.current)  /* Index 组件实例  */ 
  },[])
  return <div><HocIndex ref={node}  /></div>
}

源码位置 react/src/forwardRef.js

export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}

ref 实现组件通信

如果有种场景不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法,这种情况通常发生在一些数据层托管的组件上,比如  表单,经典案例可以参考 antd 里面的 form 表单,暴露出对外的 resetFields , setFieldsValue 等接口,可以通过表单实例调用这些 API 。

/* 子组件 */
class Son extends React.PureComponent{
  state={
     fatherMes:'',
     sonMes:'我是子组件'
  }

  fatherSay=(fatherMes)=> this.setState({ fatherMes  }) /* 提供给父组件的API */
  
  render(){
      const { fatherMes, sonMes } = this.state
      return <div className="sonbox" >
          <p>父组件对我说:{ fatherMes }</p>
          <button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) }  >to father</button>
      </div>
  }
}
/* 父组件 */
function Father(){
  const [ sonMes , setSonMes ] = React.useState('') 
  const sonInstance = React.useRef(null) /* 用来获取子组件实例 */

  const toSon =()=> sonInstance.current.fatherSay('我是父组件') /* 调用子组件实例方法,改变子组件state */
  
  return <div className="box" >
      <div className="title" >父组件</div>
      <p>子组件对我说:{ sonMes }</p>
      <button className="searchbtn"  onClick={toSon}  >to son</button>
      <Son ref={sonInstance} toFather={setSonMes} />
  </div>
}

子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。

file

函数组件 forwardRef + useImperativeHandle

对于函数组件,本身是没有实例的,但是 React Hooks 提供了,useImperativeHandle 一方面第一个参数接受父组件传递的 ref 对象,另一方面第二个参数是一个函数,函数返回值,作为 ref 对象获取的内容。一起看一下 useImperativeHandle 的基本使用。

useImperativeHandle 接受三个参数:

第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
第三个参数 deps : 依赖项 deps,依赖项更改形成新的 ref 对象。

const Son = React.forwardRef((props, ref) => {
  const state = {
      sonMes:'我是子组件'
  }
  const [fatherMes, setFatherMes] = React.useState('')
    useImperativeHandle(ref,()=>{
      const handleRefs = {
        fatherSay(fatherMss){            
          setFatherMes(fatherMss)
        }
      }
      return handleRefs
  },[])
  const { sonMes } = state
  return (
    <div>
      <p>父组件对我说: {fatherMes}</p>
      <button  onClick={ ()=> props.toFather(sonMes) }  >to father</button>
    </div>
  ) 
})
/* 父组件 */
function Father(){
  const [ sonMes , setSonMes ] = React.useState('') 
  const sonInstance = React.useRef(null) /* 用来获取子组件实例 */

  const toSon = () => {
    sonInstance.current.fatherSay('我是父组件')
  }
  return <div className="box" >
      <div className="title" >父组件</div>
      <p>子组件对我说:{ sonMes }</p>
      <button className="searchbtn"  onClick={toSon}  >to son</button>
      <Son ref={sonInstance} toFather={setSonMes} />
  </div>
}

file

forwardRef + useImperativeHandle 可以完全让函数组件也能流畅的使用 Ref 通信。其原理图如下所示:

file

函数组件缓存数据

函数组件每一次 render ,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到 state 中。如果视图层更新不依赖想要改变的数据,那么 state 改变带来的更新效果就是多余的。这时候更新无疑是一种性能上的浪费。

这种情况下,useRef 就派上用场了,上面讲到过,useRef 可以创建出一个 ref 原始对象,只要组件没有销毁,ref 对象就一直存在,那么完全可以把一些不依赖于视图更新的数据储存到 ref 对象中。这样做的好处有两个:

第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。

清除定时器

const App = () => {
  const [count, setCount] = useState(0)
  let timer;

  useEffect(() => {
    timer = setInterval(() => {
      console.log('触发了');
    }, 1000);
  },[]);

  const clearTimer = () => {
    clearInterval(timer);
  }

  return (
    <>
     <button onClick={() => {setCount(count + 1)}}>点了{count}次</button>
      <button onClick={clearTimer}>停止</button>
    </>)
}

但是上面这个写法有个巨大的问题,如果这个 App 组件里有state变化或者他的父组件重新 render 等原因导致这个 App 组件重新 render 的时候,我们会发现,点击按钮停止,定时器依然会不断的在控制台打印,定时器清除事件无效了。
为什么呢?因为组件重新渲染之后,这里的 timer 以及 clearTimer 方法都会重新创建,timer 已经不是定时器的变量了。
所以对于定时器,我们都会使用 useRef 来定义变量。

const App = () => {
  const [count, setCount] = useState(0)
  const timer = useRef();

  useEffect(() => {
    timer.current = setInterval(() => {
      console.log('触发了');
    }, 1000);
  },[]);

  const clearTimer = () => {
    clearInterval(timer.current);
  }

  return (
    <>
      <button onClick={() => {setCount(count + 1)}}>点了{count}次</button>
      <button onClick={clearTimer}>停止</button>
    </>)
}

ref 原理探究

对于 ref 标签引用,React 是如何处理的呢? 接下来先来看看一段 demo 代码

class DomRef extends React.Component{
  state={ num:0 }
  node = null
  render(){
      return <div >
          <div ref={(node)=>{
             this.node = node
             console.log('此时的参数是什么:', this.node )
          }}  >ref元素节点</div>
          <button onClick={()=> this.setState({ num: this.state.num + 1  }) } >点击</button>
      </div>
  }
}

控制台输出结果

file

提问: 第一次打印为 null ,第二次才是 div ,为什么会这样呢? 这样的意义又是什么呢?

ref 执行的时机和处理逻辑

根据 React 的生命周期可以知道,更新的两个阶段 render 阶段和 commit 阶段,对于整个 ref 的处理,都是在 commit 阶段发生的。之前了解过 commit 阶段会进行真正的 Dom 操作,此时 ref 就是用来获取真实的 DOM 以及组件实例的,所以需要 commit 阶段处理。
但是对于 ref 处理函数,React 底层用两个方法处理:
commitDetachRef

commitAttachRef
,上述两次 console.log 一次为 null,一次为 div 就是分别调用了上述的方法。

这两次正好,一次在 DOM 更新之前,一次在 DOM 更新之后。

第一阶段:一次更新中,在 commit 的 mutation 阶段, 执行commitDetachRef,commitDetachRef 会清空之前ref值,使其重置为 null。
源码先来看一下

react-reconciler/src/ReactFiberCommitWork.js

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') { /* function获取方式。 */
      currentRef(null); 
    } else {   /* Ref对象获取方式 */
      currentRef.current = null;
    }
  }
}

第二阶段:DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。
第三阶段:layout 阶段,在更新真实元素节点之后,此时需要更新 ref

react-reconciler/src/ReactFiberCommitWork.js

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent: //元素节点 获取元素
        instanceToUse = getPublicInstance(instance);
        break;
      default:  // 类组件直接使用实例
        instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      ref(instanceToUse);  //* function 和 字符串获取方式。 */
    } else {
      ref.current = instanceToUse; /* ref对象方式 */
    }
  }
}

这一阶段,主要判断 ref 获取的是组件还是 DOM 元素标签,如果 DOM 元素,就会获取更新之后最新的 DOM 元素。上面流程中讲了二种获取 ref 的方式。 如果是 函数式 ref={(node)=> this.node = node } 会执行 ref 函数,重置新的 ref 。如果是 ref 对象方式,会更新 ref 对象的 current 属性。达到更新 ref 对象的目的。

ref 的处理特性

file

接下来看一下 ref 的一些特性,首先来看一下,上述没有提及的一个问题,React 被 ref 标记的 fiber,那么每一次 fiber 更新都会调用 commitDetachRef 和 commitAttachRef 更新 Ref 吗 ?
答案是否定的,只有在 ref 更新的时候,才会调用如上方法更新 ref ,究其原因还要从如上两个方法的执行时期说起

更新 ref

在 commit 阶段 commitDetachRef 和 commitAttachRef 是在什么条件下被执行的呢 ? 来一起看一下:
commitDetachRef 调用时机

react-reconciler/src/ReactFiberWorkLoop.js

function commitMutationEffects(){
     if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }
}

commitAttachRef 调用时机

function commitLayoutEffects(){
     if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }
}

从上可以清晰的看到只有含有 ref tag 的时候,才会执行更新 ref,那么是每一次更新都会打 ref tag 吗? 跟着我的思路往下看,什么时候标记的 ref .

react-reconciler/src/ReactFiberBeginWork.js

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||      // 初始化的时候
    (current !== null && current.ref !== ref)  // ref 指向发生改变
  ) {
    workInProgress.effectTag |= Ref;
  }
}

首先 markRef 方法执行在两种情况下:
第一种就是类组件的更新过程
第二种就是更新 HostComponent 的时候
markRef 会在以下两种情况下给 effectTag 标记 ref,只有标记了 ref tag 才会有后续的 commitAttachRef 和 commitDetachRef 流程。( current 为当前调和的 fiber 节点 )

第一种 current === null && ref !== null:就是在 fiber 初始化的时候,第一次 ref 处理的时候,是一定要标记 ref 的。
第二种 current !== null && current.ref !== ref:就是 fiber 更新的时候,但是 ref 对象的指向变了。

所以回到最初的那个 DomRef 组件,为什么每一次按钮,都会打印 ref ,那么也就是 ref 的回调函数执行了,ref 更新了。每一次更新的时候,都给 ref 赋值了新的函数,那么 markRef 中就会判断成 current.ref !== ref,所以就会重新打 Ref 标签,那么在 commit 阶段,就会更新 ref 执行 ref 回调函数了。
要想解决这个问题,我们把 DomRef 组件做下修改:

 class DomRef extends React.Component{
    state={ num:0 }
    node = null
    getDom= (node)=>{
        this.node = node
        console.log('此时的参数是什么:', this.node )
     }
    render(){
        return <div >
            <div ref={this.getDom}>ref元素节点</div>
            <button onClick={()=> this.setState({ num: this.state.num + 1  })} >点击</button>
        </div>
    }
}

file

function DomRef () {
  const [num, setNum] = useState(0)
  const node = useRef(null)
      return (
      <div >
          <div ref={node}>ref元素节点</div>
          <button onClick={()=> {
            setNum(num + 1)
            console.log(node)
          }} >点击</button>
      </div>
    )
}

卸载 ref

上述讲了 ref 更新阶段的特点,接下来分析一下当组件或者元素卸载的时候,ref 的处理逻辑是怎么样的。

react-reconciler/src/ReactFiberCommitWork.js

function safelyDetachRef(current) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {  // 函数式 
        ref(null)
    } else {
      ref.current = null;  // ref 对象
    }
  }
}

被卸载的 fiber 会被打成 Deletion effect tag ,然后在 commit 阶段会进行 commitDeletion 流程。对于有 ref 标记的 ClassComponent (类组件) 和 HostComponent (元素),会统一走 safelyDetachRef 流程,这个方法就是用来卸载 ref。

总结

  • ref 对象的二种创建方式
  • 两种获取 ref 方法
  • 介绍了一下forwardRef 用法
  • ref 组件通信-函数组件和类组件两种方式
  • useRef 缓存数据的用法
  • ref 的处理逻辑原理

希望本次的分享能给大家带去一点收货;

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star