普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月4日掘金 前端

GDAL 实现自定义数据坐标系

作者 GIS之路
2026年1月3日 22:58

^ 关注我,带你一起学GIS ^

前言

在GIS开发中,经常需要进行数据的转换处理,特别是Shapefile数据的投影转换更是重中之重,如何高效、准确的将源数据坐标系转换到目标坐标系是我们需要研究解决的问题。

在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL实现自定义数据坐标系。

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 数据重投影

定义一个方法ShpProjection用于实现Shp数据的投影转换,其中参数sourcePath为源数据路径,targetPath为目标文件路径。

"""
说明:Shapfile 文件重投影
参数:
    -sourcePath:Shp 源文件路径
    -targetPath:投影文件目标路径
"""
def ShpProjection(sourcePath,targetPath):

先检查源数据文件是否存在,若不存在则退出程序

# 检查Shp源文件是否存在
if os.path.exists(sourcePath):
    print("源文件存在!")
else:
    print(f"源文件不存在:{sourcePath}")
    return

添加Shp数据驱动用于创建Shp数据源和图层,GetDriverByName方法需要一个驱动器名称。还需要检查一下目标数据路径是否正常,只有在目标路径正常时才创建数据源。

# 获取Shp数据驱动
shpDriver = gdal.GetDriverByName("ESRI Shapefile")

# 检查Shp文件是否存在
if os.path.exists(targetPath):
    try:
        shpDriver.DeleteDataSource(targetPath)
        print("文件已删除!")
    except Exception as e:
        print(f"文件路径不存在!:{e}")
        return False
# 创建数据源
targetDataSource = shpDriver.CreateDataSource(targetPath)

获取源数据图层信息并创建坐标系统。osr模块下的SpatialReference方法用于定义空间参考信息。其中空间参考信息具有三种形式,可以传递字符串名称,EPSG代码或者wkt形式的坐标定义内容。如下采用name参数形式。

# 获取Shp数据图层
sourceDataSource = ogr.Open(sourcePath)
sourceLayer = sourceDataSource.GetLayer()

# 获取坐标系
srs = osr.SpatialReference(epsg=5646)
print(f"坐标系统名称:{srs.GetName()}")

打印坐标系统名称如下:

使用CreateLayer方法创建目标图层,本例中数据类型为LineString,所以geom_type参数为ogr.wkbLineString。之后遍历源图层数据,将属性字段和要素值写入投影图层之中。

# 创建Shp投影数据图层
geomType = sourceLayer.GetGeomType()
shpProjectLayer = targetDataSource.CreateLayer(
    "layer",
    srs=srs,
    geom_type=ogr.wkbLineString
)

# 获取源图层属性结构
layerDefn = sourceLayer.GetLayerDefn()
print(layerDefn.GetFieldCount())

fieldCount = layerDefn.GetFieldCount()

# 添加属性字段
for i in range(fieldCount):
    fieldDefn = layerDefn.GetFieldDefn(i)
    shpProjectLayer.CreateField(fieldDefn)

for feature in sourceLayer:
    shpProjectLayer.CreateFeature(feature)

if __name__  == "__main__":

    sourcePath"E:\data\test_data\geojson\river.shp"
    targetPath"E:\data\test_data\geojson\river_project.shp"

    ShpProjection(sourcePath,targetPath)

运行成功之后在ArcGIS中打开图层属性显示如下:

OpenLayers示例数据下载,请在公众号后台回复:ol数据

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集

GDAL 实现矢量数据读写

GDAL 数据类型大全

百度宣布,良心画图工具停服!

GDAL 实现 GIS 数据读取转换(全)

GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据

GIS 数据转换:使用 GDAL 将 GeoJSON 转换为 Shp 数据

GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据

用另一种方式让《留白》继续存在下去

作者 codelang
2026年1月3日 21:16

最近翻阅相册,旧旧的照片有一张独特的排版,思绪一下就把我拉回了从前,这张照片是《留白》App 编辑生成的,一款非常具有艺术感的 App。《留白》是我前司创始人开发的 App,当时用户量还挺多的,首页还会推荐一些摄影师拍摄的作品。

最近想从 App Store 重新下载把玩下发现,该应用下架很多年了,并且从一些三方渠道拿到的 Apk 都是 32 位的,现在主流的手机已经无法安装了,通过知乎发现,已经很久没有维护了:

这款独特又小众的 App 无法使用着实有点可惜,留白的 preview 页:

所以,我萌生了一个想法,基于强大的 AI 能力,让这款产品重新再现一下,说干就干。

开发步骤:

  • 技术选型:最主要是实现快,能快速验证思路与想法,最终敲定下来用 react
  • AI 编码:最终敲定下来是 claude clode(后文简称 cc) + 智谱 GLM-4.7
  • 部署体验:将 react 部署到线上体验
  • 终极体验:将 react 实现的能力,让 AI 翻译成微信小程序,部署到微信小程序中

在技术选型中,我最先思考的是如何快速实现自己想要的效果,相比较知识库与社区的成熟度,对于 AI 来说,生成前端的正确率会比移动端会高点,并且能快速的调试。我将留白 preview 图片丢给了 cc,让 cc 按照该图生成前端应用,并为该图增加了滤镜、字体、主题等一类设置,如下是实现效果,你也可以访问 liubai-white.vercel.app/ 进行试玩:

cc + 智谱 GLM-4.7 的生成效果与正确率还是蛮惊艳的,并没有因为一个错误的解决而无限陷入另一种错误。

在经过各种细节与优化中,我发现这种方案可行,但提供网页让用户使用的话,一个是体验不佳,另一个是没有人会记住一个网址,所以,我决定将这个方案的实现迁移到微信小程序。

起初,我了解到 Taro + react 是可以生成微信小程序代码的,我尝试让 cc 将 react 转成 Taro 模式,然后让 Taro 编译 react 代码生成微信小程序项目,但经过我各种调试,都无法达成满意的效果,最主要的原因是 react 很多库的实现,在微信小程序上没有,Taro 无法进行转译,所以,这种方案我立马放弃了。

最终还是直接让 cc 翻译了 react 的项目结构,并生成了项目的功能指南,接着我让 cc 重新创建了个 miniapp 的目录,让他读取 react 代码与功能指南,重新生成微信小程序项目。

未来,AI 也可能是跨端的另一种实现

转义的效果依然非常惊艳,第一把就成功了,预览效果与 react 差不多,但细节还需要再做下处理,本以为没啥大问题,当我将图片下载下来时发现,各种排版错乱。检查了下项目代码实现,在微信小程序中,页面布局的展示是通过 wxml 渲染的,但生成图片是通过 canvas 画的,也就是说,canvas 要对着 wxml 的 css 进行转义,而且 AI 的转义能力特别弱,即使我用 cursor+opus 尝试过,也是一塌糊涂,如果界面渲染排版发生了变动,AI 又会忘了 canvas 也要变动, 在这块的调试上吃了太多力气。那怎么办呢,只能给 cc 添加更多的 rules 规则,去定制规则并且限制他的随意发挥。

经过各种调试对垒,第一版的 留白 微信小程序实现啦,效果图如下:

如果你想体验的话,可以扫如下的小程序码:

有任何体验的问题,或是对 AI Coding 有想法的,都可以在公众号中找到我的联系方式。

元旦这三天智谱的 tokens 消耗:

image.png

对于 AI Coding ,我有一点想说:

随着大模型越来越强,我觉得用谁家的模型会变得没那么重要,比如我用的智谱GLM 就能帮我实现非常惊艳的能力,我觉得当前最重要的还是工具,我非常推荐 claude code,Anthropic 这家公司一直走在 AI Coding 的最前沿,无论是 mcp、cli,还是最近非常火热的 skills 、subAgent,他总能从工程化思维去解决你的问题,让 AI 代码生成的更可靠。比如 prompt 的拆解与注入、token 如何减少消耗、上下文污染如何规避等等。

2026 年,AI Coding 一定是往更可靠的大方向走,但要想更可靠,交给开发者肯定是不足够的,一定是有很多工具去辅助开发者更可靠才行。

正如 Anthropic 首席产品官 Mike Krieger 所说的:

能可靠地接过你手里的活儿。

Turborepo 的 Docker 化实战

作者 无声2017
2026年1月3日 18:14

引言

从 2025-07 开始,我开始在 Vue 项目中使用 Docker + Workflow 的方式进行打包部署,在此简单列举几个好处,例如:

  • 开发、测试、生产环境完全一致,Docker 镜像封装了 OS、Node.js 版本、依赖、构建工具等,避免因环境差异导致的构建失败或运行时错误。
  • 节省人力,无需手动登录服务器执行命令。
  • 每次部署对应一个带 tag 的 Docker 镜像,出现问题时,一键回滚到上一个镜像版本,无需重新构建。

本文将先简单回顾传统 Vue 项目 Docker 打包部署的方式与 Turborepo 的项目结构。说明如何在 Turborepo 架构的项目中通过 Docker 打包部署 Vue 项目。

Vue 项目 Docker 打包部署

分为 Node 镜像构建以及 Nginx 部署两个阶段,Dockerfile 内容如下:

# 第一阶段:node镜像打包
FROM node:latest AS frontend-builder
WORKDIR /build-app
COPY . .
RUN npm install
RUN npm run build

# 第二阶段:nginx打包
FROM nginx:latest
EXPOSE 80
WORKDIR /app
# 替换nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 将第一阶段的静态文件复制到nginx中
RUN rm -rf /usr/share/nginx/html
RUN mkdir /usr/share/nginx/html
COPY --from=frontend-builder /build-app/dist /usr/share/nginx/html

# 运行
CMD ["nginx", "-g", "daemon off;"]

nginx.conf 是在项目根目录下的自定义 Nginx 配置文件,详情可见个人相关配置

Turborepo 项目结构回顾

Turborepo 本身不是构建工具,而是一个高性能的 Monorepo 任务编排器。它依赖底层包管理器(如 pnpm、yarn)来管理依赖,通过智能缓存和并行执行加速构建、测试等任务。

一个典型的 Turborepo + pnpm 项目结构如下:

my-turborepo/
├── apps/
│   ├── app1/               # 前端应用(如 Vue/React)
│   │   ├── src/
│   │   ├── public/
│   │   ├── package.json   ← name: "app1"
│   │   └── vite.config.ts
│   └── app2/
│       ├── ...
│       └── package.json   ← name: "app2"
├── packages/              # ← 缺失则 workspace 依赖安装失败
│   ├── ui/                # 共享 UI 组件库
│   │   └── package.json   ← name: "@my/ui"
│   └── utils/             # 工具函数
│       └── package.json   ← name: "@my/utils"
├── package.json           # 根工作区配置 + turbo 脚本
├── pnpm-workspace.yaml    # ← 缺失则 turbo 找不到 app1
├── turbo.json             # Turborepo 任务流水线配置
└── .npmrc                 # pnpm 配置(含 hoist 等)

Turborepo 的核心特点与 Docker 相关性如下:

特性 说明 对 Docker 的影响 ❗
Workspace 结构 所有子项目(apps/packages)通过 pnpm-workspace.yaml 被识别为 workspace 包 Docker 构建时必须复制该文件,否则 pnpm 无法识别子包
依赖链接(Linking) 子项目间通过 workspace:* 协议引用(如 "@my/ui": "workspace:*"),实际是符号链接 不能只复制单个 app 目录,必须复制整个 apps/packages/
根目录统一安装 所有依赖在根目录通过 pnpm install 安装,node_modules 位于根目录 构建命令必须在根目录执行,不能进入 apps/web 后再 pnpm install
Turbo 任务驱动 通过 turbo run build 并指定 --filter=app1 来构建特定应用 需确保 turbo.json 和子项目的 name 正确,否则 filter 失败

编写 Turborepo 中的 Dockerfile

首先展示我最终的 Dockerfile.dev(prod、test 中会使用到公司的镜像源),代码如下:

# 第一阶段:构建
FROM node:22-alpine AS frontend-builder

WORKDIR /build-app

# 启用 pnpm(通过 Corepack)
ENV COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
RUN corepack enable
RUN echo "Corepack registry: $COREPACK_NPM_REGISTRY" && \
    corepack prepare pnpm@10.24.0 --activate

# 复制所有配置文件
COPY .npmrc ./
COPY pnpm-workspace.yaml ./
COPY turbo.json ./
COPY package.json pnpm-lock.yaml ./
COPY apps/ apps/
COPY packages/ packages/

# 安装所有依赖(含 devDependencies)
RUN pnpm install

# 构建 icic(在原始位置)
RUN pnpm turbo build --filter icic -- --mode production

# 第二阶段:Nginx
FROM nginx:alpine
EXPOSE 80
COPY apps/icic/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=frontend-builder /build-app/apps/icic/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

两者的核心差异对比如下:

维度 普通 Vue 项目 Turborepo(Monorepo)
项目结构 单一目录,package.json 在根目录 多包结构,Vue 项目在 apps/xxx/,根目录是 workspace
依赖安装位置 直接在项目根目录 npm install / pnpm install 必须在 workspace 根目录 安装所有依赖
构建命令 npm run buildvite build 必须用 turbo run build --filter=xxx(或进入子目录构建)
关键配置文件 只需 package.jsonvite.config.js 还需: • pnpm-workspace.yamlturbo.json • 根目录 .npmrc
Dockerfile COPY 范围 只需复制当前项目文件 必须复制: • 整个 apps/ • 整个 packages/ • 所有根配置文件
node_modules 位置 在项目根目录 在 workspace 根目录,子项目通过链接使用

总结

普通 Vue 项目只关注自身;Turborepo 项目需要关注整个 workspace。 Docker 构建时,必须还原完整的 workspace 上下文,否则依赖解析会断裂。

其他

常见错误排查

错误现象 可能原因 解决方案
pnpm: not found 未运行 corepack enable 添加 RUN corepack enable
No package found 'app1' 缺少 pnpm-workspace.yaml 确保 COPY pnpm-workspace.yaml ./
Cannot resolve @my/utils 未复制 packages/ 添加 COPY packages/ packages/
构建成功但页面空白 Nginx 未配置 try_files 检查 nginx.conf 是否包含 try_files $uri $uri/ /index.html;

个人相关配置

nginx.conf

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;

        # 新增下面这句,其他是默认nginx配置
        # 解决部分前端框架的路由问题,在浏览器刷新报错404
        try_files $uri $uri/ /index.html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

pnpm-workspace

packages:
  - "apps/*"
  - "packages/*"

.npmrc

# China mirror of npm
registry = https://registry.npmmirror.com

# 安装依赖时锁定版本号
save-exact = true

# 启用 hoist,让 bin 文件提升到根目录
shamefully-hoist = true
hoist=true
public-hoist-pattern=*

参考

React 之 自定义 Hooks 🚀

作者 xiaoxue_
2026年1月3日 17:15

自定义 Hooks 🚀

在 React 的世界里,Hooks 就像一把神奇的钥匙,为函数组件打开了状态管理和生命周期的大门。今天我们就来深入探索自定义 Hooks 的奥秘,看看它如何让我们的代码更优雅、更可复用!

一、hooks 详解 🧐

1. 什么是 hooks ?

Hooks 是 React 16.8 引入的新特性,它是一种函数编程思想的实践,允许我们在不编写类组件的情况下,在函数组件中使用状态(State)和其他 React 特性(如生命周期)。简单来说,Hooks 就是 “钩子”,能让我们轻松 “钩入” React 的内部特性,让函数组件拥有更强大的能力。

2. hooks 分类

Hooks 主要分为两类:

  • React 内置 Hooks:如useState(管理状态)、useEffect(处理副作用)、useContext(共享上下文)等,这些是 React 官方提供的基础工具。
  • 自定义 Hooks:由开发者根据业务需求封装的 Hooks,命名以use开头,本质是对内置 Hooks 和业务逻辑的组合封装,方便复用。

(前面我们讲解过 React 内置的 hooks 函数,感兴趣的小伙伴可以去翻翻我前面的文章看看)

3. hooks 有什么作用?

  • 让函数组件具备状态管理能力,摆脱类组件的繁琐语法(如this绑定、生命周期函数嵌套)。
  • 将组件中的相关逻辑聚合在一起,而非分散在不同的生命周期函数中(比如类组件中componentDidMountcomponentDidUpdate可能写重复逻辑)。
  • 实现逻辑复用,通过自定义 Hooks 将相同的状态逻辑抽离,供多个组件使用。

4. 为什么需要自定义 hooks ?

在开发中,我们经常会遇到多个组件需要共享相同状态逻辑的场景。比如:多个组件都需要监听鼠标位置、都需要处理本地存储数据、都需要发起相同的 API 请求等。

如果没有自定义 Hooks,我们可能会通过 “复制粘贴代码” 或 “高阶组件”“render props” 等方式复用逻辑,但这些方式要么导致代码冗余,要么增加组件层级复杂度。

而自定义 Hooks 就像一个 “逻辑容器”,能将这些重复逻辑抽离成独立函数,让组件只关注 UI 渲染,极大提升代码的复用性和可维护性!

二、先来看一个简单案例:响应式显示鼠标的位置 🖱️

1. 需求分析

我们要实现一个包含两个核心功能的小应用:

(1)计数功能:一个计数器,点击按钮可以增加数字。

(2)条件显示鼠标位置:当计数器的值为偶数时,显示鼠标在页面上的实时坐标;为奇数时,不显示。

2. 核心实现(两个文件)

(1)App2.jsx:主组件,负责管理计数器状态和根据计数奇偶性条件渲染鼠标位置组件。

(2)useMouse.js:自定义 Hooks,封装鼠标位置监听的逻辑,提供xy坐标供组件使用(向外暴露的状态和方法放在 return 中返回)。

3. 代码展示

(1)App2.jsx

jsx

import { useState } from 'react';
import { useMouse } from './hooks/useMouse.js';

// 鼠标位置展示组件(纯UI组件)
function MouseMove() {
    // 调用自定义Hook获取鼠标坐标
    const { x, y } = useMouse();
    return (
        <>
            <div>
                鼠标位置:{x}, {y}
            </div>
        </>
    );
}

export default function App() {
    // 定义计数器状态,初始值为0
    const [count, setCount] = useState(0);

    return (
        <>
            {/* 显示当前计数 */}
            {count}
            {/* 点击按钮增加计数(使用函数式更新确保获取最新count) */}
            <button onClick={() => setCount((count) => count + 1)}>
                点击增加
            </button>
            {/* 当count为偶数时,渲染MouseMove组件显示鼠标位置 */}
            {count % 2 === 0 && <MouseMove />}
        </>
    )
}

代码逻辑详解

  • App组件通过useState管理计数器count的状态,点击按钮时通过setCount更新值。
  • 定义了MouseMove组件,它不包含任何业务逻辑,仅通过调用useMouse()获取鼠标坐标并渲染,是一个纯 UI 组件。
  • 通过条件渲染{count % 2 === 0 && <MouseMove />}实现 “偶数显示鼠标位置,奇数不显示” 的需求。
(2)useMouse.js

js

import { useState, useEffect } from 'react';

// 自定义Hook:封装鼠标位置监听逻辑(命名以use开头)
export function useMouse() {
    // 定义状态存储鼠标x、y坐标,初始值为0
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

    // 副作用:监听鼠标移动事件
    useEffect(() => {
        // 事件处理函数:更新x、y坐标
        const update = (event) => {
            console.log('/////');
            setX(event.pageX); // 更新x坐标为鼠标相对于文档的水平位置
            setY(event.pageY); // 更新y坐标为鼠标相对于文档的垂直位置
        }
        // 绑定mousemove事件,触发时调用update更新坐标
        window.addEventListener('mousemove', update);
        console.log('||||| 挂载'); // 组件挂载时打印

        // 清理函数:组件卸载时移除事件监听,避免内存泄漏
        return () => {
            window.removeEventListener('mousemove', update); // 移除相同的事件处理函数
            console.log('===== 清除'); // 清理时打印
        }
    }, []); // 空依赖数组:仅在组件挂载时执行一次,卸载时执行清理

    // 返回鼠标坐标,供组件使用
    return {
        x,
        y,
    }
}

代码逻辑详解

  • useMouse遵循命名规范(以use开头),内部可以调用内置 Hooks(useStateuseEffect)。

  • useState定义xy状态,分别存储鼠标的水平和垂直坐标。

  • useEffect处理鼠标监听的副作用:

    • 挂载时:绑定mousemove事件,鼠标移动时触发update函数更新xy
    • 卸载时:通过useEffect的返回函数移除mousemove事件监听,避免组件已卸载但事件仍触发的内存泄漏(比如当count为奇数时,MouseMove组件卸载,此时会执行清理函数)。
  • 最后返回包含xy的对象,让组件可以获取鼠标坐标。

QQ20260103-170603.png

4. 效果展示:

  • 当点击按钮使count为 0、2、4 等偶数时,页面会显示 “鼠标位置:x, y”,移动鼠标时坐标会实时更新,控制台打印 “||||| 挂载”。
  • 当点击按钮使count为 0、2、4 等偶数时,移动鼠标,控制台打印“/////”
  • count为 1、3、5 等奇数时,鼠标位置不显示,控制台打印 “===== 清除”且不打印“/////”(表示事件监听已移除)。

QQ202613-165111.gif

三、详解自定义 hooks 📚

通过上面的简单案例,我们可以总结出关于自定义 Hooks 的必备知识:

1. 什么时候需要自定义 hooks ?

(1)逻辑复用场景当多个组件需要共享相同的状态逻辑时,通过自定义 Hook 封装可避免代码重复。上面案例中,useMouse封装了鼠标位置监听的完整逻辑(状态管理、事件绑定 / 解绑),使得MouseMove组件无需重复编写该逻辑。若其他组件(如 “鼠标轨迹绘制组件”)也需要获取鼠标位置,可直接复用useMouse

(2)分离 UI 与业务逻辑当组件中同时包含 UI 渲染和复杂业务逻辑(如事件监听、数据处理)时,自定义 Hooks 可将业务逻辑抽离,让组件只专注于 UI 展示。上面案例中,MouseMove组件仅负责渲染鼠标坐标(纯 UI 逻辑),而鼠标监听的业务逻辑被封装在useMouse中,使组件代码更简洁易维护。

(3)抽象复杂副作用逻辑当逻辑涉及useStateuseEffect等 Hook 的组合使用(如状态管理 + 副作用处理)时,自定义 Hook 可将其抽象为独立单元,提高可读性。上面案例中useMouse整合了useState(管理 x、y 坐标)和useEffect(鼠标事件监听 / 清理),将分散的逻辑聚合为可复用的单元。

2. 如何自定义 hooks?

(1)遵循命名规范函数名必须以use开头(如useMouseuseTodos),这是 React 的强制约定,确保 React 能识别 Hook 的调用规则(如只能在组件或其他 Hook 中使用)。

(2)封装核心逻辑在函数内部可调用其他 Hook(如useStateuseEffect),并通过return暴露需要的状态或方法。案例中useMouse的实现步骤:

  • useState定义xy状态,存储鼠标坐标;
  • useEffect绑定mousemove事件,实时更新坐标;
  • 通过return { x, y }将坐标暴露给组件使用。

(3)设计返回值根据需求返回状态、方法或对象,方便组件灵活使用。案例中返回包含xy的对象,组件通过const { x, y } = useMouse()直接获取坐标;若逻辑复杂(如待办事项管理),可返回状态和操作方法的集合(如{ todos, addTodo, deleteTodo })。

3. 自定义 hooks 有什么注意事项?

(1)严格遵循 Hook 调用规则

  • 只能在组件函数或其他自定义 Hook 中调用(上面案例中useMouseMouseMove组件中调用,符合规则);
  • 不能在循环、条件判断或普通函数中调用(避免 React 无法保证 Hook 调用顺序的一致性)。

(2)清理副作用,避免内存泄漏若 Hook 包含副作用(如事件监听、定时器、API 请求),必须在useEffect的清理函数中移除副作用。上面案例中,useMouseuseEffect的返回函数中调用removeEventListener,确保MouseMove组件卸载时(count为奇数时)移除鼠标监听,避免组件已卸载但事件仍触发的内存泄漏(控制台会打印 “===== 清除” 验证)。

(3)保持单一职责一个自定义 Hook 应专注于解决一个特定问题。上面案例中useMouse仅负责鼠标位置监听,职责单一;若强行加入其他逻辑(如键盘监听)会导致 Hook 臃肿,降低复用性。

四、复杂案例实战:待办事项(Todo List)应用 📝

1. 需求分析:

本案例是一个基于 React 的待办事项应用,核心需求围绕待办事项的全生命周期管理,具体包括:

  • 输入框输入待办内容,点击添加按钮创建新待办;
  • 勾选复选框标记待办事项为 “已完成” 或 “未完成”;
  • 点击删除按钮移除特定待办事项;
  • 页面刷新后,已添加的待办数据不丢失(本地持久化);
  • 无待办事项时显示空状态提示。

核心痛点:如何将数据管理逻辑与 UI 渲染逻辑分离,实现代码复用和维护性提升 —— 这正是自定义 Hook 的核心应用场景。

2. 实现架构设计:

(1)整体架构采用 “UI 组件 + 自定义 Hook” 的分离模式:

  • UI 组件:负责渲染界面和处理用户交互(输入、点击等);
  • 自定义 Hooks(useTodos):封装数据状态、业务逻辑和持久化操作。

(2)核心设计:自定义 hooks 的职责useTodos作为数据和逻辑的 “中央处理器”,承担以下职责:

  • 管理待办事项的响应式状态(todos数组);
  • 提供操作待办的方法(添加、切换状态、删除);
  • 实现数据本地持久化(localStorage读写)。

3. 代码展示(核心代码)

(1)自定义 Hook:useTodos.js

js

// 封装响应式的todos业务逻辑
import { useState, useEffect } from 'react';

// 定义localStorage的键名,用于存储待办数据
const STORAGE_KEY = 'todos';

// 从localStorage加载待办数据
function loadFromStorge() {
    const StoredTodos = localStorage.getItem(STORAGE_KEY);
    // 若有数据则解析为JSON,否则返回空数组
    return StoredTodos ? JSON.parse(StoredTodos) : [];
}

// 将待办数据保存到localStorage
function saveToStorage(todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

export const useTodos = () => {
    // 初始化todos状态:从localStorage加载(useState接收函数,确保只执行一次)
    const [todos, setTodos] = useState(loadFromStorge);

    // 监听todos变化,实时保存到localStorage(持久化)
    useEffect(() => {
        saveToStorage(todos);
    }, [todos]); // 依赖todos:只有todos变化时才执行

    // 添加待办事项
    const addTodo = (text) => {
        setTodos([
            ...todos, // 复制现有待办
            { 
                id: Date.now(), // 用时间戳作为唯一ID
                text, // 待办内容
                completed: false // 初始状态为未完成
            }
        ]);
    }

    // 切换待办事项的完成状态
    const toggleTodo = (id) => {
        setTodos(
            todos.map((todo) => {
                // 找到目标待办,反转completed状态
                if (todo.id === id) {
                    return { ...todo, completed: !todo.completed };
                }
                return todo; // 非目标待办不变
            })
        );
    }

    // 删除待办事项
    const deleteTodo = (id) => {
        setTodos(
            todos.filter((todo) => todo.id !== id) // 过滤掉要删除的待办
        );
    }

    // 暴露状态和方法给组件使用
    return {
        todos,
        addTodo,
        toggleTodo,
        deleteTodo,
    }
}

逻辑详解

  • loadFromStorgesaveToStorage:封装localStorage的读写逻辑,实现数据持久化(页面刷新后数据不丢失)。
  • useTodos内部用useState管理todos状态,初始化时从本地存储加载数据。
  • useEffect监听todos变化,每次变化都调用saveToStorage保存到本地,确保数据实时同步。
  • 提供addTodo(添加)、toggleTodo(切换状态)、deleteTodo(删除)三个方法,通过setTodos更新状态,遵循 “不可变数据” 原则(用扩展运算符、mapfilter创建新数组)。
  • 最后返回todos状态和操作方法,供 UI 组件使用。
(2)UI 组件:App.jsx(主组件)

jsx

import { useState } from 'react';
import { useTodos } from './hooks/useTodos.js';
import TodoList from './components/TodoList.jsx';
import TodoInput from './components/TodoInput.jsx';

export default function App() {
    // 调用自定义Hook获取待办数据和操作方法
    const { todos, addTodo, deleteTodo, toggleTodo } = useTodos();

    return (
        <>
            {/* 输入组件:传递addTodo方法用于添加待办 */}
            <TodoInput addTodo={addTodo} />
            {/* 条件渲染:有待办时显示列表,否则显示空状态 */}
            {
                todos.length > 0 ?
                    <TodoList
                        todos={todos}
                        deleteTodo={deleteTodo}
                        toggleTodo={toggleTodo}
                    /> : 
                    (<div>暂无待办事项</div>)
            }
        </>
    )
}

逻辑详解

  • App组件作为入口,通过useTodos()获取待办数据(todos)和操作方法(addTodo等)。
  • 渲染TodoInput组件(负责输入待办)并传递addTodo方法,让输入组件能触发添加操作。
  • 渲染TodoList组件(负责展示待办列表)并传递todosdeleteTodotoggleTodo,让列表组件能展示数据和处理删除 / 切换操作。
  • 通过条件渲染实现 “无待办时显示空提示” 的需求。
(3)UI 组件:TodoInput.jsx(输入组件)

jsx

import { useState } from 'react';

export default function TodoInput({ addTodo }) {
    // 管理输入框文本状态(受控组件)
    const [text, setText] = useState('');

    // 输入框变化时更新text状态
    const handleChange = (e) => {
        setText(e.target.value);
    }

    // 表单提交时添加待办
    const handleSubmit = (e) => {
        e.preventDefault(); // 阻止表单默认提交行为
        if (text.trim() === '') { // 过滤空输入
            return;
        }
        addTodo(text); // 调用父组件传递的addTodo方法添加待办
        setText(''); // 清空输入框
    }

    return (
        <form className='todo-input' onSubmit={handleSubmit}>
            {/* 受控组件:value绑定text,变化触发handleChange */}
            <input type='text' value={text} onChange={handleChange} />
            <button type='submit'>添加</button>
        </form>
    )
}

逻辑详解

  • 纯 UI 组件,仅负责输入框的状态管理和提交逻辑,不关心数据如何存储或处理。
  • 通过useState管理输入框的text状态,实现 “受控组件”(输入框值由 React 状态控制)。
  • 表单提交时调用addTodo(从App组件传递的方法)添加待办,并清空输入框。
(4)UI 组件:TodoList.jsx(列表组件)

jsx

import TodoItem from './TodoItem.jsx';

export default function TodoList({ todos, deleteTodo, toggleTodo }) {
    return (
        <ul className="todo-list">
            {/* 遍历todos数组,为每个待办渲染TodoItem组件 */}
            {todos.map((todo) => (
                <TodoItem 
                 key={todo.id} // 唯一key
                 todo={todo} // 传递单个待办数据
                 deleteTodo={deleteTodo} // 传递删除方法
                 toggleTodo={toggleTodo} // 传递切换状态方法
                />
            ))}
        </ul>
    )
}

逻辑详解

  • 接收todos数组,通过map遍历渲染每个待办项(TodoItem)。
  • 仅负责列表渲染,将单个待办的数据和操作方法传递给TodoItem,自身不处理业务逻辑。
(5)UI 组件:TodoItem.jsx(单个待办项)

jsx

export default function TodoItem({ todo, deleteTodo, toggleTodo }) {
    return (
        <li className="todo-item">
            {/* 复选框:状态绑定todo.completed,点击触发toggleTodo */}
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
            />
            {/* 待办文本:已完成时添加completed类(可用于样式区分) */}
            <span className={todo.completed ? 'completed' : ''}>
                {todo.text}
            </span>
            {/* 删除按钮:点击触发deleteTodo */}
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
        </li>
    )
}

逻辑详解

  • 接收单个待办数据(todo)和操作方法,渲染复选框、文本和删除按钮。
  • 复选框状态与todo.completed绑定,点击时调用toggleTodo切换状态。
  • 文本根据completed状态添加样式类(如划线效果),删除按钮点击时调用deleteTodo删除当前待办。

QQ20260103-170824.png

4. 效果展示:

  • 输入框输入内容,点击 “添加” 按钮,待办列表会新增一项。
  • 勾选复选框,待办文本会显示为 “已完成” 样式。
  • 点击 “删除” 按钮,对应待办项会从列表中移除。
  • 刷新页面后,所有待办数据依然存在(本地存储生效)。
  • 当列表为空时,页面显示 “暂无待办事项”。

QQ202613-1709.gif

5. 案例总结:

  • 逻辑复用最大化useTodos封装了待办事项的所有核心逻辑(状态管理、CRUD 操作、本地持久化),若其他组件(如 “待办统计组件”)需要使用待办数据,可直接调用useTodos,无需重复编写逻辑。
  • UI 与业务彻底分离:所有 UI 组件(TodoInputTodoList等)仅负责渲染和传递交互,不包含任何数据处理逻辑,代码结构清晰,维护成本低。
  • 副作用集中管理:本地存储的读写逻辑被封装在useTodosuseEffect中,避免副作用分散在多个组件中,便于统一维护。

五、面试官会问 🤔

  1. 自定义 Hook 和普通函数有什么区别? 自定义 Hook 以use开头,内部可以调用其他 Hook(如useStateuseEffect),且必须遵循 Hook 调用规则(只能在组件或其他 Hook 中调用);普通函数不能调用 Hook,也没有命名限制。
  2. 为什么自定义 Hook 必须以 use 开头? 这是 React 的约定,确保 React 能通过命名识别 Hook,从而验证 Hook 的调用规则(如避免在条件语句中调用),防止出现逻辑错误。
  3. 如何避免自定义 Hook 中的内存泄漏? 若 Hook 包含副作用(如事件监听、定时器),必须在useEffect的清理函数中移除副作用(如removeEventListenerclearTimeout),确保组件卸载时副作用被清除。
  4. 自定义 Hook 如何实现状态隔离? 每个组件调用自定义 Hook 时,React 都会为其创建独立的状态实例,不同组件之间的状态互不干扰(如两个组件调用useMouse,会分别维护自己的xy状态)。
  5. 什么时候应该抽离自定义 Hook? 当多个组件需要共享相同的状态逻辑,或组件中业务逻辑过于复杂(导致 UI 与逻辑混杂)时,就应该抽离为自定义 Hook。

六、结语 🌟

自定义 Hooks 是 React 中 “逻辑复用” 的最佳实践,它让我们的代码从 “重复冗余” 走向 “简洁高效”,从 “UI 与逻辑混杂” 走向 “职责清晰分离”。

通过本文的两个案例(鼠标位置监听和待办事项应用),我们可以看到:一个设计良好的自定义 Hook,就像一个 “功能模块”,能让组件专注于 UI 渲染,让逻辑专注于业务处理。

希望大家在实际开发中多思考、多实践,将自定义 Hooks 运用到项目中,让代码更优雅、更可维护!🎉

图文+示例,带你彻底搞清楚那些加密手段...!

作者 momo06117
2026年1月3日 17:02

前言

最近,小编在学习的过程中有了解到加密的手段,因此想来分享一下自己学到的一些新知识。如有讲得不好或遗漏 欢迎各位大佬在评论区指出。

关于加密

加密是指通过特定手段将明文存储的内容加密成密文。那么为什么要进行加密呢?

互联网中各种攻击防不胜防,如果敏感内容不加密存储,例如用户密码之类的(一想到自己的密码明晃晃的暴露在传输过程中,是不是有点后背一凉的感觉~),那攻击者就能够轻易获取用户隐私信息并实施攻击。

因此加密是必不可少的。那么加密的手段有哪些?或者我们在针对什么场景应该做什么加密?

下面小编一一介绍并通过node手段简单实现。

hash加密

加密手段中,最常见的就是hash加密,指通过一定的算法将明文转化为密文。

比如将密码abc随机后退3格加密成为dfg,这是一种最简单的加密手段。

现代常见最常见的加密手段有MD5SHA加密等,由于其复杂性hash加密是不可逆的。但是由于hash算法固定,因此同一个密码加密后是相同的密文,因此攻击者想到可以通过彩虹表攻击。

故实际生产中,我们往往会往明文先加盐(一段随机的字符串插入),然后再进行hash加密。

image.png

使用场景: 数据库中的密码存储(由于hash不可逆,实则程序员也不知道你的密码)

实现示例: 使用bcrypt进行加密

//自动加盐并且内嵌
const hashPassword = bcrypt.hashSync(password,10) 
//解密
const compareResult = bcrypt.compareSync(userinfo.password,result[0].password)

对称加密

上面说到了hash算法,但由于hash算法是不可逆的,故常用于存储加密,而实际开发中我们会经常用到传输加密,故对称加密应运而生。

对称加密是指将密码通过密钥加密后生成密文,对方再通过同一密钥破解生成的密文。

常见手段AES加密

优缺点:加密简单,解密速度快。但密钥的管理和传输困难。

image.png

实际场景:大量需要被加密的内容

实现示例:使用CryptoJS进行AES加密

const cryptoJs = require('crypto-js')

const message = 'hello word'
const key = 'this is key' //实际过程中 对称密钥通常是随机生成的

//加密
const encrypted = cryptoJs.AES.encrypt(message, key)

console.log('对称加密密文:',encrypted.toString())

//将密文解密
const decryptedMessage  = cryptoJs.AES.decrypt(encrypted,key)

console.log('解密:',decryptedMessage.toString(cryptoJs.enc.Utf8))

image.png

非对称加密

目的是为了解决上面提到的密钥安全问题,对称加密密钥被截获,密文也就轻易可知了。

非对称加密是指发送端用公钥进行加密,然后采用私钥进行解密,公钥是公开的,私钥是只有接受方才有的。就像一个箱子,发送方把用锁把箱子锁上,接收方用钥匙解锁。

常见手段: RSA非对称加密

优缺点: 由于私钥只有接收方可知,保密性好。但加密和解密比较耗时。

image.png

实际场景: 密钥交换,数字签名

实现示例: 使用node原生crypto进行RSA非对称加密

const crypto = require('crypto')

// 生成RSA密钥对
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048, // 密钥长度
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem'
  }
})

const data = 'hello word';
console.log('原始数据:', data);

// 使用公钥加密
const encryptedData = crypto.publicEncrypt(
  {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
  },
  Buffer.from(data)
)

console.log('加密后 (base64):', encryptedData.toString('base64'))

// 使用私钥解密
const decryptedData = crypto.privateDecrypt(
  {
    key: privateKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
  },
  encryptedData
)
console.log('解密后:', decryptedData.toString())

image.png

混合加密

上面有说到对称加密和非对称加密的优缺点,那么有没有集两者大成,既适合传输大量内容又密钥管理安全的方案呢? 答案是有的,那就是混合加密。

混合加密是指用非对称加密 加密 对称加密的密钥,可能有点绕,下面详细介绍一下。

加密过程

  1. 服务器生成一对公钥和私钥,将公钥发送给客户端
  2. 客户端收到私钥后,随机生成一对会话密钥(对称密钥)用服务器的公钥加密后发送
  3. 服务器收到密文后,用私钥解密,会话密钥
  4. 此时,服务器和客户端都有会话密钥,此后都使用会话密钥加密对话

优缺点:只需要前期少量资源用于非对称加密,后期可用对称加密传输大量内容,但实现比较复杂

image.png

实际场景: http加密的重要手段,现代大量加密场景等。

总结

总的来说,加密是我们开发过程中必不可少的一环,针对不同的环境,做出不同的加密手段是我们作为开发者必不可少的综合技能~

特性 Hash加密 对称加密 非对称加密 混合加密
加密类型 加密存储 加密传输 加密传输 加密传输
核心原理 单向散列函数 相同密钥加解密 公钥加密,私钥解密 非对称加密保护对称密钥
密钥数量 无密钥(有盐值) 1个共享密钥 2个密钥(公钥+私钥) 3个密钥(非对称对+对称密钥)
安全性 防篡改,不可逆 依赖密钥安全 依赖数学难题安全 综合安全等级最高
推荐场景 需要验证但不需要解密的场景 内部系统、大量加密 密钥分发、身份认证 公网通信、高安全要求

制作不易 礼貌集赞

image.png

zustand源码解析

作者 lzh_hz
2026年1月3日 16:19

zustand是什么,如何使用

Zustand 是一个轻量级的 React 状态管理库,比 Redux 简单,比 Context 高效。

  • 核心代码约 100 行,API 简单直观。
  • 无Provider
  • 符合 React Hooks 使用习惯
  • 只更新订阅了变化状态的组件。
  • 完整的类型推断和支持。
import { create } from 'zustand'

// 1️⃣ 定义类型(TypeScript)
interface BearStore {
  bears: number
  fish: number
  addBear: () => void
  eatFish: () => void
  reset: () => void
}

// 2️⃣ 创建 Store
const useBearStore = create<BearStore>((set) => ({
  bears: 0,
  fish: 10,
  addBear: () => set((s) => ({ bears: s.bears + 1 })),
  eatFish: () => set((s) => ({ fish: s.fish - 1 })),
  reset: () => set({ bears: 0, fish: 10 })
}))

// 3️⃣ 组件 A:只订阅 bears
function BearCounter() {
  const bears = useBearStore((s) => s.bears)
  const addBear = useBearStore((s) => s.addBear)
  
  return (
    <div>
      <h2>🐻 Bears: {bears}</h2>
      <button onClick={addBear}>Add Bear</button>
    </div>
  )
}

// 4️⃣ 组件 B:只订阅 fish
function FishCounter() {
  const fish = useBearStore((s) => s.fish)
  const eatFish = useBearStore((s) => s.eatFish)
  
  return (
    <div>
      <h2>🐟 Fish: {fish}</h2>
      <button onClick={eatFish}>Eat Fish</button>
    </div>
  )
}

// 5️⃣ 主应用
function App() {
  return (
    <div>
      <BearCounter />
      <FishCounter />
    </div>
  )
}

当调用zustand的create时会发生什么

import create from 'zustand';
const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 }))
}))

create 方法最终会调用vanvailla.ts中的createStoreImp 函数

createStoreImp主要完成的功能:

  • 创建zustand的api(setSate,getState,getInitialState, subscribe)
  • 在调用时会执行传入的函数,将最终结果赋值给 initialState
// 这里的createState是下面这个函数
// (set) => ({
//   count: 0,
//   increment: () => set((s) => ({ count: s.count + 1 }))
// })
// 最终initialState的值是
// { 
//   count:0,
//   increment:() => set((s) => ({ count: s.count + 1 }))
// }

const initialState = (state = createState(setState, getState, api))
return api as any

createStoreImp返回的api会在react.ts中的createImp中使用

import { createStore } from './vanilla.ts'
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  const api = createStore(createState)

  const useBoundStore: any = (selector?: any) => useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

useStore会创建一个hook, 将获取到的api 传入React.useSyncExternalStore()方法,从而实现当setSate时,订阅了这个state的组件都会触发重新渲染.

export function useStore<TState, StateSlice>(
  api: ReadonlyStoreApi<TState>,
  selector: (state: TState) => StateSlice = identity as any,
) {
  const slice = React.useSyncExternalStore(
    api.subscribe,
    React.useCallback(() => selector(api.getState()), [api, selector]),
    React.useCallback(() => selector(api.getInitialState()), [api, selector]),
  )
  React.useDebugValue(slice)
  return slice
}

当在一个组件调用useStore时,都会执行React.useSyncExternalStore(),useSyncExternalStore() 会执行api.subscribe. api.subscibe定义在vanilla.ts的createStoreImp函数中

// 这里的listener是由react的useSyncExtenalStore传入的callback function, 会注册到listeners里
// 调用callback function会触发组件重新渲染
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
  }
// 当 setSate里,会执行listeners里的callback function, 从而实现组件更新
 const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
    // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
    const nextState =
      typeof partial === 'function'
        ? (partial as (state: TState) => TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== 'object' || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      // 关键实现
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

再回到react.ts中的createImpl函数,会返回一个hook给业务组件去调用

const api = createStore(...)  // api 已创建                         │
│                                                                      │
│  // 创建 Hook 函数                                                    │const useBoundStore = (selector) => useStore(api, selector)         │
│  //                                     │                            │//  闭包捕获 api ◄─────────────────────┘                            │
│                                                                      │
│  // 将 api 方法挂载到 Hook 上                                         │Object.assign(useBoundStore, api)                                   │
│  //                                                                  │//  useBoundStore.getState = api.getState                           │//  useBoundStore.setState = api.setState                           │//  useBoundStore.subscribe = api.subscribe                         │
│                                                                      │
│  return useBoundStore                                                │
│                           

create函数主要作了以下三件事:

  • 创建 vanilla store (包含zustand的api)
  • 调用用户函数
  • 包装成hook

当getState时会发生什么

const useStore = create((set) => ({
  count: 0,
  name: 'zustand'
}))

// 获取当前状态
const state = useStore.getState()
// { count: 0, name: 'zustand' }

// 获取特定字段
const count = useStore.getState().count
// 0

根据上面的create实现解析,useStore的getState()最终会调用定义在vanilla.ts中的createStoreImpl中的getState

const getState: StoreApi<TState>['getState'] = () => state
// 如果调用时未触发setState, 获取到的是用户在ceate时传入的state, 调用create时会执行以下代码
const initialState = (state = createState(setState, getState, api))

当setState时会发生什么

const useStore = create((set) => ({
  count: 0
}))

// 在任何地方
useStore.setState({ count: 10 })

据上面的create实现解析,useStore的getState()最终会调用定义在vanilla.ts中的createStoreImpl中的setSate

const setState = (partial, replace) => {
  // 1️⃣ 计算新状态
  const nextState =
    typeof partial === 'function'
      ? partial(state)    // 函数式:调用函数得到新状态
      : partial           // 对象式:直接使用

  // 2️⃣ 检查是否真的变化
  if (!Object.is(nextState, state)) {
    const previousState = state
    
    // 3️⃣ 决定合并还是替换
    state =
      (replace ?? (typeof nextState !== 'object' || nextState === null))
        ? nextState                         // 替换
        : Object.assign({}, state, nextState)  // 浅合并

    // 4️⃣ 通知所有订阅者
    listeners.forEach((listener) => listener(state, previousState))
  }
}

setSate时不会触发所有组件的更新,只会触发订阅了相关state的组件更新

onst useStore = create((set) => ({
  count: 0,
  name: 'zustand',
  updateCount: () => set({ count: 1 }),
  updateName: () => set({ name: 'bear' })
}))

// 组件 A:只订阅 count
function CounterA() {
  console.log('CounterA 渲染')
  const count = useStore((s) => s.count)  // ← 只关心 count
  return <div>{count}</div>
}

// 组件 B:只订阅 name
function CounterB() {
  console.log('CounterB 渲染')
  const name = useStore((s) => s.name)  // ← 只关心 name
  return <div>{name}</div>
}

// 组件 C:订阅全部
function CounterC() {
  console.log('CounterC 渲染')
  const state = useStore()  // ← 关心所有
  return <div>{state.count} - {state.name}</div>
}

当组件A调用userStore时,最终会执行react.ts中的useStore

// 这里的select就是组件A传入的(s) => s.name
export function useStore<TState, StateSlice>(
  api: ReadonlyStoreApi<TState>,
  selector: (state: TState) => StateSlice = identity as any,
) {
  // getSnapshot 通过 selector 只取需要的部分, 只有需要的部分的值发送变化时,才会触发更新
  const slice = React.useSyncExternalStore(
    api.subscribe,
    React.useCallback(() => selector(api.getState()), [api, selector]),
    React.useCallback(() => selector(api.getInitialState()), [api, selector]),
  )
  React.useDebugValue(slice)
  return slice
}

具体流程

  1. setState 改变状态

  2. 通知所有订阅的组件(所有使用 useStore 的组件)

  3. 每个组件执行自己的 selector(newState)

  4. 比较 selector 返回的新旧值

    │
    
    ├── 相等 → 不重渲染
    
    │
    
    └── 不等 → 重渲染
    

Rect.useSyncExternalStore解析

react 18 新增的 Hook,用于安全地订阅外部数据源。

简化实现

function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
  // 存储当前快照
  const [snapshot, setSnapshot] = useState(() => {
    // SSR 时用 getServerSnapshot
    return typeof window === 'undefined' 
      ? getServerSnapshot?.() 
      : getSnapshot()
  })

  useEffect(() => {
    // React 创建的 callback
    const callback = () => {
      const newSnapshot = getSnapshot()
      // 只有真正变化才更新
      setSnapshot(prev => {
        if (Object.is(prev, newSnapshot)) {
          return prev  // 返回旧值,不触发更新
        }
        return newSnapshot
      })
    }

    // 订阅
    const unsubscribe = subscribe(callback)

    // 清理
    return unsubscribe
  }, [subscribe, getSnapshot])

  return snapshot
}

具体流程:

  1. Mount
    ├── 1.1 getSnapshot() → 初始值
    ├── 1.2 subscribe(callback) → 注册
    └── 1.3 return 初始值
  2. 状态变化
    ├── 2.1 你调用 callback()
    ├── 2.2 React 调用 getSnapshot() → 新值
    ├── 2.3 Object.is(旧值, 新值)
    └── 2.4 相等?
    ├── 2.4.1 是 → 跳过
    └── 2.4.2 否 → 重渲染
  3. Unmount
    └── 3.1 调用 unsubscribe()

总结

核心实现是使用React.useSyncExternalStore hook, 创建一个store, 当state里的值发生改变时,触发订阅了state的值的组件重新渲染。

GDAL 实现矢量数据读写

作者 GIS之路
2026年1月3日 16:13

GIS 数据的读写作为一个基础操作,是每一个GISer的必修课。在使用GDAL读取矢量数据时,需要掌握其基本的数据结构与类型,了解常用的数据读取方法,这样开发时才会起到事半功倍的效果。

在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解GDAL实现矢量数据读写。

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 矢量数据读取接口

GDAL中,矢量数据模型遵循下图所示结构,数据驱动->数据源->图层->属性。

2.1. 获取数据源

GDAL中可以使用Open方法直接打开数据源,进而读取图层数据。在GDAL Python API中,DriverDataset 类在Raster API文档中,既适用于矢量数据,也适用于栅格数据。

"""
说明:用于打开数据源
参数:
    -utf8_path:字符串,数据源路径
    -update:布尔值类型,默认值False,是否以更新模式打开数据集(默认为只读模式)
返回值:返回数据集或者None
"""
def Open(utf8_path,update=False)

如下以只读模式打开Shp数据源。

dataSource = ogr.Open(shpPath,False) # False表示只读,True表示可写

除了ogrOpen方法之外,也可以使用gdal模块的打开方法

2.2. 获取图层

GDAL获取图层的常用方法为GetLayer,该方法只有一个参数。iLayer值固定为0,对shapefile图层来说是可选项。

"""
说明:从数据集获取图层
参数:
    -iLayer:整型或字符串,图层名称或者基于0的图层索引值。
返回值:返回图层或者None
"""
def GetLayer(iLayer=0)

获取数据集中索引值为0的图层。

 # 获取图层
layer = dataSource.GetLayer(0)

除了GetLayer方法之外,还有一个GetLayerByIndex方法用于获取图层。

"""
说明:根据索引获取图层
参数:
    -index:整型,图层索引值。
返回值:返回图层或者None
"""
def GetLayerByIndex(index)

图层索引值处于0GetLayerCount() - 1之间。

最后还有个GetLayerCount方法用于获取图层数量。

从数据集中获取图层数量。

 # 获取图层数量
layerCount = dataset.GetLayerCount()
print(f"图层数量:{layerCount}")

2.3. 获取投影信息

GetProjectionGetProjectionRef方法都可以用于读取坐标系统,两者都返回一个字符串类型WKT格式的数据集空间参考信息。

从图层获取投影信息。

 # 图层投影
projectionRef = layer.GetProjectionRef()
print(f"空间参考:{projectionRef}")

此外还有一个方法GetSpatialRef也可用于获取数据集空间参考信息。

从图层获取空间参考信息。

 # 图层空间参考
spatialRef = layer.GetSpatialRef()
print(f"空间参考:{spatialRef}")

2.4. 获取要素

在知道FId(要素编号)的情况下,可以直接使用GetFeature方法获取要素。

 # 获取指定要素
feature = layer.GetFeature(0)

也可以通过GetNextFeature方法遍历要素特征,并且返回一个要素Feature。此方法适用于少数几个OGRLayer.GetNextFeature()效率不高的驱动程序,但总体而言,OGRLayer.GetNextFeature()是一个更自然的API。

从图层读取要素特征。

 # 获取要素
feature = layer.GetNextFeature()
limitCount = 0
 # 限制打印前十个要素
while feature and limitCount < 10:
    print(f"打印第【{limitCount+1}】个要素")

GetFeatureCount方法用于获取图层要素数量。

 # 获取图层要素数量
layerFeatureCount = layer.GetFeatureCount()
print(f"要素数量:{layerFeatureCount}")

2.5. 获取属性结构

GetLayerDefn方法用于获取图层属性表,返回要素定义信息。

获取图层属性。

 # 获取图层属性
layerProperty = layer.GetLayerDefn()

GetFieldCount方法用于获取字段数量。

读取图层要素数量。

 # 获取图层名称
layerName = layer.GetName()
 # 获取图层要素数量
layerFeatureCount = layer.GetFeatureCount()

GetFieldDefn用于获取字段定义信息,其中包括字段名称,字段类型。

读取属性表结构。

 # 获取图层属性
layerProperty = layer.GetLayerDefn()
 # 获取图层字段数量
fieldCount = layerProperty.GetFieldCount()
print(f"字段数量:{fieldCount}")

 # 获取字段信息
for j in range(fieldCount):
    # 获取字段属性对象
    fieldProperty = layerProperty.GetFieldDefn(j)
    # 获取字段属性名称
    fieldName = fieldProperty.GetName()
    # 获取字段属性类型
    fieldType = fieldProperty.GetTypeName()

    print(f"第 【{j}】 个字段名称:{fieldName},字段类型:{fieldType}")

GetField用于获取属性字段值,参数可以是字符串字段名,也可以是字段索引值。

GetGeometryRef方法用于获取要素几何对象信息。

读取几何对象。

 # 读取几何属性
geom = feature.GetGeometryRef()

2.6. 获取图层范围

GetExtent获取图层四至范围。

 # 获取范围
extent = layer.GetExtent()

3. 矢量数据修改接口

Driver(驱动程序)是一个知道如何执行特定功能的对象与某种数据类型(如shapefile),为了读写数据,就需要一个合适的驱动程序。

本文以下的数据修改都已shapefile驱动为例进行讲解。

3.1. 获取目标数据驱动

首先需要导入ogr模块用于处理矢量数据。GetDriverByName方法可以根据名称获取数据驱动。

from osgeo import ogr,osr

 # 获取Shp数据驱动
shpDriver = ogr.GetDriverByName('ESRI Shapefile')

3.2. 创建数据源

CreateDataSource方法根据文件路径创建数据源。

 # 创建Shp数据源
shpDataSource = shpDriver.CreateDataSource(shpPath)

3.3. 创建图层

通过CreateLayer方法创建图层,该方法具有四个参数,图层名称、坐标系统以及几何类型是必传。

 # 创建点图层
layer = shpDataSource.CreateLayer("points",srs,ogr.wkbPoint)

3.4. 创建属性字段

通过CreateField方法创建属性字段。

if field not in [lonField, latField]:
    # 创建字段定义
    fieldDefn = ogr.FieldDefn(field, ogr.OFTString)
    fieldDefn.SetWidth(254)
    # 直接创建字段,不要存储 FieldDefn 对象
    layer.CreateField(fieldDefn)

3.5. 创建几何对象

要素对象方法SetGeometry可以创建几何对象。

 # 创建几何对象
point = ogr.Geometry(ogr.wkbPoint)
point.AddPoint(lon,lat)
feature.SetGeometry(point)

3.6. 创建要素

通过图层方法CreateFeature创建要素。

 # 创建要素
feature = ogr.Feature(layer.GetLayerDefn())

 # 设置属性
for field in fieldnames:
    if field not in [lonField, latField]:
        feature.SetField(field, str(row[field]))
 # 创建几何
point = ogr.Geometry(ogr.wkbPoint)
point.AddPoint(lon,lat)
feature.SetGeometry(point)

 # 保存要素
layer.CreateFeature(feature)

4. 关闭数据源

为了防止内存泄露问题发生,在确定数据读取完成后需要关闭数据源。

 # 关闭数据源
dataSource = None

 # 关闭要素
feature.Destroy()
 # feature = None

小提示

GDAL中的方法采用大驼峰命名法

OpenLayers示例数据下载,请在公众号后台回复:ol数据
全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

GeoTools 开发合集(全)

OpenLayers 开发合集

GDAL 数据类型大全

百度宣布,良心画图工具停服!

GDAL 实现 GIS 数据读取转换(全)

自然资源部:我国地理信息产业总产值将超9000亿元

GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据

GIS 数据转换:使用 GDAL 将 GeoJSON 转换为 Shp 数据

GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据

Vue源码分析 - 从入口到构造函数的整体流程

作者 luckyCover
2026年1月3日 15:26

Vue 中的核心源码主要放在了 src/core 目录下,我们先来看下面这段代码

image.png

上面这段代码是我们平常创建一个 Vue 项目中 main.js 入口文件里的原始代码,这也是 Vue 创建应用的起点,接下来我们就来分析从 new Vue()$mount 这一过程 Vue 都做了哪些事,也就是做了哪些初始化。

首先是 src/core/index.ts 文件

image.png

我们看到它调用 initGlobalAPI 函数,并把 Vue 作为参数传进去,我们先不着急看 initGlobalAPI 内部逻辑,因为我们连它传入的这个参数 Vue 是啥都不知道,工欲善其事,必先利其器。我们看到文件最顶上 Vue 是从同目录下 instance/index 文件中导出的。 我们打开文件看一下:

image.png

哦,原来导出的 Vue 是一个函数,而且函数名称还是大写的,所以可以当作构造函数使用,除此之外,我们还看到在函数下方还有一系列以 xxxMixin 为首的函数调用,而且也把 Vue 这个函数当作参数传入,我们可以猜测这些函数调用就是执行各种初始化操作,而且在我们 new Vue 的时候,其实下面的那些 xxxMixin 都已经执行过了,new Vue 时调用的 _init 方法实际上是在 initMixin 中的,所以我们把它归类到 initMixin 中,可以初步得到以下初始化流程图:

image.png

具体每个函数都干了哪些事,我们接下来就按照上边流程图的顺序来逐个进行分析。

initMixin

_init 方法所在文件(src/core/instance/init.ts)

image.png

它是 Vue 原型上的一个方法,接收一个 options 参数,我们主要分析它的核心逻辑: image.png

1、首先将 this 赋值给 vm

image.png

这里的 this 就是 new Vue 时构造出来的实例对象,所以 vm 特指 Vue 实例对象。

函数外部有一个 uid 变量,默认从 0 开始

image.png

接着往 Vue 实例对象上添加一个 _uid 属性作为唯一标识,值默认是 0,赋值完后 uid 自增(这样下一个 new Vue 构造的实例,它的 _uid 就是 1,以此类推)

image.png

vm 上添加 _isVue 属性,值设为 true,这个属性用来标记当前 vm 是一个 Vue 的实例(也就是通过 new Vue 构造出来的)

image.png

后边还设置了其他的一些属性,咱们先不用管,后边有出现会提到的。

然后我们来看关键方法 mergeOptions,从名字上我们就可以知道它是用于合并选项的,调用 mergeOptions 并将返回结果赋值给 vm.$options image.png

我们先来分析它的传参,第一个参数是调用 resolveConstructorOptions 函数的返回值 那么调用 resolveConstructor 函数传了 vm.constructor(实例对象的 constructor),这里通过原型相关的知识可以得知,实例的 constructor 指向实例化它的构造函数,那这里就是 Vue 构造函数了,所以就是:

vm.constructor === Vue // true

我们来看下 resolveConstructor 函数内部逻辑:

image.png

  • 接收一个参数 Ctor,我们刚才传了 vm.constructor(相当于 Vue 构造函数)
  • 取得 options 选项
  • 判断 Ctor.super 有没有(super 关键字会指向其父类的构造函数,即判断有无父类),这一般在子组件通过 extend 继承父组件时会存在这种情况,我们目前没有父子继承关系,所以不进入判断内部
  • 直接返回 options

mergeOptions 的第二个参数是 _init 接收的参数 options,也就是 new Vue 时传给构造函数中的对象,第三个参数是当前组件实例 vm。传给 mergeOptions 的三个参数都分析完后,来看下 mergeOptions 函数

image.png

通过官方注释得知,就是将两个选项合并成一个,parent 就是 Vue 构造函数的默认 options,child 是我们 new Vue 时传给构造函数的,vm 就是当前实例

  • 调用 checkComponent

image.png checkComponent 用于检测组件的名称,前提是传入的 options 上的 components 不为空,枚举 options.components 上的组件名,调用 validateComponentName 函数校验

image.png validateComponentName 接收参数 name(即组件名称),采用正则表达式校验 name 是否符合要求,不符合就调用 warn 函数,提示错误信息。 通过正则表达式校验,下边还需要校验是不是和 Vue 中一些内置名称冲突 image.png 调用 isBuiltInTag 和 isReservedAttribute 函数相当于是调用了 makeMap 返回的函数,我们看看 makeMap 函数

image.png 接收 str 字符串参数,expectsLowerCase 布尔值是否期望小写。makeMap 函数内部逻辑:

  • 构建一个空 map
  • 分割传入的 str 放到 list 数组
  • 遍历数组,将数组中的元素放到 map 中作为键,其值默认为 true
  • 返回值:如果 expectsLowerCase 为 true,返回一个函数,这个函数接口一个 val 参数,将 val 转为小写后去 map 中找到这个键对应的值,如果 expectsLowerCase 为 false,返回一个接收 val 参数的函数,函数直接返回 map 中 val 这个键对应的值。

总结:isBuiltInTag 函数对应的 makeMap 返回的函数内部的 map 为:

map: {
    'slot': true,
    'component': true
}

这些是 Vue 中内置的标签

isReservedAttribute 函数对应 makeMap 内的 map 也是:

map: {
    'key': true,
    'ref': true,
    'slot': true,
    'slot-scope': true,
    'is': true
}

这些是 Vue 中预定义的属性

默认调用 makeMap 时第二个参数是 true,也就是我们组件名称传进去会先转为小写,然后再去 map 中匹配,说白了,我们写的组件名称中不管大小写,统一转为小写后匹配上边的 map,只要匹配上键(属性名),它们的值刚好是 true,那就进入判断抛出错误提示信息。

image.png

  • 校验完组件名后,接着调用 normalizeProps 规范化 props image.png 接收 options 选项以及 vm 实例对象

    • 获取 options 上的 props,为空直接返回

    • 情况一:props 如果是数组形式

image.png

遍历数组元素,进行类型判断,数组中的元素必须是字符串类型,否则抛出错误信息,随后调用 camelize 将元素 val 传进去做处理,camelize 就是将传入的字符串转为驼峰命名的形式并返回 image.pngimage.png camelizeRE 是要匹配的正则表达式,括号内是捕获组,(\W) 就是捕获一个英文单词,对传入的字符串使用 replace 方法,就是将正则表达式匹配的部分替换为第二个参数指定的部分。举个例子,假设我们传入的 str 如下:

my-component

那么 camelizeRE 正则匹配到的就是 -c,捕获组捕获到的就是 c,replace 第二个参数是回调,回调第一个参数是匹配到的完整字符串,第二个参数是捕获到的字符,这里是小写 c,将其转为大写(调用 toUpperCase()),那这里就是将正则匹配到的 -c,替换成大写 C,替换后的字符如下:

myComponent

这个 name 就是驼峰式的了,然后放到 res 对象中作为属性,值是一个对象,对象中默认有一个 type: null 的属性,放到 res 中就是

res: {
    'xxx': {
        type: null
    }
}
  • 情况二:props 是对象形式 image.png 对象处理也很简单,枚举对象上的属性,拿到属性值 val,同样对属性名进行处理,转为驼峰命名形式,接着往 res 上放这个属性,如果 val 本身是一个对象,那就直接将这个对象作为属性值,反之就将 type 放到一个对象中,属性值设为这个 val。分别对应下边两种情况
props: {
    name: String,
    age: {
        type: Number
    }
}

name 属性值不是一个对象,将 name 属性值(String)作为新对象中 type 的属性值,放到 res 中就是

res: {
    name: {
        type: String
    }
}

age 属性值本身是一个对象,放到 res 中是

res: {
    age: {
        type: Number
    }
}
  • props 不是数组也不是对象 image.png 抛出错误信息,提示 props 必须是一个数组或对象

  • 处理好的 res 覆盖 options.props

可以看到上边就是对用户传的 props 进行归一化,所谓归一化就是将不同的形式转为相同的形式,上边归一化后的形式就是:

props: {
    key1: {
        type: String
    },
    key2: {
        type: Number
    },
    key3: {
        type: null
    }
}
  • 调用 normalizeInject 规范化 inject

normalizeInject 内部处理逻辑和 normalizeProps 很相似,都是分为数组和对象两种情况处理,如果两者都不是就抛出错误信息。只是 props 中是处理 type,injects 是 from。这里对象的处理中,如果枚举的属性其属性值是一个对象,会调用 extend 进行处理,传入两个参数,第一个是一个对象,有 from 属性,属性值是这个枚举属性,第二个参数是枚举属性对应的值,这个值是个对象,看看 extend 函数。

image.png

就是往第一个参数也就是目标对象上混入属性,枚举第二个参数传入的 val 对象,往第一个参数({from: key})混入第二个参数中的属性(如果第二个参数 val 对象上也存在 from 属性,则 val 对象上的 from 属性值覆盖源对象上的 from 属性值,混入后,源对象上就不仅只有 from 属性了,还可能有其他的一些属性)。

最后归一化后的格式就是:

injects: {
    key1: {
        from: xxx,
        ..., // 其他一些属性
    }
}
  • 调用 normalizeDirectives 规范化 directives image.png

    • 获取 directives 选项
    • 枚举选项上的属性,获取属性值
    • 判断属性值是不是一个函数,是的话就将选项上当前属性的属性值重新赋值为一个对象,对象上的 bind 和 update 属性为这个源函数
  • 枚举 parent(Vue 构造函数的 options 选项),调用 mergeField 将属性作为实参传入

  • 枚举 child(用户传入的 options 选项),如果属性在 parent(Vue 构造函数中的 options)中不存在,调用 mergeField 函数,将 key 属性作为实参传入

接着看看这个核心的 mergeField 函数 image.png

strat 是一个函数,会先从 strats 中取,strats 在文件顶部声明

image.png

默认值为 config.optionMergeStrategies, 这个 config 在(src/core/config.ts)文件中,默认是个空对象

image.png

从上边的接口类型声明来看,这个对象上的属性是字符串类型,属性值是个函数

image.png

那么如果 strats 上没有这个属性对应值,就会使用默认的策略 defaultStrat

image.png 默认策略接收父属性值和子属性值作为参数,如果子属性值不为 undefined 的话优先返回它,否则再取父属性值,这个返回值就作为最后合并好的 options 上该属性对应的值。

那么上边先枚举 Vue 构造函数的 options 上的属性,如果用户传的 options 也存在该属性,优先使用用户传的,这样后边枚举用户传的 options 时就仅需要处理 Vue 构造函数 options 上没有的属性了。

  • 最后返回合并好的终极 options

至此,mergeOptions 函数就分析完了,主要流程就是组件名校验归一化 propsinjectsdirectives,然后将 Vue 构造函数及用户的 options 进行一个合并,最后返回。

继续往下看: image.png

  • initProxy 所在文件(src/core/instance/proxy.ts),在文件顶部声明了一个 initProxy 变量,然后赋值为一个函数: image.png 接收 vm 实例作为参数,首先判断 hasProxy 值,hasProxy 判断浏览器是否支持 Proxy 这个 API,即不为 undefined,且调用 isNative 要返回 true,isNative 会判断传入参数是一个函数,且是 JavaScript 内置的函数(内置函数调用 toString 方法会返回包含 [native code] 的字符串) image.pngimage.png

进入判断后先获取 vm 上的 options,然后定义 handlers 配置项,然后创建一个代理实例赋值给 vm 上的 _renderProxy 属性,如果 hasProxy 为 false, _renderProxy 属性就赋值为 vm 实例本身,接着我们看下代理配置项 handlers 取值,有 getHandler 和 hasHandler 两种:

getHandler 中定义了一个 get 函数,参数是 target(这里是 vm 实例) 和 key(属性) image.png

也就是我们读取 vm 上的某个属性时,会触发 get 函数拦截,首先判断这个 key 属性是字符串且不在 target(vm 实例)上,接着继续判断 key 属性在不在 vm 实例的 $data(也就是我们组件中写的 data 对象里)上,如果在就调用 warnReservedPrefix 函数抛出错误提示,如果不在 vm 实例上也不在 vm.$data 属性上,调用 warnNonPresent 函数抛出另一个错误信息

image.png 因为属性 key 它不在 vm 上,但在 vm.$data 上,所以这里提示信息意思就是这个 key 属性必须访问 $data.key 上的

image.png 这种情况就是 key 属性即不在 vm 上也不在 vm.$data 上,抛出错误信息表示属性或方法在实例上未定义但是存在引用

再看下 hasHandler 函数的 has 函数,has 代理方法是针对 in 操作符的,比如我们这里判断一个属性:

name in obj

这就会触发代理对象 obj 的 has 拦截方法,同理上边代理对象是 vm,使用 in 操作符判断某个属性是否在 vm 上时就会触发 has 函数拦截。 image.png has 常量取值取决于 key 属性在不在 target(vm)上

isAllowed 常量取值满足以下其中一种就是 true,都不满足就为 false:

allowedGlobals(key) ||
typeof key === 'string' &&
          key.charAt(0) === '_' &&
          !(key in target.$data)

allowGlobals 函数也很简单,有我们熟悉的 makeMap 函数,第二个参数不传,就是内部匹配名称时不会转为小写去匹配,内部 map 的组成由每个逗号分割的关键字作为属性,其属性值默认为 true,调用这个函数时,如果当前 key 和 map 中某个属性名称匹配,那么取值就是 true image.png 下面就是判断 has 取值为 false(即属性不在 vm 上) 且 isAllowed 也为 false,内部逻辑和 getHandler 中的 get 函数相似。最后返回 has || !isAllowed 逻辑或的取值。 image.png

在 initLifecycle 调用前,还有一句赋值语句,将自身引用保持在了自身的 _self 属性上 image.png

  • initLifecycle

initLifecycle 的初始化,就是往 vm 实例上添加一些属性,并赋予默认值,比如我们熟悉且常用的 $refs$parent$root$children 对象, image.png

  • initEvents 事件初始化

image.png

核心就是 updateComponentListeners 方法,这个方法内部又调用了 updateListeners 方法

image.png

updateListeners 方法内部就是对新老事件进行处理(更新事件 on 监听,包括 add 新增事件和 remove 移除事件)

image.png

  • initRender

内部定义了 $slots$createElement$attrs$listeners

  • beforeCreate

调用了 beforeCreate 生命周期钩子

  • initInjections

处理 injects 信息,将 injects 对象中的每个属性转为响应式的,这样就能和在 data 中声明的属性一样使用了,这里的关键点就是 injects 比 data 和 props 先初始化。 image.png

  • initState

初始化 props、setup(vue3 语法糖)、methods、data、computed、watch(这边的初始化逻辑留到响应式系统篇章再来分析) image.png

  • initProvide

处理 provide 信息,将 provide 对象内的每个属性转为响应式,provide 的初始化在 data、methods 初始化之后 image.png

  • created

调用 created 生命周期钩子

  • $mount

判断选项中如果有 el 节点,那就作为实参传入 vm.$mount 函数中 image.png

这里的 el 就是我们常说的挂载的根容器 app

image.png

stateMixin

所在文件:src/core/instance/state.ts,内部逻辑也很简单 image.png

  • 拦截 Vue 原型上的 $data$props
  • Vue 原型上添加 $set 方法
  • Vue 原型上添加 $delete 方法
  • Vue 原型上添加 $watch 方法

看看是怎么拦截的

image.png

访问 Vue.prototype.$data 时实际上是这样访问 vm._data(当前 vm 实例上的 _data 对象)

访问 Vue.prototype.$props 时实际上是这样访问 vm._props(当前 vm 实例上的 _props 对象)

如果是修改 Vue.prototype.$data 或者 Vue.prototype.$props,会走 set 拦截方法抛出错误信息

image.png

eventsMixin

eventsMixin 函数接收 Vue 构造函数作为参数,往构造函数原型上添加四个方法:

  • $on
  • $once
  • $off
  • $emit image.png

lifecycleMixin

lifecycleMixin 函数接收 Vue 构造函数作为参数,往 Vue 原型添加三个方法:

  • _update
  • $forceUpdate
  • $destroy image.png

renderMixin

renderMixin 函数接收 Vue 构造函数作为参数,往 Vue 原型添加两个方法:

  • $nextTick
  • _render

image.png

调用 installRenderHelpers 函数时将 Vue.prototype 作为实参传入 image.png

target 就是 Vue.prototype,往 Vue 原型上添加各种以 _ 开头的方法

initGlobalAPI

接收 Vue 构造函数作为形参

先代理 Vue 上的 config 属性(是个对象),属性描述符项是 configDef,提供 get 和 set 函数,当你尝试修改 Vue.config 时会被 configDef 的 set 函数拦截,抛出错误信息,意思就是不能替换 Vue.config 对象。如果是读取 Vue.config,就被 configDef 的 get 函数拦截,直接返回了 config,这个 config 是个对象(所在文件:core/config.ts)中,就是暴露了一堆全局属性(比如 async、devtools 等) image.png

接着往 Vue.util 对象上添加一些方法 image.png Vue 官方也提供了注释,意思就是这些不被认为是公共的 API,虽然暴露出去你能用,但是不建议用,因为这几个 API 其实是 Vue 内部自己在用的。

再下边就往 Vue 上添加了几个方法,在之后文章会详细介绍

image.png

下边初始化 Vue.options 为一个空对象,遍历 ASSETS_TYPE 数组中的元素然后追加到 options 上 image.png

ASSETS_TYPE 所在文件(src/shared/constants.tsimage.png 数组中每个元素就是遍历时的 type,是字符串类型,往 type 对应元素名称后边拼上 s 后作为属性名,属性值默认是空对象,遍历追加完后 Vue.options 上就有如下属性:

Vue.options = {
    components: {},
    directives: {},
    filters: {}
}

接着往 Vue.options 追加 _base 属性,属性值是 Vue 本身 image.png 现在 Vue.options 就多了一个属性

Vue.options = {
    _base: Vue,
    components: {},
    directives: {},
    filters: {}
}

然后是 extend 函数,将 Vue.options.components 作为第一个参数(是一个对象),builtInComponent 作为第二个参数

image.png

builtInComponent(所在文件:src/core/components/index.ts),其实就是暴露了一个 KeepAlive 组件

image.png

那看下 extend 函数(所在文件:src/shared/utils.ts

其实很简单,就是往目标对象混合属性,目标对象就是传的第一个参数(Vue.options.components),混合的属性在第二个参数里,刚才看了是一个对象,对象里边有 KeepAlive 属性(组件)

image.png

混合后就是

Vue.options = {
    _base: Vue,
    components: {
        KeepAlive
    },
    directives: {},
    filters: {}
}

接着下边又往 Vue 上添加了一系列方法:

initUse,往 Vue 上添加了 use 方法 image.png

initMixin,往 Vue 上添加 mixin 方法 image.png

initExtend,往 Vue 上 添加 extend 方法

image.png

initAssetRegisters,往 Vue 上添加 componentdirectivefilter 方法,ASSET_TYPES 刚才看过了是个数组,数组中每个元素作为函数名,在 Vue 上注册成了函数。

image.png

至此,从 new Vue() 到 $mount 期间的一系列初始化操作我们就看完了,上边为了先分析传给 initGlobalAPI 的参数 Vue,所以就先分析了 initMixin 等函数,实际上 initGlobalAPI 作为核心入口文件是最先执行的。下边再来看看流程图,这会就清晰多了

无标题-2025-12-18-2128.png

Vue 初始化流程,每一步都做了哪些初始化,现在看就一目了然了,下篇文章我们就进击 Vue 的响应式系统~

React 自定义 Hook 实战:从鼠标追踪到待办事项管理

作者 ohyeah
2026年1月3日 14:46

在现代前端开发中,React 的自定义 Hook 已成为提升代码复用性、逻辑抽象能力和可维护性的核心手段。本文将通过两个典型示例——鼠标位置追踪本地存储的待办事项管理,深入剖析如何利用自定义 Hook 将复杂业务逻辑封装为可复用、可测试、高内聚的模块,并探讨其背后的 React 响应式编程思想。


一、Demo 1:使用 useMouse 封装鼠标位置追踪

1.1 初始实现的问题

最初,开发者可能直接在组件内部使用 useStateuseEffect 来监听鼠标移动事件:

function MouseMove() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    const update = (event) => {
      setX(event.pageX);
      setY(event.pageY);
    };
    window.addEventListener('mousemove', update);
    return () => {
      window.removeEventListener('mousemove', update);
    };
  }, []);

  return <div>鼠标位置: {x} {y}</div>;
}

这种写法虽然功能完整,但存在明显缺陷:

  • 逻辑耦合:UI 渲染与事件监听混杂,组件职责不清;
  • 难以复用:若其他组件也需要获取鼠标坐标,需重复编写相同逻辑;
  • 潜在内存泄漏风险:若未正确清理事件监听器(如忘记返回清理函数),组件卸载后仍会执行回调,事件监听/定时器 不会因为函数组件卸载而自动销毁,当卸载组件后又开启组件,相当于是又进行了一次事件监听,多次重复导致内存泄漏。

1.2 提炼为自定义 Hook:useMouse

为解决上述问题,我们将鼠标追踪逻辑提取至独立的 useMouse Hook:

// src/hooks/useMouse.js
import { useState, useEffect } from "react";

export const useMouse = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    const update = (event) => {
      setX(event.pageX);
      setY(event.pageY);
    };
    window.addEventListener('mousemove', update);
    return () => {
      window.removeEventListener('mousemove', update);
    };
  }, []);
   
  return { x, y };
  // 把要向外暴露的状态和方法返回
};

关键设计点解析:

  1. 状态封装
    使用 useState 管理 xy 坐标,对外仅暴露只读值,避免外部直接修改状态。
  2. 副作用隔离
    useEffect 负责添加/移除全局事件监听器。依赖数组为空([]),确保仅在组件挂载时注册一次监听器,并在卸载时自动清理,彻底规避内存泄漏。
  3. 单一职责原则
    useMouse 只关注“获取鼠标位置”这一核心能力,不涉及任何 UI 渲染或业务判断,高度内聚。
  4. 可组合性
    返回对象 { x, y },便于在任意组件中解构使用,符合 React 的声明式风格。

1.3 在组件中使用

App.jsx 中,只需一行代码即可接入鼠标位置数据:

function MouseMove() {
  const { x, y } = useMouse();
  return <div>鼠标位置: {x} {y}</div>;
}

此时 MouseMove 组件完全退化为纯展示层,逻辑与视图分离,极大提升了可读性和可维护性。


二、Demo 2:构建完整的待办事项系统 —— useTodos

相比鼠标追踪,待办事项管理涉及状态管理、持久化存储等多个维度,是检验自定义 Hook 能力的绝佳场景。

2.1 整体架构拆解

整个系统由以下部分组成:

  • Hook 层useTodos —— 核心逻辑容器

  • 组件层

    • TodoInput:输入新任务
    • TodoList:渲染任务列表
    • TodoItem:单个任务项(含完成状态切换与删除)

这种分层结构体现了典型的“逻辑下沉,UI 上浮”原则:复杂状态流转由 Hook 处理,组件仅负责调用方法与展示数据。

2.2 useTodos Hook 深度解析

// src/hooks/useTodos.js
import { useState, useEffect } from 'react';

const STORAGE_KEY = 'todos';

function loadFromStorage() {
  const stored = localStorage.getItem(STORAGE_KEY);
  return stored ? JSON.parse(stored) : [];
}

function saveToStorage(todos) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

export const useTodos = () => {
  const [todos, setTodos] = useState(loadFromStorage);

  useEffect(() => {
    saveToStorage(todos);
  }, [todos]);
  // todos改变 进行本地存储

  const addTodo = (text) => {
    setTodos([
      ...todos,
      { id: Date.now(), text, completed: false }
    ]);
  };

  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return { todos, addTodo, toggleTodo, deleteTodo };
};

核心机制详解:

(1)初始化与持久化同步
  • 延迟初始化useState(loadFromStorage) 利用函数式初始化,避免每次渲染都读取 localStorage,提升性能。
  • 自动持久化useEffect 监听 todos 变化,一旦状态更新立即写入 localStorage,实现“状态即存储”的无缝体验。

注意:此处使用 Date.now() 作为 ID 虽简便,但在高频操作下可能冲突。

(2)不可变更新原则

所有状态变更均通过创建新数组实现:

  • addTodo:使用展开运算符 [...todos, newTodo]
  • toggleTodomap 返回新数组,仅修改目标项
  • deleteTodofilter 排除指定 ID 项

这保证了 React 能正确触发重渲染,同时避免意外修改原始状态。

(3)API 设计清晰

返回对象包含:

  • 状态todos(当前任务列表)
  • 行为addTodo, toggleTodo, deleteTodo(纯函数,无副作用)

调用者无需关心内部实现,只需按约定传参即可操作状态。

2.3 组件层协作流程

TodoInput:任务创建入口

// src/components/TodoInput.jsx
export default function TodoInput({ onAddTodo }) {
  const [text, setText] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAddTodo(text.trim());
    setText('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={text}
        onChange={e => setText(e.target.value)}
      />
    </form>
  );
}
  • 通过 onAddTodo 回调将新任务文本传递给父组件(即 useTodos.addTodo
  • 表单提交后清空输入框,提供良好 UX

TodoListTodoItem:状态展示与交互

// TodoList.jsx
export default function TodoList({ todos, onDelete, onToggle }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onDelete={onDelete}
          onToggle={onToggle}
        />
      ))}
    </ul>
  );
}

// TodoItem.jsx
export default function TodoItem({ todo, onDelete, onToggle }) {
  return (
    <li>
      <input 
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span className={todo.completed ? 'completed' : ''}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
}
  • 单向数据流todosApp 传入,TodoItem 仅消费数据
  • 事件委托:点击复选框或删除按钮时,调用 onToggle / onDelete,最终触发 useTodos 内部状态更新
  • Key 唯一性:使用 todo.id 作为 key,确保 React Diff 算法高效更新列表

2.4 App 组件:胶水层整合

// App.jsx
export default function App() {
  const { todos, addTodo, deleteTodo, toggleTodo } = useTodos();

  return (
    <>
      <TodoInput onAddTodo={addTodo} />
      {todos.length > 0 ? (
        <TodoList 
          todos={todos} 
          onDelete={deleteTodo} 
          onToggle={toggleTodo} 
        />
      ) : (
        <div>暂无待办事项</div>
      )}
    </>
  );
}

App 组件几乎不包含业务逻辑,仅负责:

  1. 调用 useTodos 获取状态与方法
  2. 将方法作为 props 传递给子组件
  3. 根据 todos.length 控制空状态显示

这种“瘦容器”模式使应用结构清晰,易于扩展(例如未来加入筛选、编辑等功能)。


三、自定义 Hook 的价值与最佳实践

3.1 为什么需要自定义 Hook?

  • 逻辑复用:跨组件共享状态逻辑
  • 关注点分离:将副作用、数据获取、状态管理从 UI 组件中剥离
  • 测试友好:Hook 可独立于组件进行单元测试
  • 团队协作:形成可沉淀的“前端资产库”,新人可快速接入

3.2 编写高质量 Hook 的准则

  1. 命名规范:以 use 开头(如 useTodos),这是 React 的约定,也是 ESLint 规则的要求。
  2. 返回结构清晰:通常返回对象,便于按需解构;避免返回数组导致顺序依赖。
  3. 避免副作用外泄:Hook 内部处理所有订阅/清理,调用者无需关心生命周期。
  4. 考虑性能优化:对返回的函数使用 useCallback 包裹(本例因简单省略,复杂场景需注意)。

结语

通过 useMouseuseTodos 两个案例,我们见证了自定义 Hook 如何将“面条式代码”转化为模块化、可维护的现代 React 应用。它不仅是语法糖,更是一种架构思维——鼓励开发者将复杂问题分解为独立、可组合的逻辑单元。

在实际项目中,你可以继续延伸这一模式:封装网络请求(useFetch)、表单验证(useForm)、主题切换(useTheme)等通用能力。当你的 Hook 库逐渐丰富,你会发现:优秀的前端工程,始于对逻辑的敬畏,成于对复用的追求

本文所有代码均可直接运行,建议读者动手实践,尝试为 useTodos 添加“编辑任务”或“按状态筛选”功能,进一步巩固自定义 Hook 的设计能力。

npm包开发及私有仓库配置使用

2026年1月3日 14:32

原因:一个项目多个系统之间存在可复用的代码,重复的复制粘贴及修改,偶尔也会遗漏。集中管理和维护,提升代码复用率和维护效率

开发npm包

初始化项目

生成package.json文件

npm init -y
{
  "name": "product-npm", // 包名称
  "version": "1.0.0", // 包版本信息
  "description": "",
  "main": "dist/index.js", // 包的入口文件
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup --config"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

编写代码内容及打包

安装 rollupjs 进行打包,生成dist文件夹,内容存放在该文件夹下。 main的值也需根据输出地址进行变更

选择 verdaccio 进行私有仓库管理

全局安装verdaccio

npm i verdaccio -g

检查是否安装成功

显示版本号即为成功

verdaccio -v

运行 verdaccio

verdaccio

终端会显示 config file 和 http address

config file - 配置文件的地址

htpp address - 仓库的管理界面

image.png

添加用户

执行命令后,根据终端的提示设置用户名、密码、邮箱

npm add user --registry http://localhost:4873/
# --registry http://localhost:4873/ 指定仓库源

如出现报错需要检查配置文件中max_users是否有设置(默认被注释了)且值不能为1

auth:
  htpasswd:
    file: ./htpasswd
    # Maximum amount of users allowed to register, defaults to "+inf".
    # You can set this to -1 to disable registration. 如果为-1禁止注册
    max_users: 1000
    # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt".
    # algorithm: bcrypt # by default is crypt, but is recommended use bcrypt for new installations
    # Rounds number for "bcrypt", will be ignored for other algorithms.
    # rounds: 10

登录用户

npm login --registry http://localhost:4873/

发布\更新包

在包根目录执行

npm publish --registry http://localhost:4873/

如出现报错需要检查package.json文件下version是否有变更

删除包

npm unpublish product-npm[包名] --registry http://localhost:4873/

使用

由于verdaccio部署在自己的电脑下,所以在项目里直接 install 即可

npm i product-npm --registry http://localhost:4873/

同事需要使用时,则将localhost变为对应的我的电脑的ip

npm i product-npm --registry http://192.168.1.6:4873/

如出现无法安装需要检查配置文件中listen参数,开启0.0.0.0,以致可通过ip访问

# https://verdaccio.org/docs/configuration#listen-port
listen:
  # - localhost:4873            # default value
  # - http://localhost:4873     # same thing
  - 0.0.0.0:4873              # listen on all addresses (INADDR_ANY)
#   - https://example.org:4873  # if you want to use https
#   - "[::1]:4873"              # ipv6
#   - unix:/tmp/verdaccio.sock  # unix socket

AI设计类产品分析:Lovart

作者 听风说图
2026年1月3日 14:14

核心观点:AI 设计类产品的两大核心壁垒——矢量渲染引擎是底座,结构化转换是灵魂。AI 大模型本身不是护城河。


1. 核心结论(先说结论)

┌─────────────────────────────────────────────────────┐
│           用户感知层(产品体验)                      │
├─────────────────────────────────────────────────────┤
│     结构化转换层(灵魂)                             │  ← 决定"好不好用"
│     - 混沌 AI 输出 → 语义化图层树                   │
│     - 静态布局 → 响应式约束                         │
│     - 位图 → 可编辑矢量                             │
├─────────────────────────────────────────────────────┤
│     矢量渲染引擎(底座)                             │  ← 决定"能不能做"
│     - 路径渲染 / 图层混合 / 60fps 性能              │
│     - 所有上层能力的物理承载                         │
├─────────────────────────────────────────────────────┤
│     AI 生成模型(水电煤)                            │  ← 可购买,非壁垒
│     - Sora / Midjourney / Veo 等                   │
└─────────────────────────────────────────────────────┘

一句话总结

铁打的引擎,流水的模型。 用户住的是房子(渲染引擎),不是水电煤(AI 模型)。而结构化转换层决定了房子住得舒不舒服。


2. Lovart 简介

Lovart 是一款定位为「AI 设计智能体(Design Agent)」的产品,由 Resonate International INC 开发。其核心能力是将用户的自然语言需求,自动转化为专业级的设计产出(图像、视频、3D 等)。

  • 官网www.lovart.ai
  • 定位:全球首个 AI 设计智能体
  • Slogan:「让设计更加智能,让交付更加高效」

3. 产品架构分析

Lovart 的产品架构可以分为三层:

┌─────────────────────────────────────────────────────────────┐
│                    用户交互层 (UI)                           │
│         自然语言输入 → 实时画布渲染 → 可视化编辑              │
├─────────────────────────────────────────────────────────────┤
│                  结构化转换层 (灵魂)                         │
│       AI 输出解析 | 图层分离 | 布局推断 | 语义理解            │
├─────────────────────────────────────────────────────────────┤
│                   AI 模型聚合层 (水电煤)                     │
│   Sora2 | Veo3.1 | Midjourney | Kling | Hailuo | Seedream  │
├─────────────────────────────────────────────────────────────┤
│                   矢量渲染引擎层 (底座)                      │
│         矢量渲染 | 图层管理 | 实时预览 | 导出交付             │
└─────────────────────────────────────────────────────────────┘

关键洞察

Lovart 并没有自研大部分 AI 生成模型——它聚合了市面上几乎所有顶级的图像/视频生成模型(Sora、Veo、Midjourney、Kling、Hailuo 等),通过统一的 API 调用提供给用户。

这意味着:AI 生成能力是「可购买」的标准化服务,不是 Lovart 的护城河。


4. 壁垒一:矢量渲染引擎(底座)

没有渲染引擎,一切都是空中楼阁。

4.1 为什么是「底座」?

挑战 说明
工程复杂度 专业级设计软件需要处理矢量路径、布局约束、图层混合、渐变/阴影等,工程量巨大
性能要求 实时预览需要 60fps 流畅渲染,对 WebGL/WebGPU 有极高要求
交互体验 拖拽、缩放、对齐等交互需要毫秒级响应,前端优化深度决定用户体验
格式兼容 需要支持与 Figma、Sketch、Adobe 等工具的格式互通
长期积累 这类技术无法靠资金快速堆积,需要团队长期沉淀

4.2 可替代性分析

环节 技术实现 可替代性
AI 生成 调用第三方 API(Sora、Midjourney 等) 极高,任何人都可以接入
自然语言理解 LLM(GPT 等) 极高,标准化 API
实时画布渲染 自研前端矢量引擎 极低,需要大量工程投入
图层编辑系统 自研图层管理 极低,专业级设计软件门槛
设计元素组合 布局/对齐/变换系统 极低,Figma 级的工程复杂度

5. 壁垒二:结构化转换层(灵魂)

渲染引擎决定「能不能做」,结构化转换决定「好不好用」。

5.1 核心问题:AI 输出 ≠ 可编辑设计稿

很多人假设 AI 生成的内容可以直接进入引擎编辑,但实际上:

AI 输出 渲染引擎需要
位图(Pixels) 结构化矢量路径
杂乱的 SVG 清晰的图层树
无语义的像素块 带命名的组件/约束

5.2 真正的技术深水区

不仅仅是「渲染」,而是「如何将 AI 的混沌输出瞬间转化为有序的图层结构」

AI 生成结果 → [结构化转换层] → 可编辑的设计稿
                    ↑
              这里才是灵魂

核心能力包括:

  • 自动矢量化:位图 → 可编辑矢量路径
  • 自动图层分离:混沌像素 → 语义化图层树
  • 自动建立约束:静态布局 → AutoLayout 响应式结构
  • 自动命名/分组:无序元素 → 符合设计规范的组件

这才是让设计师觉得「好用」的关键——用户不是想要一张图,而是想要一个可以继续编辑的设计稿


6. 为什么 AI 模型不是壁垒?

  1. 模型同质化:Sora、Veo、Midjourney 等模型的 API 对所有人开放,成本差异仅在于规模效应。
  2. 模型迭代快:今天的最强模型,3 个月后可能被新模型超越。押注单一模型是高风险策略。
  3. 用户无感知:用户不关心后端用的是 Sora 还是 Veo,只关心最终产出的质量和可用性。

Lovart 的聪明之处在于:做模型的「聚合者」而非「自研者」,将精力聚焦在用户真正能感知到的前端体验上。


7. 商业模式验证

Lovart 的订阅定价印证了上述分析:

套餐 月费 核心卖点
Starter $16/月 2000 积分,2 并发
Basic $27/月 3500 积分,4 并发
Pro $45/月 11000 积分,8 并发
Ultimate $99/月 27000 积分,10 并发

注意:Lovart 的积分是用来消耗 AI 模型调用的(即可变成本),而订阅费用的核心是为了支撑其前端产品体验的持续迭代


8. 对 FigDev 的启示

8.1 我们的优势

FigDev 的技术路线与「底座 + 灵魂」的成功逻辑高度契合:

壁垒类型 FigDev 的对应能力
矢量渲染引擎(底座) WebGPU 自研矢量渲染引擎
实时画布渲染 flare/ecs + ECS 架构
图层编辑系统 Figma 视图层实现
设计元素组合 AutoLayout (yoga-layout)
结构化转换层(灵魂) dataTransformation 模块
Figma JSON → ECS 节点 已实现
矢量路径解析 svgPathParser
自动布局约束 AutoLayoutSys

8.2 战略建议

优先级 策略 说明
⭐⭐⭐ 深耕渲染引擎 60fps、复杂路径、渐变填充等「难啃的骨头」才是真正的壁垒
⭐⭐⭐ 扩展转换层 dataTransformation 能力扩展到「AI 输出 → 结构化设计稿」场景
⭐⭐ 自研专用小模型 训练「最懂设计编辑」的专用模型(见下文)
聚合 AI 模型 不做大模型军备竞赛,直接聚合 Sora、Midjourney 等

8.3 差异化定位

Lovart:AI 生成 → 静态内容(2D/3D/视频)
FigDev:AI 生成 → 静态或带交互的内容(2D/3D/视频/网页)

9. AI 小模型策略:建立独特壁垒

虽然不做大模型军备竞赛,但自研专用小模型是必要的。

9.1 为什么需要小模型?

结构化转换层的「灵魂」能力,需要 AI 来增强:

小模型类型 核心作用
结构化识别模型 将 AI 生成的位图/SVG 转化为语义化图层结构
布局推断模型 自动识别元素关系,建立 AutoLayout 约束
设计修正模型 根据设计规范自动优化间距、对齐、配色

9.2 数据飞轮:用户行为反哺模型

核心洞察:用户在编辑器中的每一次修改,都是最高质量的标注数据。

AI 生成 → 用户在编辑器中修改 → 修改记录作为训练数据 → 反哺小模型 → AI 生成更准确
              ↑                                                    ↓
              └──────────────────────────────────────────────────────┘
用户行为 数据价值
调整图层层级 训练「图层分离模型」
修改布局约束 训练「布局推断模型」
重命名组件 训练「语义理解模型」
修正颜色/间距 训练「设计修正模型」

10. 总结

核心框架

层级 定位 战略优先级
矢量渲染引擎 底座 — 决定「能不能做」 ⭐⭐⭐
结构化转换层 灵魂 — 决定「好不好用」 ⭐⭐⭐
AI 小模型 增强 — 形成独特壁垒 ⭐⭐
AI 大模型 水电煤 — 聚合即可

一句话总结

渲染是底座,转换是灵魂。 FigDev 的核心竞争力不在于生成了什么,而在于如何承接、编辑、组织和交付这些生成的内容。

更多精彩内容可关注风起的博客 ,微信公众号:听风说图

第4章 Nest.js业务合并

作者 XiaoYu2002
2026年1月3日 13:40

第4章 Nest.js业务合并

在实际项目中,不同的业务操作需要明确的反馈信息。例如:

  • 登录操作返回消息为「登录成功」,状态码为 1。
  • 注册操作返回消息为「注册成功」,状态码为 2。

状态码 作为业务逻辑判断的操作标识,是一个数字或字符串标识符。不同数值代表不同的业务含义(如1表示登录成功、2表示注册成功),前端或调用方可根据具体数值执行相应的逻辑分支。业务描述 则面向用户或开发者,用自然语言清晰传达操作结果,提供直观的反馈信息,辅助理解状态码对应的具体业务场景。

在全局拦截器的使用中,message 和 code 这两个参数需要根据业务需求进行自定义。下面将介绍如何在 Nest.js 中对这两个参数,以及更多同类参数进行规范化管理。

// src/interceptor/interceptor.interceptor.ts
return {
  timestmap: new Date().toISOString(),
  data: transformBigInt(data),
  path: request.url,
  message: 'success',//业务逻辑自定义
  code: 200,//业务逻辑自定义
  success: true,
};

要对全局拦截器的统一返回数据格式中的 message 和 code 进行自定义,应从专门的业务定义文件中引入自定义的业务状态码和描述信息,然后传递给 message 和 code 参数。

在自定义参数时,有一个重要原则:message 和 code 的自定义数据需要与业务逻辑层分离。这些数据应作为纯粹的配置信息使用,类似于 JSON 配置文件。业务层则建立在统一的业务规范基础上进行进一步封装。

首先创建一个响应格式模块(response module)和服务(response service),用于统一处理业务响应。通过 Nest CLI 命令生成的 response.service.ts 文件会自动关联到 response.module.ts 文件中。

nest g mo response
nest g s response

在 src 目录下创建 business 文件夹,并在其中创建 index.ts 文件。该文件专门存放自定义的业务状态码和描述信息,我们只关心每个业务操作对应的状态码和消息内容。后续新增业务需求时,只需在此文件中添加相应的状态码和消息。

注意:业务字段命名通常采用大写字母和下划线组合的格式,即「功能_状态」的表达方式,如 LOGIN_SUCCESS。

// src/business/index.ts
export const business = {
  LOGIN_SUCCESS: {
    code: 1,
    message: '登录成功'
  },
  LOGIN_ERROR: {
    code: 2,
    message: '登录失败'
  },
  REGISTER_SUCCESS: {
    code: 2,
    message: '注册成功'
  },
}

在 response 目录的 response.service.ts 文件中编写具体的响应格式逻辑,定义操作成功和操作失败时需要返回的数据结构。

// src/response/response.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ResponseService {
  success(data: any = null, message: string = '操作成功', code: number = 200) {
    return {
      data,
      message,
      code
    }
  }
  error(message: string = '操作失败', code: number = 500) {
    return {
      message,
      code
    }
  }
}

如果我们想要使用自定义的业务状态码,要如何使用?假设要在user模块的业务层中使用。

// src/user/user.service.ts(业务层)
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    return `This action returns all user`;
  }
 // 省略...
}

假设需要在 user 模块的业务层中使用自定义业务状态码,操作步骤如下:

(1)引入依赖文件:同时引入业务定义文件和响应格式文件。

(2)依赖注入:在 UserService 类中注入 ResponseService 类。

(3)使用响应格式:在 findAll() 方法中使用 ResponseService 类,按照 data、message 和 code 的顺序传入数据。

(4)引用业务定义:message 和 code 从 business 业务文件中读取,本次「登录成功操作」使用业务字段 LOGIN_SUCCESS。

// src/user/user.service.ts(业务层)
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { ResponseService } from 'src/response/response.service';
import { business } from 'src/business';
@Injectable()
export class UserService {
  constructor(private readonly responseService: ResponseService) { }
  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    // 登录成功的消息内容和业务码
    const message = business.LOGIN_SUCCESS.message;
    const code = business.LOGIN_SUCCESS.code;
    return this.responseService.success('This action returns all user', message, code);
  }
}

此时启动项目,访问 localhost:3000/user 路由应返回登录成功的业务状态码。但可能出现以下错误:

RROR [ExceptionHandler] UnknownDependenciesException [Error]: Nest can't resolve dependencies of the UserService (?). Please make sure that the argument ResponseService at index [0] is available in the UserModule context.

错误信息表明 Nest 无法解析 UserService 的依赖,需要确保 ResponseService 在 UserModule 上下文中可用。这是因为 ResponseService 未被正确导出和导入。

解决上述问题需要如下3个步骤:

(1)导出服务:在 response.module.ts 文件中将 ResponseService 添加到 exports 数组中。

(2)导入模块:在 user.module.ts 文件中导入 ResponseModule 模块。

(3)完成注入:此时 UserModule 可以读取到 ResponseModule 导出的 ResponseService,UserService 才能正常使用 ResponseService。

// src/response/response.module.ts
import { Module } from '@nestjs/common';
import { ResponseService } from './response.service';

@Module({
  providers: [ResponseService],
  exports: [ResponseService]
})
export class ResponseModule { }

在user.module.ts 文件中导入 ResponseModule 模块,完成导入模块。

// src/user/user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { ResponseModule } from 'src/response/response.module';
@Module({
  imports: [ResponseModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

完成以上配置后,重新启动项目访问 http://localhost:3000/user,可见 data、message 和 code 三个业务字段的信息都成功输出。

image-20251213233534395

图4-1 业务字段信息插入

接下来只需从全局拦截器中重新统一数据格式即可。

// src/interceptor/interceptor.interceptor.ts
return {
  timestmap: new Date().toISOString(),
  data: transformBigInt(data.data) ?? null,
  path: request.url,
  message: data.message ?? 'success',//业务逻辑自定义
  code: data.code ?? 200,//业务逻辑自定义
  success: true,
};

统一业务字段格式如图4-2所示。

image-20251213234031967

图4-2 统一业务字段格式

通过以上步骤,Nest.js 的业务状态码和业务状态描述已成功应用到对应的 user 模块接口中。但在实现过程中,我们发现每个使用 ResponseService 的模块都需要:

(1)在提供模块中导出服务。

(2)在使用模块中导入模块。

如果项目有十几个模块都需要使用,这种重复导入导出的操作会变得繁琐。为此,可以将 response 模块注册为全局模块,这样在整个项目中都可以直接使用,无需在每个使用模块的 module.ts 文件中重复导入。

将 response 模块注册为全局模块的方法为以下2步:

(1)从 @nestjs/common 中导入 Global 装饰器

(2)将 @Global() 装饰器应用到 ResponseModule 上

注册为全局模块后,ResponseService 可以在任何模块中直接使用,无需在各使用模块中导入 ResponseModule。

// src/response/response.module.ts
import { Module, Global } from '@nestjs/common';
import { ResponseService } from './response.service';

@Global()
@Module({
  providers: [ResponseService],
  exports: [ResponseService]
})
export class ResponseModule { }

此时如果我们回到user.module.ts文件中,将刚才注册的ResponseModule删除,项目也不会报错。

对于全局模块的使用,若我想在xiaoyu模块中使用ResponseService,主要步骤如下,无需在XiaoyuModule中导入ResponseModule:

(1)在 xiaoyu.service.ts 文件中导入 ResponseService 服务类和 business 业务常量。

(2)在 XiaoyuService 的构造函数中注入 ResponseService,并使用其方法按照业务数据格式规范处理数据。

(3)在 xiaoyu.controller.ts 文件的控制器中注入 XiaoyuService,并在路由处理器中调用其业务方法返回处理结果。

注意:虽然 ResponseService 是全局模块无需导入,但 XiaoyuModule 仍需要在自身的 providers 中注册 XiaoyuService,在 controllers 中注册 XiaoyuController。

// src/xiaoyu/xiaoyu.service.ts
import { Injectable } from '@nestjs/common';
import { ResponseService } from 'src/response/response.service';
import { business } from 'src/business';
@Injectable()
export class XiaoyuService {
  constructor(private readonly responseService: ResponseService) {}
  getHello(): any {
    const message = business.LOGIN_SUCCESS.message;
    const code = business.LOGIN_SUCCESS.code;
    return this.responseService.success('This action returns all user', message, code);
  }
}
// src/xiaoyu/xiaoyu.controller.ts
import { Controller, Get } from '@nestjs/common';
import { XiaoyuService } from './xiaoyu.service';

@Controller('xiaoyu')
export class XiaoyuController {
  // 依赖注入
  constructor(private readonly xiaoyuService: XiaoyuService) {}
  @Get()
  getHello(): any {
    return this.xiaoyuService.getHello();
  }
}

xiaoyu模块业务合并如图4-3所示。

image-20251214004117303

图4-3 xiaoyu模块业务合并

以上就是Nest.js业务层的所有内容,我们回顾一下,Nest.js 的业务处理分为 business 和 response 两个部分:

(1)business 文件夹:集中管理业务状态码和描述信息。

(2)response 模块:专门用于构建统一的响应格式。

在实际项目中,这两者的变化频率不同:响应格式通常保持稳定,而业务状态码会随着业务发展不断新增或调整。因此需要将两者分离管理。response 模块的功能与全局拦截器中统一返回客户端响应格式的功能是一致的,区别在于我们将自定义部分拆分出来,提高了灵活性和可维护性,更符合实际项目的需求变化。

这种架构设计的优势在于:业务状态定义与响应格式构建职责明确(单一职责);业务状态变化不影响响应格式,响应格式调整不影响业务逻辑;统一的响应格式可跨模块、跨项目使用;新增业务状态只需在 business 文件中添加,不影响现有结构。通过这种规范化管理,可以构建出清晰、可维护、可扩展的业务层架构,适应各种复杂的业务场景需求。

除了message和code这两个字段,我们还可以有权限与安全控制(例如IP白名单、频率限制、黑白名单)、数据持久化(创建、读取、更新、删除业务数据)、业务流程(多级审批、会签、或签逻辑)、第三方集成(支付宝、微信支付回调处理)、监控与统计(接口响应时间、成功率)等多方面基于实际业务需求去增添。

Vue 父子组件双向绑定的终极指南:告别数据同步烦恼!

作者 北辰alk
2026年1月3日 13:08

Vue 父子组件双向绑定的终极指南:告别数据同步烦恼!

Vue父子组件绑定头图

引言

在Vue开发中,父子组件之间的数据通信是每个开发者都会遇到的挑战。当父组件需要控制子组件状态,同时子组件也需要更新父组件数据时,如何优雅地实现双向绑定就成了关键问题。

今天,我将为你详细介绍Vue中实现父子组件双向绑定的4种核心方法,从基础到高级,让你彻底掌握这一重要技能!

一、基础知识:单向数据流原则

在深入解决方案之前,我们先理解Vue的核心设计原则——单向数据流:

父组件 → Props → 子组件
子组件 → Events → 父组件

这种设计保证了数据流向的可预测性,但当我们确实需要双向绑定时,就需要一些技巧来实现。

二、方法一:Props + $emit(基础方法)

这是Vue官方推荐的"单向数据流"实现双向绑定的标准方式。

实现原理

  1. 父组件通过props向子组件传递数据
  2. 子组件通过$emit触发事件通知父组件更新数据

代码实现

子组件 ChildComponent.vue

<template>
  <div class="child">
    <h3>子组件</h3>
    <input 
      :value="value" 
      @input="$emit('update:value', $event.target.value)"
      placeholder="输入内容..."
    />
    <p>当前值: {{ value }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: {
    value: {
      type: String,
      required: true
    }
  }
}
</script>

父组件 ParentComponent.vue

<template>
  <div class="parent">
    <h2>父组件</h2>
    <p>父组件中的值: {{ parentValue }}</p>
    
    <ChildComponent 
      :value="parentValue" 
      @update:value="parentValue = $event"
    />
    
    <button @click="resetValue">重置为默认值</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data() {
    return {
      parentValue: '初始值'
    }
  },
  methods: {
    resetValue() {
      this.parentValue = '默认值'
    }
  }
}
</script>

流程图解

graph TD
    A[父组件数据变更] --> B[Props传递给子组件]
    B --> C[子组件接收数据]
    D[子组件用户输入] --> E[$emit触发更新事件]
    E --> F[父组件监听并更新数据]
    F --> A
    
    style A fill:#e1f5fe
    style C fill:#f3e5f5
    style F fill:#e8f5e8

优缺点分析

优点:

  • 符合Vue设计哲学
  • 代码清晰,数据流向明确
  • 易于调试和维护

缺点:

  • 需要编写较多样板代码
  • 对于深层嵌套组件略显繁琐

三、方法二:v-model指令(语法糖)

Vue 2.2.0+ 提供了.sync修饰符的替代方案,使用自定义组件的v-model

实现原理

v-model在组件上实际上是以下写法的语法糖:

<ChildComponent 
  :value="parentValue"
  @input="parentValue = $event"
/>

代码实现

子组件 ChildComponent.vue

<template>
  <div class="child">
    <h3>子组件 (v-model)</h3>
    <select :value="value" @change="$emit('input', $event.target.value)">
      <option value="vue">Vue.js</option>
      <option value="react">React</option>
      <option value="angular">Angular</option>
    </select>
    <p>选中的框架: {{ value }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: {
    value: {
      type: String,
      required: true
    }
  },
  model: {
    prop: 'value',    // 指定v-model绑定的prop名
    event: 'input'    // 指定v-model监听的事件名
  }
}
</script>

父组件 ParentComponent.vue

<template>
  <div class="parent">
    <h2>父组件 (v-model示例)</h2>
    <p>选择的框架: {{ selectedFramework }}</p>
    
    <!-- 使用v-model语法糖 -->
    <ChildComponent v-model="selectedFramework" />
    
    <!-- 等价于 -->
    <!-- <ChildComponent :value="selectedFramework" @input="selectedFramework = $event" /> -->
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data() {
    return {
      selectedFramework: 'vue'
    }
  }
}
</script>

多个v-model绑定(Vue 2.3.0+)

从Vue 2.3.0开始,可以通过model选项配置不同的prop和event,但在Vue 3中更加简化:

Vue 3中的多个v-model

<!-- 父组件 -->
<UserName
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>

<!-- 子组件 -->
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

四、方法三:.sync修饰符(Vue 2.3.0+)

.sync修饰符是另一种语法糖,用于实现prop的"双向绑定"。

实现原理

<!-- 使用.sync -->
<ChildComponent :title.sync="pageTitle" />

<!-- 等价于 -->
<ChildComponent 
  :title="pageTitle" 
  @update:title="pageTitle = $event"
/>

代码实现

子组件 ChildComponent.vue

<template>
  <div class="child">
    <h3>子组件 (.sync修饰符)</h3>
    <div class="counter">
      <button @click="decrement">-</button>
      <span>{{ count }}</span>
      <button @click="increment">+</button>
    </div>
    <p>当前计数: {{ count }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: {
    count: {
      type: Number,
      required: true
    }
  },
  methods: {
    increment() {
      this.$emit('update:count', this.count + 1)
    },
    decrement() {
      this.$emit('update:count', this.count - 1)
    }
  }
}
</script>

<style scoped>
.counter {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0;
}
.counter button {
  padding: 5px 15px;
  font-size: 16px;
}
</style>

父组件 ParentComponent.vue

<template>
  <div class="parent">
    <h2>父组件 (.sync示例)</h2>
    <p>当前计数: {{ counter }}</p>
    
    <!-- 使用.sync修饰符 -->
    <ChildComponent :count.sync="counter" />
    
    <!-- 可以同时绑定多个prop -->
    <!-- <ChildComponent :count.sync="counter" :title.sync="pageTitle" /> -->
    
    <button @click="counter = 0">重置计数</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data() {
    return {
      counter: 0
    }
  }
}
</script>

.sync与v-model的区别

特性 v-model .sync修饰符
绑定数量 一个组件通常一个 可以多个
事件名 默认input update:propName
Prop名 默认value 任意prop名
Vue 3支持 有变化 已移除,用v-model代替

五、方法四:Vuex状态管理(复杂场景)

对于大型应用或深层嵌套组件,使用Vuex进行状态管理是最佳选择。

实现架构

graph TB
    A[组件] --> B[触发Action]
    B --> C[提交Mutation]
    C --> D[更新State]
    D --> E[响应式更新所有组件]
    
    subgraph "Vuex Store"
        C
        D
    end
    
    style D fill:#fff3e0
    style E fill:#e8f5e8

代码实现

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userSettings: {
      theme: 'light',
      fontSize: 14,
      notifications: true
    }
  },
  mutations: {
    UPDATE_SETTING(state, { key, value }) {
      if (key in state.userSettings) {
        state.userSettings[key] = value
      }
    },
    UPDATE_SETTINGS(state, settings) {
      state.userSettings = { ...state.userSettings, ...settings }
    }
  },
  actions: {
    updateSetting({ commit }, payload) {
      commit('UPDATE_SETTING', payload)
    },
    resetSettings({ commit }) {
      commit('UPDATE_SETTINGS', {
        theme: 'light',
        fontSize: 14,
        notifications: true
      })
    }
  },
  getters: {
    userSettings: state => state.userSettings,
    darkMode: state => state.userSettings.theme === 'dark'
  }
})

子组件 SettingsEditor.vue

<template>
  <div class="settings-editor" :class="settings.theme">
    <h3>设置编辑器 (Vuex)</h3>
    
    <div class="setting-item">
      <label>主题模式:</label>
      <select :value="settings.theme" @change="updateSetting('theme', $event.target.value)">
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
    </div>
    
    <div class="setting-item">
      <label>字体大小:</label>
      <input 
        type="range" 
        min="10" 
        max="24" 
        :value="settings.fontSize"
        @input="updateSetting('fontSize', parseInt($event.target.value))"
      />
      <span>{{ settings.fontSize }}px</span>
    </div>
    
    <div class="setting-item">
      <label>
        <input 
          type="checkbox" 
          :checked="settings.notifications"
          @change="updateSetting('notifications', $event.target.checked)"
        />
        启用通知
      </label>
    </div>
    
    <p>当前主题: {{ settings.theme }} | 字体大小: {{ settings.fontSize }}px</p>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  name: 'SettingsEditor',
  computed: {
    ...mapState(['userSettings']),
    settings() {
      return this.userSettings
    }
  },
  methods: {
    ...mapActions(['updateSetting'])
  }
}
</script>

<style scoped>
.settings-editor {
  padding: 20px;
  border-radius: 8px;
  transition: all 0.3s;
}
.settings-editor.light {
  background-color: #ffffff;
  color: #333333;
}
.settings-editor.dark {
  background-color: #333333;
  color: #ffffff;
}
.setting-item {
  margin: 15px 0;
}
.setting-item label {
  margin-right: 10px;
}
</style>

父组件 ParentComponent.vue

<template>
  <div class="parent">
    <h2>父组件 (Vuex示例)</h2>
    
    <div class="settings-display">
      <h3>当前设置</h3>
      <ul>
        <li>主题: {{ userSettings.theme }}</li>
        <li>字体大小: {{ userSettings.fontSize }}px</li>
        <li>通知: {{ userSettings.notifications ? '开启' : '关闭' }}</li>
      </ul>
    </div>
    
    <SettingsEditor />
    
    <div class="actions">
      <button @click="resetSettings">恢复默认设置</button>
      <button @click="toggleTheme">切换主题</button>
    </div>
  </div>
</template>

<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import SettingsEditor from './SettingsEditor.vue'

export default {
  name: 'ParentComponent',
  components: {
    SettingsEditor
  },
  computed: {
    ...mapState(['userSettings']),
    ...mapGetters(['darkMode'])
  },
  methods: {
    ...mapActions(['updateSetting', 'resetSettings']),
    toggleTheme() {
      const newTheme = this.darkMode ? 'light' : 'dark'
      this.updateSetting({ key: 'theme', value: newTheme })
    }
  }
}
</script>

六、方法对比与选择指南

方法对比表

方法 适用场景 优点 缺点 Vue 2支持 Vue 3支持
Props + $emit 简单父子通信 官方推荐,清晰易懂 代码量多
v-model 表单类组件 语法简洁,使用广泛 单个绑定有限制 ✓ (增强)
.sync修饰符 多个prop需要双向绑定 支持多个绑定 Vue 3已移除
Vuex 复杂应用/多组件共享 集中管理,响应式 增加复杂度

选择流程图

graph TD
    A[开始选择双向绑定方案] --> B{数据共享范围}
    
    B -->|父子组件| C{绑定复杂度}
    B -->|多个组件共享| D[使用Vuex/Pinia]
    
    C -->|简单绑定| E{vue版本?}
    C -->|多个prop绑定| F{Vue版本?}
    
    E -->|Vue 2| G[Props + $emit 或 v-model]
    E -->|Vue 3| H[v-model]
    
    F -->|Vue 2| I[.sync修饰符]
    F -->|Vue 3| J[多个v-model]
    
    G --> K[完成选择]
    H --> K
    I --> K
    J --> K
    D --> K

七、实战技巧与最佳实践

1. 使用计算属性优化

<script>
export default {
  props: ['value'],
  computed: {
    internalValue: {
      get() {
        return this.value
      },
      set(newValue) {
        this.$emit('input', newValue)
      }
    }
  }
}
</script>

<template>
  <input v-model="internalValue" />
</template>

2. 深度监听对象变化

<script>
export default {
  props: {
    config: {
      type: Object,
      required: true
    }
  },
  watch: {
    config: {
      handler(newVal) {
        // 处理对象变化
        this.$emit('update:config', { ...newVal })
      },
      deep: true
    }
  }
}
</script>

3. 使用Provide/Inject处理深层嵌套

<!-- 祖先组件 -->
<script>
export default {
  provide() {
    return {
      sharedState: this.sharedState,
      updateSharedState: this.updateSharedState
    }
  },
  data() {
    return {
      sharedState: {
        theme: 'light',
        language: 'zh'
      }
    }
  },
  methods: {
    updateSharedState(key, value) {
      this.sharedState[key] = value
    }
  }
}
</script>

<!-- 深层子组件 -->
<script>
export default {
  inject: ['sharedState', 'updateSharedState'],
  methods: {
    changeTheme(theme) {
      this.updateSharedState('theme', theme)
    }
  }
}
</script>

八、Vue 3的Composition API新玩法

Vue 3的Composition API为双向绑定带来了更灵活的解决方案:

<!-- 子组件 -->
<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (newValue) => emit('update:modelValue', newValue)
})
</script>

<template>
  <input v-model="value" />
</template>

总结

掌握Vue父子组件双向绑定的多种方法,能让你在不同场景下选择最合适的解决方案:

  1. 简单场景:使用Props + $emit,保持代码清晰
  2. 表单组件:使用v-model,提高开发效率
  3. 多个绑定:Vue 2用.sync,Vue 3用多个v-model
  4. 复杂应用:使用Vuex/Pinia,集中管理状态

记住,没有绝对的最佳方案,只有最适合当前场景的选择。希望这篇文章能帮助你在Vue开发中游刃有余地处理组件通信问题!

昨天 — 2026年1月3日掘金 前端

VS Code 终端崩溃问题分析与解决方案

作者 凯哥1970
2026年1月3日 10:35

VS Code 终端崩溃问题分析与解决方案

错误代码:-2147023895 (0x800703E9)

显示如下

终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe”已终止,退出代码: -2147023895。

问题描述

在 VS Code 中打开终端时,PowerShell 进程异常退出,返回错误代码 -2147023895。该错误会导致终端无法正常启动或使用,影响开发效率。


错误原因分析

错误代码 -2147023895 对应十六进制值 0x800703E9,是一个标准的 HRESULT 错误码,其结构解析如下:

  • 严重性位(Bit 31) :1,表示失败。
  • 设备代码(Bits 16-26) :7(FACILITY_WIN32),表示错误源自 Windows API 调用。
  • 低位代码(Bits 0-15)0x03E9(十进制 1001)。
可能的原因:
  1. 栈溢出(Stack Overflow)
    PowerShell 启动时脚本陷入无限递归,耗尽线程栈空间,触发系统异常。
  2. 文件完整性校验失败(Invalid Image Hash)
    Windows 代码完整性机制或安全软件(如 AppLocker)检测到脚本文件签名无效、文件损坏或哈希不匹配,导致加载被拒绝。
  3. 环境变量冲突
    脚本执行过程中展开的环境变量(如 PATH)过长,引发内存或栈溢出。

根本原因定位

多数情况下,该错误与 VS Code 终端 Shell 集成脚本 shellIntegration.ps1 有关。该脚本在终端启动时被自动加载,若文件损坏或与用户配置冲突,即会触发上述错误。


解决方案:手动替换脚本文件(治本)

无需禁用终端功能,直接替换损坏的脚本文件即可根治问题。

操作步骤:
  1. 定位脚本文件
    根据 VS Code 安装方式,找到目标目录:

    • 用户安装
      %LOCALAPPDATA%\Programs<EditorName>\resources\app\out\vs\workbench\contrib\terminal\common\scripts
    • 系统安装
      C:\Program Files\Microsoft VS Code\resources\app\out\vs\workbench\contrib\terminal\common\scripts
  2. 备份原文件
    将目录中的 shellIntegration.ps1 重命名为 shellIntegration.ps1.bak,作为备份。

  3. 下载官方脚本
    访问 VS Code 官方 GitHub 仓库,下载最新版本的脚本文件:
    raw.githubusercontent.com/microsoft/v…

  4. 替换文件
    将下载的 shellIntegration.ps1 复制到步骤 1 的目录中,确保当前用户有读取权限。

  5. 重启验证
    完全关闭 VS Code(包括后台进程),重新启动并打开终端,检查是否恢复正常。


方案原理

通过替换为官方完好的脚本文件,确保:

  • PowerShell 解析器能正常解析语法,避免因文件损坏导致的崩溃。
  • 脚本与用户环境兼容,避免递归冲突或安全校验失败。
  • 保留完整的终端 Shell 集成功能(如命令装饰、状态提示等)。

注意事项

  • 若问题仍然存在,可检查用户 PowerShell 配置文件($PROFILE)中是否存在与 Shell 集成冲突的自定义代码。
  • 建议定期更新 VS Code,以获取官方修复的脚本版本。

通过以上步骤,可从根本上解决终端崩溃问题,无需临时禁用功能或修改启动命令,确保开发环境稳定可用。

使用 json-server 快速创建一个完整的 REST API

2026年1月3日 01:12

json-server 使用教程

什么是 json-server

json-server 是一个基于 Node.js 的工具,可以快速创建一个完整的 REST API,使用 JSON 文件作为数据源。非常适合前端开发、原型设计和测试。

安装

全局安装

npm install -g json-server

本地安装

npm install json-server --save-dev

使用 npx(推荐)

npx json-server

快速开始

1. 创建数据文件

创建一个 db.json 文件:

{
  "posts": [
    {
      "id": 1,
      "title": "第一篇文章",
      "author": "张三"
    },
    {
      "id": 2,
      "title": "第二篇文章",
      "author": "李四"
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "很好的文章",
      "postId": 1
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

2. 启动服务器

# 默认端口 3000
json-server db.json

# 指定端口
json-server db.json --port 4000

# 指定主机
json-server db.json --host 0.0.0.0

# 静默模式
json-server db.json --quiet

# 监视文件变化
json-server db.json --watch

3. 访问 API

启动后,服务器会提供以下端点:

# 资源端点
GET    /posts
GET    /posts/1
POST   /posts
PUT    /posts/1
PATCH  /posts/1
DELETE /posts/1

# 其他资源
GET    /comments
GET    /profile

REST API 操作

GET - 获取数据

# 获取所有文章
curl http://localhost:3000/posts

# 获取指定 ID 的文章
curl http://localhost:3000/posts/1

# 获取个人资料
curl http://localhost:3000/profile

POST - 创建数据

# 创建新文章
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "新文章",
    "author": "王五"
  }'

# 使用 JavaScript fetch
fetch('http://localhost:3000/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '新文章',
    author: '王五'
  })
})

PUT - 完整更新

# 完整更新文章(必须包含所有字段)
curl -X PUT http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "id": 1,
    "title": "更新后的标题",
    "author": "张三"
  }'

PATCH - 部分更新

# 部分更新文章(只更新指定字段)
curl -X PATCH http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "只更新标题"
  }'

DELETE - 删除数据

# 删除文章
curl -X DELETE http://localhost:3000/posts/1

高级查询

过滤

# 按字段过滤
GET /posts?author=张三

# 多条件过滤
GET /posts?author=张三&id=1

# 嵌套属性过滤
GET /comments?postId=1

分页

# 分页查询
GET /posts?_page=1&_limit=10

# 获取总数(在响应头 X-Total-Count 中)

排序

# 升序排序
GET /posts?_sort=id&_order=asc

# 降序排序
GET /posts?_sort=id&_order=desc

# 多字段排序
GET /posts?_sort=author,id&_order=asc,desc

切片

# 范围查询
GET /posts?_start=0&_end=10

# 限制数量
GET /posts?_limit=5

# 跳过数量
GET /posts?_start=5

操作符

# 大于
GET /posts?id_gte=1

# 小于
GET /posts?id_lte=10

# 不等于
GET /posts?id_ne=1

# 包含
GET /posts?q=关键词

# 正则表达式
GET /posts?id_like=1

全文搜索

# 搜索包含关键词的记录
GET /posts?q=文章

# 搜索会在所有字段中查找

关联查询

# 获取文章及其评论
GET /posts/1?_embed=comments

# 获取评论及其所属文章
GET /comments?_expand=post

# 嵌套多个关系
GET /posts/1?_embed=comments&_embed=likes

配置选项

命令行选项

# 完整命令示例
json-server db.json \
  --port 4000 \
  --host 0.0.0.0 \
  --watch \
  --routes routes.json \
  --middlewares middleware.js \
  --static ./public \
  --read-only \
  --no-cors

选项说明

选项 说明
--port 指定端口号(默认 3000)
--host 指定主机地址(默认 localhost)
--watch 监视文件变化
--routes 自定义路由文件
--middlewares 中间件文件
--static 静态文件目录
--read-only 只读模式(禁用 POST/PUT/DELETE)
--no-cors 禁用 CORS
--quiet 静默模式

自定义路由

创建路由文件

创建 routes.json

{
  "/api/*": "/$1",
  "/:resource/:id/show": "/:resource/:id",
  "/posts/:category": "/posts?category=:category",
  "/articles\\?id=:id": "/posts/:id"
}

使用自定义路由

json-server db.json --routes routes.json

路由示例

# 原始路由
GET /posts/1

# 自定义路由
GET /api/posts/1        # 映射到 /posts/1
GET /posts/1/show       # 映射到 /posts/1
GET /posts/tech         # 映射到 /posts?category=tech

中间件

创建中间件

创建 middleware.js

module.exports = (req, res, next) => {
  // 添加响应头
  res.header('X-Custom-Header', 'Custom Value')
  
  // 记录请求
  console.log(`${req.method} ${req.url}`)
  
  // 模拟延迟
  if (req.url.includes('delay')) {
    setTimeout(next, 2000)
  } else {
    next()
  }
}

使用中间件

json-server db.json --middlewares middleware.js

高级中间件示例

module.exports = (req, res, next) => {
  // 身份验证模拟
  if (req.method === 'POST' && !req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  
  // 请求日志
  const timestamp = new Date().toISOString()
  console.log(`[${timestamp}] ${req.method} ${req.url}`)
  
  // 修改响应
  const originalSend = res.send
  res.send = function (data) {
    const modifiedData = JSON.parse(data)
    modifiedData.timestamp = timestamp
    originalSend.call(this, JSON.stringify(modifiedData))
  }
  
  next()
}

数据生成

使用 Faker.js 生成测试数据

const faker = require('faker')
const fs = require('fs')

const generateData = () => {
  const users = []
  for (let i = 1; i <= 100; i++) {
    users.push({
      id: i,
      name: faker.name.findName(),
      email: faker.internet.email(),
      phone: faker.phone.phoneNumber(),
      address: {
        street: faker.address.streetAddress(),
        city: faker.address.city(),
        country: faker.address.country()
      },
      company: faker.company.companyName()
    })
  }
  
  const db = { users }
  fs.writeFileSync('db.json', JSON.stringify(db, null, 2))
}

generateData()

运行生成器

node generate-data.js
json-server db.json

与前端框架集成

React 示例

import axios from 'axios'

const API_URL = 'http://localhost:3000'

// 获取所有文章
export const getPosts = async () => {
  const response = await axios.get(`${API_URL}/posts`)
  return response.data
}

// 创建文章
export const createPost = async (post) => {
  const response = await axios.post(`${API_URL}/posts`, post)
  return response.data
}

// 更新文章
export const updatePost = async (id, post) => {
  const response = await axios.put(`${API_URL}/posts/${id}`, post)
  return response.data
}

// 删除文章
export const deletePost = async (id) => {
  const response = await axios.delete(`${API_URL}/posts/${id}`)
  return response.data
}

Vue 示例

import axios from 'axios'

const api = axios.create({
  baseURL: 'http://localhost:3000'
})

export default {
  async getPosts() {
    const response = await api.get('/posts')
    return response.data
  },
  
  async createPost(post) {
    const response = await api.post('/posts', post)
    return response.data
  },
  
  async updatePost(id, post) {
    const response = await api.put(`/posts/${id}`, post)
    return response.data
  },
  
  async deletePost(id) {
    const response = await api.delete(`/posts/${id}`)
    return response.data
  }
}

package.json 配置

添加脚本

{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "json-server": "json-server --watch db.json --port 4000",
    "dev": "npm run json-server"
  },
  "devDependencies": {
    "json-server": "^0.17.3"
  }
}

运行脚本

npm run dev

常见问题

1. 端口被占用

# 使用其他端口
json-server db.json --port 4000

2. CORS 问题

json-server 默认启用 CORS,如果遇到问题:

# 确保没有禁用 CORS
json-server db.json

3. 数据持久化

json-server 会将修改保存到 db.json 文件中,确保文件有写入权限。

4. 重置数据

# 删除 db.json 或恢复备份
rm db.json
cp db.json.backup db.json

最佳实践

1. 版本控制

# 添加到 .gitignore
echo "db.json" >> .gitignore

# 创建示例数据文件
cp db.json db.example.json

2. 环境配置

// config.js
module.exports = {
  port: process.env.JSON_SERVER_PORT || 3000,
  host: process.env.JSON_SERVER_HOST || 'localhost',
  watch: process.env.NODE_ENV !== 'production'
}

3. 数据验证

// middleware.js
module.exports = (req, res, next) => {
  if (req.method === 'POST' || req.method === 'PUT') {
    const body = req.body
    
    // 验证必填字段
    if (!body.title) {
      return res.status(400).json({ error: 'Title is required' })
    }
  }
  next()
}

4. 性能优化

# 使用静态文件服务
json-server db.json --static ./public

# 禁用日志(生产环境)
json-server db.json --quiet

总结

json-server 是一个强大且易于使用的工具,适合:

  • ✅ 前端开发和原型设计
  • ✅ API 测试和调试
  • ✅ 快速搭建演示项目
  • ✅ 学习 REST API 概念

通过本教程,你应该能够:

  • 安装和启动 json-server
  • 执行 CRUD 操作
  • 使用高级查询功能
  • 自定义路由和中间件
  • 与前端框架集成

开始使用 json-server,快速构建你的 REST API 吧!

Vue 原生渲染真要来了?Lynx引擎首次跑通Vue

作者 小小荧
2026年1月2日 23:44

Vue 原生渲染真要来了?Lynx引擎首次跑通Vue

“这一次,Vue 终于能在移动端跑出原生性能了。”

最新动态:一位前端工程师在48小时内,成功将Vue 3的响应式系统与字节跳动的Lynx.js引擎对接,实现了首个Vue自定义渲染器原型。这标志着近200万Vue开发者有望直接使用熟悉的ref<SFC>等语法,驱动iOS/Android的原生控件,告别WebView的性能束缚。

一、突破性进展:Vue 已在 Lynx 上跑通

近日,前端工程师 @Shenqingchuan 在社交平台展示了他的成果:一个在Lynx引擎上运行的Vue 3计数器Demo。

这项原型验证了技术可行性,他也公开邀请对 “Vue Lynx” 感兴趣且熟悉Vue核心代码(尤其是runtime-core)的开发者加入共建。

二、什么是 Lynx.js?

Lynx 是字节跳动于今年3月开源的一款高性能双线程原生渲染框架,其核心架构优势在于:

  • UI线程:使用自研PrimJS配合基于Rust的Rspack(Rspeedy),实现毫秒级首帧直出。
  • 后台线程:独立运行业务逻辑、网络请求等,避免复杂计算阻塞界面。
  • 原生渲染:直接调用平台原生控件,其渲染性能与Flutter属于同一梯队。
  • 实战验证:已广泛应用于TikTok搜索、直播等亿级月活业务场景。

Lynx框架本身保持中立,其团队曾公开表示欢迎Vue等框架接入,这为此次“Vue-Lynx”的原型诞生提供了土壤。

三、官方与社区的积极信号

此次尝试迅速获得了来自双方核心人物的关注:

  • Lynx架构师 @Huxpro 转发并帮助招募合作者。
  • Vue作者 @youyuxi 的转发,相当于给予了项目“官方默许”的认证。

此外,在最近的React Advanced大会上,@Huxpro预告了lynx-ui组件库将于12月开源,这将为上层框架提供丰富的原生UI物料,进一步夯实生态基础。

四、核心优势:为什么这次可能成了?

相比历史上的类似尝试(如Weex),此次“Vue + Lynx”的组合在多个层面具备了更坚实的基础:

维度 Vue + Lynx 方案 传统方案的典型痛点
渲染性能 双线程原生控件,无WebView层级 WebView易掉帧、卡顿
开发体验 完整Vue 3组合式API,对接现代构建工具(Vite/Rspeedy) 需学习新语法,或构建速度慢
调试支持 拥有Lynx DevTool,支持真机断点调试 调试依赖日志,体验差
技术验证 底层引擎已在10亿+DAU产品中验证 多为实验室级原型,缺乏大规模验证

五、代码一瞥:Vue Lynx 初体验

一个简单的Vue组件在Lynx环境下可能这样编写:

<!-- HelloLynx.vue -->
<script setup>
import logo from './assets/lynx-logo.png'
import { ref } from 'vue'

const count = ref(0)
setInterval(() => count.value++, 1800)
</script>

<template>
  <view class="container">
    <image :src="logo" class="logo" />
    <text class="h1">Hello Vue-Lynx</text>
    <text class="p">双线程原生渲染,首帧直出!</text>
    <button class="btn" @click="count++">点我:{{ count }}</button>
  </view>
</template>

其中的 <view><text><image> 等标签将被编译并映射为平台原生组件,而开发者使用的仍然是百分之百标准的Vue语法。

六、技术实现路径展望

要实现生产可用的“Vue Lynx”,还需攻克几个关键节点:

  1. 编译链路适配:需要开发新的插件(如vue-loader-rs),将Vue SFC编译为Lynx双线程可识别的代码包,并严格区分UI线程与后台线程的职责。
  2. 定制运行时:在Vue核心库中新增一个vue/runtime-lynx包,实现与PrimJS API对接的节点操作、调度器和事件系统。
  3. 线程边界管理:可能通过扩展SFC语法(如引入<script main>标签),或在编译时进行静态分析,来明确代码的运行线程,确保开发者既能畅快编码又不违反架构约束。

七、Vue Native 生态路线图

目前,让Vue开发移动原生应用的方案并非唯一,开发者可根据需求选择:

路线 渲染方式 性能 开发体验 适用场景
NativeScript-Vue 3 原生控件 ★★★★ Vite + Tailwind,成熟 追求100%原生UI,无需WebView
Ionic Vue + Capacitor WebView ★★★ 最接近Web开发,PWA友好 一套代码覆盖Web/App,重开发效率
uni-app / uni-appx WebView → 原生渲染 ★★★☆ 中文生态完善,工具链强 需同时发布国内多端(小程序+App)
Vue + Lynx 双线程原生 ★★★★☆ 早期,需配置,潜力大 追求极致性能,愿参与生态共建

简单决策参考

  • “我现在就要用” → 选择 NativeScript-Vue 或 uni-app。
  • “我要最像Web的开发体验” → 选择 Ionic Vue。
  • “我看重未来性能和前沿技术” → 密切关注并尝试参与 Vue + Lynx

八、结语:这一次,不再缺席?

Vue社区对“原生渲染”的期待由来已久。如今,多条技术路径正在并行发展:

  • NativeScript-Vue 3 已趋成熟。
  • uni-appx 持续拓展多端能力。
  • 而最具颠覆性的 Vue + Lynx 路径,正以开源共建的模式吸引开发者。

或许在不久的将来,我们只需一条命令:

npm create vue-native@latest

便可从多个生产就绪的Vue原生渲染模板中任选其一。

Weex时代的遗憾,或许真的能在2025年被彻底填补。Vue Native,这一次可能真的要启动了。

保持关注

  • Lynx 项目:github.com/lynx-family/lynx
  • 生态动态:可关注 @Huxpro 等核心开发者的最新消息。

Vue3 应用实例创建及页面渲染底层原理

2026年1月2日 23:13

整体流程

完整的创建与渲染流程可以分成这些阶段:

  1. 创建 App 实例
  2. 创建根组件实例
  3. 设置响应式状态
  4. 创建渲染器(Renderer)
  5. 挂载 Mount
  6. vnode -> DOM 渲染
  7. 数据变更触发更新
  8. 重新渲染 / diff / patch

流程图大致如下:

createApp() ───> app.mount('#app')
         │                 │
         ▼                 ▼
   createRootComponent    createRenderer
         │                 │
         ▼                 ▼
 setup() / render()   render(vnode) -> patch
         │                 │
         ▼                 ▼
   effect(fn) ────> scheduler -> patch updates

1、createApp 初始化

Vue 应用的入口通常是:

createApp(App).mount('#app')

从源码看 createApp:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      _container: null,
      _context: createAppContext()
    }
    const proxy = (app._instance = {
      app
    })
    // register global APIs
    // ...
    return {
      mount(container) {
        const vnode = createVNode(rootComponent, rootProps)
        app._container = container
        render(vnode, container)
      },
      unmount() { /* ... */ }
    }
  }
}

关键点:

  • createAppAPI(render) 生成 createApp 函数
  • app 内保存 _component、上下文 _context
  • app.mount 调用 render(vnode, container)

render平台渲染器 注入(在 web 下是 DOM 渲染器)。

2、createVNode 创建虚拟节点(VNode)

在 mount 前会创建一个虚拟节点:

function createVNode(type, props, children) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
    key: props && props.key
  }
  return vnode
}

vnode 是渲染的基础单元:

shapeFlag 用来快速判断 vnode 类型,是内部性能优化。

3、渲染器 Renderer 初始化

Vue3 是平台无关的(runtime-core),真正依赖 DOM 的是在 runtime-dom 中。

创建 Renderer:

export const renderer = createRenderer({
  createElement: hostCreateElement,
  patchProp: hostPatchProp,
  insert: hostInsert,
  remove: hostRemove,
  setElementText: hostSetElementText
})

createRenderer 返回了我们前面在 createApp 中使用的 render(vnode, container) 函数。

4、render & patch

核心渲染入口:

function render(vnode, container) {
  patch(null, vnode, container, null, null)
}

patch 是渲染补丁函数:

function patch(n1, n2, container, parentComponent, anchor) {
  const { type, shapeFlag } = n2
  if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement()
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    processComponent(...)
  }
}

简化为:

  • 如果是 DOM 元素 vnode → 挂载/更新
  • 如果是 组件 vnode → 创建组件实例、挂载、渲染子树

5、组件实例创建

当渲染组件时:

function processComponent(n1, n2, container, parentComponent, anchor) {
  mountComponent(n2, container, parentComponent, anchor)
}
function mountComponent(vnode, container, parentComponent, anchor) {
  const instance = createComponentInstance(vnode, parentComponent)
  setupComponent(instance)
  setupRenderEffect(instance, container, anchor)
}
  • processComponent 处理组件
  • mountComponent 挂载组件
    • createComponentInstance 创建组件实例
    • setupComponent 创建组件对象

createComponentInstance:

function createComponentInstance(vnode, parent) {
  const instance = {
    vnode,
    parent,
    proxy: null,
    ctx: {},
    props: {},
    attrs: {},
    slots: {},
    setupState: {},
    isMounted: false,
    subTree: null
  }
  return instance
}

实例保存基础信息,还没运行 setup。

6、 setupComponent(初始化组件)

function setupComponent(instance) {
  initProps(instance, vnode.props)
  initSlots(instance, vnode.children)
  setupStatefulComponent(instance)
}

内部会执行:

const { setup } = Component
if (setup) {
  const setupResult = setup(props, ctx)
  handleSetupResult(instance, setupResult)
}

setup 返回值

  • 返回对象 → 作为响应式状态 state
  • 返回函数 → render 函数

最终让组件拥有 instance.render

7、创建响应式状态

Vue3 的响应式来自 reactivity 包:

const state = reactive({ count: 0 })

底层是 Proxy 拦截 getter/setter:

  • getter:收集依赖
  • setter:触发依赖更新

依赖管理核心是 effect / track / trigger

8、 setupRenderEffect 与首次渲染

创建渲染器副作用,并调度组件挂载和异步更新:

function setupRenderEffect(instance, container, anchor) {
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      const subTree = (instance.subTree = instance.render.call(proxy))
      patch(null, subTree, container, instance, anchor)
      instance.isMounted = true
    } else {
      // 更新更新逻辑
    }
  }, {
    scheduler: queueJob
  })
}

这里:

  • 创建一个 响应式 effect
  • 第一次执行 render 得到 subTree
  • patch 子树到 DOM

effect + scheduler 实现异步更新。

9、vnode-> 真实 DOM(DOM mount)

当 patch 到真正的 DOM 时,走的是 element 分支:

function processElement(...) {
  if (!n1) {
    mountElement(vnode, container)
  } else {
    patchElement(n1, n2)
  }
}

mountElement

function mountElement(vnode, container) {
  const el = (vnode.el = hostCreateElement(vnode.type))
  // props
  for (key in props) {
    hostPatchProp(el, key, null, props[key])
  }
  // children
  if (typeof children === 'string') {
    hostSetElementText(el, children)
  } else {
    children.forEach(c => patch(null, c, el))
  }
  hostInsert(el, container)
}

10、更新 & Diff 算法

当响应式状态改变:

state.count++

触发 setter → trigger

  • 将 effect 放入更新队列
  • 异步执行 scheduler
  • 调用 instance.update 再次 patch

更新阶段:

patchElement(n1, n2)

核心逻辑:

  1. props diff
  2. children diff
  3. unkeyed/keyed diff 算法(最小化移动)

具体见 patchChildrenpatchKeyedChildren

整体核心对象关系架构

App
 └─ vnode(root)
     └─ ComponentInstance
         ├─ props / slots
         ├─ setupState
         └─ render() -> subTree
             └─ vnode tree
                 └─ DOM nodes

响应式依赖结构:

reactive state
 ├─ effects[]
 └─ track -> effect
              └─ scheduler -> patch

webpack异步加载原理梳理解构

作者 sophie旭
2026年1月2日 22:03

背景:

这几天重新梳理了一下 webpack异步加载的原理,并对实现细节进行了一番拆解,再次让我感叹:真是万变不离其宗,基础知识真的是构建上层建筑的坚实底座,在此也分享给大家,希望大家可以领略到webpack实现异步加载之美

基座一:webpack模块化方案

你可能也吐槽过 webpack产物为啥这么丑?

原始源代码

// src/index.js (入口文件)
let title = require('./title.js')
console.log(title);
// src/title.js
module.exports = 'bu';

构建后产物结构概览

/******/ (() => { // webpack bootstrap 启动函数
/******/ var __webpack_modules__ = ([
/* 0 */ /* title.js 模块 */
((module) => {
module.exports = 'bu';
}),
/* 1 */ /* index.js 模块 */
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
let title = __webpack_require__(0); // 加载模块0
console.log(title);
})
/******/ ]);
/******/ /* 模块缓存 */
/******/ var __webpack_module_cache__ = {};
/******/ /* Webpack 自实现的 require 函数 */
/******/ function __webpack_require__(moduleId) {
/******/ /* 检查缓存 */
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ /* 创建新模块并加入缓存 */
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ exports: {}
/******/ };
/******/ /* 执行模块函数 */
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ /* 返回模块的 exports 对象 */
/******/ return module.exports;
/******/ }
/******/ /* 启动入口模块 */
/******/ return __webpack_require__(1); // 加载入口模块(index.js)
/******/ })()
;

✅ 核心原因:规范转换成本(模块系统适配)

浏览器原生不支持模块规范
浏览器无法直接执行 CommonJS/AMP/UMD 模块语法(如 require() / module.exports / define())。Webpack 必须将这些规范统一转换为浏览器可执行的函数包装形式

  • 整个打包产物被包裹在一个外层 IIFE 中(立即执行)

    • 意义:创建独立作用域
     // 未包裹:变量暴露全局
    var utils = {...} // 可能覆盖其他脚本的同名变量
    
    // IIFE 包裹后:
    (function() {
      var utils = {...} // 安全隔离
    })();
    
  • 每个模块被转换为标准函数(非立即执行),作为参数传递给运行时:function(module, exports...)

    • 意义模块环境隔离 -每次调用模块函数时都会创建新的:
    const module = { exports: {} };
    modules[moduleId].call(module.exports, ...);
    
  • 模块路径被替换为 数字 ID

    • 提升性能 & 减少体积
      • ✅ 大幅缩短引用路径
        './src/utils/string-format.js' → __webpack_require__(17)
      • ✅ 避免路径解析开销:浏览器无需处理文件路径逻辑
  • 原生 require/module 被替换为 Webpack 自实现的  __webpack_require__  函数

    • 规范统一:将 ESM/CommonJS/AMD 转为浏览器可执行格式
  • 生成复杂的 运行时(runtime)代码 处理模块加载/缓存

    • 避免重复执行(如多次 require 同一模块)
  • 模块字典(所有代码被打包成键值对(键:上面提到的数字ID,值:上面提到的每个模块被转换的标准函数))

    • ✅ 快速索引:通过数字 ID 实现 O(1) 复杂度的模块查找

基座二:jsonp

传统的 JSONP 流程:


sequenceDiagram

participant Client as 客户端

participant Server as 服务器

Client->>Client: 创建回调函数

Note right of Client: window.myCallback = <br/>function(data){...}

Client->>Server: 动态创建<script src="api?callback=myCallback">

Server->>Server: 准备数据

Server->>Client: 返回 myCallback({...数据...})

Client->>Client: 执行回调函数

Webpack 异步加载机制与 JSONP 的对比

  1. 脚本加载机制相同

// 两者都使用相同的基础加载方式

const script = document.createElement('script');

script.src = 'resource.js';

document.head.appendChild(script);

  1. 全局回调设计

// JSONP

window.jsonpCallback = function(data) { ... }

// Webpack

window.webpackJsonp = [];

window.webpackJsonp.push = function(data) { ... }

  1. 执行流程相似

graph TD

A[创建 script 标签] --> B[设置 src]

B --> C[添加到 DOM]

C --> D[服务器返回 JS]

D --> E[执行 JS 代码]

E --> F[触发全局回调]

那么不一样的地方在哪呢?

对了就是全局回调,异步加载的回调函数设计是非常精妙的,咱们往下看

精妙的全局回调设计-webpackJsonpCallback

好了,现在我们拿到异步组件脚本了,我们应该做什么呢?

对了,就是要接入 我们上面聊的 webpack模块化方案,只有这样webpack 才能正常加载并缓存我们的异步模块

那么问题来了,怎样才能和上面说的 webpack模块化方案 接上轨呢?

答案就在 这个jsonp的全局回调上

显然我们要在回调里 把当前模块加入到 模块字典里,及 webpack_modules 里,

我们管这一步,叫做 全局模块注册,注意这里仅仅是注册模块,并没有执行模块

按需执行 如何做到

好了,注册模块我们实现了,那么按需执行呢?

按需执行,即我们希望由我们来控制 什么时候执行该模块,那么如何实现呢?

精妙设计一:加载模块和执行模块分离--解耦请求与响应

graph LR
A[发起请求] --> B[存储控制器]
C[响应到达] --> D[取出控制器]
D --> E[触发回调]


// 步骤1: 初始化Promise
const promise = new Promise((resolve, reject) => {
  // 这个回调会立即执行!
  installedChunks[chunkId] = [[resolve, reject]];
});

// 步骤2: 文件加载完成后
function chunkLoaded() {
  const callbacks = installedChunks[chunkId];
  for (const [res] of callbacks) {
    res(); // 手动触发所有resolve
  }
  installedChunks[chunkId] = 0; // 标记为已加载
}

// 步骤3: 触发.then()
promise.then(() => {
  // 这里才会执行!
  __webpack_require__(moduleId);
});

也就是说在异步加载模块流程会封装成一个promise, 在加载模块前,我们会提前将该promise的resolve回调存储起来,存到 installedChunks;

当加载模块请求响应回来之后,我们从 installedChunks里拿到 resolve 回调执行 我们可以在resolve 里面控制何时 执行模块


sequenceDiagram

participant T as .then()调用

participant R as Runtime(运行时)

participant S as 网络请求

T->>R: __webpack_require__.e("hello_chunk")

activate R

R->>R: 创建Promise<br>installedChunks["hello_chunk"] = [[resolve, reject]]

R->>S: 发起chunk加载请求

deactivate R

S-->>R: 返回chunk内容

activate R

R->>R: 执行webpackJsonpCallback

R->>R: 找到对应resolve函数

R->>Promise: 执行resolve()

deactivate R

Promise-->>T: 触发.then()回调

到这里我们可以给出__webpack_require__.e 和 webpackJsonpCallback的代码了:

// 异步加载函数 (修正版)
  __webpack_require__.e = (chunkId) => {
    return new Promise((resolve, reject) => {
      // 检查模块是否已加载
      if (installedChunks[chunkId] === 0) {
        resolve();
        return;
      }
      
      // 检查是否已在加载中
      if (installedChunks[chunkId]) {
        installedChunks[chunkId].push([resolve, reject]);
        return;
      }
      
      // 初始化加载状态
      installedChunks[chunkId] = [[resolve, reject]];
      
      // 创建脚本标签加载 chunk
      const script = document.createElement('script');
      script.src = `${chunkId}.js`;
      script.onerror = () => {
        reject(new Error(`Failed to load chunk ${chunkId}`));
        // 清理加载状态
        if (installedChunks[chunkId]) {
          installedChunks[chunkId] = undefined;
        }
      };
      
      // 关键步骤:将脚本添加到文档头部 (之前遗漏的部分)
      document.head.appendChild(script);
    });
  };
  
function webpackJsonpCallback(data) {
    const [chunkIds, moreModules] = data;
    
    // 注册新模块
    for (const moduleId in moreModules) {
      __webpack_modules__[moduleId] = moreModules[moduleId];
    }
    
    // 处理每个 chunk 的 Promise
    for (const chunkId of chunkIds) {
      const chunkState = installedChunks[chunkId];
      if (!chunkState) continue;
      
      // 执行所有 resolve 回调
      for (const [resolve] of chunkState) {
        resolve();
      }
      
      // 标记 chunk 为已加载
      installedChunks[chunkId] = 0;
    }
  }

是不是很棒,加载由我们控制,执行也由我们控制,

精妙设计二:加载时序问题解决方案

当我们加载异步组件后,我们发现它并不是用 webpackJsonpCallback 包裹起来,而是用 webpackJsonp.push

// hello_chunk.js
webpackJsonp.push([["hello_chunk"], {"./src/hello.js": function(){...}}])

在webpack 实现里我们会看到这句话:

webpackJsonp.push = webpackJsonpCallback;

为什么 webpack 要多此一举 ?为什么不直接用 webpackJsonpCallback 包裹起来呢?

核心原因:解决异步加载的顺序问题

加载顺序不确定性

  • 异步 chunk 可能在主 runtime 加载完成之前就加载完毕(关键:此时webpackJsonpCallback 可能还没有定义,因为 webpackJsonpCallback 是写在主 runtime里的

  • 也可能在主 runtime 加载完成之后才加载

关键设计:劫持push方法

此时就算找不到 webpackJsonpCallback,但是 webpackJsonp.push 是原生方法,肯定可以找到,这样即使 webpackJsonpCallback 未定义,也不会让 已加载的分块 丢失

也就是允许任何时间加载的分块,webpack都能处理,完全解耦加载顺序

// 关键设计:劫持push方法
webpackJsonp.push = webpackJsonpCallback;

// 处理初始化前已加载的分块
for (var i = 0; i < webpackJsonp.length; i++) {
    webpackJsonpCallback(webpackJsonp[i]);
}

webpackJsonp.length = 0; // 清空初始队列

// 将处理后的队列暴露到全局
window.webpackJsonp = webpackJsonp;

场景1:分块在主runtime之前加载完成


sequenceDiagram

participant Browser

participant Chunk as 异步分块

participant Runtime as Webpack Runtime

Browser->>Chunk: 1. 加载分块文件

Chunk->>Browser: 2. 执行分块代码

Note right of Chunk: webpackJsonp.push([[1], modules])

Browser->>Runtime: 3. 加载主runtime

Runtime->>Runtime: 4. 初始化时重写push方法

Runtime->>Runtime: 5. 处理已缓存的推送

Runtime->>Runtime: 6. 执行回调逻辑

场景2:分块在主runtime之后加载完成


sequenceDiagram

participant Browser

participant Runtime as Webpack Runtime

participant Chunk as 异步分块

Browser->>Runtime: 1. 加载主runtime

Runtime->>Runtime: 2. 初始化时重写push方法

Runtime->>Runtime: 3. 处理初始队列(空)

Browser->>Chunk: 4. 加载分块文件

Chunk->>Browser: 5. 执行分块代码

Note right of Chunk: webpackJsonp.push([[1], modules])

Browser->>Runtime: 6. 推送触发回调

总流程概览

1. 编译阶段(Build Time)

  • 语法识别:Webpack 解析 AST 时识别 import('./LazyComponent') 语法

  • 模块分离

  // 原始代码
  import('./LazyComponent');
  
  // Webpack 处理:
  1.LazyComponent 及其依赖抽离为独立 chunk(如 `src_LazyComponent_js.js`2. 生成 chunk ID(如 "chunk-lazy"3. 生成模块 ID(如 42)
- **代码转换**:
  ```javascript
  // 转换后代码
  __webpack_require__.e("chunk-lazy")
    .then(__webpack_require__.bind(__webpack_require__, 42))
  • 生成 chunk 文件
// src_LazyComponent_js.js 内容
  (window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    ["chunk-lazy"],
    {
      "./src/LazyComponent.js": (module, __webpack_exports__, __webpack_require__) => {
        // 模块实际代码
      }
    }
  ]);

2. 运行时阶段(Runtime)

  • 触发加载
// 执行编译后的代码
const promise = __webpack_require__.e("chunk-lazy");
  • 加载器执行
// __webpack_require__.e 核心逻辑
  __webpack_require__.e = (chunkId) => {
    // 检查缓存
    if (installedChunks[chunkId] === 0) return Promise.resolve();
    
    // 创建加载 Promise
    const promise = new Promise((resolve, reject) => {
      // 创建 script 标签
      const script = document.createElement('script');
      script.src = `${publicPath}${chunkId}.chunk.js`;
      
      // 错误处理
      script.onerror = () => reject(new Error(`Loading failed ${chunkId}`));
      
      // 注册全局回调
      const originalPush = webpackJsonp.push.bind(webpackJsonp);
      webpackJsonp.push = (item) => {
        webpackJsonpCallback(item);
        originalPush(item);
      };

      // 处理运行时初始化前已加载的数据
      for (let i = 0; i < webpackJsonp.length; i++) {
        webpackJsonpCallback(webpackJsonp[i]);
      }

      // 清空队列但不移除引用
      webpackJsonp.splice(0, webpackJsonp.length);


      
      // 触发加载
      document.head.appendChild(script);
    });
    
    // 标记为加载中
    installedChunks[chunkId] = [promise, resolve, reject];
    return promise;
  };

3. 模块注册阶段(Chunk Execution)

  • Chunk 脚本执行
// 浏览器加载并执行 src_LazyComponent_js.js
  window.webpackJsonp.push([
    ["chunk-lazy"], 
    { 
      "./src/LazyComponent.js": function(module, exports) {
        // 组件实现
        exports.default = function LazyComp() { ... }
      }
    }
  ]);
  • 回调触发
function webpackJsonpCallback(data) {
    const [chunkIds, modules] = data;
    
    // 1. 注册模块到全局存储
    for (const moduleId in modules) {
      __webpack_modules__[moduleId] = modules[moduleId];
    }
    
    // 2. 标记chunk为已加载
    chunkIds.forEach(chunkId => {
      installedChunks[chunkId] = 0; // 0 = 已加载
    });
    
    // 3. 执行所有等待中的resolve
    const resolves = [];
    chunkIds.forEach(chunkId => {
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][1]); // 获取resolve函数
        installedChunks[chunkId] = 0; // 清除等待状态
      }
    });
    // resolve可以控制何时执行模块
    // 比如resolve可以是() => {

    // 执行模块加载
    //const module = __webpack_require__(42);
    //return module;
    //}
    resolves.forEach(resolve => resolve());
  }

至此,webpack异步加载原理 我们已经大致清楚了,是不是还挺有意思的,同时也能给我们一些启发,尤其精妙设计那里,希望有机会用到,这样我们也是站在巨人的肩膀上了~

面试官 : “ 说一下 Vue 的 8 个生命周期钩子都做了什么 ? ”

作者 千寻girling
2026年1月2日 20:53

一、Vue3 8 个核心生命周期钩子(按执行顺序)

阶段 选项式 API 名称 组合式 API 名称 执行时机 核心作用 & 实战场景
初始化阶段 beforeCreate 无(setup 替代) 实例创建前,数据 / 方法未初始化,this 不可用 Vue2 中用于初始化非响应式数据;Vue3 中逻辑移到 setup 最顶部(无响应式操作)
初始化阶段 created 无(setup 替代) 实例创建完成,数据 / 方法已初始化,DOM 未生成 1. 发起异步请求(接口请求);2. 初始化非 DOM 相关逻辑(如数据格式化)
挂载阶段 beforeMount onBeforeMount 挂载开始前,模板编译完成,DOM 未挂载到页面($el 未生成) 1. 预操作 DOM 结构(如计算 DOM 尺寸,需结合 nextTick);2. 初始化第三方库(挂载前准备)
挂载阶段 mounted onMounted DOM 挂载完成($el 已挂载),页面可见 1. 操作真实 DOM(如初始化 ECharts / 地图);2. 发起依赖 DOM 的异步请求;3. 监听 DOM 事件
更新阶段 beforeUpdate onBeforeUpdate 数据更新后,DOM 重新渲染前 1. 获取更新前的 DOM 状态(如旧输入框值);2. 取消不必要的监听 / 定时器(避免重复执行)
更新阶段 onUpdated onUpdated DOM 重新渲染完成,页面已更新 1. 获取更新后的 DOM 状态;2. 重新计算 DOM 相关数据(如滚动位置重置)
卸载阶段 beforeUnmount onBeforeUnmount 组件卸载前(实例仍可用,DOM 未销毁) 1. 清理副作用(清除定时器 / 事件监听);2. 销毁第三方库实例(如 ECharts 销毁)
卸载阶段 unmounted onUnmounted 组件卸载完成,DOM 销毁,实例失效 1. 最终清理(如取消接口请求);2. 释放内存(清空大型数组 / 对象引用)

二、关键细节(Vue3 核心变化)

1. setup 替代 beforeCreate/created

Vue3 中 setup 执行时机 = beforeCreate + created,这两个钩子在组合式 API 中被废弃,所有初始化逻辑直接写在 setup 中:

以下是 Vue3 生命周期钩子的完整可运行代码示例,包含选项式 API 和组合式 API(<script setup> 推荐写法)  两种风格,附带详细注释和实战场景(如接口请求、DOM 操作、定时器清理等),可直接复制到 Vue3 项目中运行。

三、组合式 API 示例(<script setup> 推荐)

Vue3 生命周期演示

<template>
  <div class="life-cycle-demo">
    <h3>Vue3 生命周期演示(组合式 API)</h3>
    <!-- 绑定响应式数据,触发更新阶段 -->
    <p>当前计数:{{ count }}</p>
    <button @click="count++">点击更新计数(触发更新钩子)</button>
    <!-- 挂载 ECharts 示例 DOM -->
    <div id="chart" style="width: 300px; height: 200px; margin: 20px 0;"></div>
  </div>
</template>

<script setup>
import { 
  ref, 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated, 
  onBeforeUnmount, 
  unmounted 
} from 'vue';
// 模拟 ECharts(实际需安装:npm install echarts)
import * as echarts from 'echarts';

// 🫱🫱🫱 1. setup 本身替代 beforeCreate + created(初始化阶段)
console.log('===== setup 执行(等价于 beforeCreate + created)=====');
// 响应式数据初始化
const count = ref(0);
// 模拟接口请求(created 阶段核心场景)
const fetchData = async () => {
  try {
    console.log('发起异步接口请求(created 阶段)');
    // 模拟接口延迟
    const res = await new Promise(resolve => {
      setTimeout(() => resolve({ data: '模拟接口返回数据' }), 1000);
    });
    console.log('接口请求完成:', res.data);
  } catch (err) {
    console.error('接口请求失败:', err);
  }
};
// 执行接口请求(等价于 created 中调用)
fetchData();

// 🫱🫱🫱 2. 挂载阶段:beforeMount(DOM 未挂载)
onBeforeMount(() => {
  console.log('===== onBeforeMount 执行 =====');
  console.log('DOM 未挂载,#chart 元素:', document.getElementById('chart')); // null
  // 若需提前操作 DOM,需结合 nextTick
});

// 🫱🫱🫱 3. 挂载阶段:mounted(DOM 已挂载,核心操作 DOM 场景)
let myChart = null;
onMounted(() => {
  console.log('===== onMounted 执行 =====');
  console.log('DOM 已挂载,#chart 元素:', document.getElementById('chart')); // 存在
  // 初始化 ECharts(依赖 DOM 的第三方库)
  myChart = echarts.init(document.getElementById('chart'));
  myChart.setOption({
    title: { text: '生命周期演示图表' },
    xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
    yAxis: { type: 'value' },
    series: [{ data: [120, 200, 150], type: 'bar' }]
  });
  // 模拟定时器(需在卸载阶段清理)
  const timer = setInterval(() => {
    console.log('定时器运行中(count:', count.value, ')');
  }, 1000);
  // 把定时器存到全局,方便卸载时清理
  window.lifeCycleTimer = timer;
});

// 🫱🫱🫱 4. 更新阶段:beforeUpdate(数据更新,DOM 未重新渲染)
onBeforeUpdate(() => {
  console.log('===== onBeforeUpdate 执行 =====');
  console.log('数据已更新(count:', count.value, '),DOM 未刷新');
  // 可获取更新前的 DOM 状态(如旧的图表数据)
});

// 🫱🫱🫱 5. 更新阶段:updated(DOM 已重新渲染)
onUpdated(() => {
  console.log('===== onUpdated 执行 =====');
  console.log('DOM 已更新(count:', count.value, ')');
  // 若数据更新后需重新渲染图表
  if (myChart) {
    myChart.setOption({
      series: [{ data: [120 + count.value * 10, 200 + count.value * 10, 150 + count.value * 10] }]
    });
  }
});

// 🫱🫱🫱 6. 卸载阶段:beforeUnmount(组件即将卸载,清理副作用)
onBeforeUnmount(() => {
  console.log('===== onBeforeUnmount 执行 =====');
  // 清理定时器
  clearInterval(window.lifeCycleTimer);
  // 销毁 ECharts 实例
  if (myChart) {
    myChart.dispose();
    myChart = null;
  }
  console.log('副作用已清理(定时器、ECharts 已销毁)');
});

// 🫱🫱🫱 7. 卸载阶段:unmounted(组件已完全卸载)
unmounted(() => {
  console.log('===== unmounted 执行 =====');
  console.log('组件已卸载,DOM 已销毁,实例失效');
});
</script>

四、选项式 API 示例(兼容 Vue2 写法)

<template>
  <div class="life-cycle-demo">
    <h3>Vue3 生命周期演示(选项式 API)</h3>
    <p>当前计数:{{ count }}</p>
    <button @click="count++">点击更新计数</button>
    <div id="chart" style="width: 300px; height: 200px; margin: 20px 0;"></div>
  </div>
</template>

<script>
import * as echarts from 'echarts';

export default {
  // 响应式数据
  data() {
    return {
      count: 0,
      myChart: null,
      timer: null
    };
  },

  // 🫱🫱🫱 1. 初始化阶段:beforeCreate(实例刚创建,数据/方法未初始化)
  beforeCreate() {
    console.log('===== beforeCreate 执行 =====');
    console.log('数据未初始化:', this.count); // undefined
    console.log('方法未初始化:', this.fetchData); // undefined
  },

  // 🫱🫱🫱 2. 初始化阶段:created(数据/方法已初始化,DOM 未生成)
  created() {
    console.log('===== created 执行 =====');
    console.log('数据已初始化:', this.count); // 0
    // 发起异步请求
    this.fetchData();
  },

  // 🫱🫱🫱 3. 挂载阶段:beforeMount(模板编译完成,DOM 未挂载)
  beforeMount() {
    console.log('===== beforeMount 执行 =====');
    console.log('DOM 未挂载,#chart 元素:', document.getElementById('chart')); // null
  },

  // 🫱🫱🫱 4. 挂载阶段:mounted(DOM 已挂载,可操作真实 DOM)
  mounted() {
    console.log('===== mounted 执行 =====');
    console.log('DOM 已挂载,#chart 元素:', document.getElementById('chart')); // 存在
    // 初始化 ECharts
    this.myChart = echarts.init(document.getElementById('chart'));
    this.myChart.setOption({
      title: { text: '选项式 API 图表' },
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
      yAxis: { type: 'value' },
      series: [{ data: [120, 200, 150], type: 'bar' }]
    });
    // 启动定时器
    this.timer = setInterval(() => {
      console.log('定时器运行中(count:', this.count, ')');
    }, 1000);
  },

  // 🫱🫱🫱 5. 更新阶段:beforeUpdate(数据更新,DOM 未重新渲染)
  beforeUpdate() {
    console.log('===== beforeUpdate 执行 =====');
    console.log('数据已更新(count:', this.count, '),DOM 未刷新');
  },

  // 🫱🫱🫱 6. 更新阶段:updated(DOM 已重新渲染)
  updated() {
    console.log('===== updated 执行 =====');
    console.log('DOM 已更新(count:', this.count, ')');
    // 重新渲染图表
    if (this.myChart) {
      this.myChart.setOption({
        series: [{ data: [120 + this.count * 10, 200 + this.count * 10, 150 + this.count * 10] }]
      });
    }
  },

  // 🫱🫱🫱 7. 卸载阶段:beforeUnmount(组件即将卸载,清理副作用)
  beforeUnmount() {
    console.log('===== beforeUnmount 执行 =====');
    // 清理定时器
    clearInterval(this.timer);
    // 销毁 ECharts
    if (this.myChart) {
      this.myChart.dispose();
      this.myChart = null;
    }
  },

  // 🫱🫱🫱 8. 卸载阶段:unmounted(组件已完全卸载)
  unmounted() {
    console.log('===== unmounted 执行 =====');
    console.log('组件已卸载,资源已清理');
  },

  // 自定义方法:模拟接口请求
  methods: {
    async fetchData() {
      try {
        console.log('发起接口请求(created 阶段)');
        const res = await new Promise(resolve => {
          setTimeout(() => resolve({ data: '选项式 API 接口数据' }), 1000);
        });
        console.log('接口请求完成:', res.data);
      } catch (err) {
        console.error('接口请求失败:', err);
      }
    }
  }
};
</script>

五、测试方式(验证生命周期执行)

  1. 挂载阶段:页面加载后,控制台会依次打印 setup/beforeCreatecreatedbeforeMountmounted,同时 ECharts 图表渲染完成,定时器开始运行。

  2. 更新阶段:点击 “点击更新计数” 按钮,触发 beforeUpdateupdated,图表数据随计数更新。

  3. 卸载阶段

    • 若使用路由,跳转到其他页面(组件卸载);
    • 或手动移除组件(如用 v-if 控制),控制台会打印 beforeUnmountunmounted,定时器停止,ECharts 实例销毁。

六、核心注意点

  1. 组合式 API 无 beforeCreate/created:所有初始化逻辑直接写在 <script setup> 顶部,等价于这两个钩子。
  2. 副作用必须清理:定时器、事件监听、第三方库实例(如 ECharts)需在 onBeforeUnmount/beforeUnmount 中清理,避免内存泄漏。
  3. DOM 操作仅在 mounted/updated 中安全beforeMount 中操作 DOM 需结合 nextTick
  4. updated 中避免无限循环:不要在 updated 中直接修改响应式数据(除非加条件判断)。

通过这个示例,你可以直观看到每个生命周期钩子的执行时机和实际用途,覆盖日常开发中 90% 以上的生命周期场景

❌
❌