3.26

3.26

es5里的两种导出方式有什么区别

在 ES5 中,模块化主要通过 CommonJS 规范实现,它有两种主要的导出方式:module.exportsexports。它们之间的区别主要体现在以下几个方面:

1. 导出类型:

  • module.exports
    • 可以导出任何类型的值,包括对象、函数、字符串、数字等。
    • 它导出的实际上是一个对象,可以理解为模块的“公共接口”。
  • exports
    • exports 实际上是 module.exports 的一个引用。
    • 它只能用于导出 module.exports 对象的属性。

2. 导出方式:

  • module.exports
    • 可以一次性导出整个模块,或者在模块的不同部分逐步添加导出内容。
    • 如果需要完全替换模块的导出内容,应该使用 module.exports
  • exports
    • 通常用于导出模块的多个成员(例如函数或变量)。
    • 不能直接将 exports 赋值为一个新的对象,否则会断开与 module.exports 的引用。

3. 使用场景:

  • module.exports
    • 适用于需要导出单个值或完全控制模块导出内容的情况。
    • 当需要导出一个类或函数时,通常使用 module.exports
  • exports
    • 适用于需要导出多个相关成员的情况,例如创建一个包含多个实用函数的模块。

示例说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 使用 module.exports 导出单个值
module.exports = function() {
  console.log('Hello from module.exports!');
};

// 使用 exports 导出多个成员
exports.greet = function(name) {
  console.log('Hello, ' + name + '!');
};

exports.farewell = function(name) {
  console.log('Goodbye, ' + name + '!');
};

// 错误示例:直接给 exports 赋值
exports = function() { // 这不会生效
  console.log('This will not be exported.');
};

总结:

  • module.exports 是模块导出的核心,可以导出任何类型的值。
  • exportsmodule.exports 的引用,用于导出多个成员,但不能直接赋值。

在实际开发中,根据需要选择合适的导出方式。如果只需要导出一个值或完全控制导出内容,使用 module.exports;如果需要导出多个相关成员,可以使用 exports

实现 Promise.all 并处理错误中断

好的,下面是如何自己实现 Promise.all 并处理错误中断的详细解释和代码示例:

Promise.all 的核心逻辑:

  1. 输入: 接收一个 Promise 数组(或任何可迭代的 Promise 对象)。
  2. 输出: 返回一个新的 Promise。
    • 如果输入数组中的所有 Promise 都成功 resolve,则返回的 Promise 也成功 resolve,并返回一个包含所有 resolve 值的数组。
    • 如果输入数组中的任何一个 Promise 被 reject,则返回的 Promise 立即被 reject,并返回第一个被 reject 的错误原因。
  3. 错误中断: 一旦遇到 reject,立即中断并返回 reject 的错误,不再等待其他 Promise。

代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    // 如果输入的 promises 数组为空,直接 resolve 一个空数组
    if (!promises || promises.length === 0) {
      resolve([]);
      return;
    }

    const results = []; // 用于存储所有 Promise 的 resolve 值
    let completedCount = 0; // 用于跟踪已完成的 Promise 数量

    // 遍历输入的 Promise 数组
    for (let i = 0; i < promises.length; i++) {
      // 确保当前元素是一个 Promise
      Promise.resolve(promises[i])
        .then((value) => {
          results[i] = value; // 存储 resolve 值
          completedCount++;

          // 如果所有 Promise 都已完成,则 resolve 结果数组
          if (completedCount === promises.length) {
            resolve(results);
          }
        })
        .catch((error) => {
          // 遇到 reject,立即 reject 并中断
          reject(error);
        });
    }
  });
}

代码解释:

  1. myPromiseAll(promises) 函数:
    • 接收一个 promises 数组作为参数。
    • 返回一个新的 Promise 对象。
  2. 空数组处理:
    • 如果 promises 数组为空,则立即使用空数组 [] 调用 resolve
  3. 初始化:
    • results 数组用于存储每个 Promise 的 resolve 值,并保持与输入数组相同的顺序。
    • completedCount 变量用于跟踪已完成的 Promise 数量。
  4. 遍历 promises 数组:
    • 使用 for 循环遍历 promises 数组。
    • Promise.resolve(promises[i]) 确保数组中的每个元素都是一个 Promise 对象。如果不是,则将其转换为 Promise 对象。
    • .then((value) => { ... }) 处理 Promise 成功 resolve 的情况:
      • 将 resolve 的值存储到 results 数组中相应的位置。
      • completedCount 加 1。
      • 如果 completedCount 等于 promises.length,则表示所有 Promise 都已完成,调用 resolve(results) 返回结果数组。
    • .catch((error) => { ... }) 处理 Promise 被 reject 的情况:
      • 立即调用 reject(error),将第一个 reject 的错误原因传递给返回的 Promise。
  5. 错误中断:
    • 一旦任何一个 Promise 被 reject,.catch 中的 reject(error) 会立即触发,导致返回的 Promise 被 reject,并停止等待其他 Promise。

使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});
const promise4 = new Promise((resolve, reject) => {
  setTimeout(reject, 50,'error');
})

myPromiseAll([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // 输出:[3, 42, 'foo']
  })
  .catch((error)=>{
      console.log(error);
  })

myPromiseAll([promise1, promise2, promise4, promise3])
  .then((values) => {
    console.log(values);
  })
  .catch((error)=>{
      console.log(error); // 输出:error
  })

这个实现与原生的 Promise.all 行为一致,并且正确处理了错误中断。

手写深拷贝函数(处理循环引用+特殊对象)

好的,下面是一个手写的深拷贝函数,它能够处理循环引用和特殊对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function deepClone(obj, map = new WeakMap()) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果不是对象或为 null,直接返回
  }

  // 处理特殊对象:Date、RegExp、Set、Map
  if (obj instanceof Date) {
    return new Date(obj);
  }
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  if (obj instanceof Set) {
    const newSet = new Set();
    obj.forEach((value) => newSet.add(deepClone(value, map)));
    return newSet;
  }
  if (obj instanceof Map) {
    const newMap = new Map();
    obj.forEach((value, key) => newMap.set(key, deepClone(value, map)));
    return newMap;
  }

  // 处理循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }

  // 创建新对象或数组
  const newObj = Array.isArray(obj) ? [] : {};
  map.set(obj, newObj); // 将原对象和新对象的映射存储到 map 中

  // 递归拷贝属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepClone(obj[key], map);
    }
  }

  // 处理 Symbol 类型的 key
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  for (const symbolKey of symbolKeys) {
    newObj[symbolKey] = deepClone(obj[symbolKey], map);
  }

  return newObj;
}

代码解释:

  1. deepClone(obj, map = new WeakMap()) 函数:
    • 接收要拷贝的对象 obj 和一个 map(用于存储已拷贝的对象,解决循环引用问题)作为参数。
    • map 使用 WeakMap,因为 WeakMap 的键是弱引用,不会阻止垃圾回收。
  2. 基本类型和 null 处理:
    • 如果 obj 不是对象或为 null,则直接返回 obj
  3. 特殊对象处理:
    • 如果 objDateRegExp 对象,则创建一个新的相同类型的对象并返回。
    • 如果 objSetMap 对象,则创建一个新的相同类型的对象,并递归拷贝每个元素。
  4. 循环引用处理:
    • 如果 map 中已经存在 obj,则表示 obj 已经被拷贝过,直接返回 map 中存储的拷贝对象。
  5. 创建新对象或数组:
    • 根据 obj 的类型,创建一个新的空对象或空数组 newObj
    • objnewObj 的映射存储到 map 中,以便后续处理循环引用。
  6. 递归拷贝属性:
    • 使用 for...in 循环遍历 obj 的属性。
    • 使用 obj.hasOwnProperty(key) 检查属性是否为 obj 自身的属性(而不是原型链上的属性)。
    • 递归调用 deepClone 拷贝每个属性值,并将拷贝结果赋值给 newObj 的相应属性。
  7. 处理 Symbol 类型的 key:
    • 使用 Object.getOwnPropertySymbols(obj) 获取 obj 的所有 Symbol 类型的 key。
    • 递归调用 deepClone 拷贝每个 Symbol 类型的属性值,并将拷贝结果赋值给 newObj 的相应属性。
  8. 返回拷贝对象:
    • 返回拷贝后的新对象 newObj

使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const obj = {
  name: 'John',
  age: 30,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'New York',
    zip: '10001',
  },
  birthday: new Date('1993-01-01'),
  regex: /abc/g,
  set: new Set([1, 2, 3]),
  map: new Map([['a', 1], ['b', 2]]),
};

obj.circular = obj; // 添加循环引用

const clonedObj = deepClone(obj);

console.log(clonedObj);

这个 deepClone 函数能够处理循环引用和特殊对象,并返回一个完全独立的拷贝对象。

事件循环:宏任务/微任务执行顺序(含Node.js差异)

事件循环是 JavaScript 实现异步非阻塞的关键机制。它通过管理宏任务(macrotask)和微任务(microtask)队列,确保代码有序执行。以下是对事件循环、宏任务、微任务及其在 Node.js 中的差异的详细解释。

1. 事件循环概念

事件循环不断地从宏任务队列中取出任务执行,并在执行完当前宏任务后,立即清空微任务队列。这个过程循环往复,直到所有任务执行完毕。

2. 宏任务(Macrotask)

  • 宏任务是事件循环中较大的任务单元,包括:
    • setTimeout
    • setInterval
    • setImmediate (Node.js)
    • requestAnimationFrame (浏览器)
    • I/O 操作(如文件读写、网络请求)
    • 用户交互事件(如鼠标点击、键盘输入)
    • script(首次代码执行)
  • 宏任务按照它们被添加到队列的顺序执行。
  • 每执行完一个宏任务,事件循环都会检查微任务队列。

3. 微任务(Microtask)

  • 微任务是比宏任务更小的任务单元,通常用于处理异步操作的后续步骤,包括:
    • Promise.thenPromise.catchPromise.finally
    • queueMicrotask
    • MutationObserver (浏览器)
    • process.nextTick (Node.js)
  • 微任务在当前宏任务执行完成后、下一个宏任务执行前执行。
  • 如果微任务队列中又有新的微任务加入,新的微任务也会在当前微任务队列清空前执行。

4. 执行顺序

  1. 执行全局 script 代码(这是一个宏任务)。
  2. 执行当前宏任务队列中的第一个宏任务。
  3. 检查微任务队列,如果有微任务,则依次执行所有微任务。
  4. 更新渲染(浏览器)。
  5. 检查宏任务队列,如果有宏任务,则取出下一个宏任务执行,重复步骤 2-4。

5. Node.js 中的差异

  • Node.js 的事件循环与浏览器略有不同,主要体现在以下几个方面:
    • process.nextTick:Node.js 中的 process.nextTick 优先级高于 Promise.then 等微任务。它会在微任务队列的开头执行,因此会优先于其他微任务执行。
    • setImmediate:Node.js 中的 setImmediate 是一个宏任务,它会在 I/O 操作的回调之后执行。
    • Node.js 的事件循环分为多个阶段,每个阶段都有自己的任务队列,包括:
      • 定时器(Timers):执行 setTimeoutsetInterval 的回调。
      • I/O 回调(I/O Callbacks):执行 I/O 操作的回调。
      • 闲置、准备(Idle, Prepare):仅系统内部使用。
      • 轮询(Poll):获取新的 I/O 事件。
      • 检测(Check):执行 setImmediate 的回调。
      • 关闭回调(Close Callbacks):执行关闭事件的回调。
  • Node.js 的事件循环阶段性执行,会影响宏任务与微任务执行的顺序。

总结

  • 宏任务和微任务是 JavaScript 异步编程的关键概念。
  • 浏览器和 Node.js 在事件循环的实现上略有差异,主要体现在微任务的优先级和宏任务的执行顺序上。
  • 理解事件循环有助于编写更高效、更可靠的异步代码。

实现防抖/节流函数(支持立即执行与取消)

好的,以下是如何实现支持立即执行和取消的防抖和节流函数的详细解释和代码示例:

1. 防抖(Debounce)函数

防抖函数确保在连续多次触发事件后,只在最后一次触发事件的指定延迟时间后执行回调函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function debounce(func, delay, immediate = false) {
  let timer;
  let result;

  const debounced = function(...args) {
    if (timer) clearTimeout(timer); // 清除之前的定时器

    if (immediate && !timer) {
      // 立即执行模式,且首次触发
      result = func.apply(this, args);
    }

    timer = setTimeout(() => {
      if (!immediate) {
        // 非立即执行模式,延迟执行
        result = func.apply(this, args);
      }
      timer = null; // 清除定时器
    }, delay);

    return result;
  };

  debounced.cancel = function() {
    // 取消执行
    clearTimeout(timer);
    timer = null;
  };

  return debounced;
}

代码解释:

  • debounce(func, delay, immediate = false) 函数:
    • func:要防抖的函数。
    • delay:延迟时间(毫秒)。
    • immediate:是否立即执行(默认为 false)。
  • timer:定时器 ID。
  • result:函数执行结果。
  • debounced 函数:
    • 如果 timer 存在,则清除之前的定时器。
    • 如果 immediatetruetimer 不存在,则立即执行 func
    • 设置一个新的定时器,延迟 delay 毫秒后执行 func
    • 返回 result
  • debounced.cancel 函数:
    • 清除定时器,取消执行。

2. 节流(Throttle)函数

节流函数确保在连续多次触发事件时,每隔指定的时间间隔只执行一次回调函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function throttle(func, delay, immediate = false) {
  let timer;
  let firstInvoke = true; // 首次调用标志

  const throttled = function(...args) {
    if (timer) return; // 如果定时器存在,则不执行

    if (immediate && firstInvoke) {
      // 立即执行模式,且首次调用
      func.apply(this, args);
      firstInvoke = false;
    } else {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null; // 清除定时器
      }, delay);
    }
  };

  throttled.cancel = function() {
    // 取消执行
    clearTimeout(timer);
    timer = null;
  };

  return throttled;
}

代码解释:

  • throttle(func, delay, immediate = false) 函数:
    • func:要节流的函数。
    • delay:延迟时间(毫秒)。
    • immediate:是否立即执行(默认为 false)。
  • timer:定时器 ID。
  • firstInvoke:首次调用标志。
  • throttled 函数:
    • 如果 timer 存在,则不执行。
    • 如果 immediatetruefirstInvoketrue,则立即执行 func
    • 设置一个新的定时器,延迟 delay 毫秒后执行 func
    • 清除定时器。
  • throttled.cancel 函数:
    • 清除定时器,取消执行。

使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function handleInput(event) {
  console.log('Input value:', event.target.value);
}

const debouncedInput = debounce(handleInput, 500); // 防抖
const throttledInput = throttle(handleInput, 500); // 节流

// 使用防抖
document.getElementById('input1').addEventListener('input', debouncedInput);

// 使用节流
document.getElementById('input2').addEventListener('input', throttledInput);

// 取消防抖/节流
document.getElementById('cancelDebounce').addEventListener('click', () => {
  debouncedInput.cancel();
});

document.getElementById('cancelThrottle').addEventListener('click', () => {
  throttledInput.cancel();
});

这些防抖和节流函数都支持立即执行和取消功能,可以根据需要选择合适的函数来优化事件处理。

原型链污染攻击原理与防御方案

原型链污染攻击是一种利用 JavaScript 原型链机制的漏洞,攻击者通过修改 Object.prototype 或其他内置对象的原型,从而影响所有继承自这些原型的对象。这种攻击可能导致拒绝服务、代码注入甚至远程代码执行等严重后果。

原理

  1. 原型链: 在 JavaScript 中,对象可以通过原型链继承属性和方法。每个对象都有一个内部链接指向它的原型对象,而原型对象本身也可能有一个原型,依此类推,形成一个原型链。当试图访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达原型链的末端。
  2. 原型污染: 攻击者利用某些操作(例如递归合并对象、设置 __proto__ 属性等)修改原型对象,从而影响所有继承自该原型的对象。
  3. 利用: 一旦原型被污染,攻击者就可以修改或添加全局属性,从而影响应用程序的逻辑。例如,攻击者可以修改 Object.prototype.toString 方法,导致所有对象的字符串表示都被篡改。或者,攻击者可以添加一个恶意属性,在应用程序的某些地方被意外使用,从而执行恶意代码。

示例

1
2
3
4
5
6
7
8
// 攻击者控制的输入
const maliciousInput = '{"__proto__": {"isAdmin": true}}';

// 将输入解析为 JSON
const parsedInput = JSON.parse(maliciousInput);

// 此时,所有对象都继承了 isAdmin 属性
console.log({}.isAdmin); // 输出:true

防御方案

  1. 避免使用不安全的 API:
    • 尽量避免使用 __proto__ 属性来设置原型。
    • 谨慎使用递归合并对象等可能导致原型污染的操作。
  2. 使用不可变对象:
    • 使用 Object.freezeObject.seal 冻结或封闭对象,防止对象被修改。
    • 使用 Object.create(null) 创建没有原型的对象,避免继承 Object.prototype
  3. 验证和清理用户输入:
    • 对用户输入进行严格的验证和清理,防止恶意数据进入应用程序。
    • 使用 JSON Schema 等工具验证 JSON 数据的结构和类型。
  4. 使用 Map 代替 Object:
    • 当对象的键来自于用户输入时,尽量使用Map来代替object,因为Map不会从Object.prototype继承属性。
  5. 使用防御性编程技术:
    • 在访问对象属性之前,使用 hasOwnProperty 方法检查属性是否为对象自身的属性。
    • 使用 Object.getPrototypeOf 获取对象的原型,并进行必要的检查。
  6. 使用安全工具和库:
    • 使用安全的 JSON 解析库,例如 secure-json-parse
    • 使用代码静态分析工具,例如 ESLint,检测潜在的原型污染漏洞。
  7. 及时更新依赖:
    • 保持 Node.js 和第三方库的更新,及时修复已知的安全漏洞。

总结

原型链污染攻击是一种隐蔽且危险的漏洞,需要开发人员高度重视。通过采用上述防御方案,可以有效地降低原型链污染攻击的风险,保护应用程序的安全。

BFC 原理与实战应用场景

BFC(Block Formatting Context,块级格式化上下文)是 CSS 布局模型中一个重要的概念。它是一个独立的渲染区域,内部元素的布局不会影响到外部元素,反之亦然。理解 BFC 的原理和应用场景对于解决 CSS 布局问题至关重要。

BFC 原理

BFC 可以看作是一个隔离的容器,它具有以下特性:

  • 内部元素布局与外部无关: BFC 内部元素的定位和浮动不会影响到 BFC 外部的元素。
  • 计算高度时包含浮动元素: BFC 会计算内部浮动元素的高度,防止父元素高度塌陷。
  • 阻止外边距折叠: BFC 内部相邻的块级元素的外边距不会发生折叠。
  • 阻止元素被浮动元素覆盖: BFC 可以阻止元素被浮动元素覆盖。

触发 BFC 的条件

以下 CSS 属性值会触发 BFC:

  • <html> 根元素
  • float 属性值不为 none
  • position 属性值为 absolutefixed
  • display 属性值为 inline-blocktable-celltable-captionflexinline-flex
  • overflow 属性值不为 visibleautoscrollhidden

BFC 的实战应用场景

  1. 清除浮动,防止父元素高度塌陷

    • 当父元素内部的子元素都浮动时,父元素会发生高度塌陷。通过触发父元素的 BFC,可以使其包含浮动元素的高度。
    1
    2
    3
    
    .clearfix {
      overflow: auto; /* 触发 BFC */
    }
    
  2. 解决外边距折叠问题

    • 相邻块级元素垂直方向上的外边距会发生折叠。通过触发其中一个元素的 BFC,可以阻止外边距折叠。
    1
    2
    3
    
    .container {
      overflow: hidden; /* 触发 BFC */
    }
    
  3. 创建自适应两栏布局

    • 通过触发右侧元素的 BFC,可以使其不被左侧浮动元素覆盖,实现自适应两栏布局。
    1
    2
    3
    4
    5
    6
    7
    
    .left {
      float: left;
      width: 200px;
    }
    .right {
      overflow: auto; /* 触发 BFC */
    }
    
  4. 阻止元素被浮动元素覆盖

    • 通过触发BFC可以阻止内容被浮动元素覆盖。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    .float{
        float:left;
        width: 100px;
        height: 100px;
        background-color: red;
    }
    .bfc{
        overflow:hidden;
        background-color: blue;
    }
    

总结

BFC 是 CSS 布局中一个强大的工具,它可以帮助我们解决许多常见的布局问题。通过理解 BFC 的原理和应用场景,我们可以编写出更健壮、更灵活的 CSS 代码。

CSS 图层合成原理与性能优化

CSS 图层合成是浏览器渲染引擎在绘制网页时,将多个视觉元素(如背景、边框、文本、图像等)组合成最终图像的过程。理解其原理和优化方法,有助于提升网页的渲染性能。

1. 图层合成原理

  • 渲染流水线:
    • 浏览器渲染引擎将网页内容转换为最终图像的过程,可以分为多个阶段,包括解析 HTML、构建 DOM 树、计算样式、布局、绘制和合成。
    • 在合成阶段,渲染引擎将多个绘制层组合成最终图像。
  • 图层概念:
    • 为了优化渲染性能,浏览器会将一些视觉元素提升为独立的图层。
    • 这些图层可以独立于其他图层进行绘制和合成,从而减少重绘和重排的范围。
  • 合成线程:
    • 合成工作通常由独立的合成线程完成,这使得合成操作不会阻塞主线程,从而提高渲染性能。
  • 合成过程:
    • 合成线程将多个图层组合成最终图像,包括处理图层的叠加顺序、透明度、变换等。
    • 如果图层的位置或属性发生变化,合成线程只需重新合成受影响的图层,而无需重新绘制整个页面。

2. 触发图层合成的条件

以下 CSS 属性或情况可能触发浏览器创建新的合成图层:

  • 3D 变换:
    • transform: translate3d(...)transform: matrix3d(...) 等。
  • 视频和 Canvas:
    • <video><canvas> 元素。
  • CSS 动画:
    • 使用 transformopacity 等属性的 CSS 动画。
  • 特定 CSS 属性:
    • will-changeopacityfilter 等。
  • z-index:
    • z-index 值较高的元素。
  • fixed 定位:
    • position: fixed

3. 性能优化

  • 减少图层数量:
    • 过多的图层会增加合成的开销,降低渲染性能。
    • 避免不必要的图层创建,例如,使用 transform: translate() 代替 transform: translate3d()
  • 使用 will-change
    • will-change 属性可以提前告知浏览器元素将发生变化,从而优化渲染性能。
    • 但过度使用 will-change 可能会导致内存占用过高,因此应谨慎使用。
  • 避免重绘和重排:
    • 重绘和重排会触发图层的重新绘制和合成,降低渲染性能。
    • 尽量使用 transformopacity 等属性进行动画,避免触发重排。
  • 优化动画性能:
    • 使用 CSS 动画代替 JavaScript 动画,可以利用合成线程的优势,提高动画性能。
    • 避免在动画中使用复杂的 CSS 属性,例如 box-shadowborder-radius 等。
  • 使用 Chrome DevTools:
    • Chrome DevTools 提供了图层面板,可以帮助开发者分析图层合成情况,找出性能瓶颈。
    • web.dev 网站提供了关于坚持使用仅限合成器的属性并管理图层数量的文章。

总结

理解 CSS 图层合成的原理和优化方法,有助于提升网页的渲染性能,改善用户体验。开发者应根据实际情况,合理使用 CSS 属性,减少不必要的图层创建,避免重绘和重排,从而优化网页的渲染性能。

响应式布局:rem/vw方案的选择与适配

响应式布局是前端开发中至关重要的一环,而 remvw 则是两种常用的响应式布局方案。它们各自有着独特的原理和适用场景,开发者需要根据项目需求进行选择和适配。

rem 方案

  • 原理:
    • rem(root em)单位相对于 HTML 根元素(<html>)的字体大小。
    • 通过动态修改根元素的字体大小,可以实现页面元素的等比缩放。
    • 通常配合 JavaScript 使用,根据屏幕宽度计算根元素的字体大小。
  • 优点:
    • 布局稳定,元素之间的比例关系保持不变。
    • 适配精度高,可以精确控制元素的大小。
    • 兼容性较好。
  • 缺点:
    • 需要 JavaScript 支持,增加了页面的复杂度。
    • 可能存在计算误差,导致元素大小不精确。
    • 在某些情况下,可能需要频繁修改根元素的字体大小,影响性能。
  • 适配:
    • 设置根元素的字体大小:
      • 使用 JavaScript 获取屏幕宽度,并计算根元素的字体大小。
      • 例如,可以将根元素的字体大小设置为屏幕宽度的 1/10。
    • 使用 rem 单位设置元素大小:
      • 将设计稿中的像素值转换为 rem 单位。
      • 例如,如果设计稿中一个元素的宽度为 100px,而根元素的字体大小为 10px,则该元素的宽度应设置为 10rem。

vw 方案

  • 原理:
    • vw(viewport width)单位相对于视口宽度的 1%。
    • 元素的大小会随着视口宽度的变化而线性缩放。
    • 无需 JavaScript 支持,纯 CSS 方案。
  • 优点:
    • 代码简洁,无需 JavaScript 支持。
    • 实现线性缩放,视觉效果流畅。
    • 适用于需要全屏适配的场景。
  • 缺点:
    • 元素之间的比例关系可能发生变化。
    • 在小屏幕上,元素可能过小,影响阅读体验。
    • 兼容性略逊于 rem
  • 适配:
    • 直接使用 vw 单位设置元素大小:
      • 将设计稿中的像素值转换为 vw 单位。
      • 例如,如果设计稿中一个元素的宽度为 375px,而视口宽度为 375px,则该元素的宽度应设置为 100vw。
    • 配合 calc() 函数使用:
      • 可以使用 calc() 函数将 vw 单位与其他单位(如 pxrem)结合使用。
      • 例如,width: calc(50vw + 10px);

方案选择

  • rem 方案:
    • 适用于需要精确控制元素大小和比例关系的场景。
    • 适用于需要兼容较旧浏览器的场景。
    • 适用于复杂的页面布局。
  • vw 方案:
    • 适用于需要实现简单线性缩放的场景。
    • 适用于需要快速开发或全屏适配的场景。
    • 适用于移动端页面。

总结

remvw 都是响应式布局的有效方案,开发者应根据项目需求和实际情况进行选择。在实际开发中,也可以将两种方案结合使用,以达到更好的适配效果。

CSS 动画性能优化(will-change/合成层管理)

CSS 动画是提升网页交互性和用户体验的重要手段。然而,不当的动画实现可能会导致性能问题,影响用户体验。本文将深入探讨 CSS 动画性能优化的关键技术:will-change 属性和合成层管理。

1. CSS 动画性能瓶颈

  • 重绘(Repaint): 当元素的样式发生变化,但不影响其布局时,浏览器只需重新绘制该元素。
  • 重排(Reflow/Layout): 当元素的布局发生变化(例如,尺寸、位置),浏览器需要重新计算整个页面的布局,然后重绘。
  • 合成(Composite): 浏览器将多个图层合并成最终图像的过程。

重排的开销远高于重绘,而合成的开销最小。因此,优化 CSS 动画的关键是避免重排,尽量减少重绘,并利用合成层的优势。

2. will-change 属性

  • 作用: will-change 属性可以提前告知浏览器元素将要发生变化,让浏览器提前进行优化。
  • 用法:
    • will-change: transform; // 提示浏览器元素将发生 transform 变化。
    • will-change: opacity; // 提示浏览器元素将发生 opacity 变化。
    • will-change: top, left; // 提示浏览器元素将发生 top 和 left 变化。
    • will-change: scroll-position; // 提示浏览器元素将发生滚动位置变化。
    • will-change: contents; // 提示浏览器元素内容将发生变化。
    • will-change: custom-ident; // 提示浏览器元素自定义属性将发生变化。
  • 注意事项:
    • will-change 只是提示,浏览器不一定会进行优化。
    • 过度使用 will-change 会导致内存占用过高,应谨慎使用。
    • will-change 应该在元素即将发生变化时添加,在变化结束后移除。

3. 合成层管理

  • 合成层: 浏览器会将一些视觉元素提升为独立的合成层,以便进行独立的绘制和合成。
  • 触发合成层的条件:
    • 3D 变换:transform: translate3d(...)transform: matrix3d(...) 等。
    • 视频和 Canvas:<video><canvas> 元素。
    • CSS 动画:使用 transformopacity 等属性的 CSS 动画。
    • 特定 CSS 属性:will-changeopacityfilter 等。
    • z-index 值较高的元素。
    • position: fixed
  • 合成层优势:
    • 独立的绘制和合成,减少重绘和重排的范围。
    • 利用 GPU 加速,提高渲染性能。
  • 合成层管理:
    • 避免创建不必要的合成层,减少内存占用。
    • 合理使用 z-index,避免创建过多的层叠上下文。
    • 使用 Chrome DevTools 的图层面板,分析图层合成情况。

4. 优化建议

  • 使用 transformopacity 进行动画: 这两个属性不会触发重排,只会触发合成。
  • 避免使用复杂的 CSS 属性进行动画: 例如,box-shadowborder-radius 等。
  • 合理使用 will-change 避免过度使用,仅在需要时添加。
  • 利用合成层优势: 尽量将动画元素提升为合成层。
  • 使用 Chrome DevTools 进行性能分析: 找出性能瓶颈,进行针对性优化。

通过合理使用 will-change 属性和管理合成层,可以显著提升 CSS 动画的性能,为用户带来更流畅的体验。

Canvas 与 SVG 的性能差异与选型标准

Canvas 和 SVG 都是 Web 前端开发中用于绘制图形的重要技术,但它们在性能和适用场景上存在显著差异。了解这些差异有助于开发者根据具体需求选择合适的方案。

性能差异

  • Canvas:
    • Canvas 是一种基于像素的绘图方式,它通过 JavaScript 代码直接在画布上绘制像素。
    • 对于大量图形元素的绘制,Canvas 的性能通常优于 SVG。因为它不需要维护复杂的 DOM 结构。
    • Canvas 的性能主要取决于绘制的像素数量和复杂度。
  • SVG:
    • SVG 是一种基于矢量的绘图方式,它使用 XML 描述图形,通过浏览器解析和渲染。
    • 当图形元素较少或需要频繁修改时,SVG 的性能通常优于 Canvas。因为它支持 DOM 操作,可以方便地修改和动画图形。
    • SVG 的性能主要取决于 DOM 元素的数量和复杂度。

选型标准

  1. 图形复杂度:
    • 对于复杂、大量的图形绘制(例如,游戏、数据可视化),Canvas 通常是更好的选择。
    • 对于简单、少量的图形绘制(例如,图标、Logo),SVG 更为合适。
  2. 交互性:
    • 如果需要频繁修改图形或添加交互效果,SVG 更为方便。因为它支持 DOM 操作和事件监听。
    • Canvas 的交互性相对较弱,需要通过 JavaScript 代码手动实现。
  3. 缩放和分辨率:
    • SVG 是矢量图形,可以无损缩放,适用于响应式布局和高分辨率屏幕。
    • Canvas 是像素图形,缩放时可能会失真,需要注意分辨率适配。
  4. 文件大小:
    • 对于简单的图形,SVG 文件通常比 Canvas 生成的图像文件更小。
    • 对于复杂的图形,Canvas 生成的图像文件可能会更小,尤其是在使用压缩技术的情况下。
  5. 浏览器兼容性:
    • Canvas 和 SVG 都得到了现代浏览器的广泛支持。
    • 但是,在一些老旧的浏览器上,可能存在兼容性问题。

总结

  • Canvas 适用于大量、复杂的图形绘制,性能较好,但交互性较弱。
  • SVG 适用于少量、简单的图形绘制,支持 DOM 操作,交互性强,缩放无损。

在实际开发中,开发者可以根据具体需求,综合考虑性能、交互性、缩放和文件大小等因素,选择合适的绘图方案。

React Fiber 架构设计动机与实现原理

React Fiber 架构是 React 16 引入的一种全新的协调(reconciliation)引擎。它的出现解决了 React 在处理复杂组件树和频繁更新时可能出现的性能瓶颈。下面我将详细介绍 React Fiber 架构的设计动机和实现原理:

1. 设计动机

在 React 16 之前的版本(也就是 React Stack),React 使用同步的递归方式来遍历组件树,进行协调和更新。这种方式存在以下问题:

  • 同步递归导致阻塞:
    • 当组件树庞大或更新频繁时,同步递归会长时间占用主线程,导致浏览器无法响应用户交互,出现卡顿。
  • 无法中断和恢复:
    • 一旦开始协调过程,就必须一直执行到完成,无法中断和恢复。这使得 React 无法处理高优先级的任务,例如用户输入。
  • 无法实现优先级调度:
    • React Stack 无法为不同的更新任务分配优先级,导致所有更新任务都以相同的优先级执行。

为了解决这些问题,React 团队提出了 Fiber 架构,它将同步的递归更新转变为异步的可中断更新,从而提高了 React 的性能和响应能力。

2. 实现原理

React Fiber 架构的核心思想是将组件树的协调过程分解为多个小的任务单元,称为 Fiber。每个 Fiber 节点都代表一个 React 元素,并包含该元素的相关信息。

  • Fiber 节点:
    • Fiber 节点是一个 JavaScript 对象,它包含了组件的类型、属性、子节点等信息。
    • Fiber 节点还包含了一些额外的属性,用于跟踪组件的状态和更新。
  • Fiber 树:
    • React 使用 Fiber 节点构建一个 Fiber 树,它与组件树一一对应。
    • Fiber 树的结构与组件树类似,但它还包含了一些额外的指针,用于在 Fiber 节点之间建立联系。
  • 工作循环(Work Loop):
    • React 使用工作循环来遍历 Fiber 树,执行更新任务。
    • 工作循环可以中断和恢复,从而允许 React 处理高优先级的任务。
  • 调度器(Scheduler):
    • 调度器负责管理更新任务的优先级,并决定何时执行哪些任务。
    • 调度器可以根据任务的优先级,将任务分配到不同的队列中。
  • 渲染器(Renderer):
    • 渲染器负责将 Fiber 树转换为 DOM 节点,并更新页面。
    • 渲染器使用双缓冲技术,在内存中构建新的 DOM 树,然后一次性更新页面。

3. Fiber 架构的优势

  • 异步可中断:
    • Fiber 架构将同步更新转变为异步可中断更新,提高了 React 的响应能力。
  • 优先级调度:
    • Fiber 架构允许 React 为不同的更新任务分配优先级,从而优化用户体验。
  • 增量渲染:
    • Fiber 架构支持增量渲染,可以逐步更新页面,避免长时间的白屏。
  • 更好的错误处理:
    • Fiber 架构提供了更好的错误处理机制,可以捕获和处理组件更新过程中发生的错误。

通过 Fiber 架构,React 能够更好地处理复杂的组件树和频繁的更新,从而提供更流畅、更高效的用户体验。

Hooks 闭包陷阱与解决方案

React Hooks 的闭包陷阱是指在使用 Hooks 时,由于 JavaScript 闭包的特性,可能会导致组件状态更新不符合预期。以下是对闭包陷阱的详细解释和解决方案:

闭包陷阱原理

  • 闭包:
    • 闭包是指函数可以访问并记住其词法作用域中的变量,即使该函数在其词法作用域之外执行。
    • 在 React Hooks 中,当组件重新渲染时,会创建一个新的闭包,该闭包会捕获当前渲染周期中的状态值。
  • 陷阱:
    • 当在 useEffect 或事件处理函数中使用捕获的状态值时,如果状态值在后续渲染周期中发生变化,闭包仍然会引用旧的状态值。
    • 这可能导致组件状态更新不符合预期,出现逻辑错误。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log('Count:', count); // 引用旧的 count 值
    }, 1000);
  }, []); // 空依赖数组,只在组件挂载时执行

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • 在这个示例中,useEffect 中的 setTimeout 回调函数引用了组件挂载时的 count 值。
  • 即使 count 在后续渲染周期中发生变化,setTimeout 回调函数仍然会引用旧的 count 值。

解决方案

  1. 使用函数式更新:
    • 当更新状态值依赖于旧的状态值时,可以使用函数式更新。
    • 函数式更新会将旧的状态值作为参数传递给更新函数,从而确保引用最新的状态值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount(prevCount => {
        console.log('Count:', prevCount); // 引用最新的 count 值
        return prevCount;
      });
    }, 1000);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. 使用 useRef:
    • useRef 可以创建一个可变的 ref 对象,其 current 属性可以在组件的整个生命周期中保持不变。
    • 可以使用 useRef 存储状态值,并在 useEffect 或事件处理函数中引用 ref.current,从而确保引用最新的状态值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useEffect, useRef } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count; // 更新 ref.current
  }, [count]);

  useEffect(() => {
    setTimeout(() => {
      console.log('Count:', countRef.current); // 引用最新的 count 值
    }, 1000);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. 添加依赖项:
    • 在 useEffect 的依赖数组中添加需要引用的状态值,确保 useEffect 在状态值发生变化时重新执行。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log('Count:', count); // 引用最新的 count 值
    }, 1000);
  }, [count]); // 添加 count 作为依赖项

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

总结

  • 闭包陷阱是 React Hooks 中常见的问题,需要开发者注意。
  • 使用函数式更新、useRef 或添加依赖项可以有效解决闭包陷阱。
  • 在实际开发中,应根据具体情况选择合适的解决方案。

Context 性能优化策略(memoization)

React Context 是 React 中一种用于跨组件层级传递数据的方式。然而,如果不加以优化,Context 可能会导致不必要的组件重新渲染,从而影响性能。本文将深入探讨 Context 性能优化的策略,重点介绍 memoization 技术。

Context 性能问题

  • Provider 重新渲染:
    • 当 Context Provider 的 value 发生变化时,所有消费该 Context 的组件都会重新渲染。
    • 即使消费组件并不依赖于 Provider value 的所有属性,也会触发重新渲染。
  • 组件树更新:
    • 如果 Provider 位于组件树的较高层级,其 value 的变化可能会导致整个组件树的重新渲染。

memoization 优化策略

memoization 是一种缓存技术,它可以将函数或组件的计算结果缓存起来,并在输入参数相同时直接返回缓存结果,从而避免重复计算。

  1. useMemo 缓存 Context value

    • 使用 useMemo 缓存 Context Provider 的 value,确保只有当 value 的依赖项发生变化时才重新计算。
    • 这样可以避免 Provider value 的不必要更新,从而减少消费组件的重新渲染。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    import React, { createContext, useMemo, useState } from 'react';
    
    const MyContext = createContext();
    
    function MyProvider({ children }) {
      const [count, setCount] = useState(0);
    
      // 使用 useMemo 缓存 value
      const value = useMemo(() => ({
        count,
        setCount,
      }), [count, setCount]);
    
      return (
        <MyContext.Provider value={value}>
          {children}
        </MyContext.Provider>
      );
    }
    
  2. React.memo 缓存消费组件

    • 使用 React.memo 缓存消费 Context 的组件,确保只有当组件的 props 或 Context value 发生变化时才重新渲染。
    • React.memo 会对组件的 props 进行浅比较,如果 props 没有变化,则直接返回缓存的组件实例。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    import React, { useContext } from 'react';
    
    const MyContext = React.createContext();
    
    // 使用 React.memo 缓存消费组件
    const MyConsumer = React.memo(() => {
      const { count, setCount } = useContext(MyContext);
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
      );
    });
    
  3. 拆分 Context

    • 如果 Context value 包含多个属性,可以将 Context 拆分为多个较小的 Context。
    • 这样可以确保只有当消费组件依赖的 Context 属性发生变化时才重新渲染。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    import React, { createContext, useContext, useState } from 'react';
    
    const CountContext = createContext();
    const ThemeContext = createContext();
    
    function MyProvider({ children }) {
      const [count, setCount] = useState(0);
      const [theme, setTheme] = useState('light');
    
      return (
        <CountContext.Provider value={{ count, setCount }}>
          <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
          </ThemeContext.Provider>
        </CountContext.Provider>
      );
    }
    
    function MyConsumer() {
      const { count, setCount } = useContext(CountContext);
      const { theme, setTheme } = useContext(ThemeContext);
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
          <p>Theme: {theme}</p>
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </div>
      );
    }
    

总结

  • 使用 useMemo 缓存 Context value,避免不必要的更新。
  • 使用 React.memo 缓存消费组件,减少不必要的重新渲染。
  • 拆分 Context,确保只有依赖属性变化时才触发重新渲染。

通过这些优化策略,可以有效地提升 Context 的性能,避免不必要的组件重新渲染,从而提高 React 应用的整体性能。

React 18 并发模式实战场景

React 18 引入的并发模式(Concurrent Mode)旨在提升 React 应用的响应性和用户体验。它通过允许 React 中断、恢复和优先处理渲染工作,从而优化性能。以下是一些 React 18 并发模式的实战场景:

1. 交互密集型应用

  • 场景:
    • 具有复杂表单、实时搜索或拖放功能的应用程序。
  • 优势:
    • 并发模式可以确保用户交互(如输入或点击)始终保持流畅,即使在处理大量数据或执行复杂计算时也是如此。
    • startTransition API 允许将非紧急的 UI 更新标记为过渡,从而避免阻塞用户交互。
  • 示例:
    • 实时搜索框:在用户输入时,使用 startTransition 延迟搜索结果的更新,以防止输入卡顿。
    • 拖放:在拖动元素时,使用并发模式确保拖动过程的流畅性,并优先处理用户的拖动操作。

2. 数据密集型应用

  • 场景:
    • 需要加载和渲染大量数据的应用程序,如数据可视化或仪表板。
  • 优势:
    • useDeferredValue Hook 允许延迟渲染非关键的 UI 部分,直到数据加载完成,从而避免阻塞主线程。
    • 并发模式可以实现“选择性水合”(Selective Hydration),即优先水合用户可见的部分,提高初始加载速度。
  • 示例:
    • 数据仪表板:使用 useDeferredValue 延迟渲染图表或表格,直到数据加载完成,先显示占位符或骨架屏。
    • 无限滚动列表:使用并发模式和“选择性水合”优化初始加载和滚动性能。

3. 动画和过渡效果

  • 场景:
    • 具有复杂动画或页面过渡效果的应用程序。
  • 优势:
    • 并发模式可以确保动画和过渡效果的流畅性,即使在执行其他任务时也是如此。
    • startTransition API 允许将过渡效果标记为非紧急更新,避免阻塞用户交互。
  • 示例:
    • 页面切换动画:使用 startTransition 延迟页面内容的更新,先播放过渡动画。
    • 复杂 UI 动画:使用并发模式确保动画的流畅性,避免卡顿。

4. 服务端渲染(SSR)应用

  • 场景:
    • 使用服务端渲染提高首屏加载速度的应用程序。
  • 优势:
    • “选择性水合”可以优先水合用户可见的部分,提高初始交互速度。
    • 并发模式可以优化 SSR 性能,减少服务端渲染的阻塞时间。
  • 示例:
    • 电商网站:优先水合商品列表和导航栏,延迟水合商品详情或评论。
    • 新闻网站:优先水合文章内容和标题,延迟水合侧边栏或广告。

核心概念

  • startTransition:
    • 将非紧急的 UI 更新标记为过渡,允许 React 在不阻塞用户交互的情况下执行更新。
  • useDeferredValue:
    • 延迟渲染非关键的 UI 部分,直到数据加载完成或浏览器空闲。
  • 选择性水合(Selective Hydration):
    • 优先水合用户可见的部分,提高初始交互速度。

总结

React 18 的并发模式为 React 应用带来了显著的性能提升,特别是在交互密集型、数据密集型和动画效果复杂的场景中。通过合理利用 startTransitionuseDeferredValue 和“选择性水合”等特性,开发者可以构建出更流畅、更高效的 React 应用。

好的,这里有一些 React 18 并发模式的示例代码,展示了 startTransitionuseDeferredValue 的用法。

1. startTransition 示例:实时搜索框

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState, useTransition } from 'react';

function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const inputValue = e.target.value;
    setQuery(inputValue);

    startTransition(() => {
      // 模拟异步搜索
      fetch(`/api/search?q=${inputValue}`)
        .then((response) => response.json())
        .then((data) => setResults(data));
    });
  };

  return (
    <div>
      <input type="text" value={query} onChange={handleChange} />
      {isPending && <p>搜索中...</p>}
      <ul>
        {results.map((result) => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}
  • 在这个示例中,startTransition 用于延迟搜索结果的更新,以防止输入卡顿。
  • 当用户输入时,handleChange 函数会更新 query 状态,并使用 startTransition 包裹异步搜索请求。
  • isPending 状态表示搜索是否正在进行,可以用于显示加载指示器。

2. useDeferredValue 示例:数据仪表板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { useState, useDeferredValue, useMemo } from 'react';

function Chart({ data }) {
  // 模拟复杂图表渲染
  const chartData = useMemo(() => {
    console.log('Rendering chart...');
    return data.map((item) => ({ x: item.x, y: item.y }));
  }, [data]);

  return (
    <div>
      {/* 图表渲染逻辑 */}
      {chartData.map((point) => (
        <div key={point.x} style={{ left: point.x, top: point.y }}>
          .
        </div>
      ))}
    </div>
  );
}

function Dashboard({ data }) {
  const deferredData = useDeferredValue(data);
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(!showChart)}>
        {showChart ? 'Hide Chart' : 'Show Chart'}
      </button>
      {showChart && <Chart data={deferredData} />}
    </div>
  );
}

function App() {
  const [data, setData] = useState([]);

  const handleUpdate = () => {
    // 模拟数据更新
    setData(Array.from({ length: 1000 }, (_, i) => ({ x: i, y: Math.random() * 100 })));
  };

  return (
    <div>
      <button onClick={handleUpdate}>Update Data</button>
      <Dashboard data={data} />
    </div>
  );
}
  • 在这个示例中,useDeferredValue 用于延迟渲染图表,直到数据加载完成。
  • data 状态更新时,deferredData 会延迟更新,从而避免阻塞主线程。
  • Chart 组件使用 deferredData 渲染图表,useMemo 用于缓存图表数据,避免重复计算。

3. 页面切换动画

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useTransition } from 'react';

function PageA() {
  return <div>Page A Content</div>;
}

function PageB() {
  return <div>Page B Content</div>;
}

function App() {
  const [page, setPage] = useState('A');
  const [isPending, startTransition] = useTransition();

  const handlePageChange = (newPage) => {
    startTransition(() => {
      setPage(newPage);
    });
  };

  return (
    <div>
      <button onClick={() => handlePageChange('A')}>Go to A</button>
      <button onClick={() => handlePageChange('B')}>Go to B</button>
      {isPending ? <div>Loading...</div> : page === 'A' ? <PageA /> : <PageB />}
    </div>
  );
}
  • 在这个示例中,startTransition 用于延迟页面内容的更新,先播放过渡动画。
  • 当用户点击按钮切换页面时,handlePageChange 函数会使用 startTransition 包裹 setPage 调用。
  • isPending 状态用于显示加载指示器。

这些示例展示了如何在实际场景中使用 React 18 并发模式,以提升应用性能和用户体验。

服务端组件(RSC)与传统SSR差异

服务端组件(RSC)和传统的服务端渲染(SSR)虽然都涉及在服务器上生成内容,但它们在架构、性能和开发体验上存在显著差异。以下是对它们之间区别的详细解释:

1. 架构差异

  • 传统 SSR:
    • 传统的 SSR 主要是在服务器上将 React 组件渲染成 HTML 字符串,然后将其发送到客户端。
    • 客户端接收到 HTML 后,需要执行 JavaScript 代码进行“水合”(hydration),即将服务器渲染的 HTML 与客户端的 React 应用关联起来,使其具有交互性。
    • 整个应用仍然主要在客户端运行,服务器只负责初始 HTML 的生成。
  • 服务端组件(RSC):
    • RSC 允许开发者将组件分为服务端组件和客户端组件。
    • 服务端组件完全在服务器上运行,可以访问后端数据源(如数据库)和执行复杂的计算。
    • 服务器将服务端组件的渲染结果序列化为一种特殊的数据格式,然后发送到客户端。
    • 客户端接收到数据后,只渲染客户端组件,服务端组件的渲染结果只用于构建 UI。
    • 这使得部分组件可以完全在服务端执行,减少了客户端 JavaScript 的负担。

2. 性能差异

  • 传统 SSR:
    • 首屏加载速度快,因为服务器直接返回 HTML。
    • 但“水合”过程可能会导致性能瓶颈,尤其是在大型应用中。
    • 客户端仍然需要下载和执行所有 JavaScript 代码,增加了客户端的负担。
  • 服务端组件(RSC):
    • 进一步提升首屏加载速度,因为部分组件的渲染在服务器完成。
    • 显著减少客户端 JavaScript 的体积,提高客户端性能。
    • 服务器可以直接访问数据源,减少了客户端的数据请求。

3. 开发体验差异

  • 传统 SSR:
    • 开发者需要处理服务器和客户端之间的状态同步和数据传递。
    • “水合”过程可能会引入一些复杂性。
  • 服务端组件(RSC):
    • 简化了数据获取和状态管理,服务端组件可以直接访问后端数据。
    • 提供了更清晰的组件划分,便于构建高性能应用。
    • 开发者可以更专注于业务逻辑,而无需过多关注客户端性能优化。

总结

  • RSC 是一种更先进的服务端渲染方式,它将组件分为服务端和客户端,从而实现了更好的性能和开发体验。
  • 与传统的 SSR 相比,RSC 减少了客户端 JavaScript 的负担,提高了首屏加载速度和客户端性能。
  • RSC 允许在服务器上进行数据获取和复杂的运算,简化了开发流程。

希望以上信息对你有所帮助。

Licensed under CC BY-NC-SA 4.0
最后更新于 Mar 29, 2025 08:13 UTC