乐闻世界logo
搜索文章和话题

面试题手册

WebGL 中的立方体贴图(Cubemap)是什么?有哪些应用场景?

WebGL 立方体贴图概述立方体贴图(Cubemap)是一种特殊的纹理,由 6 张独立的 2D 纹理组成,分别对应立方体的 6 个面。它使用 3D 方向向量进行采样,常用于实现天空盒、环境反射和折射等效果。立方体贴图的结构立方体贴图由 6 个面组成: ┌─────────┐ │ +Y │ (Top)┌──────┼─────────┼──────┬─────────┐│ -X │ +Z │ +X │ -Z ││ Left │ Front │ Right│ Back │└──────┴─────────┴──────┴─────────┘ │ -Y │ (Bottom) └─────────┘创建立方体贴图基本创建流程// 创建立方体贴图const cubemap = gl.createTexture();gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap);// 6 个面的图片 URLconst faceImages = [ 'px.jpg', // +X (Right) 'nx.jpg', // -X (Left) 'py.jpg', // +Y (Top) 'ny.jpg', // -Y (Bottom) 'pz.jpg', // +Z (Front) 'nz.jpg' // -Z (Back)];// 加载 6 个面的图片const targets = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z];let loadedCount = 0;faceImages.forEach((src, index) => { const image = new Image(); image.onload = () => { gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.texImage2D( targets[index], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image ); loadedCount++; if (loadedCount === 6) { // 所有面加载完成,生成 mipmap gl.generateMipmap(gl.TEXTURE_CUBE_MAP); } }; image.src = src;});// 设置纹理参数gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);程序化生成立方体贴图// 创建纯色立方体贴图function createSolidColorCubemap(gl, color) { const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); const targets = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ]; const size = 1; const data = new Uint8Array([ color[0] * 255, color[1] * 255, color[2] * 255, (color[3] || 1) * 255 ]); targets.forEach(target => { gl.texImage2D( target, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data ); }); return cubemap;}在着色器中使用立方体贴图顶点着色器attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}片段着色器precision mediump float;varying vec3 v_worldPos;uniform vec3 u_cameraPos;uniform samplerCube u_cubemap;void main() { // 计算从相机指向片段的方向向量 vec3 direction = normalize(v_worldPos - u_cameraPos); // 使用方向向量采样立方体贴图 vec4 color = textureCube(u_cubemap, direction); gl_FragColor = color;}主要应用场景1. 天空盒(Skybox)天空盒用于渲染远处的环境背景,给人一种无限大的感觉。// 天空盒顶点着色器attribute vec3 a_position;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_texCoord;void main() { // 移除平移分量,只保留旋转 mat4 viewRotation = mat4(mat3(u_viewMatrix)); vec4 pos = u_projectionMatrix * viewRotation * vec4(a_position, 1.0); // 确保天空盒在远裁剪面 gl_Position = pos.xyww; // 使用位置作为纹理坐标 v_texCoord = a_position;}// 天空盒片段着色器precision mediump float;varying vec3 v_texCoord;uniform samplerCube u_skybox;void main() { gl_FragColor = textureCube(u_skybox, v_texCoord);}// 渲染天空盒function drawSkybox(gl, skyboxProgram, cubemap) { // 禁用深度写入 gl.depthMask(false); gl.useProgram(skyboxProgram); // 绑定立方体贴图 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.uniform1i(gl.getUniformLocation(skyboxProgram, 'u_skybox'), 0); // 绘制立方体 drawCube(gl); // 恢复深度写入 gl.depthMask(true);}2. 环境反射(Environment Reflection)使用立方体贴图模拟光滑表面的反射效果。// 反射顶点着色器attribute vec3 a_position;attribute vec3 a_normal;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;varying vec3 v_normal;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; v_normal = mat3(u_modelMatrix) * a_normal; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_reflectivity;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); // 采样环境贴图 vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 可以结合基础颜色和反射 vec3 baseColor = vec3(0.8, 0.8, 0.8); vec3 finalColor = mix(baseColor, reflectionColor.rgb, u_reflectivity); gl_FragColor = vec4(finalColor, 1.0);}3. 环境折射(Environment Refraction)模拟光线穿过透明物体的折射效果。// 折射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_refractiveIndex; // 折射率,如 1.52 表示玻璃void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算折射向量 // refract(I, N, eta) 其中 eta = 入射介质折射率 / 折射介质折射率 vec3 refractDir = refract(-viewDir, normal, 1.0 / u_refractiveIndex); // 采样环境贴图 vec4 refractionColor = textureCube(u_environmentMap, refractDir); gl_FragColor = refractionColor;}4. 菲涅尔反射(Fresnel Reflection)模拟真实世界中反射强度随视角变化的效应。// 菲涅尔反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_fresnelPower;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 计算菲涅尔因子 // 视线与法线夹角越大,反射越强 float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), u_fresnelPower); // 基础颜色 vec3 baseColor = vec3(0.1, 0.2, 0.3); // 混合基础颜色和反射 vec3 finalColor = mix(baseColor, reflectionColor.rgb, fresnel); gl_FragColor = vec4(finalColor, 1.0);}5. 动态环境贴图(Dynamic Environment Mapping)实时生成立方体贴图用于反射。// 动态生成环境贴图function generateEnvironmentMap(gl, scene, centerPos, size = 256) { // 创建立方体贴图 const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); // 创建帧缓冲区 const framebuffer = gl.createFramebuffer(); // 6 个面的相机方向 const directions = [ { target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, eye: [1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, eye: [-1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, eye: [0, 1, 0], up: [0, 0, 1] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, eye: [0, -1, 0], up: [0, 0, -1] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, eye: [0, 0, 1], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, eye: [0, 0, -1], up: [0, -1, 0] } ]; // 设置投影矩阵(90度视野) const projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, Math.PI / 2, 1, 0.1, 1000); // 渲染 6 个面 directions.forEach(dir => { // 设置视图矩阵 const viewMatrix = mat4.create(); mat4.lookAt(viewMatrix, centerPos, [centerPos[0] + dir.eye[0], centerPos[1] + dir.eye[1], centerPos[2] + dir.eye[2]], dir.up ); // 渲染场景到帧缓冲区 renderSceneToCubemapFace(gl, scene, framebuffer, cubemap, dir.target, projectionMatrix, viewMatrix, size); }); return cubemap;}立方体贴图采样原理立方体贴图使用 3D 方向向量 (x, y, z) 进行采样,选择哪个面取决于哪个分量的绝对值最大:如果 |x| 最大: x > 0: 使用 +X 面,坐标 ( -z/x, -y/x ) x < 0: 使用 -X 面,坐标 ( z/x, -y/x )如果 |y| 最大: y > 0: 使用 +Y 面,坐标 ( x/y, z/y ) y < 0: 使用 -Y 面,坐标 ( x/y, -z/y )如果 |z| 最大: z > 0: 使用 +Z 面,坐标 ( x/z, -y/z ) z < 0: 使用 -Z 面,坐标 ( -x/z, -y/z )性能优化建议预过滤环境贴图:为不同粗糙度预计算模糊的立方体贴图使用 mipmap 级别存储不同粗糙度的反射立方体贴图压缩:使用压缩纹理格式减少内存占用DXT、ETC、PVRTC 等格式动态环境贴图优化:降低分辨率(如 128x128)减少更新频率(如每 10 帧更新一次)只更新可见物体的反射LOD 系统:远处物体使用低分辨率立方体贴图近处物体使用高分辨率立方体贴图常见问题接缝问题立方体贴图面与面之间可能出现接缝。解决方案:// 使用 CLAMP_TO_EDGE 环绕模式gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);图片方向问题不同面的图片可能需要翻转。解决方案:使用图像编辑软件预先调整或在着色器中翻转纹理坐标// 翻转 Y 坐标vec2 flippedCoord = vec2(texCoord.x, 1.0 - texCoord.y);
阅读 0·3月7日 12:04

WebGL 中的缓冲区(Buffer)是什么?如何使用 VBO 和 VAO?

WebGL 缓冲区概述缓冲区(Buffer)是 GPU 内存中的一块区域,用于存储顶点数据、索引数据等图形渲染所需的信息。使用缓冲区可以高效地将数据从 CPU 传输到 GPU。缓冲区类型1. 顶点缓冲区对象(VBO - Vertex Buffer Object)存储顶点属性数据,如位置、颜色、法线、纹理坐标等。2. 索引缓冲区对象(IBO/EBO - Index/Element Buffer Object)存储顶点索引,用于定义图元的连接方式,减少重复顶点数据。3. 顶点数组对象(VAO - Vertex Array Object)存储顶点属性配置的状态,简化绘制调用前的设置。VBO(顶点缓冲区对象)详解创建和使用 VBO// 1. 创建缓冲区const vbo = gl.createBuffer();// 2. 绑定缓冲区(指定当前操作的缓冲区类型)gl.bindBuffer(gl.ARRAY_BUFFER, vbo);// 3. 上传数据到 GPUconst vertices = new Float32Array([ // 位置(x, y, z) 颜色(r, g, b) -0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // 顶点1 0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // 顶点2 0.0, 0.5, 0.0, 0.0, 0.0, 1.0 // 顶点3]);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 4. 配置顶点属性指针gl.vertexAttribPointer( 0, // 属性位置(对应着色器中的 layout(location=0)) 3, // 每个属性的分量数(这里是3:x, y, z) gl.FLOAT, // 数据类型 false, // 是否归一化 6 * 4, // 步长(每个顶点的字节数:6个float * 4字节) 0 // 偏移量);gl.enableVertexAttribArray(0); // 启用位置属性// 配置颜色属性gl.vertexAttribPointer( 1, // 属性位置 3, // 分量数(r, g, b) gl.FLOAT, false, 6 * 4, // 步长 3 * 4 // 偏移量(跳过位置数据));gl.enableVertexAttribArray(1); // 启用颜色属性缓冲区使用模式// gl.STATIC_DRAW:数据不经常改变,多次绘制 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);// gl.DYNAMIC_DRAW:数据经常改变,多次绘制gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);// gl.STREAM_DRAW:数据每次绘制都改变gl.bufferData(gl.ARRAY_BUFFER, data, gl.STREAM_DRAW);IBO/EBO(索引缓冲区对象)详解创建和使用 IBO// 顶点数据(4个顶点定义一个四边形)const vertices = new Float32Array([ -0.5, 0.5, 0.0, // 左上 (0) -0.5, -0.5, 0.0, // 左下 (1) 0.5, -0.5, 0.0, // 右下 (2) 0.5, 0.5, 0.0 // 右上 (3)]);// 索引数据(2个三角形 = 6个索引)const indices = new Uint16Array([ 0, 1, 2, // 第一个三角形 0, 2, 3 // 第二个三角形]);// 创建并绑定 VBOgl.bindBuffer(gl.ARRAY_BUFFER, vbo);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 创建并绑定 IBOconst ibo = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);// 绘制(使用索引绘制)gl.drawElements( gl.TRIANGLES, // 绘制模式 6, // 索引数量 gl.UNSIGNED_SHORT, // 索引数据类型 0 // 偏移量);VAO(顶点数组对象)详解VAO 存储了所有顶点属性的配置状态,包括:启用的顶点属性每个属性的配置(大小、类型、步长、偏移)绑定的 VBOWebGL 2.0 / WebGL 1.0 + OESvertexarray_object 扩展// 创建 VAOconst vao = gl.createVertexArray(); // WebGL 2.0// const vao = ext.createVertexArrayOES(); // 使用扩展// 绑定 VAO(后续配置将存储在 VAO 中)gl.bindVertexArray(vao);// 配置顶点属性(这些配置会被 VAO 记录)gl.bindBuffer(gl.ARRAY_BUFFER, positionVBO);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, colorVBO);gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);// 解绑 VAOgl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);完整示例代码class Mesh { constructor(gl) { this.gl = gl; this.vertexCount = 0; this.indexCount = 0; // WebGL 2.0 创建 VAO this.vao = gl.createVertexArray(); this.vbo = gl.createBuffer(); this.ibo = gl.createBuffer(); } setVertices(vertices, attributes) { const gl = this.gl; gl.bindVertexArray(this.vao); // 上传顶点数据 gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 配置属性 let offset = 0; const stride = attributes.reduce((sum, attr) => sum + attr.size, 0) * 4; attributes.forEach((attr, index) => { gl.vertexAttribPointer( index, attr.size, gl.FLOAT, false, stride, offset ); gl.enableVertexAttribArray(index); offset += attr.size * 4; }); this.vertexCount = vertices.length / (stride / 4); } setIndices(indices) { const gl = this.gl; gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); this.indexCount = indices.length; } draw() { const gl = this.gl; gl.bindVertexArray(this.vao); if (this.indexCount > 0) { gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); } }}// 使用示例const mesh = new Mesh(gl);mesh.setVertices( new Float32Array([/* 顶点数据 */]), [ { size: 3 }, // 位置 { size: 3 }, // 颜色 { size: 2 } // 纹理坐标 ]);mesh.setIndices(new Uint16Array([/* 索引数据 */]));mesh.draw();性能优化建议减少状态切换:使用 VAO 减少绘制前的配置开销合并缓冲区:将多个小网格合并到一个大缓冲区使用索引绘制:减少顶点数据重复选择合适的绘制模式:STATIC_DRAW:静态几何体DYNAMIC_DRAW:频繁更新的数据STREAM_DRAW:每帧都更新的数据批量绘制:使用实例化渲染(Instanced Rendering)绘制多个相同对象
阅读 0·3月7日 12:04

WebGL 中的雾效(Fog)是如何实现的?

WebGL 雾效概述雾效(Fog)是一种模拟大气散射效果的渲染技术,使远处的物体逐渐融入到背景颜色中。雾效不仅能增加场景的真实感,还能隐藏远处的裁剪边界,优化性能。雾效的基本原理雾效的核心思想是根据物体与相机的距离,在物体颜色和雾颜色之间进行线性或指数插值:最终颜色 = 物体颜色 × (1 - 雾因子) + 雾颜色 × 雾因子雾的类型1. 线性雾(Linear Fog)雾的浓度随距离线性增加。公式:fogFactor = (end - distance) / (end - start)start:雾开始距离end:雾完全覆盖距离distance:片段到相机的距离// 顶点着色器varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; // 计算视图空间深度(正值) v_fogDepth = -viewPos.z; gl_Position = u_projectionMatrix * viewPos;}// 片段着色器uniform vec3 u_fogColor;uniform float u_fogStart;uniform float u_fogEnd;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算线性雾因子 float fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); fogFactor = clamp(fogFactor, 0.0, 1.0); // 混合物体颜色和雾颜色 vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}2. 指数雾(Exponential Fog)雾的浓度随距离指数增加,效果更自然。公式:fogFactor = exp(-density × distance)uniform vec3 u_fogColor;uniform float u_fogDensity;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数雾因子 float fogFactor = exp(-u_fogDensity * v_fogDepth); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}3. 指数平方雾(Exp2 Fog)雾的浓度随距离平方指数增加,效果更加柔和。公式:fogFactor = exp(-(density × distance)²)void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数平方雾因子 float fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}完整的雾效实现顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec2 v_texCoord;varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; v_texCoord = a_texCoord; v_fogDepth = length(viewPos.xyz); // 使用实际距离而非仅 Z 值 gl_Position = u_projectionMatrix * viewPos;}片段着色器precision mediump float;uniform sampler2D u_texture;uniform vec3 u_fogColor;uniform float u_fogDensity;uniform int u_fogType; // 0: 线性, 1: 指数, 2: 指数平方uniform float u_fogStart;uniform float u_fogEnd;varying vec2 v_texCoord;varying float v_fogDepth;float calculateFogFactor() { float fogFactor = 0.0; if (u_fogType == 0) { // 线性雾 fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); } else if (u_fogType == 1) { // 指数雾 fogFactor = exp(-u_fogDensity * v_fogDepth); } else if (u_fogType == 2) { // 指数平方雾 fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); } return clamp(fogFactor, 0.0, 1.0);}void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); float fogFactor = calculateFogFactor(); // mix(fogColor, objectColor, fogFactor) vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}JavaScript 控制class Fog { constructor(gl, program) { this.gl = gl; this.program = program; // 获取 uniform 位置 this.fogColorLoc = gl.getUniformLocation(program, 'u_fogColor'); this.fogDensityLoc = gl.getUniformLocation(program, 'u_fogDensity'); this.fogTypeLoc = gl.getUniformLocation(program, 'u_fogType'); this.fogStartLoc = gl.getUniformLocation(program, 'u_fogStart'); this.fogEndLoc = gl.getUniformLocation(program, 'u_fogEnd'); // 默认设置 this.color = [0.7, 0.8, 0.9]; // 淡蓝色雾 this.density = 0.02; this.type = 2; // 指数平方雾 this.start = 10.0; this.end = 50.0; } apply() { const gl = this.gl; gl.uniform3fv(this.fogColorLoc, this.color); gl.uniform1f(this.fogDensityLoc, this.density); gl.uniform1i(this.fogTypeLoc, this.type); gl.uniform1f(this.fogStartLoc, this.start); gl.uniform1f(this.fogEndLoc, this.end); } // 设置雾的类型 setLinear(start, end) { this.type = 0; this.start = start; this.end = end; } setExponential(density) { this.type = 1; this.density = density; } setExponentialSquared(density) { this.type = 2; this.density = density; }}// 使用示例const fog = new Fog(gl, program);fog.setExponentialSquared(0.015);fog.apply();高度雾(Height Fog)模拟根据高度变化的雾效,如山谷中的雾。// 顶点着色器varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 片段着色器uniform float u_fogHeight;uniform float u_fogHeightFalloff;varying vec3 v_worldPos;float calculateHeightFog() { // 基于高度的雾密度 float heightFactor = (u_fogHeight - v_worldPos.y) * u_fogHeightFalloff; heightFactor = clamp(heightFactor, 0.0, 1.0); // 结合距离雾 float distanceFactor = exp(-u_fogDensity * length(v_worldPos - u_cameraPos)); return distanceFactor * (1.0 - heightFactor);}雾效与光照结合void main() { // 计算光照 vec3 ambient = ...; vec3 diffuse = ...; vec3 specular = ...; vec3 lighting = ambient + diffuse + specular; vec4 texColor = texture2D(u_texture, v_texCoord); vec3 objectColor = lighting * texColor.rgb; // 应用雾效 float fogFactor = calculateFogFactor(); vec3 finalColor = mix(u_fogColor, objectColor, fogFactor); gl_FragColor = vec4(finalColor, texColor.a);}雾效的性能优化1. 顶点级雾效计算当顶点数较少时,可以在顶点着色器中计算雾因子:// 顶点着色器varying float v_fogFactor;void main() { // ... 计算位置 float fogDepth = length(viewPos.xyz); v_fogFactor = exp(-u_fogDensity * fogDepth); v_fogFactor = clamp(v_fogFactor, 0.0, 1.0);}// 片段着色器varying float v_fogFactor;void main() { vec3 finalColor = mix(u_fogColor, objectColor, v_fogFactor);}2. 使用深度纹理在后处理阶段应用雾效:// 1. 渲染场景到颜色纹理和深度纹理renderSceneToTextures();// 2. 后处理阶段应用雾效applyFogPostProcess();// 后处理雾效着色器uniform sampler2D u_colorTexture;uniform sampler2D u_depthTexture;void main() { vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; float depth = texture2D(u_depthTexture, v_texCoord).r; // 将深度转换为世界空间距离 float linearDepth = linearizeDepth(depth); // 计算雾因子 float fogFactor = exp(-u_fogDensity * linearDepth); vec3 finalColor = mix(u_fogColor, color, fogFactor); gl_FragColor = vec4(finalColor, 1.0);}不同类型的雾效对比| 雾类型 | 公式 | 特点 ||--------|------|------|| 线性雾 | (end - d) / (end - start) | 简单,雾边界明显 || 指数雾 | exp(-density × d) | 自然,适合大多数场景 || 指数平方雾 | exp(-(density × d)²) | 更柔和,雾边界不明显 || 高度雾 | 结合 Y 轴 | 适合山谷、水面等场景 |实际应用建议选择合适的雾类型:大多数场景使用指数或指数平方雾需要精确控制雾边界时使用线性雾调整雾的颜色:通常与天空盒或背景色一致可以随时间变化模拟昼夜效果性能考虑:移动端建议使用顶点级雾效复杂场景可以使用后处理雾效与其他效果结合:雾效可以与体积光、大气散射结合注意雾效对透明物体的影响
阅读 0·3月7日 12:04

区块链中使用了哪些密码学技术?详解哈希函数、数字签名和 Merkle Tree

密码学(Cryptography) 是区块链安全性的基石,通过数学算法保证数据的机密性、完整性、真实性和不可抵赖性。1. 哈希函数(Hash Function)定义与特性哈希函数是将任意长度输入转换为固定长度输出的单向函数。关键特性:| 特性 | 说明 | 区块链应用 || -------- | ---------------- | ----- || 确定性 | 相同输入总是产生相同输出 | 数据验证 || 单向性 | 无法从哈希值反推原始数据 | 保护隐私 || 抗碰撞性 | 难以找到两个不同输入产生相同输出 | 防篡改 || 雪崩效应 | 输入微小变化导致输出巨大差异 | 检测篡改 |常用哈希算法SHA-256(比特币使用):输入:"Hello Blockchain"输出:a8f5f167f44f4964e6c998dee827110c9a0c5e1e7a5b6e5f8d9c2b1a4e7f3d6(固定 256 位 / 64 个十六进制字符)Keccak-256(以太坊使用):SHA-3 标准的变种抗长度扩展攻击用于生成以太坊地址区块链中的应用区块结构中的哈希应用:┌─────────────────────────────────────┐│ 区块头 (Block Header) │├─────────────────────────────────────┤│ 前一区块哈希 (Previous Hash) │ ← 链接区块,形成链条│ Merkle Root │ ← 交易数据完整性验证│ 时间戳 (Timestamp) ││ Nonce │└─────────────────────────────────────┘区块哈希 = SHA256(SHA256(区块头))2. 数字签名(Digital Signature)非对称加密基础密钥对生成:┌──────────────┐ ┌──────────────┐│ 私钥 (SK) │ ←─────→ │ 公钥 (PK) ││ (保密) │ 数学关联 │ (公开) │└──────────────┘ └──────────────┘ ↓ ↓ 用于签名 用于验证常用算法:ECDSA(Elliptic Curve Digital Signature Algorithm):比特币、以太坊使用EdDSA(Edwards-curve Digital Signature Algorithm):更快更安全签名与验证流程交易签名过程:1. 交易数据准备 {"from": "0xabc...", "to": "0xdef...", "value": 100}2. 计算交易哈希 txHash = Keccak256(交易数据)3. 使用私钥签名 signature = ECDSA_Sign(私钥, txHash)4. 广播交易 交易数据 + 签名 + 公钥验证过程:ECDSA_Verify(公钥, txHash, signature) → true/false以太坊地址生成1. 生成私钥(256 位随机数) 私钥 = 随机数2. 通过椭圆曲线计算公钥 公钥 = 私钥 × G(曲线基点)3. 计算公钥哈希 hash = Keccak256(公钥)4. 取后 20 字节作为地址 地址 = "0x" + hash[12:32]3. Merkle Tree(默克尔树)结构与原理Merkle Tree 结构: Root Hash / \ Hash 1-2 Hash 3-4 / \ / \ Hash 1 Hash 2 Hash 3 Hash 4 | | | | Tx 1 Tx 2 Tx 3 Tx 4(每个 Hash = SHA256(子节点哈希拼接))"默克尔证明(Merkle Proof)轻节点验证交易:验证 Tx 3 是否存在于区块中:需要数据:- Tx 3 的哈希- Hash 4(兄弟节点)- Hash 1-2(叔节点)- Root Hash(区块头中)验证步骤:1. Hash 3 = SHA256(Tx 3)2. Hash 3-4 = SHA256(Hash 3 + Hash 4)3. 计算 Root = SHA256(Hash 1-2 + Hash 3-4)4. 比较计算的 Root 与区块头中的 Root区块链中的应用交易完整性验证:快速验证大量交易轻节点同步:SPV(简单支付验证)只需下载区块头状态证明:以太坊的状态树使用 Merkle Patricia Tree4. 零知识证明(Zero-Knowledge Proof)基本概念定义:证明者向验证者证明某个陈述为真,而不透露任何有用信息。zk-SNARKs(以太坊 ZK Rollup 使用):Succinct:证明体积小Non-interactive:无需交互ARgument of Knowledge:知识论证应用场景隐私交易示例:┌─────────────────────────────────────┐│ 证明:我知道一个秘密 x,使得 ││ hash(x) = 0xabc... ││ ││ 不透露:x 的具体值 │└─────────────────────────────────────┘面试要点理解哈希函数的单向性和抗碰撞性掌握公钥私钥的关系和数字签名原理能够解释 Merkle Tree 如何高效验证数据了解零知识证明的基本概念和应用知道不同区块链使用的具体算法(SHA-256 vs Keccak-256)
阅读 0·3月7日 12:04

如何在 Zustand 中优化状态更新和性能?

Zustand 中的性能优化方法:选择性订阅:// 不推荐:订阅整个 store,会导致组件在任何状态变化时都重渲染const { count, user } = useStore();// 推荐:只订阅需要的状态部分const count = useStore((state) => state.count);const user = useStore((state) => state.user);使用 shallow 比较(对于复杂对象):import { create } from 'zustand';import { shallow } from 'zustand/shallow';// 订阅多个状态并使用 shallow 比较const { count, user } = useStore( (state) => ({ count: state.count, user: state.user }), shallow // 只有当 count 或 user 真正变化时才重渲染);状态拆分:// 按功能拆分多个 store// userStore.jsconst useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user })}));// counterStore.jsconst useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 }))}));使用 get 访问当前状态(避免闭包陷阱):const useStore = create((set, get) => ({ count: 0, // 推荐:使用 get 获取最新状态 increment: () => set((state) => ({ count: state.count + 1 })), // 也可以使用 get incrementAsync: async () => { await someAsyncOperation(); set({ count: get().count + 1 }); }}));批量更新:// Zustand 会自动批量处理多个 set 调用const updateMultiple = () => { set({ count: 1 }); set({ user: { name: 'John' } }); // 只会触发一次重渲染};避免在组件渲染时创建新函数:// 不推荐:每次渲染都创建新函数const incrementBy = (value) => useStore.getState().incrementBy(value);// 推荐:在 store 中定义方法// 在 store 中:incrementBy: (value) => set((state) => ({ count: state.count + value }))// 在组件中:const incrementBy = useStore((state) => state.incrementBy);关键点:选择性订阅是 Zustand 性能优化的核心使用 shallow 比较可以优化复杂对象的订阅状态拆分可以减少不必要的重渲染合理使用 get 可以避免闭包陷阱Zustand 会自动处理批量更新
阅读 0·3月7日 12:01

如何在 Zustand 中创建自定义中间件?

在 Zustand 中创建自定义中间件非常灵活,可以用来实现各种功能,如日志记录、状态验证、性能监控等。基本自定义中间件结构:const customMiddleware = (config) => (set, get, api) => { // 在原始 store 之前执行的逻辑 const originalSet = set; // 包装 set 函数 const wrappedSet = (partial, replace) => { // 在状态更新前执行逻辑 console.log('State will update:', partial); // 调用原始 set const result = originalSet(partial, replace); // 在状态更新后执行逻辑 console.log('State updated:', get()); return result; }; // 创建 store const store = config(wrappedSet, get, api); // 返回增强后的 store return store;};示例 1:日志中间件const loggerMiddleware = (config) => (set, get, api) => { const originalSet = set; const wrappedSet = (partial, replace) => { const previousState = get(); const result = originalSet(partial, replace); const nextState = get(); console.log('Previous state:', previousState); console.log('Action:', partial); console.log('Next state:', nextState); return result; }; return config(wrappedSet, get, api);};// 使用const useStore = create( loggerMiddleware((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })) })));示例 2:状态验证中间件const validationMiddleware = (schema) => (config) => (set, get, api) => { const originalSet = set; const wrappedSet = (partial, replace) => { // 验证状态更新 const newState = typeof partial === 'function' ? partial(get()) : partial; const validation = schema.safeParse({ ...get(), ...newState }); if (!validation.success) { console.error('State validation failed:', validation.error); throw new Error('Invalid state update'); } return originalSet(partial, replace); }; return config(wrappedSet, get, api);};// 使用import { z } from 'zod';const storeSchema = z.object({ count: z.number().min(0), user: z.object({ id: z.string(), name: z.string().min(1) }).nullable()});const useStore = create( validationMiddleware(storeSchema)((set) => ({ count: 0, user: null, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: Math.max(0, state.count - 1) })) })));示例 3:性能监控中间件const performanceMiddleware = (config) => (set, get, api) => { const originalSet = set; const renderCounts = {}; const wrappedSet = (partial, replace) => { const startTime = performance.now(); const result = originalSet(partial, replace); const endTime = performance.now(); const duration = endTime - startTime; if (duration > 10) { console.warn(`Slow state update: ${duration.toFixed(2)}ms`, partial); } return result; }; const store = config(wrappedSet, get, api); // 跟踪组件渲染次数 const originalSubscribe = api.subscribe; api.subscribe = (listener, selector) => { const wrappedListener = (state, previousState) => { const key = selector ? selector.toString() : 'full-store'; renderCounts[key] = (renderCounts[key] || 0) + 1; if (renderCounts[key] % 10 === 0) { console.log(`Render count for ${key}:`, renderCounts[key]); } listener(state, previousState); }; return originalSubscribe(wrappedListener, selector); }; return store;};// 使用const useStore = create( performanceMiddleware((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })) })));示例 4:撤销/重做中间件const undoRedoMiddleware = (config) => (set, get, api) => { let history = []; let future = []; const MAX_HISTORY = 50; const originalSet = set; const wrappedSet = (partial, replace) => { const previousState = get(); const result = originalSet(partial, replace); const nextState = get(); // 保存到历史记录 history.push(previousState); if (history.length > MAX_HISTORY) { history.shift(); } // 清空未来记录 future = []; return result; }; const store = config(wrappedSet, get, api); // 添加撤销功能 store.undo = () => { if (history.length === 0) return; const previousState = history.pop(); future.push(get()); originalSet(previousState, true); }; // 添加重做功能 store.redo = () => { if (future.length === 0) return; const nextState = future.pop(); history.push(get()); originalSet(nextState, true); }; // 清空历史 store.clearHistory = () => { history = []; future = []; }; return store;};// 使用const useStore = create( undoRedoMiddleware((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })) })));// 在组件中使用function Counter() { const { count, increment, decrement } = useStore(); const undo = useStore((state) => state.undo); const redo = useStore((state) => state.redo); return ( <div> <h1>Count: {count}</h1> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={undo}>Undo</button> <button onClick={redo}>Redo</button> </div> );}关键点:自定义中间件是一个高阶函数,接收 config 并返回新的配置函数可以包装 set、get 和 api 来增强功能中间件的执行顺序很重要,通常外层中间件先执行可以在中间件中添加额外的功能,如日志、验证、性能监控等中间件可以返回增强后的 store,添加新的方法或属性
阅读 0·3月7日 12:01

如何在 Zustand 中处理异步操作?

在 Zustand 中处理异步操作的方法:基本异步操作:import { create } from 'zustand';const useStore = create((set, get) => ({ // 状态 user: null, loading: false, error: null, // 异步操作 fetchUser: async (userId) => { try { set({ loading: true, error: null }); const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); set({ user: userData, loading: false }); } catch (err) { set({ error: err.message, loading: false }); } }, // 另一种方式:使用 get 获取最新状态 updateUserProfile: async (updates) => { try { set({ loading: true, error: null }); const currentUser = get().user; const response = await fetch(`/api/users/${currentUser.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); const updatedUser = await response.json(); set({ user: updatedUser, loading: false }); } catch (err) { set({ error: err.message, loading: false }); } }}));使用 Promise 链:const useStore = create((set) => ({ data: null, status: 'idle', // idle, loading, success, error fetchData: () => { set({ status: 'loading' }); return fetch('/api/data') .then((response) => response.json()) .then((data) => { set({ data, status: 'success' }); return data; }) .catch((error) => { set({ error: error.message, status: 'error' }); throw error; }); }}));结合 React Query 或 SWR:// 可以在 Zustand 中存储查询结果,同时使用 React Query 处理缓存和失效import { create } from 'zustand';import { useQuery } from 'react-query';const useStore = create((set) => ({ user: null, setUser: (user) => set({ user })}));// 在组件中function UserProfile({ userId }) { const { data, isLoading, error } = useQuery( ['user', userId], () => fetch(`/api/users/${userId}`).then(res => res.json()) ); // 当查询成功时,更新 Zustand store React.useEffect(() => { if (data) { useStore.getState().setUser(data); } }, [data]); // 使用 Zustand 中的用户数据 const user = useStore(state => state.user); return ( <div> {isLoading && <p>Loading...</p>} {error && <p>Error: {error.message}</p>} {user && <p>User: {user.name}</p>} </div> );}关键点:Zustand 支持直接在 store 方法中使用 async/await可以在异步操作中管理 loading 和 error 状态使用 get() 获取最新状态,避免闭包陷阱可以返回 Promise 以便在组件中处理异步操作的结果可以与 React Query 或 SWR 等库结合使用,获得更好的缓存和失效策略
阅读 0·3月7日 11:48

Zustand 中级面试题:如何对 Zustand store 进行单元测试?

对 Zustand store 进行单元测试相对简单,因为 store 是纯 JavaScript 对象。基本测试示例:// store.jsimport { create } from 'zustand';const useStore = create((set) => ({ count: 0, user: null, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), setUser: (user) => set({ user }), reset: () => set({ count: 0, user: null })}));export default useStore;// store.test.jsimport { renderHook, act } from '@testing-library/react';import useStore from './store';describe('Zustand Store', () => { beforeEach(() => { // 每个测试前重置 store useStore.setState({ count: 0, user: null }); }); test('should initialize with default values', () => { const { result } = renderHook(() => useStore()); expect(result.current.count).toBe(0); expect(result.current.user).toBeNull(); }); test('should increment count', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); test('should decrement count', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(-1); }); test('should set user', () => { const { result } = renderHook(() => useStore()); const mockUser = { id: 1, name: 'John' }; act(() => { result.current.setUser(mockUser); }); expect(result.current.user).toEqual(mockUser); }); test('should reset store', () => { const { result } = renderHook(() => useStore()); const mockUser = { id: 1, name: 'John' }; act(() => { result.current.setUser(mockUser); result.current.increment(); }); expect(result.current.count).toBe(1); expect(result.current.user).toEqual(mockUser); act(() => { result.current.reset(); }); expect(result.current.count).toBe(0); expect(result.current.user).toBeNull(); });});测试异步操作:// store.jsconst useStore = create((set) => ({ user: null, loading: false, error: null, fetchUser: async (userId) => { try { set({ loading: true, error: null }); const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); set({ user: userData, loading: false }); } catch (err) { set({ error: err.message, loading: false }); } }}));// store.test.jsimport { renderHook, act, waitFor } from '@testing-library/react';import useStore from './store';describe('Zustand Store - Async Operations', () => { beforeEach(() => { useStore.setState({ user: null, loading: false, error: null }); }); test('should fetch user successfully', async () => { global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'John' }) }) ); const { result } = renderHook(() => useStore()); await act(async () => { await result.current.fetchUser(1); }); expect(result.current.user).toEqual({ id: 1, name: 'John' }); expect(result.current.loading).toBe(false); expect(result.current.error).toBeNull(); }); test('should handle fetch error', async () => { global.fetch = jest.fn(() => Promise.reject(new Error('Network error'))); const { result } = renderHook(() => useStore()); await act(async () => { await result.current.fetchUser(1); }); expect(result.current.error).toBe('Network error'); expect(result.current.loading).toBe(false); });});测试选择性订阅:import { renderHook } from '@testing-library/react';import useStore from './store';describe('Zustand Store - Selective Subscription', () => { beforeEach(() => { useStore.setState({ count: 0, user: null }); }); test('should only re-render when subscribed state changes', () => { const renderCount = jest.fn(); const { result } = renderHook(() => { renderCount(); return useStore((state) => state.count); }); expect(renderCount).toHaveBeenCalledTimes(1); act(() => { useStore.getState().setUser({ id: 1, name: 'John' }); }); // 不应该重新渲染,因为 user 变化,但订阅的是 count expect(renderCount).toHaveBeenCalledTimes(1); act(() => { useStore.getState().increment(); }); // 应该重新渲染,因为 count 变化 expect(renderCount).toHaveBeenCalledTimes(2); });});关键点:使用 @testing-library/react 的 renderHook 和 act 进行测试在每个测试前重置 store 状态对于异步操作,使用 waitFor 等待状态更新测试选择性订阅时,验证重渲染次数使用 useStore.getState() 直接访问和操作 store
阅读 0·3月7日 11:44

如何在 React Native 中使用 Zustand 管理状态?

React Native 中使用 Zustand 的方法:安装 Zustand npm install zustand # 或 yarn add zustand创建 Store import { create } from 'zustand'; const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }));在 React Native 组件中使用 import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { useCounterStore } from './store'; export default function CounterScreen() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); const decrement = useCounterStore((state) => state.decrement); return ( <View style={styles.container}> <Text style={styles.count}>{count}</Text> <View style={styles.buttons}> <TouchableOpacity style={styles.button} onPress={decrement}> <Text style={styles.buttonText}>-</Text> </TouchableOpacity> <TouchableOpacity style={styles.button} onPress={increment}> <Text style={styles.buttonText}>+</Text> </TouchableOpacity> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, count: { fontSize: 48, marginBottom: 20, }, buttons: { flexDirection: 'row', gap: 20, }, button: { backgroundColor: '#007AFF', padding: 15, borderRadius: 8, }, buttonText: { color: 'white', fontSize: 24, fontWeight: 'bold', }, });持久化状态使用 persist 中间件保存状态到 AsyncStorage示例: import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage'; const useUserStore = create( persist( (set) => ({ user: null, setUser: (user) => set({ user }), }), { name: 'user-storage', storage: { getItem: async (name) => { const item = await AsyncStorage.getItem(name); return item ? JSON.parse(item) : null; }, setItem: async (name, value) => { await AsyncStorage.setItem(name, JSON.stringify(value)); }, removeItem: async (name) => { await AsyncStorage.removeItem(name); }, }, } ) );React Native 特定优化避免在选择器中使用复杂计算合理使用 useCallback 缓存回调函数注意 AsyncStorage 的性能影响常见使用场景用户认证状态管理应用设置和偏好购物车状态导航状态管理
阅读 0·3月7日 11:44