热门推荐
-
使用certimate自动申请和续签域名证书,并部署到本地Nginx及腾讯云CDNLinux在个人站点、博客或者内部系统中,HTTPS几乎已经是标配。但手动申请证书、上传到服务器、再同步到CDN,不仅繁琐,还容易因为忘记续签导致证书过期,影响访问甚至被浏览器拦截。本文将介绍如何使用Certimate来:自动申请Let’sEncrypt免费证书定期续签证书,并重新部署将证书自动部署到本地服务器,供nginx使用将证书自动部署到腾讯云CDN整套流程配置一次,后续基本零维护。整体思路流程大致如下:Certimate通过DNS验证域名所有权向Let’sEncrypt申请证书证书保存到本地自动执行部署动作:复制到本地证书目录(供Nginx使用)调用腾讯云API,将证书上传并绑定到CDN域名环境准备安装Certimate官网:https://docs.certimate.me/以Linux为例,本文采用二进制文件安装方式:请前往GitHubReleases页面下载预先编译好的二进制可执行文件压缩包。压缩包文件名后缀包含系统架构信息,请结合你的设备需要自行选择相应的压缩包,下载并解压缩全部文件。#切入到/usr/local/目录下cd/usr/local/#创建文件夹mkdircertimate#以后certimate便放到该目录下cdcertimate#下载最新版本(请替换为最新版)wgethttps://github.com/certimate-go/certimate/releases/download/v0.4.30/certimate_v0.4.30_linux_amd64.zip#解压文件unzipcertimate_v0.4.30_linux_amd64.zip#删除文件rmcertimate_v0.4.30_linux_amd64.zip#授予执行权限chmod+xcertimate#出于安全考虑,Certimate默认监听127.0.0.1:8090,这意味着你只能通过本地地址访问Certimate。#你可以在启动时修改这个配置:启动服务(默认监听8090端口)#监听任意IPv4地址,端口15100./certimateserve--http"0.0.0.0:15100"安全提醒:因为需要传输授权、证书等敏感数据,请尽量避免在非可信网络环境下以HTTP方式暴露Certimate。建议通过反向代理(如Nginx)配置HTTPS或仅在内网访问。浏览器访问http://你的服务器IP:15100,使用默认账号登录(需要放开防火墙端口和云厂商网络端口限制,如果设置了反向代理,则可以直接通过反向代理访问):用户名:admin@certimate.fun密码:1234567890首次登录后请立即修改默认账号及密码!点击左侧菜单:系统设置,修改账号和密码,避免安全风险。开机自启动Certimate需要在后台运行,才能周期性地执行工作流程以自动续期证书。你可以利用systemd来管理开机自启动项。在/etc/systemd/system/目录下创建服务文件:sudovim/etc/systemd/system/certimate.service编写服务配置:[Unit]Description=CertimateAfter=network.target[Service]#请将以下路径替换为你的实际安装路径WorkingDirectory=/usr/local/certimate/ExecStart=/usr/local/certimate/certimateserve--http"0.0.0.0:15100"Restart=on-failureUser=rootPrivateTmp=true[Install]WantedBy=multi-user.target启用并启动服务#启用服务sudosystemctlenablecertimate.service#启动服务sudosystemctlstartcertimate.service#停止服务sudosystemctlstopcertimate.service#查看状态sudosystemctlstatuscertimate.service#如果修改了配置文件,如修改了端口,则需要重新加载sudosystemctldaemon-reload准备域名、SSL、CDN访问密钥进入腾讯云控制台→访问管理→用户→用户列表,新建一个子账号,专门用于Certimate的API调用。为该子账号授予以下权限策略:策略名称用途QcloudDNSPodFullAccess管理域名解析记录(DNS-01验证需要)QcloudSSLFullAccess上传和管理SSL证书QcloudCDNFullAccess将证书绑定到CDN加速域名创建完成后,复制该账号的SecretId和SecretKey,妥善保存。准备通知渠道(可选但推荐)当证书申请失败或部署出错时,及时的通知能帮你快速发现问题。Certimate支持多种通知渠道,以邮箱通知为例,我们这里使用163邮箱,步骤如下:1.浏览器登录https://mail.163.com(用账号+登录密码)2.右上角「设置」→「POP3/SMTP/IMAP」3.找到IMAP/SMTP服务或POP3/SMTP服务,点「开启」4.弹窗走短信验证(绑手机收验证码/按提示发短信)5.验证通过后页面给出一串16位授权码,只显示一次,立刻复制存好配置Certimate第一步:添加提供商授权登录Certimate→左侧菜单点击授权管理选择提供商页签,点击新建授权选择提供商类型为腾讯云(TencentCloud)填写信息:名称:自定义,如腾讯云SecretId:刚才创建的子账号SecretIdSecretKey:对应的SecretKey,如果刚刚没保存,可以重新生成点击保存,保存成功后,授权凭据提供商页面就会有一条新的纪录第二步:添加通知渠道进入授权管理→选择通知渠道页签→新建授权以邮件通知为例:选择邮件(SMTP)填写SMTP服务器地址、端口、发件人邮箱、授权码测试发送,确认能收到测试邮件点击保存重新回到通知渠道列表,点击该条记录,弹出编辑页面,点击编辑弹窗左下角可进行测试是否正常新建流程选择标准业务模板,按需求设置自己的流程,以下为一个本地部署+更新腾讯云CDN证书+若失败则邮件通知的流程示例以下为几个重要节点的配置说明申请节点配置DNS提供商授权为之前填写的授权凭据-提供商-腾讯云配置,其它默认即可本地主机节点配置私钥文件输出路径、证书文件输出路径、后置命令请按照实际填写部署到腾讯云CDN节点配置主机提供商授权和申请节点的DNS提供商授权一致邮件通知节点配置通知渠道授权为之前填写的通知渠道发布验证所有节点配置完成后:点击右上角发布更改确认发布手动执行测试第一次建议手动执行一次,验证整条链路是否通畅:点击右上角执行进入运行历史查看实时日志,正常情况下,你会看到类似日志:已成功#XLi7ZfZYsdeohBI4g6qmg开始[2026-08-1211:19:14]theworkflowisstarting#Qd-u0_5B5tBi-LSVNGLb4申请[2026-08-1211:19:14]readytorequestcertificate...[2026-08-1211:19:14]nofoundlastrequestedcertificate,begintoapply[2026-08-1211:19:14]acmeconfiginitialized[2026-08-1211:19:14]acmeaccountinitialized[2026-08-1211:19:15]go-acme/lego:ObtainingbundledSANcertificate.[2026-08-1211:19:17]go-acme/lego:Authorizationisalreadyvalid;skippingthechallenge.[2026-08-1211:19:17]go-acme/lego:Usesolver.[2026-08-1211:19:17]go-acme/lego:dns01:preparingtosolvethechallenge.[2026-08-1211:19:18]go-acme/lego:dns01:tryingtosolvethechallenge.[2026-08-1211:19:18]go-acme/lego:dns01:waitingforrecordpropagation[2026-08-1211:19:20]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:22]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:24]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:26]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:28]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:30]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:32]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:34]go-acme/lego:dns01:waitingforrecordpropagation.[2026-08-1211:19:41]go-acme/lego:Theservervalidatedourrequest.[2026-08-1211:19:41]go-acme/lego:dns01:cleaningDNS-01challenge.[2026-08-1211:19:43]go-acme/lego:Validationssucceeded;requestingcertificates.[2026-08-1211:19:46]go-acme/lego:Serverrespondedwithacertificate.[2026-08-1211:19:46]certificatesaved[2026-08-1211:19:46]applicationcompleted#UfpIRMvuUwFVl1cKC14EE部署[2026-08-1211:19:46]readytodeploycertificate...[2026-08-1211:19:46]sslprivatekeyfilesaved[2026-08-1211:19:46]sslcertificatefilesaved[2026-08-1211:19:46]runpost-command[2026-08-1211:19:46]deploymentcompleted#k2ekrPGvlAt-jo0JTgeTM部署[2026-08-1211:19:46]readytodeploycertificate...[2026-08-1211:19:46]sdkrequest'ssl.UploadCertificate'[2026-08-1211:19:46]sslcertificateuploaded[2026-08-1211:19:46]foundcdndomainstodeploy[2026-08-1211:19:47]sdkrequest'cdn.DescribeDomainsConfig'[2026-08-1211:19:50]sdkrequest'cdn.UpdateDomainConfig'[2026-08-1211:19:50]deploymentcompleted#nR5q5dd9c6KlJC1ZFYjjr结束[2026-08-1211:19:50]theworkflowisending验证证书生效本地Nginx:#查看证书文件是否已成功生成ls-la/usr/local/nginx/cert/beilika.com也可以直接通过谷歌浏览器访问网站,按F5刷新后,查看证书是否有效腾讯云CDN:登录腾讯云控制台→内容分发网络CDN→域名管理找到对应域名,点击管理→HTTPS配置,确认证书已绑定且状态正常定时执行Certimate的工作流定时执行机制如下:机制说明Cron触发按你设置的Cron表达式定期触发工作流智能续签每次执行时检查证书有效期,仅当剩余有效期
-
Linux 自动备份实战:MySQL + 目录打包 + 腾讯云 COS 一体化方案Linux在生产环境中,数据备份是运维的最后一道防线。一个合格的备份脚本不仅要能“把文件传上去”,更要解决备份完整性验证、网络抖动重试、本地历史轮转、并发安全等一系列工程问题。本文分享一个经过生产验证的Bash自动备份脚本,支持MySQL分库备份、指定目录打包、腾讯云COS上传及智能历史清理,开箱即用。1.功能介绍该脚本并非简单的命令拼接,而是针对生产痛点做了针对性设计:MySQL分库备份与完整性校验:自动遍历所有业务数据库(排除系统库),逐库执行mysqldump。关键创新在于三重校验机制——不仅检查返回码和文件大小,还会验证SQL文件末尾的--Dumpcompleted标记,有效拦截中途断开导致的不完整备份。COS上传指数退避重试:网络传输不可避免会遇到抖动或API限流。脚本内置可配置的重试次数与指数退避延迟(5s→10s→20s),大幅提升弱网环境下的上传成功率。双策略历史清理:支持“保留天数”和“最大容量”两种清理策略同时生效。当磁盘空间紧张时,自动从最旧文件开始删除,避免备份撑爆服务器磁盘。并发安全与优雅退出:使用flock文件描述符锁防止cron重叠执行;捕获SIGTERM/SIGINT信号后仅清理本次产生的临时文件,不会误删历史备份,锁随进程退出自动释放。跨平台兼容:对du、stat等命令在GNU/Linux与BSD/macOS上的差异做了自适应处理,同一份脚本可在不同发行版上运行。结构化日志与汇总报告:每个阶段输出带时间戳的结构化日志,MySQL备份和COS上传结束后分别输出成功/失败计数汇总,便于监控告警系统解析。2.配置介绍脚本顶部为集中配置区,所有可调参数一目了然:#========================配置区域============================BACKUP_DIR="/data/backup"#备份工作目录HISTORY_DIR="${BACKUP_DIR}/history"#已上传文件的本地归档目录LOG_FILE="${BACKUP_DIR}/backup.log"#日志文件路径LOCK_FILE="/tmp/auto_backup.lock"#并发锁文件#尽量不要使用硬编码密码,避免密码暴漏在ps,history等命令中!请使用MYSQL_CNF指向配置文件(权限600)MYSQL_USER="root"MYSQL_PASS=""#留空,通过cnf文件认证MYSQL_HOST="localhost"#可以为IP,也可以为localhost,一般新装mysql默认是localhostMYSQL_PORT="3306"MYSQL_CNF="/etc/mysql/backup.cnf"#推荐方式EXCLUDE_DBS="^(information_schema|performance_schema|sys|mysql)$"#需要备份的目录,格式:"源路径:别名"BACKUP_DIRS=("/etc/redis:redis-config""/etc/nginx:nginx-config")COSCLI_PATH="/usr/local/bin/coscli"#coscli二进制路径COS_BUCKET="cos://your-bucket/backup-path/"#COS目标路径COS_MAX_RETRIES=3#上传最大重试次数COS_RETRY_BASE_DELAY=5#首次重试延迟(秒),后续指数增长HISTORY_RETAIN_DAYS=30#历史文件保留天数HISTORY_MAX_SIZE_GB=50#历史目录最大容量(GB)#========================配置结束============================MySQL安全认证配置请尽量不要在脚本中硬编码数据库密码。请创建专用配置文件:cat>/etc/mysql/backup.cnf/data/backup/cron.log2>&13.4验证清单部署完成后,请逐项确认:检查项验证方法并发锁生效同时启动两个实例,第二个应输出“另一个备份实例正在运行”并退出MySQL备份完整解压任意.tar.gz,检查SQL文件末尾含--DumpcompletedCOS上传成功登录腾讯云控制台确认文件存在且大小一致历史清理正常手动调小HISTORY_MAX_SIZE_GB触发清理,观察日志删除记录中断清理安全备份过程中kill-SIGTERM,确认仅本次临时文件被清理日志可解析确认汇总行格式固定,便于接入Prometheus/Grafana等监控系统最后提醒:备份的终极验证是恢复测试。请定期从COS下载备份文件并在隔离环境中执行恢复演练,确保备份真正可用。未经恢复验证的备份,等于没有备份。4.完整文件以下为完整脚本文件,附件里也可以直接下载#!/bin/bash#================================================================#Linux自动备份脚本(MySQL+指定目录+腾讯云COS+History清理)#版本:2.4#注意:必须使用bash执行(./auto_backup.sh或bashauto_backup.sh)#================================================================set-opipefail#========================配置区域============================BACKUP_DIR="/data/backup"HISTORY_DIR="${BACKUP_DIR}/history"LOG_FILE="${BACKUP_DIR}/backup.log"TIMESTAMP=$(date+"%Y%m%d-%H%M%S")LOCK_FILE="/tmp/auto_backup.lock"#[安全]禁止硬编码密码,请使用MYSQL_CNF指向含[mysqldump]段的配置文件(权限600)MYSQL_USER="root"MYSQL_PASS=""MYSQL_HOST="localhost"MYSQL_PORT="3306"MYSQL_CNF="/etc/mysql/backup.cnf"EXCLUDE_DBS="^(information_schema|performance_schema|sys|mysql)$"BACKUP_DIRS=("/etc/redis:redis-config""/etc/nginx:nginx-config")COSCLI_PATH="/usr/local/bin/coscli"COS_BUCKET="cos://your-bucket-name/backup-path/"COS_MAX_RETRIES=3COS_RETRY_BASE_DELAY=5HISTORY_RETAIN_DAYS=30HISTORY_MAX_SIZE_GB=50#========================配置结束============================log(){echo"[$(date'+%Y-%m-%d%H:%M:%S')]$1"|tee-a"$LOG_FILE"}#跨平台获取目录字节大小(GNUdu-sb/BSDdu-sk)get_dir_bytes(){localdir="$1"ifdu--version&>/dev/null2>&1;thendu-sb"$dir"2>/dev/null|awk'{print$1}'elselocalkbkb=$(du-sk"$dir"2>/dev/null|awk'{print$1}')echo$((kb*1024))fi}format_bytes(){localbytes="$1"localgb=$((bytes/1073741824))localmb_pct=$(((bytes%1073741824)*100/1073741824))printf"%d.%02dGB""$gb""$mb_pct"}#跨平台查找最旧文件(GNUstat-c/BSDstat-f)find_oldest_file(){localdir="$1"ifstat--version&>/dev/null2>&1;thenfind"$dir"-maxdepth1-typef-execstat-c'%Y%n'{}\;2>/dev/null\|sort-n|head-1|cut-d''-f2-elsefind"$dir"-maxdepth1-typef-execstat-f'%m%N'{}\;2>/dev/null\|sort-n|head-1|cut-d''-f2-fi}#中断时仅清理本次时间戳产生的不完整文件,锁随进程退出自动释放cleanup_on_exit(){log"WARN:收到中断信号,正在清理本次临时文件..."localcleaned=0whileIFS=read-rf;do[-z"$f"]&&continuerm-f"$f"2>>"$LOG_FILE"((cleaned++))done/dev/null;thenlog"ERROR:mysqldump未安装";exit1fiif[!-x"$COSCLI_PATH"];thenCOSCLI_PATH=$(command-vcoscli2>/dev/null||true)fiif[-z"$COSCLI_PATH"]||[!-x"$COSCLI_PATH"];thenlog"ERROR:coscli不可用,请检查配置或PATH";exit1filog"INFO:使用coscli:$COSCLI_PATH"}backup_mysql(){log"=====开始MySQL分库备份====="localmysql_auth=""if[-n"$MYSQL_CNF"]&&[-f"$MYSQL_CNF"];thenmysql_auth="--defaults-extra-file=$MYSQL_CNF"elif[-n"$MYSQL_PASS"];thenmysql_auth="-u${MYSQL_USER}-p${MYSQL_PASS}-h${MYSQL_HOST}-P${MYSQL_PORT}"elsemysql_auth="-u${MYSQL_USER}-h${MYSQL_HOST}-P${MYSQL_PORT}"fidatabases=$(mysql$mysql_auth-N-e"SHOWDATABASES;"2>>"$LOG_FILE")if[$?-ne0];thenlog"ERROR:无法连接MySQL";return1filocaldb_total=0db_success=0db_fail=0fordbin$databases;do[["$db"=~$EXCLUDE_DBS]]&&continue((db_total++))localdump_file="${BACKUP_DIR}/mysql-${db}-${TIMESTAMP}.sql"localtar_file="${BACKUP_DIR}/mysql-${db}-${TIMESTAMP}.tar.gz"log"正在备份数据库:$db"mysqldump$mysql_auth--single-transaction--routines--triggers"$db">"$dump_file"2>>"$LOG_FILE"#三重校验:返回码+非空+尾部完成标记(防止中途断开的不完整备份)localdump_ok=falseif[$?-eq0]&&[-s"$dump_file"];theniftail-1"$dump_file"2>/dev/null|grep-q"^--Dumpcompleted";thendump_ok=trueelselog"ERROR:数据库$dbdump缺少完成标记,可能不完整"fielselog"ERROR:数据库$dbmysqldump失败或输出为空"fiif$dump_ok;thentar-czf"$tar_file"-C"$BACKUP_DIR""$(basename"$dump_file")"2>>"$LOG_FILE"rm-f"$dump_file"if[$?-eq0]&&[-s"$tar_file"];thenlog"SUCCESS:$tar_file($(du-h"$tar_file"|cut-f1))"((db_success++))elselog"ERROR:数据库$db压缩失败"rm-f"$tar_file"((db_fail++))fielserm-f"$dump_file"((db_fail++))fidonelog"MySQL备份汇总:总计=${db_total}成功=${db_success}失败=${db_fail}"["$db_fail"-gt0]&&log"WARN:有${db_fail}个数据库备份失败"return0}backup_dirs(){log"=====开始目录备份====="localvalid_count=0foritemin"${BACKUP_DIRS[@]}";do[-n"$item"]&&((valid_count++))doneif["$valid_count"-eq0];thenlog"INFO:BACKUP_DIRS为空,跳过目录备份"return0fiforitemin"${BACKUP_DIRS[@]}";do[-z"$item"]&&continuelocalsrc_path="${item%%:*}"localalias_name="${item##*:}"localtar_file="${BACKUP_DIR}/${alias_name}-${TIMESTAMP}.tar.gz"if[!-d"$src_path"];thenlog"WARN:目录不存在,跳过:$src_path"continuefilog"正在备份目录:$src_path->$alias_name"tar-czf"$tar_file"-C"$(dirname"$src_path")""$(basename"$src_path")"2>>"$LOG_FILE"if[$?-eq0]&&[-s"$tar_file"];thenlog"SUCCESS:$tar_file($(du-h"$tar_file"|cut-f1))"elselog"ERROR:目录$src_path备份失败"fidone}upload_with_retry(){localfile="$1"localdest="$2"localretry_delay=$COS_RETRY_BASE_DELAYfor((i=1;i>"$LOG_FILE"&&return0if["$i"-lt"$COS_MAX_RETRIES"];thenlog"WARN:第${i}次上传失败,${retry_delay}s后重试..."sleep"$retry_delay"retry_delay=$((retry_delay*2))fidonereturn1}#使用进程替换而非管道while,确保计数器在当前Shell作用域内正确累加upload_to_cos(){log"=====开始上传至腾讯云COS====="localupload_ok=0upload_fail=0whileIFS=read-rfile;do[-z"$file"]&&continuelocalfilenamefilename=$(basename"$file")log"上传中:$filename"ifupload_with_retry"$file""${COS_BUCKET}${filename}";thenmv"$file""$HISTORY_DIR/"log"SUCCESS:$filename已上传并移至history"((upload_ok++))elselog"ERROR:$filename上传失败(已重试${COS_MAX_RETRIES}次),保留在备份目录"((upload_fail++))fidone/dev/null;thenlocalold_filesold_files=$(find"$HISTORY_DIR"-maxdepth1-typef-mtime+"$HISTORY_RETAIN_DAYS"2>>"$LOG_FILE")if[-n"$old_files"];thenlocalcountcount=$(echo"$old_files"|wc-l)echo"$old_files"|xargsrm-f2>>"$LOG_FILE"log"按时间清理:删除超过${HISTORY_RETAIN_DAYS}天的文件${count}个"elselog"按时间清理:无过期文件"fifiif["$HISTORY_MAX_SIZE_GB"-gt0]2>/dev/null;thenlocalmax_bytes=$((HISTORY_MAX_SIZE_GB*1024*1024*1024))whiletrue;dolocalcurrent_bytescurrent_bytes=$(get_dir_bytes"$HISTORY_DIR")[-z"$current_bytes"]&&breakif["$current_bytes"-le"$max_bytes"];thenlog"按容量清理:当前$(format_bytes"$current_bytes"),未超限(${HISTORY_MAX_SIZE_GB}GB)"breakfilocaloldest_fileoldest_file=$(find_oldest_file"$HISTORY_DIR")if[-z"$oldest_file"];thenlog"按容量清理:目录已空但仍超限,请检查子目录或非备份文件"breakfirm-f"$oldest_file"2>>"$LOG_FILE"log"按容量清理:已删除最旧文件$(basename"$oldest_file")"donefilog"History清理完成"}main(){exec200>"$LOCK_FILE"if!flock-n200;thenlog"WARN:另一个备份实例正在运行,本次跳过"exit0filocalstart_seconds=$SECONDSlog"=========================================="log"备份任务启动|时间戳:$TIMESTAMP"log"=========================================="check_envbackup_mysqlbackup_dirsupload_to_coscleanup_historylocalelapsed=$((SECONDS-start_seconds))localexit_code=0localresidual_countresidual_count=$(find"$BACKUP_DIR"-maxdepth1-typef\(-name"*.tar.gz"-o-name"*.sql"\)2>/dev/null|wc-l)if["$residual_count"-gt0];thenexit_code=1log"WARN:备份目录存在${residual_count}个未上传文件"filog"==========备份任务结束|耗时:${elapsed}s|退出码:${exit_code}=========="exit$exit_code}main"$@"
-
Vite6+React18+Ts项目-04.使用zustand状态管理React1.简单使用安装zustandnpminstallzustand创建用来保存状态的store.ts文件import{create}from'zustand'typeState={count:number}typeActions={increment:(qty:number)=>void}constuseCountStore=create((set)=>({count:0,increment:(qty:number)=>set((state)=>({count:state.count+qty})),}))在app.ts组件中使用functionApp(){constcount=useCountStore((state)=>state.count)constincrement=useCountStore((state)=>state.increment)return(increment(1)}>增加当前值:{count})}2.一个保存用户语言的例子store.ts下面的代码使用了两个中间件immer:更加方便的操作对象,改变对象的值时,可直接操作对象persist:持久化存储,将对象存储到localStorageimport{create}from'zustand'import{createJSONStorage,persist,subscribeWithSelector}from'zustand/middleware';import{immer}from'zustand/middleware/immer';//定义类型interfaceLang{code:string;name:string;}//定义初始值constinit:Lang={code:'zh-CN',name:'中文简体',}//存储store的keyconststoreKey='store-lang';//创建一个storeconstuseLangStore=create()(immer(persist(()=>({...init}),{//持久化时存储到localStorage的keyname:storeKey,//存储方式,默认localStoragestorage:createJSONStorage(()=>localStorage),},)))exportdefaultuseLangStore;//设置语言exportconstsetLang=(state:Lang)=>{useLangStore.setState(state);}//重置语言exportconstresetLang=()=>{useLangStore.setState(init);}//获取语言编码,通过这种方法获取,在组件里不会同步刷新状态,即当code改变时,不会触发组件渲染exportconstgetLangCodee=():string=>{returnuseLangStore.getState().code;}app.tsimport{useState}from'react'import'./app.css'importuseLangStore,{setLang}from'./stores/lang/lang.store'functionApp(){const[inputLang,setInputLang]=useState('en-Es')//如果同时获取多个值可使用useShallowconstlangCode=useLangStore((state)=>state.code)constsetLangData=()=>{setLang({code:inputLang,name:inputLang});}return(设置语言setInputLang(e.target.value)}/>当前语言编码:{langCode})}exportdefaultApp3.监听localStorage变化通过监听localStorage的变化,可以使用当一个页签的内容发送改变时,其它页签的store也会发生改变//监听localStorage变化window.addEventListener('storage',(event)=>{if(event.key===storeKey&&event.newValue!==null){constval=JSON.parse(event.newValue);//这里判断用户状态,如果用户变化了,则进入登录页面或进入首页,根据具体业务自行处理if(JSON.stringify(useLangStore.getState())!=JSON.stringify(val.state)){useLangStore.setState(val.state);}}});4.使用useShallow的例子可以更加方便的同时获取多个值,而不会因为其它未使用到值的改变触发组件渲染使用前constlangCode=useLangStore((state)=>state.code)constlangName=useLangStore((state)=>state.name)使用后const{code,name}=useLangStore(useShallow((state)=>({code:state.code,name:state.name})))5.使用订阅的例子可优化组件渲染次数,如下示例,只有达到一定条件才会触发渲染,如果是在外层直接取出state的值,则state的值每次改变都会触发渲染useEffect(()=>{returnuseCountStore.subscribe((state,prevState)=>{if(state.count>=7&&prevState.count=7&&prevState.countdevtools->subscribeWithSelector->persist本文未使用到devtools,devtools中间件可以帮助你在开发过程中调试和检查应用程序的状态变化,因为我们将状态存储到了localStorage,可直接通过浏览器控制台查看,故无需使用该中间件示例constuseCountStore=create()(immer(subscribeWithSelector(persist(()=>({...init}),{//持久化时存储到localStorage的keyname:storeKey,//存储方式,默认localStoragestorage:createJSONStorage(()=>localStorage),},))))用法useEffect(()=>{returnuseCountStore.subscribe(state=>state.count,(count,prevCount)=>{if(count>=7&&prevCount=7&&prevCount
-
React18.2x源码解析(一)react应用加载React前言react和vue作为最流行的前端框架,只要你熟悉其中一个框架,上手另外一个也是非常快的。刚开始看react源码时进度非常缓慢,不仅是因为时间比较有限,平时基本是利用空闲时间和周末来学习,更多的还是因为React源码比较复杂,代码量也非常庞大,之前也看过Vue2和Vue3的源码,但是难度方面确实也是显而易见的。后面也是经常看网上一些资料文章进行参考学习,期间也正好是看到了卡颂的《React设计原理》,就买了这本书辅助学习,这本书对我来说最大的帮助就是梳理了react源码整体的架构和源码中一些抽象概念的理解,所以想要学习react源码的也推荐参考这本书帮助学习,当然最重要的还是要自己去看源码,去调试实操,不然收获是比较有限的。个人喜欢在学习新的内容之后书写markdown文档进行总结,大大小小也已经积累了几十个文档了。所以这类文章算是我学习源码之后的一个笔记总结,当然还是进行了一定的优化,也绘制了一些相关的图例。因为水平有限可能存在部分理解有误,也希望理解和指出以便修正。本系列预计会有十篇左右,目前已经完成了七篇,后面可能每一两天更新一篇;第一到第四章节是讲解一个react应用加载的核心流程,所以一些细节会有所省略,比如组件详细的加载过程和hooks的处理等,这些内容会放在新的章节里面讲解。源码解析的文章其实书写起来麻烦而且阅读体验可能也并不友好,主要是内容太多且枯燥,只能尽力而为,补充相关的调试截图以及案例配图,能对正在学习react源码的朋友有一点帮助即可。1.源码结构React18.2的目录结构如下:#根目录├──fixtures#一些React测试项目├──packages#react框架的各种源码包,比如react/react-dom/react-server等├──scripts#各种工具的脚本文件,比如babel/devtools/eslint等我们主要关注packages目录,里面是react框架的各种核心源码包:├──react#通用的react框架核心API;比如Component、createRef、useState、useRef等├──react-dom#dom平台相关的源码├──react-art#处理canvas,svg等├──react-server#ssr服务器渲染相关的源码├──react-native-renderer#native环境├──react-reconciler#Fiber架构源码├──scheduler#调度器源码├──shared#整个项目通用的工具包,包含各种utils公用方法...2.应用入口根据React18版本,每一个React应用的入口都是从react-dom这个源码包中引入一个ReactDOM对象,然后调用createRoot方法进行一系列的初始化准备,这个方法会返回一个root对象【ReactDOMRoot构造函数的实例】,最后调用root.render()方法开始一个React应用的加载渲染。本源码系列都以浏览器端为视角,展开对React源码的学习。importReactfrom'react';importReactDOMfrom'react-dom/client';constroot=ReactDOM.createRoot(document.getElementById('root'));root.render();createRoot接下来就从createRoot这个方法开始React源码的学习。首先查看createRoot的源码://packages\react-dom\src\client\ReactDOM.jsfunctioncreateRoot(container:Element|Document|DocumentFragment,options?:CreateRootOptions,):RootType{returncreateRootImpl(container,options);}这里createRootImpl方法为ReactDOMRoot.js文件中的createRoot方法。继续深入源码://packages\react-dom\src\client\ReactDOMRoot.jsexportfunctioncreateRoot(container:Element|Document|DocumentFragment,options?:CreateRootOptions,):RootType{#container:默认是#rootdom元素//校验container是否为合法的dom元素if(!isValidContainer(container)){thrownewError('createRoot(...):TargetcontainerisnotaDOMelement.');}//开发环境校验警告warnIfReactDOMContainerInDEV(container);//默认严格模式为falseletisStrictMode=false;letconcurrentUpdatesByDefaultOverride=false;letidentifierPrefix='';letonRecoverableError=defaultOnRecoverableError;lettransitionCallbacks=null;...#创建root应用根节点对象,为FiberRootNode类型://注:ConcurrentRoot为1,代表了v18版本的并发渲染模式constroot=createContainer(container,ConcurrentRoot,//1默认开启并发模式null,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks,);//给#rootDOM元素设置一个内部属性,存储root.current即HostFiber对象markContainerAsRoot(root.current,container);constrootContainerElement:Document|Element|DocumentFragment=container.nodeType===COMMENT_NODE?(container.parentNode:any):container;//监听[#rootdom元素]的所有事件listenToAllSupportedEvents(rootContainerElement);#创建ReactDOMRoot实例对象并返回将root应用根节点对象存储为ReactDOM的内部对象returnnewReactDOMRoot(root);}createRoot方法首先对传入的container【#root】容器元素进行合法校验:校验失败,抛出异常,停止应用加载。校验成功,继续向下执行应用加载。然后初始化了一些默认的状态,这个有一个变量需要注意:letisStrictMode=false;react18默认没有开启严格模式,需要自己使用组件包裹App根组件,才能开启整个应用的严格模式。但是React18会默认开启并发渲染模式://历史遗留模式,针对16,17版本LegacyRoot=0;//并发模式【react18默认开启】ConcurrentRoot=1;注意:react18虽然创建的是ConcurrentRoot并发渲染模式的应用,但是默认还是同步创建Fiber树,只有在使用useTransition等并发hook时,触发的更新才能开启真正的并发渲染,即启动react的时间切片功能。createRoot方法中主要有三个重点逻辑处理:调用createContainer方法,创建root应用根节点对象。在#root应用容器元素上监听所有事件,这是React事件系统的关键。创建一个ReactDOMRoot实例对象并返回,这就是createRoot方法最后返回的root对象。下面我们开始逐个解析每个逻辑过程。createContainer首先进入createContainer源码://packages\react-reconciler\src\ReactFiberReconciler.new.jsexportfunctioncreateContainer(containerInfo:Container,tag:RootTag,hydrationCallbacks:null|SuspenseHydrationCallbacks,isStrictMode:boolean,concurrentUpdatesByDefaultOverride:null|boolean,identifierPrefix:string,onRecoverableError:(error:mixed)=>void,transitionCallbacks:null|TransitionTracingCallbacks,):OpaqueRoot{//hydrate代表ssr,默认为falseconsthydrate=false;constinitialChildren=null;//只调用了一个方法returncreateFiberRoot(containerInfo,tag,hydrate,initialChildren,hydrationCallbacks,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks,);}继续查看createFiberRoot源码://packages\react-reconciler\src\ReactFiberRoot.new.js//创建root应用根节点对象exportfunctioncreateFiberRoot(containerInfo:any,tag:RootTag,hydrate:boolean,...):FiberRoot{#创建root应用根节点对象【FiberRootNode】constroot:FiberRoot=newFiberRootNode(containerInfo,tag,//1hydrate,identifierPrefix,onRecoverableError,);#创建一个FiberNode对象【HostRootFiber】,它是Fiber树的根节点,非常重要constuninitializedFiber=createHostRootFiber(tag,isStrictMode,concurrentUpdatesByDefaultOverride,);//【关联起来,以便在后续的渲染过程中能够正确地处理该组件树的更新和重新渲染。】//将root应用根节点对象的current属性指向了当前CurrentFiberTree组件树的根节点【HostRootFiber】root.current=uninitializedFiber;//然后将HostFiber.stateNode属性值:设置为root应用根节点对象uninitializedFiber.stateNode=root;if(enableCache){constinitialCache=createCache();retainCache(initialCache);root.pooledCache=initialCache;retainCache(initialCache);constinitialState:RootState={element:initialChildren,isDehydrated:hydrate,cache:initialCache,transitions:null,pendingSuspenseBoundaries:null,};uninitializedFiber.memoizedState=initialState;}else{constinitialState:RootState={element:initialChildren,isDehydrated:hydrate,cache:(null:any),//notenabledyettransitions:null,pendingSuspenseBoundaries:null,};//初始化数据uninitializedFiber.memoizedState=initialState;}//初始化HostRootFiber根节点对象的updateQueue属性initializeUpdateQueue(uninitializedFiber);#返回root应用根节点对象【容器】returnroot;}createFiberRoot方法一进来就创建了一个root实例对象,它的类型为FiberRootNode。它是根据#root容器元素创建的对象,可以称为应用根节点。constroot=newFiberRootNode()我们要理解这个对象的内容,就要查看FiberRootNode构造函数:functionFiberRootNode(containerInfo,tag,hydrate,identifierPrefix,onRecoverableError,){this.tag=tag;//应用模式:1表示并发渲染模式this.containerInfo=containerInfo;//存储#rootdom对象this.pendingChildren=null;this.current=null;#这个属性指向CurrentFiberTree的根节点也就是HostFiberRootthis.pingCache=null;this.finishedWork=null;//存储创建完成的FiberTreethis.timeoutHandle=noTimeout;this.context=null;this.pendingContext=null;this.callbackNode=null;//回调节点,存储当前任务taskthis.callbackPriority=NoLane;//回调任务优先级this.eventTimes=createLaneMap(NoLanes);this.expirationTimes=createLaneMap(NoTimestamp);this.pendingLanes=NoLanes;//默认0this.suspendedLanes=NoLanes;this.pingedLanes=NoLanes;this.expiredLanes=NoLanes;this.mutableReadLanes=NoLanes;this.finishedLanes=NoLanes;this.entangledLanes=NoLanes;this.entanglements=createLaneMap(NoLanes);...}下图示例:为debug调试中创建的FiberRootNode实例对象:FiberRootNode中的实例属性很多,我们只需要先了解几个常用的。下面我们继续回到createFiberRoot方法,然后又创建了一个Fiber对象://hostFiberconstuninitializedFiber=createHostRootFiber()//packages\react-reconciler\src\ReactFiber.new.jsexportfunctioncreateHostRootFiber(tag:RootTag,//1isStrictMode:boolean,concurrentUpdatesByDefaultOverride:null|boolean,):Fiber{letmode;...省略#创建Fiber对象tag为3即代表HostRoot节点returncreateFiber(HostRoot,null,null,mode);}查看createFiber方法,这个内部直接调用FiberNode构造函数,创建了一个Fiber对象:constcreateFiber=function(tag:WorkTag,pendingProps:mixed,key:null|string,mode:TypeOfMode,):Fiber{//创建FiberNode实例对象returnnewFiberNode(tag,pendingProps,key,mode);};FiberNode继续查看FiberNode构造函数:functionFiberNode(tag:WorkTag,pendingProps:mixed,key:null|string,mode:TypeOfMode,){//Instancethis.tag=tag;//节点类型,非常重要,不同的值代表不同的节点对象this.key=key;//节点keythis.elementType=null;//大部分情况同type,存储组件对象Elementthis.type=null;#组件原始定义//存储FiberNode对象对应的dom元素【hostCompoent】,//函数组件此属性无值,//类组件此属性存储的是组件实例instancethis.stateNode=null;#FiberNode节点之间的链接this.return=null;//指向父级节点对象FiberNodethis.child=null;//指向第一个子节点FiberNodethis.sibling=null;//指向下一个兄弟节点FiberNodethis.index=0;this.ref=null;//ref引用#hooks相关this.pendingProps=pendingProps;//新的,等待处理的propsthis.memoizedProps=null;//旧的,上一次存储的propsthis.updateQueue=null;//存储update更新对象链表this.memoizedState=null;//类组件:旧的,上一次存储的state;函数组件:存储hook链表this.dependencies=null;this.mode=mode;//存储的是当前应用渲染模式:默认是concurrentmode//各种effect副作用相关的执行标记this.flags=NoFlags;this.subtreeFlags=NoFlags;//子树节点的副作用标记,默认无副作用this.deletions=null;//删除标记//优先级调度,默认为0this.lanes=NoLanes;//节点自身的更新Lanesthis.childLanes=NoLanes;//子树节点的更新lanes#这个属性指向另外一个缓冲区对应的FiberNode//current.alternate===workInProgress//workInProgress.alternate===currentthis.alternate=null;...}可以看到FiberNode同样定义了很多的实例属性,讲到这里必须要对FiberNode进行一些说明:Fiber是VDOM是在React源码中的实现,FiberNode即虚拟dom,在Vue源码中定义为VNode。在React源码中有两种Fiber节点定义:FiberRootNode和FiberNode,但实际上我们说的Fiber节点是指的普通的FiberNode,我们从上面两个构造函数的定义可以发现,它们的实例属性完全不同,因为它们本来就是不一样的作用。FiberRootNode:是针对React应用挂载的容器元素【#root】创建的一个对象,它是应用根节点。它的作用是负责应用加载相关的内容【应用级】,比如应用加载模式mode,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree等。FiberNode:这才是针对普通DOM元素或者组件创建的Fiber对象,是虚拟DOM的真实体现,它的属性存储了元素的或者组件的类型,对应的DOM信息,以及数据State和组件更新的相关信息,所以FiberNode才是Fiber架构的核心。回到createContainer方法内:constuninitializedFiber=createHostRootFiber()这里要注意创建的对象为HostFiber,虽然它也是普通的FiberNode类型,但是它的tag属性为3,代表着它是虚拟DOM树的根节点。exportconstHostRoot=3;然后将FiberRootNode与HostRootFiber进行关联,方便在后续使用中可以互相访问:root.current=uninitializedFiber;uninitializedFiber.stateNode=root;以一张图表示它们的关系:FiberRootNode是React应用的根节点【容器】,HostRootFiber是虚拟DOM树的根节点【FiberTree】。通过属性关联之后,它们就可以在需要的时候互相访问。回到createContainer方法内:functioncreateContainer(){...省略//初始化HostFiber节点的updateQueue属性initializeUpdateQueue(uninitializedFiber);//返回root应用根节点对象returnroot;}再返回root对象之前,还需要对HostRootFiber进行处理://初始化一个Fiber对象的updateQueue属性exportfunctioninitializeUpdateQueue(fiber:Fiber):void{//创建一个更新队列对象constqueue:UpdateQueue={baseState:fiber.memoizedState,//初始state数据firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:NoLanes,},effects:null,};//设置更新队列对象fiber.updateQueue=queue;}初始化HostFiber节点的updateQueue属性,为一个更新队列对象。总结createContainer方法:创建FiberRootNode应用根节点对象。创建HostFiber虚拟DOM树根节点对象。关联两个对象,可以互相访问。初始化HostFiber的updateQueue属性。返回FiberRootNode节点。事件监听回到createRoot方法,在创建FiberRootNode应用根节点对象后,然后给#root根元素绑定了所有事件,任何子孙元素触发的该类型事件都会委托给【根元素的事件回调】处理。listenToAllSupportedEvents(rootContainerElement);这里涉及到了React的合成事件系统,它的实现非常复杂,后面如果有时间会考虑单开一个章节详细解析,这里暂时只简单介绍一下它的基本原理,想了解更多的朋友也可以查阅相关资料学习。在#root根元素上绑定所有的事件回调,任何子孙元素触发的改类型事件都会委托给【根元素的事件回调】处理。寻找触发事件的dom元素,找到对应的FiberNode节点。收集从当前FiberNode节点到HostFiber之间的所有注册该事件的回调函数。循环执行一次收集的所有回调函数。回到createRoot方法最后,创建了一个ReactDOMRoot实例对象并返回。returnnewReactDOMRoot(root);查看ReactDOMRoot构造函数的定义:functionReactDOMRoot(internalRoot:FiberRoot){//存储应用根节点[FiberRootNode]this._internalRoot=internalRoot;}//原型方法ReactDOMRoot.prototype.render=function(){}ReactDOMRoot.prototype.unmount=function(){}ReactDOMRoot构造函数只定义了一个实例属性_internalRoot,它的作用是存储创建完成的应用根节点FiberRootNode对象。还在原型对象上定义了两个静态方法render和unmount。importReactfrom'react';importReactDOMfrom'react-dom/client';constroot=ReactDOM.createRoot(document.getElementById('root'));//调用render方法,开始应用加载root.render();最后对createRoot方法做一个总结:它的主要作用就是为react应用加载做准备工作,初始化了一些根对象和基础信息,最后返回了一个ReactDOMRoot对象,调用它的render方法开启React应用的加载流程。3.应用加载最后我们再来看一下这个render方法:ReactDOMRoot.prototype.render=function(children){constroot=this._internalRoot;if(root===null){thrownewError('Cannotupdateanunmountedroot.');}//开始加载react应用children为App根组件,root为应用根节点对象FiberRootNode/***react组件都会被转换为react-element对象:*{*$$typeof:Symbol(react.element),*key:null,*props:{},*ref:null,*type:funApp(){}*}**/updateContainer(children,root,null,null);}updateContainer继续查看updateContainer源码://packages\react-reconciler\src\ReactFiberReconciler.new.js#加载应用:触发调度更新任务exportfunctionupdateContainer(element:ReactNodeList,//App根组件container:OpaqueRoot,parentComponent:?React$Component,callback:?Function,):Lane{//取出current对象,为HostFiber【它是当前Fiber树的根节点】constcurrent=container.current;//获取当前的程序执行时间【默认是performenct.now返回的微秒时间】consteventTime=requestEventTime();#获取更新优先级laneconstlane=requestUpdateLane(current);//获取父组件的上下文,因为parentComponent为null,所以这里context为空对象constcontext=getContextForSubtree(parentComponent);if(container.context===null){//从null变为{}//如果容器的上下文为null,则把父级上下文赋值container.context=context;}else{//如果容器存在上下文,则把父级的上下文设置为等待处理的上下文container.pendingContext=context;}#创建一个update更新对象constupdate=createUpdate(eventTime,lane);//将更新对象的payload属性设置为App根组件的内容,【即:本次应用加载的内容为App根组件】update.payload={element};//本来就没有,设置完还是nullcallback=callback===undefined?null:callback;//检查回调函数callback是否为空,如果不为空,则将其添加到更新对象的callback属性中if(callback!==null){update.callback=callback;}#将更新对象update:添加到当前current对象的更新队列中constroot=enqueueUpdate(current,update,lane);if(root!==null){#开启一个新的调度更新任务scheduleUpdateOnFiber(root,current,lane,eventTime);entangleTransitions(root,current,lane);}returnlane;}updateContainer方法的内容非常重要,主要是比较典型。你可以在react源码中的很多地方看到类似的执行逻辑,react应用的加载会执行这些逻辑,一个state状态的变化,触发的更新也会执行这些逻辑。我们可以把它的执行过程分为四个重点部分:获取更新优先级lane,也就是生成一个本次更新对应的lane。创建update更新对象。将update更新对象添加到目标Fiber对象的更新队列中。开启一个新的调度更新任务。接下来我们逐个分析执行过程:(一)获取更新优先级laneconstlane=requestUpdateLane(current);//HostFiber查看requestUpdateLane方法://packages\react-reconciler\src\ReactFiberWorkLoop.new.js#获取更新优先级laneexportfunctionrequestUpdateLane(fiber:Fiber):Lane{constmode=fiber.mode;if((mode&ConcurrentMode)===NoMode){//1,模式为0时,返回同步优先级return(SyncLane:Lane);}elseif(!deferRenderPhaseUpdateToNextBatch&&(executionContext&RenderContext)!==NoContext&&workInProgressRootRenderLanes!==NoLanes){//2,render阶段产生的update【比如调用fun()组件的过程中又触发了更新】,返回render阶段进行中最高的优先级returnpickArbitraryLane(workInProgressRootRenderLanes);}constisTransition=requestCurrentTransition()!==NoTransition;//3,Transition相关优先级:如果当前不是渲染上下文,并且请求的更新是过渡(transition),则进入下一个条件。if(isTransition){transition._updatedFibers.add(fiber);}if(currentEventTransitionLane===NoLane){currentEventTransitionLane=claimNextTransitionLane();}returncurrentEventTransitionLane;}//4,使用手动设置的优先级:获取当前更新通道优先级:默认为0constupdateLane:Lane=(getCurrentUpdatePriority():any);//NoLane0if(updateLane!==NoLane){//只要更新通道不等于0,则返回有效的更新通道returnupdateLane;}//5,获取当前事件级的优先级:默认为16consteventLane:Lane=(getCurrentEventPriority():any);returneventLane;}在react中,为了实现并发渲染和优先级控制,使用了多个更新通道(lane)来管理更新任务。每个更新通道都对应一个优先级,根据不同的优先级,react可以决定哪些更新任务可以同时进行,哪些需要等待之前的更新任务完成后才能进行。requestUpdateLane函数的作用是:接收一个Fiber对象作为参数,并返回一个新的更新通道【更新优先级】,这个优先级lane会根据当前Fiber对象的相关属性或者当前的更新环境确定的。requestUpdateLane方法内有以下五种情况对应的优先级lane:判断当前是否为并发渲染模式,如果不是则返回syncLane同步优先级。判断当前是否为render中的更新,即在组件渲染中修改了状态,又触发了更新。如果是则会从当前workInProgressRootRenderLanes中选择最高优先级的Lane,作为本次更新的优先级。判断当前是否为useTransitionhook相关的更新,如果是则在此计算对应的优先级lane,并设置为当前的更新优先级。判断是否存在调度中的更新优先级,如果存在则使用此优先级lane。在以上情况都不满足时,就会从事件中来寻找对应的优先级。当前为react应用首次加载,所以不满足前面的条件,最后就会返回eventLane,作为本次更新的优先级lane。consteventLane=getCurrentEventPriority()exportfunctiongetCurrentEventPriority(){constcurrentEvent=window.event;//应用首次加载,这里为undefinedif(currentEvent===undefined){#返回默认事件优先级16returnDefaultEventPriority;}//返回事件对应的优先级returngetEventPriority(currentEvent.type);}因为react应用的首次加载并不是由某个具体的事件触发的,所以window.event的值为undefined,这里就会返回默认的事件优先级lane来作为应用初始加载的优先级。优先级体系在react源码中,Scheduler调度器是一个独立的包,React与Scheduler没有共用一套优先级体系:react中有四种优先级,称为事件优先级,Scheduler有五种优先级,称为调度优先级。react的事件优先级://1,对应离散事件的优先级,就是普通的事件比如click,input,blur等,这些事件对应的是同步优先级exportconstDiscreteEventPriority:EventPriority=SyncLane;//2,对应的连续事件的优先级,比如drag,mousemove,scroll,wheel等,exportconstContinuousEventPriority:EventPriority=InputContinuousLane;//3,默认的优先级exportconstDefaultEventPriority:EventPriority=DefaultLane;//4,空闲情况的优先级,最低exportconstIdleEventPriority:EventPriority=IdleLane;scheduler的调度优先级:#值越小优先级越高exportconstImmediatePriority=1;//最高优先级,同步执行exportconstUserBlockingPriority=2;exportconstNormalPriority=3;//默认优先级exportconstLowPriority=4;exportconstIdlePriority=5;//最低优先级React与Scheduler的优先级转换需要:通过lane转化。从React到Scheduler的优先级转换:consteventLane=lanesToEventPriority(nextLanes)letschedulerPriorityLevel;#lanes转换成事件优先级,匹配符合的事件优先级,然后赋值对应的scheduler的优先级switch(eventLane){//同步优先级caseDiscreteEventPriority:schedulerPriorityLevel=ImmediateSchedulerPriority;break;//连续事件优先级caseContinuousEventPriority:schedulerPriorityLevel=UserBlockingSchedulerPriority;break;//默认事件优先级caseDefaultEventPriority:schedulerPriorityLevel=NormalSchedulerPriority;break;//空闲事件优先级caseIdleEventPriority:schedulerPriorityLevel=IdleSchedulerPriority;break;//默认default:schedulerPriorityLevel=NormalSchedulerPriority;break;}从Scheduler到React的优先级转换://获取当前调度的优先级,然后根据优先级匹配到对应的优先级,最后返回对应的事件优先级constschedulerPriority=getCurrentSchedulerPriorityLevel();switch(schedulerPriority){caseImmediateSchedulerPriority:returnDiscreteEventPriority;caseUserBlockingSchedulerPriority:returnContinuousEventPriority;caseNormalSchedulerPriority:caseLowSchedulerPriority:returnDefaultEventPriority;caseIdleSchedulerPriority:returnIdleEventPriority;default:returnDefaultEventPriority;}react中的位运算这里还有一个注意点:exportfunctionrequestUpdateLane(fiber:Fiber):Lane{constmode=fiber.mode;//位运算if((mode&ConcurrentMode)===NoMode){return(SyncLane:Lane);}}mode&ConcurrentMode这里算是我们在react源码中遇到的第一个位运算处理,所以需要提前说明一下。如果你阅读过react源码,就可以发现其内部使用了大量的位运算操作,其中最典型的就是lanes优先级和flags副作用标记。这里只介绍三种基本的位运算,因为react源码中主要使用的就是三种,关于位运算的更多信息可以参考《JavaScript高级程序设计4》中的位操作符章节。按位与&第一个数值的位第二个数值的位结果111100010000按位与操作在两个位都是1时返回1,在任何一位是0时返回0。这里直接以上面的代码为例:此时mode为3,ConcurrentMode为1:3&1计算3&1,首先将操作数转化为Int32://30b000000000000000000000000000011//10b000000000000000000000000000001//&计算0b000000000000000000000000000001////结果13&1计算结果为1,不等于NoMode【0】,所以不满足条件。按位或|第一个数值的位第二个数值的位结果111101011000按位或操作在至少一位是1时返回1,两位都是0时返回0。计算3|1://30b000000000000000000000000000011//10b000000000000000000000000000001//3|1计算0b000000000000000000000000000011//结果为3按位非~一个操作数的bit位:取反结果1001按位或操作是对一个操作数进行取反操作【0,1互换】。计算~3://30b000000000000000000000000000011//~3逐位取反0b111111111111111111111111111100//结果为-4lanes计算常见的几个通过位运算来计算lanes的方法:合并lanes://packages\react-reconciler\src\ReactFiberLane.new.js//合并lanesexportfunctionmergeLanes(a:Lanes|Lane,b:Lanes|Lane):Lanes{returna|b;}移除lanes://移除lanesexportfunctionremoveLanes(set:Lanes,subset:Lanes|Lane):Lanes{returnset&~subset;}是否包含lanes://判断当前的lanes是否包含某个laneexportfunctionincludesSomeLane(a:Lanes|Lane,b:Lanes|Lane){return(a&b)!==NoLanes;}(二)创建update更新对象constupdate=createUpdate(eventTime,lane);//packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js#创建更新对象exportfunctioncreateUpdate(eventTime:number,lane:Lane):Update{//定义update对象constupdate:Update={eventTime,lane,//更新优先级tag:UpdateState,//不同的tag对应不同的更新场景payload:null,//更新的内容==fun组件用action字段callback:null,//回调函数next:null,//链表形式:指向下一个更新的对象【保证了update之间的顺序】};//返回创建的update对象returnupdate;}update对象的存储了更新相关的一些基本信息。tag属性:不同值对应着不同的更新场景://1,默认情况:通过ReactDOM.createRoot或者this.setState触发exportconstUpdateState=0;//2,在classCompont组件生命周期函数中使用this.setState触发更新exportconstReplaceState=1;//3,通过this.forceUpdate触发exportconstForceUpdate=2;//4,发生错误的情况下在classComponent或者HostRoot中触发更新exportconstCaptureUpdate=3;当前是通过ReactDOM.createRoot触发的应用加载,所以update对象的tag属性值为0。payload属性值对应的情况:#1,ReactDOM.reacteRoot{payload:{element}//即组件对应的element对象}#2,classComponent组件//this.setState({count:1}){payload:{count:1}//也有可能是一个函数}当前的payload属性存储的是App根组件对应的element对象。callback属性:当前为null。如果是this.setState触发的更新,则为此方法传入的第二个参数【回调函数】。next属性:指向下一个update更新对象,形成一个单向环状链表。总结:根据上面的tag值,我们可以看出类组件的更新与ReactDOM.createRoot共用同一个update更新对象结构,对于ReactDOM.createRoot来说,当前创建的这个update对象,它的作用主要在于在react应用的首次加载【提供的初始信息】。(三)将update更新对象添加到目标Fiber对象的更新队列中constroot=enqueueUpdate(current,update,lane);//packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.jsexportfunctionenqueueUpdate(fiber:Fiber,//要更新的节点,当前也就是hostFiberupdate:Update,//更新对象lane:Lane,//更新的优先级):FiberRoot|null{#取出当前Fiber节点的updateQueue属性constupdateQueue=fiber.updateQueue;if(updateQueue===null){//Onlyoccursifthefiberhasbeenunmounted.//表明当前节点已经被卸载returnnull;}//一个Fiber节点会先初始化updateQueue属性,后创建Update对象//取出updateQueue.shared对象{lanes:0,pending:null}constsharedQueue:SharedQueue=(updateQueue:any).shared;//判断当前是否为不安全的渲染阶段更新if(isUnsafeClassRenderPhaseUpdate(fiber)){constpending=sharedQueue.pending;if(pending===null){update.next=update;}else{update.next=pending.next;pending.next=update;}sharedQueue.pending=update;returnunsafe_markUpdateLaneFromFiberToRoot(fiber,lane);}else{#默认是处于安全的渲染阶段更新:讲update对象添加到queue之中returnenqueueConcurrentClassUpdate(fiber,sharedQueue,update,lane);}}前面我们已经知道了update对象是存储更新相关的一些基本信息,可以说它是一个基本的更新装置,而updateQueue很明显就是来保存或者说管理update对象的数据结构。经过上面的处理,最终会将update更新对象存储到当前Fiber节点中,形成一个单向环状链表。这里描述的比较简单,因为对于react应用加载的场景,update和updateQueue的作用并不明显。它们更重要的使用场景,在于组件更新时的逻辑处理。关于类组件updateQueue这部分详细内容可以查看《React18.2x源码解析:类组件的加载过程》。关于函数组件updateQueue这部分详细内容可以查看《React18.2x源码解析:函数组件的加载过程》。(四)开启一个新的调度更新任务//开启一个新的的调度更新任务scheduleUpdateOnFiber(root,current,lane,eventTime);scheduleUpdateOnFiber函数非常重要,在react源码中有很多地方在调用,它是react开始执行调度更新的入口函数。从这个函数的名称我们也可以知道它的作用:开启一个新的调度更新任务。下一章节我们将从这个函数开始进入scheduler调度程序的执行过程。
最新文章
-
使用certimate自动申请和续签域名证书,并部署到本地Nginx及腾讯云CDN
在个人站点、博客或者内部系统中,HTTPS几乎已经是标配。但手动申请证书、上传到服务器、再同步到CDN,不仅繁琐,还容易因为忘记续签导致证书过期,影响访问甚至被...
-
Linux 自动备份实战:MySQL + 目录打包 + 腾讯云 COS 一体化方案
在生产环境中,数据备份是运维的最后一道防线。一个合格的备份脚本不仅要能“把文件传上去”,更要解决备份完整性验证、网络抖动重试、本地历史轮转、并发安全等一系列工程...
-
Gin学习-05.gin中间件和路由
路由r.GET()r.POST()r.PUT()r.PATCH()r.DELETE()路由分组把一类api划分到一个组可以给这个组加上统一的中间件RESETFu...
-
Gin学习-04.gin参数绑定
参数绑定使用binding可以很好的完成参数的绑定查询参数typeUserstruct{Namestring`form:"name"`Ageint`form:"...
-
Gin学习-03.gin请求
查询参数?key=xxx&name=xxxx&name=yyyy这种就被称为查询参数查询参数不是GET请求专属的name:=c.Query("name")age...
-
Gin学习-02.gin响应
gin提供了非常多的响应方法例如字符串、json、html等json响应现在大部分的前后端交互都是以json为主,所以gin中最常用的就是json响应它的用法非...
-
Gin学习-01.go原生http服务
在go中写一个web服务非常方便快速packagemainimport("encoding/json""fmt""io""net/http")typeRespo...
-
Go基础-20.go部署
go项目的部署特别简单,编写完成之后,只需要执行gobuild即可打包为可执行文件注意,这个操作是不同平台不一样的windows下打包就是exe文件,linux...
-
Go基础-19.go网络编程
TCP传输控制协议(TCP,TransmissionControlProtocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议如何保证连接可靠呢?(面...
-
Go基础-18.go反射
类型判断判断一个变量是否是结构体,切片,mappackagemainimport("fmt""reflect")funcrefType(objany){type...