普通视图

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

Lodash源码阅读-cloneBuffer

作者 好_快
2025年4月6日 09:46

Lodash 源码阅读-cloneBuffer

概述

cloneBuffer 是 Lodash 内部用于克隆 Node.js Buffer 对象的函数。Buffer 是 Node.js 中用于处理二进制数据的类,该函数可以创建一个与原 Buffer 对象内容相同但内存地址不同的新 Buffer 对象,支持浅克隆和深克隆。

前置学习

依赖函数

  • 无直接依赖其他函数

相关技术知识

  • Node.js Buffer 对象
  • Buffer.allocUnsafe 方法
  • Buffer.slice 方法
  • Buffer.copy 方法
  • JavaScript 条件判断和对象构造

源码实现

/**
 * Creates a clone of  `buffer`.
 *
 * @private
 * @param {Buffer} buffer The buffer to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Buffer} Returns the cloned buffer.
 */
function cloneBuffer(buffer, isDeep) {
  if (isDeep) {
    return buffer.slice();
  }
  var length = buffer.length,
    result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);

  buffer.copy(result);
  return result;
}

实现思路

cloneBuffer 函数的实现思路如下:

  1. 通过参数 isDeep 来决定是进行深克隆还是浅克隆
  2. 如果是深克隆(isDeeptrue),直接调用 Buffer 的 slice() 方法创建一个全新的 Buffer 副本
  3. 如果是浅克隆:
    • 首先获取原 Buffer 的长度
    • 然后通过 allocUnsafe(如果可用)或通过构造函数创建一个相同长度的新 Buffer 对象
    • 使用 copy 方法将原 Buffer 的内容复制到新 Buffer 中
  4. 返回克隆后的新 Buffer 对象

源码解析

深克隆处理

if (isDeep) {
  return buffer.slice();
}

isDeeptrue 时,函数直接调用 Buffer 对象的 slice() 方法创建一个新的 Buffer 对象。在 Node.js 中,Buffer.slice() 会返回一个新的 Buffer 对象,它包含原 Buffer 的一个副本,而不是引用同一内存区域,因此是一种深克隆方式。

示例:

const buf1 = Buffer.from("hello");
const buf2 = buf1.slice();

console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // false

// 修改 buf2 不会影响 buf1
buf2[0] = 0x48;
console.log(buf1.toString()); // 'hello' - 未改变
console.log(buf2.toString()); // 'Hello' - 已改变

浅克隆处理

var length = buffer.length,
  result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);

buffer.copy(result);

当进行浅克隆时,函数会:

  1. 获取原 Buffer 的长度
  2. 创建一个新的 Buffer:
    • 优先使用 allocUnsafe 方法(如果可用)
    • 如果 allocUnsafe 不可用,则使用原 Buffer 的构造函数创建
  3. 使用 buffer.copy() 方法将原 Buffer 的内容复制到新创建的 Buffer 中

这里的 allocUnsafe 是一个从 Node.js 环境中获取的 Buffer 方法引用:

var Buffer = moduleExports ? context.Buffer : undefined,
    // ...其他代码
    allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined,

Buffer.allocUnsafe() 是 Node.js 中创建 Buffer 的一种高效方法,它分配内存但不会初始化内存内容,因此比 Buffer.alloc() 更快,但需要手动填充缓冲区以避免潜在的安全风险。

示例:

const buf1 = Buffer.from("hello");
const buf2 = Buffer.allocUnsafe(buf1.length);
buf1.copy(buf2);

console.log(buf1.toString()); // 'hello'
console.log(buf2.toString()); // 'hello'
console.log(buf1 === buf2); // false

应用场景

在 Lodash 内部的应用

cloneBuffer 主要在 Lodash 的 baseClone 函数中使用,该函数是 clonecloneDeep 方法的核心实现。当需要克隆 Buffer 类型的值时,会使用该函数:

// 在 baseClone 函数中
if (isBuffer(value)) {
  return cloneBuffer(value, isDeep);
}

总结

  1. 效率优先:优先使用 allocUnsafe 方法提高性能,同时兼顾了不同环境下的兼容性
  2. 灵活性:通过 isDeep 参数支持深克隆和浅克隆两种模式
  3. 依赖隔离:检测 allocUnsafe 是否可用,确保在非 Node.js 环境中也能正常工作

这种针对特定类型(Buffer)数据的专门处理方法,展示了 Lodash 库在处理各种 JavaScript 数据类型时的全面性和精确性,使得 Lodash 的克隆功能能够正确处理包括 Buffer 在内的各种复杂数据类型。

Lodash源码阅读-initCloneArray

作者 好_快
2025年4月6日 09:46

Lodash 源码阅读-initCloneArray

概述

initCloneArray 是 Lodash 内部用于初始化数组克隆的函数,主要用于创建一个与原数组相同长度的新数组,并且当原数组具有特定属性(如通过正则表达式匹配产生的数组属性)时,会将这些特定属性也复制到新数组中。

前置学习

依赖函数

  • 无直接依赖函数

相关技术知识

  • JavaScript 数组构造器(Array Constructor)
  • RegExp.exec() 方法产生的结果数组的特殊属性
  • JavaScript 对象属性的检测与复制
  • hasOwnProperty 方法的使用

源码实现

/**
 * Initializes an array clone.
 *
 * @private
 * @param {Array} array The array to clone.
 * @returns {Array} Returns the initialized clone.
 */
function initCloneArray(array) {
  var length = array.length,
    result = new array.constructor(length);

  // Add properties assigned by `RegExp#exec`.
  if (
    length &&
    typeof array[0] == "string" &&
    hasOwnProperty.call(array, "index")
  ) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}

实现思路

initCloneArray 函数的实现思路比较简单:

  1. 首先获取原数组的长度
  2. 使用原数组的构造器创建一个相同长度的新数组(这样可以保证克隆结果与原数组类型一致)
  3. 然后检查是否需要复制 RegExp.exec() 方法产生的特殊属性:
    • 检查数组长度是否大于 0
    • 检查数组第一个元素是否为字符串类型
    • 检查数组是否有自己的 'index' 属性(RegExp.exec() 结果特有)
  4. 如果满足上述条件,则将原数组的 index 和 input 属性复制到新数组中
  5. 最后返回初始化后的克隆数组

源码解析

获取原数组长度并创建新数组

var length = array.length,
  result = new array.constructor(length);

这里使用 array.constructor 而不是直接使用 Array 构造函数,是为了保证创建的新数组与原数组具有相同的类型。这在处理类数组对象或特殊数组(如 TypedArray)时特别重要。

示例:

// 普通数组
const arr1 = [1, 2, 3];
const newArr1 = new arr1.constructor(arr1.length); // 等同于 new Array(3)

// 类型化数组
const arr2 = new Uint8Array([1, 2, 3]);
const newArr2 = new arr2.constructor(arr2.length); // 创建 Uint8Array 而非普通数组

复制 RegExp.exec() 的特殊属性

if (
  length &&
  typeof array[0] == "string" &&
  hasOwnProperty.call(array, "index")
) {
  result.index = array.index;
  result.input = array.input;
}

这段代码处理的是当数组来自 RegExp#exec() 方法调用的特殊情况。

当使用正则表达式的 exec 方法匹配字符串时,返回的数组会带有两个特殊属性:

  • index: 表示匹配的文本在原字符串中的位置
  • input: 表示原始的字符串

下面是一个例子:

const regex = /c/;
const result = regex.exec("abcde");
console.log(result); // ['c', index: 2, input: 'abcde', groups: undefined]
console.log(result.index); // 2
console.log(result.input); // 'abcde'

函数通过三个条件来判断是否为 exec 结果:

  1. 数组长度大于 0(length
  2. 第一个元素是字符串(typeof array[0] == 'string'
  3. 数组拥有自己的 index 属性(hasOwnProperty.call(array, 'index')

如果满足这些条件,就将原数组的 indexinput 属性复制到新数组中,以保持这些特殊属性。

应用场景

在 Lodash 内部的应用

initCloneArray 主要用在 Lodash 的 clonecloneDeep 方法内部,是 baseClone 函数的一部分。当需要克隆数组类型的值时,会使用该函数初始化克隆结果:

// 在 baseClone 函数中的应用
if (isArr) {
  result = initCloneArray(value);
  if (!isDeep) {
    return copyArray(value, result);
  }
}

如果不是进行深度克隆,会在初始化数组后通过 copyArray 函数复制数组元素。如果是深度克隆,则会在后续代码中递归处理每个数组元素。

总结

initCloneArray 函数虽然短小,但体现了 Lodash 源码的几个核心设计思想:

  1. 类型保持:通过使用原对象的构造器而不是硬编码的 Array 构造函数,确保克隆结果与原始数据类型一致
  2. 特殊情况处理:识别并处理 RegExp.exec() 结果这种特殊情况,保证克隆的完整性
  3. 职责单一:该函数只负责初始化数组克隆,不负责复制数组元素内容,符合单一职责原则

这种精细的类型处理和特殊情况的考虑,是 Lodash 库稳定性和适用性广泛的重要原因之一。在我们自己实现工具函数时,也应当学习这种严谨和全面的思考方式。

Lodash源码阅读-get

作者 好_快
2025年4月6日 09:45

Lodash 源码阅读-get

概述

get 是 Lodash 库中用于安全获取嵌套对象属性值的函数。它允许我们从深层嵌套的对象中获取属性,不必担心中间路径不存在而导致的错误,同时支持提供默认值作为备选返回结果。

前置学习

依赖函数

  • baseGet:核心实现,根据路径从对象中获取值的内部函数
  • castPath:将属性路径转换为标准化的数组形式
  • toKey:将路径片段转换为合法的对象属性键

技术知识

  • 路径访问:JavaScript 中的对象属性访问方式
  • 空值处理:处理 null 和 undefined 的技巧
  • 短路求值:JavaScript 中的逻辑短路求值原理

源码实现

/**
 * Gets the value at `path` of `object`. If the resolved value is
 * `undefined`, the `defaultValue` is returned in its place.
 *
 * @static
 * @memberOf _
 * @since 3.7.0
 * @category Object
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @param {*} [defaultValue] The value returned for `undefined` resolved values.
 * @returns {*} Returns the resolved value.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 3 } }] };
 *
 * _.get(object, 'a[0].b.c');
 * // => 3
 *
 * _.get(object, ['a', '0', 'b', 'c']);
 * // => 3
 *
 * _.get(object, 'a.b.c', 'default');
 * // => 'default'
 */
function get(object, path, defaultValue) {
  var result = object == null ? undefined : baseGet(object, path);
  return result === undefined ? defaultValue : result;
}

实现思路

get 函数的实现思路非常直接:

  1. 首先检查对象是否为 null 或 undefined,如果是则直接返回 undefined
  2. 如果对象有效,则调用 baseGet 函数根据指定的路径获取嵌套属性值
  3. 检查获取到的结果是否为 undefined,如果是则返回提供的默认值,否则返回实际获取的结果

这种设计允许安全地访问任意深度的嵌套对象属性,而不必担心中间路径不存在导致的错误。同时通过提供默认值参数,可以在目标值不存在时返回预设的结果,增加了函数的实用性和灵活性。

源码解析

整个函数只有一行实际代码,但它利用了多个 JavaScript 语言特性和辅助函数。让我们逐步解析:

function get(object, path, defaultValue) {
  var result = object == null ? undefined : baseGet(object, path);
  return result === undefined ? defaultValue : result;
}

参数解析

  • object:要查询的目标对象
  • path:属性路径,可以是字符串形式(如 'a.b.c''a[0].b.c')或数组形式(如 ['a', '0', 'b', 'c']
  • defaultValue:当解析得到的值为 undefined 时返回的默认值(可选)

空值检查

object == null ? undefined : baseGet(object, path);

这行代码使用了逻辑短路操作:

  • object == null 检查 object 是否为 nullundefined(使用 == 而不是 === 来同时匹配这两种情况)
  • 如果条件为真,直接返回 undefined,不调用 baseGet
  • 如果条件为假,则调用 baseGet(object, path) 来获取路径指定的值

这种检查避免了在对象为 null 或 undefined 时尝试访问其属性而导致的错误。

baseGet 调用

baseGet 是实际执行属性路径查找的内部函数:

// baseGet 简化版实现
function baseGet(object, path) {
  path = castPath(path, object);

  var index = 0,
    length = path.length;

  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  return index && index == length ? object : undefined;
}

baseGet 函数首先通过 castPath 将路径转换为标准化的数组形式,然后循环遍历路径各部分,逐层获取嵌套对象的属性值。

默认值处理

return result === undefined ? defaultValue : result;

这行代码使用三元操作符检查从 baseGet 获取的结果:

  • 如果结果是 undefined(表示路径无效或值不存在),则返回提供的 defaultValue
  • 否则返回找到的实际值 result

这提供了一种优雅的方式来处理属性不存在的情况,避免了大量的条件检查代码。

总结

get 函数是 Lodash 中最常用、最实用的函数之一,它解决了 JavaScript 中访问深层嵌套对象属性的常见问题。其核心设计思想包括:

  1. 防御性编程:通过预先检查 null 和 undefined 以及提供默认值,避免运行时错误
  2. 路径灵活性:支持字符串和数组两种路径表示方式,增加了使用的灵活性
  3. 短小精悍:只有一行实际代码,但解决了一个常见的复杂问题
  4. 功能分离:将核心实现放在 baseGet 中,保持 get 函数的简洁

这种设计方法不仅使代码更加健壮,还大大提高了开发效率,减少了条件检查代码的编写。

Lodash源码阅读-baseGet

作者 好_快
2025年4月6日 09:45

Lodash 源码阅读-baseGet

概述

baseGet 是 Lodash 内部的一个工具函数,用于根据指定的路径从对象中获取值。它是 _.get 方法的基础实现,支持使用点号路径(如 'a.b.c')或数组路径(如 ['a', 'b', 'c'])来访问嵌套对象的属性,是 Lodash 中实现对象属性访问功能的核心。

前置学习

依赖函数

  • castPath:将路径转换为标准格式(数组形式),处理字符串路径和数组路径
  • toKey:将路径片段转换为有效的属性键,处理字符串、数字、Symbol 等不同类型的键

技术知识

  • 路径解析:将字符串路径解析为路径片段数组
  • 对象属性访问:使用方括号语法 object[key] 动态访问对象属性
  • 空值处理:处理访问路径中的 nullundefined
  • 短路求值:在遇到无效路径时提前返回
  • 循环遍历:使用 while 循环逐层访问嵌套对象

源码实现

function baseGet(object, path) {
  path = castPath(path, object);

  var index = 0,
    length = path.length;

  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  return index && index == length ? object : undefined;
}

实现思路

baseGet 函数的实现思路非常直接:

  1. 首先使用 castPath 将路径转换为标准的数组格式,无论输入的是字符串路径还是数组路径。

  2. 然后使用 while 循环逐层访问对象的嵌套属性:

    • 只要当前对象不是 nullundefined,且还有路径片段未处理,就继续循环
    • 在每次循环中,使用 toKey 将当前路径片段转换为有效的属性键,然后访问对象的对应属性
    • 将获取到的属性值作为新的当前对象,继续处理下一个路径片段
  3. 最后,检查是否成功遍历了整个路径:

    • 如果 index 大于 0(表示至少处理了一个路径片段)且等于路径长度(表示处理了所有路径片段),则返回最终获取到的值
    • 否则,返回 undefined,表示路径无效或中途遇到了 nullundefined

这种实现既简洁又高效,能够处理各种复杂的嵌套对象访问场景。

源码解析

路径标准化

path = castPath(path, object);

函数首先调用 castPath(path, object) 将路径转换为标准的数组格式。castPath 函数的作用是:

  • 如果路径已经是数组,则直接返回
  • 如果路径是简单属性键(通过 isKey 判断),则将其包装为单元素数组
  • 否则,将字符串路径解析为路径片段数组(通过 stringToPath

例如:

castPath("a.b.c", obj); // 返回 ['a', 'b', 'c']
castPath(["a", "b", "c"], obj); // 返回 ['a', 'b', 'c']
castPath("a", obj); // 如果 'a' 是简单键,返回 ['a']

初始化变量

var index = 0,
  length = path.length;

这里初始化了两个变量:

  • index:当前处理的路径片段索引,初始为 0
  • length:路径片段的总数

逐层访问属性

while (object != null && index < length) {
  object = object[toKey(path[index++])];
}

这个 while 循环是函数的核心,它逐层访问对象的嵌套属性:

  1. 循环条件 object != null && index < length 确保:

    • 当前对象不是 nullundefinedobject != null 等价于 object !== null && object !== undefined
    • 还有路径片段未处理(index < length
  2. 循环体 object = object[toKey(path[index++])] 执行以下操作:

    • 获取当前路径片段 path[index]
    • 使用 toKey 将路径片段转换为有效的属性键
    • 访问对象的对应属性 object[key]
    • 将获取到的属性值赋给 object,作为下一次循环的当前对象
    • 递增 index,准备处理下一个路径片段

toKey 函数的作用是将路径片段转换为有效的属性键:

  • 如果是字符串或 Symbol,则直接返回
  • 否则,将其转换为字符串
  • 特殊处理 -0,确保 -00 被视为不同的键

例如:

// 对于路径 'a.b.c' 和对象 { a: { b: { c: 42 } } }
// 第一次循环:object = { a: { b: { c: 42 } } }['a'] = { b: { c: 42 } }
// 第二次循环:object = { b: { c: 42 } }['b'] = { c: 42 }
// 第三次循环:object = { c: 42 }['c'] = 42
// 循环结束,object = 42

返回结果

return index && index == length ? object : undefined;

这行代码决定函数的返回值:

  • 如果 index 大于 0(index 为真)且等于路径长度(index == length),表示成功遍历了整个路径,则返回最终获取到的值 object
  • 否则,返回 undefined,表示路径无效或中途遇到了 nullundefined

这个条件检查确保了函数的行为符合预期:

  • 如果对象为 nullundefined,返回 undefined
  • 如果路径中的某个中间属性为 nullundefined,返回 undefined
  • 如果成功遍历了整个路径,返回找到的值,即使该值本身是 nullundefined

例如:

// 对于 object = { a: { b: null } } 和 path = 'a.b.c'
// 第一次循环后:object = { b: null }
// 第二次循环后:object = null
// 第三次循环不会执行,因为 object 为 null
// index = 2, length = 3,所以返回 undefined

总结

baseGet 函数是 Lodash 中对象属性访问功能的核心实现,它通过巧妙的设计实现了安全、灵活的嵌套属性访问。其设计体现了几个重要的软件工程原则:

  1. 健壮性

    • 安全处理 nullundefined 值,避免运行时错误
    • 对无效路径返回 undefined 而不是抛出异常
    • 支持各种类型的路径表示(字符串、数组)
  2. 灵活性

    • 支持点号路径(如 'a.b.c')和数组路径(如 ['a', 'b', 'c']
    • 通过 toKey 支持各种类型的属性键(字符串、数字、Symbol)
    • _.get 结合提供默认值功能
  3. 性能优化

    • 使用 while 循环而不是递归,避免函数调用开销
    • 通过 castPath 缓存路径解析结果,避免重复解析
    • 在遇到无效路径时提前返回,避免不必要的计算
  4. 可组合性

    • 作为基础函数被其他高级函数(如 _.get_.has_.set)复用
    • 与路径处理函数(如 castPathtoKey)协同工作
    • 遵循单一职责原则,专注于属性访问逻辑

这些设计思想不仅适用于对象属性访问,也可以应用到其他需要处理嵌套数据结构的场景中,是处理复杂数据的重要参考。

Lodash源码阅读-stringToPath

作者 好_快
2025年4月6日 09:45

Lodash 源码阅读-stringToPath

概述

stringToPath 是 Lodash 内部的一个小工具函数,它的作用很直接:把字符串形式的属性路径(比如 'a.b.c''a[0].b')转换成数组形式(比如 ['a', 'b', 'c']['a', '0', 'b'])。这个函数是 Lodash 中处理嵌套对象属性访问的基础,让我们可以用字符串来描述如何层层深入一个对象。

前置学习

依赖函数

  • memoizeCapped:带缓存上限的记忆化函数,防止缓存过大占用过多内存
  • reEscapeChar:一个正则表达式,用于处理转义字符

技术知识

  • 属性路径表示法:JavaScript 中用点号(.)或方括号([])访问对象属性的语法
  • 正则表达式:用于匹配字符串中复杂模式的工具,这里用 rePropName 解析路径
  • 记忆化技术:通过缓存函数结果提高重复调用的性能
  • 字符编码:使用 charCodeAt 方法判断字符的 ASCII 码值

源码实现

var stringToPath = memoizeCapped(function (string) {
  var result = [];
  if (string.charCodeAt(0) === 46 /* . */) {
    result.push("");
  }
  string.replace(rePropName, function (match, number, quote, subString) {
    result.push(
      quote ? subString.replace(reEscapeChar, "$1") : number || match
    );
  });
  return result;
});

实现思路

stringToPath 的实现其实挺简单:先创建一个空数组用来存放解析结果,然后检查输入字符串是否以点号开头,如果是(比如 .name),就先放一个空字符串表示从根对象开始。接着用一个精心设计的正则表达式去匹配路径中的每一段,可能是普通属性名(如 name)、数字索引(如 [0] 中的 0)或带引号的属性名(如 ["foo"] 中的 foo)。找到一段就往结果数组里推一个,最后返回整个数组。整个函数还用 memoizeCapped 包了一层,这样对相同的输入字符串就能直接返回之前的结果,不用重复解析,提高性能。

源码解析

记忆化处理

var stringToPath = memoizeCapped(function (string) {
  // 函数体...
});

这里用 memoizeCapped 包装了整个函数,这样做有什么好处?

  1. 性能提升:相同的路径字符串只解析一次,后续直接用缓存的结果
  2. 内存保护memoizeCapped 会限制最多缓存 500 个结果,避免内存泄漏

想象一下,如果你的程序中经常需要访问 obj.user.profile.name,每次都从头解析 "user.profile.name" 就很浪费。有了缓存,第一次解析后,后续直接返回 ["user", "profile", "name"]

处理以点号开头的路径

if (string.charCodeAt(0) === 46 /* . */) {
  result.push("");
}

为什么要检查第一个字符是不是点号?因为如果路径以点号开头(如 .name),表示从当前对象开始,需要在结果数组中添加一个空字符串作为第一个元素。

// 例如:
stringToPath(".name"); // 返回 ['', 'name']

这里用 charCodeAt(0) === 46 而不是 string[0] === '.',是为了提高性能,直接比较字符的 ASCII 码值更快。

使用正则表达式解析各部分

string.replace(rePropName, function (match, number, quote, subString) {
  result.push(quote ? subString.replace(reEscapeChar, "$1") : number || match);
});

这段代码是整个函数的核心,它利用正则表达式的 replace 方法来遍历并解析路径中的每一段。看起来复杂,我们拆开来看:

  1. string.replace 并不是真的要替换什么,而是利用它的回调函数来处理匹配到的每一段
  2. rePropName 正则表达式能匹配三种路径形式:
    • 普通属性名:如 userprofile
    • 数字索引:如 [0] 中的 0
    • 带引号的属性名:如 ["foo"]['bar'] 中的 foobar
  3. 回调函数接收四个参数:
    • match:匹配到的完整文本
    • number:如果是数字索引,这里就是数字部分
    • quote:如果是带引号的属性名,这里是引号字符
    • subString:如果是带引号的属性名,这里是引号中间的内容

举个例子:

// 路径 "a[0]['b.c']"
// 第一次匹配: match="a", number=undefined, quote=undefined, subString=undefined
// 第二次匹配: match="[0]", number="0", quote=undefined, subString=undefined
// 第三次匹配: match="['b.c']", number=undefined, quote="'", subString="b.c"

// 结果数组: ["a", "0", "b.c"]

对于带引号的属性名,还需要处理可能存在的转义字符(如 \"\'),这就是 subString.replace(reEscapeChar, '$1') 的作用。

返回最终结果

return result;

最后返回填充好的结果数组,表示解析完成的属性路径。

总结

stringToPath 虽然是个小函数,但它在 Lodash 中扮演着重要角色,是属性访问相关功能的基础。它的设计体现了几个关键点:

  1. 实用性:解决了 JavaScript 中访问嵌套属性的常见问题
  2. 性能优化:通过记忆化缓存避免重复解析相同的路径
  3. 鲁棒性:能处理各种形式的属性路径表示法
  4. 内部实现复用:作为基础工具函数被多个 Lodash 方法使用

如果你经常处理复杂的嵌套对象,可以考虑学习 stringToPath 的实现思路,或直接使用 Lodash 提供的 _.get_.set_.has 这些基于它构建的高级功能。

webpack 核心编译器 十六 节

作者 excel
2025年4月6日 07:44

getPath(filename, data = {})

总结:根据 filename 模板和 hash 插值,生成资源路径字符串。

/**
 * 获取插值后的资源路径字符串。
 * 如果 data 中没有提供 hash,则补上 this.hash。
 * 然后调用 getAssetPath 生成最终的路径。
 */
getPath(filename, data = {}) {
if (!data.hash) {
data = {
hash: this.hash,
...data
};
}
return this.getAssetPath(filename, data);
}

getPathWithInfo(filename, data = {})

总结:返回带资源信息(assetInfo)的插值路径。

/**
 * 获取带 assetInfo 的插值路径。
 * 逻辑与 getPath 类似,但调用 getAssetPathWithInfo,
 * 返回的结果包括 path 和 assetInfo 两部分。
 */
getPathWithInfo(filename, data = {}) {
if (!data.hash) {
data = {
hash: this.hash,
...data
};
}
return this.getAssetPathWithInfo(filename, data);
}

getAssetPath(filename, data)

总结:通过调用 assetPath 钩子插值资源路径字符串。

/**
 * 插值资源路径。
 * 如果 filename 是函数,先执行它。
 * 然后将其传给 assetPath 钩子处理。
 */
getAssetPath(filename, data) {
return this.hooks.assetPath.call(
typeof filename === "function" ? filename(data) : filename,
data,
undefined
);
}

getAssetPathWithInfo(filename, data)

总结:获取插值后的路径和可被插件使用的资源信息对象。

/**
 * 获取插值路径和附带的资源信息。
 * 创建 assetInfo 空对象,传给钩子,供插件修改。
 * 返回 path 和 info。
 */
getAssetPathWithInfo(filename, data) {
const assetInfo = {};
const newPath = this.hooks.assetPath.call(
typeof filename === "function" ? filename(data, assetInfo) : filename,
data,
assetInfo
);
return { path: newPath, info: assetInfo };
}

getWarnings()

总结:通过钩子处理构建过程中收集的 warnings。

/**
 * 调用 processWarnings 钩子处理警告。
 */
getWarnings() {
return this.hooks.processWarnings.call(this.warnings);
}

getErrors()

总结:通过钩子处理构建过程中收集的 errors。

/**
 * 调用 processErrors 钩子处理错误。
 */
getErrors() {
return this.hooks.processErrors.call(this.errors);
}

createChildCompiler(name, outputOptions, plugins)

总结:创建一个子编译器实例,用于插件中执行嵌套编译任务。

/**
 * 创建一个子编译器。
 * 可用于插件中运行嵌套的 webpack 构建(如 HtmlWebpackPlugin)。
 * 子编译器会复制父编译器的 hook 和 plugin,但允许使用不同配置。
 */
createChildCompiler(name, outputOptions, plugins) {
const idx = this.childrenCounters[name] || 0;
this.childrenCounters[name] = idx + 1;
return this.compiler.createChildCompiler(
this,
name,
idx,
outputOptions,
plugins
);
}

executeModule 是 Webpack 中用于“构建时执行模块”的方法,关键用于某些插件(如 ModuleFederationPlugin、MiniCssExtractPlugin)需要在构建阶段获取模块执行结果的场景。

核心步骤包括:

  1. 收集模块及其依赖:递归收集所有相关模块,等待其构建与依赖处理完成。
  2. 创建 chunk 与 entrypoint:模拟一个构建环境,为模块分配 chunkGraph、chunk。
  3. 生成模块 hash 和代码:计算模块 hash,进行代码生成。
  4. 准备执行上下文:为每个模块准备执行上下文、缓存信息和资源依赖。
  5. 执行模块:通过自定义的 __webpack_require__ 执行入口模块,并收集其 exports
  6. 返回结果:将模块导出值与构建资源等信息回传。
/**
 * 执行一个模块,并在构建时收集其依赖、生成代码、执行并获取其导出内容
 *
 * @param {Module} module - 需要执行的入口模块
 * @param {ExecuteModuleOptions} options - 执行模块的附加选项,如 entry 相关配置
 * @param {ExecuteModuleCallback} callback - 执行完成后的回调函数
 */
executeModule(module, options, callback) {
  // 初始化模块集合,包含初始模块
  const modules = new Set([module]);

  // 递归收集所有依赖模块,确保所有模块都完成构建和依赖处理
  processAsyncTree(
    modules,
    10, // 并发限制
    (module, push, callback) => {
      // 等待模块构建完成
      this.buildQueue.waitFor(module, err => {
        if (err) return callback(err);
        // 等待依赖处理完成
        this.processDependenciesQueue.waitFor(module, err => {
          if (err) return callback(err);
          // 获取当前模块的所有依赖模块(出边模块)
          for (const { module: m } of this.moduleGraph.getOutgoingConnections(module)) {
            const size = modules.size;
            modules.add(m); // 添加依赖模块
            if (modules.size !== size) push(m); // 如果是新增模块,则继续递归处理
          }
          callback();
        });
      });
    },
    err => {
      if (err) return callback(/** @type {WebpackError} */(err));

      // 构建运行时信息,包括 chunkGraph、chunk、entrypoint
      const chunkGraph = new ChunkGraph(this.moduleGraph, this.outputOptions.hashFunction);
      const runtime = "build time"; // 运行时名
      const { hashFunction, hashDigest, hashDigestLength } = this.outputOptions;
      const runtimeTemplate = this.runtimeTemplate;

      // 创建一个虚拟的 chunk 和 entrypoint 用于该模块的执行
      const chunk = new Chunk("build time chunk", this._backCompat);
      chunk.id = /** @type {ChunkId} */ (chunk.name);
      chunk.ids = [chunk.id];
      chunk.runtime = runtime;

      const entrypoint = new Entrypoint({
        runtime,
        chunkLoading: false,
        ...options.entryOptions
      });

      // 建立 chunk、entrypoint 与模块之间的关系
      chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
      connectChunkGroupAndChunk(entrypoint, chunk);
      entrypoint.setRuntimeChunk(chunk);
      entrypoint.setEntrypointChunk(chunk);

      const chunks = new Set([chunk]);

      // 为模块分配 ID 并连接 chunk 与模块
      for (const module of modules) {
        const id = module.identifier();
        chunkGraph.setModuleId(module, id);
        chunkGraph.connectChunkAndModule(chunk, module);
      }

      /** @type {WebpackError[]} */
      const errors = [];

      // 为所有模块生成哈希值
      for (const module of modules) {
        this._createModuleHash(
          module,
          chunkGraph,
          runtime,
          hashFunction,
          runtimeTemplate,
          hashDigest,
          hashDigestLength,
          errors
        );
      }

      const codeGenerationResults = new CodeGenerationResults(this.outputOptions.hashFunction);

      // 生成模块代码的函数
      const codeGen = (module, callback) => {
        this._codeGenerationModule(
          module,
          runtime,
          [runtime],
          chunkGraph.getModuleHash(module, runtime),
          this.dependencyTemplates,
          chunkGraph,
          this.moduleGraph,
          runtimeTemplate,
          errors,
          codeGenerationResults,
          (err, codeGenerated) => {
            callback(err);
          }
        );
      };

      // 将错误写入 this.errors 中
      const reportErrors = () => {
        if (errors.length > 0) {
          errors.sort(compareSelect(err => err.module, compareModulesByIdentifier));
          for (const error of errors) {
            this.errors.push(error);
          }
          errors.length = 0;
        }
      };

      // 为每个模块生成代码
      asyncLib.eachLimit(modules, 10, codeGen, err => {
        if (err) return callback(err);
        reportErrors();

        // 设置临时 chunkGraph(用于兼容)
        const old = this.chunkGraph;
        this.chunkGraph = chunkGraph;

        // 处理运行时依赖
        this.processRuntimeRequirements({
          chunkGraph,
          modules,
          chunks,
          codeGenerationResults,
          chunkGraphEntries: chunks
        });

        this.chunkGraph = old;

        const runtimeModules = chunkGraph.getChunkRuntimeModulesIterable(chunk);

        // 为运行时代码模块生成哈希
        for (const module of runtimeModules) {
          modules.add(module);
          this._createModuleHash(
            module,
            chunkGraph,
            runtime,
            hashFunction,
            runtimeTemplate,
            hashDigest,
            hashDigestLength,
            errors
          );
        }

        // 为运行时模块生成代码
        asyncLib.eachLimit(runtimeModules, 10, codeGen, err => {
          if (err) return callback(err);
          reportErrors();

          /** @type {Map<Module, ExecuteModuleArgument>} */
          const moduleArgumentsMap = new Map();
          /** @type {Map<string, ExecuteModuleArgument>} */
          const moduleArgumentsById = new Map();

          // 初始化模块依赖集合
          const fileDependencies = new LazySet();
          const contextDependencies = new LazySet();
          const missingDependencies = new LazySet();
          const buildDependencies = new LazySet();

          // 初始化生成的资源集合
          const assets = new Map();

          let cacheable = true;

          // 创建模块执行上下文
          const context = {
            assets,
            __webpack_require__: undefined,
            chunk,
            chunkGraph
          };

          // 准备每个模块的执行
          asyncLib.eachLimit(modules, 10, (module, callback) => {
            const codeGenerationResult = codeGenerationResults.get(module, runtime);
            const moduleArgument = {
              module,
              codeGenerationResult,
              preparedInfo: undefined,
              moduleObject: undefined
            };

            moduleArgumentsMap.set(module, moduleArgument);
            moduleArgumentsById.set(module.identifier(), moduleArgument);

            // 添加模块的依赖
            module.addCacheDependencies(
              fileDependencies,
              contextDependencies,
              missingDependencies,
              buildDependencies
            );

            // 判断是否可缓存
            if (module.buildInfo?.cacheable === false) {
              cacheable = false;
            }

            // 将模块的 asset 信息放入资源集合
            if (module.buildInfo && module.buildInfo.assets) {
              const { assets: moduleAssets, assetsInfo } = module.buildInfo;
              for (const assetName of Object.keys(moduleAssets)) {
                assets.set(assetName, {
                  source: moduleAssets[assetName],
                  info: assetsInfo ? assetsInfo.get(assetName) : undefined
                });
              }
            }

            // 执行 prepareModuleExecution 钩子
            this.hooks.prepareModuleExecution.callAsync(
              moduleArgument,
              context,
              callback
            );
          }, err => {
            if (err) return callback(err);

            let exports;
            try {
              const {
                strictModuleErrorHandling,
                strictModuleExceptionHandling
              } = this.outputOptions;

              // 自定义的 __webpack_require__ 实现
              const __webpack_require__ = id => {
                const cached = moduleCache[id];
                if (cached !== undefined) {
                  if (cached.error) throw cached.error;
                  return cached.exports;
                }
                const moduleArgument = moduleArgumentsById.get(id);
                return __webpack_require_module__(moduleArgument, id);
              };

              // 初始化模块缓存和拦截器
              const interceptModuleExecution = (__webpack_require__[RuntimeGlobals.interceptModuleExecution.replace(`${RuntimeGlobals.require}.`, "")] = []);
              const moduleCache = (__webpack_require__[RuntimeGlobals.moduleCache.replace(`${RuntimeGlobals.require}.`, "")] = {});

              context.__webpack_require__ = __webpack_require__;

              // 内部模块执行函数
              const __webpack_require_module__ = (moduleArgument, id) => {
                const execOptions = {
                  id,
                  module: {
                    id,
                    exports: {},
                    loaded: false,
                    error: undefined
                  },
                  require: __webpack_require__
                };

                // 执行所有模块拦截器
                for (const handler of interceptModuleExecution) {
                  handler(execOptions);
                }

                const module = moduleArgument.module;
                this.buildTimeExecutedModules.add(module);

                const moduleObject = execOptions.module;
                moduleArgument.moduleObject = moduleObject;

                try {
                  if (id) moduleCache[id] = moduleObject;

                  // 执行 executeModule 钩子
                  tryRunOrWebpackError(() => {
                    this.hooks.executeModule.call(moduleArgument, context);
                  }, "Compilation.hooks.executeModule");

                  moduleObject.loaded = true;
                  return moduleObject.exports;
                } catch (execErr) {
                  if (strictModuleExceptionHandling) {
                    if (id) delete moduleCache[id];
                  } else if (strictModuleErrorHandling) {
                    moduleObject.error = execErr;
                  }
                  if (!execErr.module) execErr.module = module;
                  throw execErr;
                }
              };

              // 执行所有 runtime 模块
              for (const runtimeModule of chunkGraph.getChunkRuntimeModulesInOrder(chunk)) {
                __webpack_require_module__(moduleArgumentsMap.get(runtimeModule));
              }

              // 执行入口模块
              exports = __webpack_require__(module.identifier());

            } catch (execErr) {
              const err = new WebpackError(
                `Execution of module code from module graph (${module.readableIdentifier(this.requestShortener)}) failed: ${execErr.message}`
              );
              err.stack = execErr.stack;
              err.module = execErr.module;
              return callback(err);
            }

            // 返回最终执行结果
            callback(null, {
              exports,
              assets,
              cacheable,
              fileDependencies,
              contextDependencies,
              missingDependencies,
              buildDependencies
            });
          });
        });
      });
    }
  );
}

checkConstraints()

总结:检查模块 ID 是否唯一,以及 chunkGraph 与 module 集合的一致性。

/**
 * 检查 chunkGraph 与模块集合的完整性:
 * - 没有重复 moduleId
 * - chunk 中所有模块都存在于 this.modules
 * - chunkGroup 的约束也会检查
 */
checkConstraints() {
const chunkGraph = this.chunkGraph;
const usedIds = new Set();

for (const module of this.modules) {
if (module.type === WEBPACK_MODULE_TYPE_RUNTIME) continue;
const moduleId = chunkGraph.getModuleId(module);
if (moduleId === null) continue;
if (usedIds.has(moduleId)) {
throw new Error(`checkConstraints: duplicate module id ${moduleId}`);
}
usedIds.add(moduleId);
}

for (const chunk of this.chunks) {
for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
if (!this.modules.has(module)) {
throw new Error(`checkConstraints: module in chunk but not in compilation ${chunk.debugId} ${module.debugId}`);
}
}
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
if (!this.modules.has(module)) {
throw new Error(`checkConstraints: entry module in chunk but not in compilation ${chunk.debugId} ${module.debugId}`);
}
}
}

for (const chunkGroup of this.chunkGroups) {
chunkGroup.checkConstraints();
}
}

webpack 核心编译器 十五 节

作者 excel
2025年4月6日 07:31

getAssets()

📌 功能

返回一个所有已生成资源(assets)的只读数组。

getAssets() {
/** @type {Readonly<Asset>[]} */
const array = [];
for (const assetName of Object.keys(this.assets)) {
if (Object.prototype.hasOwnProperty.call(this.assets, assetName)) {
array.push({
name: assetName,
source: this.assets[assetName],
info: this.assetsInfo.get(assetName) || EMPTY_ASSET_INFO
});
}
}
return array;
}

getAsset(name)

📌 功能

根据名称获取一个特定的资源(asset),如果没有找到则返回 undefined

getAsset(name) {
if (!Object.prototype.hasOwnProperty.call(this.assets, name)) return;
return {
name,
source: this.assets[name],
info: this.assetsInfo.get(name) || EMPTY_ASSET_INFO
};
}

clearAssets()

📌 功能

清空所有 chunk 中的主文件与辅助文件列表。

clearAssets() {
for (const chunk of this.chunks) {
chunk.files.clear();
chunk.auxiliaryFiles.clear();
}
}

createModuleAssets()

📌 功能

从模块的构建信息中提取出资源并注册到对应的 chunk 中,同时调用钩子。

createModuleAssets() {
const { chunkGraph } = this;
for (const module of this.modules) {
const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
if (buildInfo.assets) {
const assetsInfo = buildInfo.assetsInfo;
for (const assetName of Object.keys(buildInfo.assets)) {
const fileName = this.getPath(assetName, {
chunkGraph: this.chunkGraph,
module
});
for (const chunk of chunkGraph.getModuleChunksIterable(module)) {
chunk.auxiliaryFiles.add(fileName);
}
this.emitAsset(
fileName,
buildInfo.assets[assetName],
assetsInfo ? assetsInfo.get(assetName) : undefined
);
this.hooks.moduleAsset.call(module, fileName);
}
}
}
}

getRenderManifest(options)

📌 功能

生成给定 chunk 的渲染清单(RenderManifest),供后续生成实际的文件。

getRenderManifest(options) {
return this.hooks.renderManifest.call([], options);
}

createChunkAssets(callback)

为所有 chunk 生成最终的资源文件(即构建产物),包括代码块、source map、辅助文件等,并存储在 this.assets 中。

📌 功能

这是生成所有 chunk 的最终产物(assets)的主流程。支持缓存、冲突检测和异步并发控制。

createChunkAssets(callback) {
const outputOptions = this.outputOptions;

// 用于缓存 Source -> CachedSource 的映射,避免多次包装
const cachedSourceMap = new WeakMap();

// 检测是否有多个 chunk 输出了相同的文件名
const alreadyWrittenFiles = new Map();

// 并发处理每个 chunk,最多 15 个同时进行
asyncLib.forEachLimit(
this.chunks,
15,
(chunk, callback) => {
let manifest;

try {
// 调用钩子获取当前 chunk 的渲染清单(renderManifest)
manifest = this.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
codeGenerationResults: this.codeGenerationResults,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates,
chunkGraph: this.chunkGraph,
moduleGraph: this.moduleGraph,
runtimeTemplate: this.runtimeTemplate
});
} catch (err) {
// 如果获取 manifest 失败,记录错误并继续下一个 chunk
this.errors.push(new ChunkRenderError(chunk, "", err));
return callback();
}

// 并发处理 manifest 中的每一项输出文件(比如 main.js, chunk.js 等)
asyncLib.each(
manifest,
(fileManifest, callback) => {
const ident = fileManifest.identifier;
const usedHash = fileManifest.hash;

// 资源缓存项(根据 identifier + hash 唯一定位)
const assetCacheItem = this._assetsCache.getItemCache(ident, usedHash);

// 从缓存中尝试获取 source(如 main.js 的内容)
assetCacheItem.get((err, sourceFromCache) => {
let filenameTemplate;
let file;
let assetInfo;

// 统一处理错误:构建 ChunkRenderError 并调用回调
const errorAndCallback = err => {
const filename =
file ||
(typeof file === "string"
? file
: typeof filenameTemplate === "string"
? filenameTemplate
: "");
this.errors.push(new ChunkRenderError(chunk, filename, err));
return callback();
};

try {
// 确定输出文件名和资源信息
if ("filename" in fileManifest) {
file = fileManifest.filename;
assetInfo = fileManifest.info;
} else {
filenameTemplate = fileManifest.filenameTemplate;
const pathAndInfo = this.getPathWithInfo(
filenameTemplate,
fileManifest.pathOptions
);
file = pathAndInfo.path;
assetInfo = fileManifest.info
? { ...pathAndInfo.info, ...fileManifest.info }
: pathAndInfo.info;
}

if (err) return errorAndCallback(err);

let source = sourceFromCache;

// 文件名是否已由其他 chunk 使用?
const alreadyWritten = alreadyWrittenFiles.get(file);
if (alreadyWritten !== undefined) {
// 文件名冲突,且 hash 不同(表示内容不同),报错
if (alreadyWritten.hash !== usedHash) {
return callback(new WebpackError(
`Conflict: Multiple chunks emit assets to the same filename ${file}` +
` (chunks ${alreadyWritten.chunk.id} and ${chunk.id})`
));
}
// hash 相同,直接复用已生成的 source
source = alreadyWritten.source;
} else if (!source) {
// 没有缓存,调用 render 函数生成源码
source = fileManifest.render();

// 强制缓存化处理,避免多次访问 source 成本过高
if (!(source instanceof CachedSource)) {
const cacheEntry = cachedSourceMap.get(source);
if (cacheEntry) {
source = cacheEntry;
} else {
const cachedSource = new CachedSource(source);
cachedSourceMap.set(source, cachedSource);
source = cachedSource;
}
}
}

// 注册资源(添加到 this.assets 中)
this.emitAsset(file, source, assetInfo);

// 将文件归类到 chunk 中:主文件还是辅助文件?
if (fileManifest.auxiliary) {
chunk.auxiliaryFiles.add(file);
} else {
chunk.files.add(file);
}

// 调用 chunkAsset 钩子
this.hooks.chunkAsset.call(chunk, file);

// 记录此文件名已被使用
alreadyWrittenFiles.set(file, {
hash: usedHash,
source,
chunk
});

// 如果 source 是新生成的,存入缓存
if (source !== sourceFromCache) {
assetCacheItem.store(source, err => {
if (err) return errorAndCallback(err);
return callback();
});
} else {
callback();
}
} catch (err) {
errorAndCallback(err);
}
});
},
callback // 所有 manifest 处理完成后
);
},
callback // 所有 chunk 处理完成后
);
}


vue如何实现触摸板双指滑动(非长按滑动)

作者 fayeyoko
2025年4月5日 23:51

引入:模仿idea中文网站时,里面出现的一个轮播图,既可以通过按钮点击来实现图片的切换,也可以通过双指滑动来实现切换

屏幕截图 2025-04-05 221524.png

我首先实现的是按钮点击来实现图片切换,@click来监听两个按钮 屏幕截图 2025-04-05 221821.png

具体代码如下,不过多赘述 屏幕截图 2025-04-05 222103.png

屏幕截图 2025-04-05 222129.png

接下来写的即是触摸板滑动功能

一开始我写的只有@touchsart和@touchend,以此监听触摸板的滑动,代码如下,这个兼容性比较强,在手机上也可以实现滑动,但是在PC端,只能通过检查,再点击触摸板后滑动才能实现图片切换,具体原因暂时还不清楚。 屏幕截图 2025-04-05 222724.png

所以在后面我添加了@mousewheel监听事件,这个方法是监听鼠标滚轮事件,而两个手指滑动触摸板就相当于鼠标滚动事件;

而当鼠标滚轮滚动的时候,e.wheelDeltaX的值会变化,就意味着有了滚动,滚动值最后为负值是向右滚动了,滚动值最后为负值是向左滚动了,以此来写函数changeTouch; 屏幕截图 2025-04-05 223119.png

通过使用changeTouch方法调用左右切换,但同时也会出现一个问题,changeTouch被调用了多次,这是由于e.wheelDeltaX的值会多次变化,滚动一小点就会调用changeTouch一次,这个时候想到的就是节流函数throttle了;

屏幕截图 2025-04-05 223107.png

提出疑问:为什么节流函数一开始要在组件创建时调用,而不是在toMousewheel里面直接使用节流,换句话说,为什么要在created里面调用changeTouch去节流,而不是去toMousewheel里面节流?

回答:一开始写的时候是在调用节流函数时,直接让节流函数调用changeTouch,从而执行了一次,如下【错误写法】 28c5a7bb323f779572eccb584aa023f.jpg

在toMousewheel方法里,直接调用了this.changeTouch(e),这就使得changeTouch方法会被立即执行,而不是被节流,为什么?

因为changeTouch有参数,加了括号传参,当我们去调用有括号传参的函数时就是被执行了,但changeTouch不能被执行,只能被调用;正确的做法应该是把changeTouch函数本身传递给throttle函数,而非调用它的返回值,所以在created的时候赋值一个新变量,这个变量不是函数,所以不会被调用执行,即只会被调用而不会执行;

综上,节流函数第一参数的函数只能被调用,不能被执行,你可以填写函数名,但是不可以填函数名+括号(实际上是在立即执行这个函数),更不能填函数名+括号里面传参(这个时候这个函数会被立即执行,并且它的返回值【而不是函数本身】会被传递给throttle,这显然不是我们想要得,因为我们希望throttle在合适的时机调用函数,而不是立即执行它);

而对于这个toMousewheel方法里,我们直接去写就可以实现触摸板的双指滑动了,没有用到@touchsart和@touchend,把这两个代码保留的好处:这两个是触摸事件,手机端有触摸滑动事件,【有些电脑的触摸板支持“双击滚动”功能,即先用手指再触摸板上快速点击两次,双击后,触摸板会进入滚动模式,然后用户可以移动手指来模拟鼠标滚轮的滚动操作】,我这个要在检查的时候才可以实现电脑的触摸板“双击滚动功能”,具体原因暂时不知;所以保留可以让手机端也能够实现滑动功能。

拓展:(touchstart和mousewheel的区别)

touchstart是触摸事件,属于移动端或触摸设备的输入事件,用于处理触摸操作的初始阶段,例如要做PC端长按类型(长按复制、长按语音)【上面不用到touchstart是因为上面本质是滚轮事件,而不是长按事件,而移动端的长按滑动和滚轮有点类似,这种时候就不用多写一个滚轮事件了】;

mousewheel是鼠标事件,属于桌面端鼠标设备的输入事件。用于处理鼠标滚轮滚动事件,例如控制页面滚动、缩放内容或如上功能。

五分钟看懂 alien signals 依赖收集原理

作者 淋着141
2025年4月5日 23:10

众所周知,Vue 3.6 计划引入一个全新的响应式机制:alien-signals,用来进一步优化 Vue 的响应式系统。

目前 vue3.6 还没正式发布,但可以先通过以下命令打包alien-signals源码:

esbuild src/index.ts --bundle --format=esm --outfile=esm/index.mjs

打包后的代码还不到 500 行,体积小、结构也比较清晰。趁现在还不那么复杂,我正在尝试解析一下 alien-signals 的源码,顺便记录一些理解过程

首先我们先有一个 2x2 的单元测试,其中 fn1 和 fn2 分别有两个依赖 count1、count2

test("debugger 2*2", () => {
  const count1 = signal(1);
  const count2 = signal(100);
  effect(function fn1() {
    console.log(`effect1-> count1 is: ${count1()}`);
    console.log(`effect1-> count2 is: ${count2()}`);
  });
  effect(function fn2() {
    console.log(`effect2-> count1 is: ${count1()}`);
    console.log(`effect2-> count2 is: ${count2()}`);
  });
  count1(2);
  count2(200);
});

这是 signal 的源码(build 后)如下,重点关心这个 this,也就是 dep,后续我们用蓝色表示dep

function signal(initialValue) {
  return signalGetterSetter.bind({
    currentValue: initialValue,
    subs: void 0,
    subsTail: void 0,
  });
}

这是 effect 的源码(build 后)如下,重点关心这个 e,也就是 sub,后续我们用黄色表示sub

function effect(fn) {
  // sub
  const e = { fn, subs: void 0, subsTail: void 0, deps: void 0, depsTail: void 0, flags: 2 /* Effect */
  };
  ... 省略部分与当前单元测试无关的代码
  const prevSub = activeSub;
  activeSub = e;
  try {
    e.fn();
  } finally {
    activeSub = prevSub;
  }
  ...省略部分与当前单元测试无关的代码
}

在 effect 中会默认执行一次 fn 进行初始的依赖收集,当执行 fn1 时,我们可以得到这几个数据 image.png

在 fn1 中访问 count1()时就会link(this, activeSub),将当前的依赖和订阅关联起来

function signalGetterSetter<T>(this: Signal<T>, ...value: [T]): T | void {
  if (activeSub !== undefined) {
+   关注这个link
    link(this, activeSub);
  }
  return this.currentValue;
}

link这个函数会复用节点,如果无法复用,说明这是一个新的link,当前是第一次执行依赖收集,当然是新的一个link,所以会执行linkNewDep(dep1,sub1,undefined,undefined)

function link(dep: Dependency, sub: Subscriber): Link | undefined {
  // 获取当前这个sub的最后一个依赖
  const currentDep = sub.depsTail; 
  ...
  // 获取currentDep的下一个依赖,如果depsTail不存在,就是当前这个sub的第一个依赖
  // 这段逻辑主要在依赖触发后重新依赖收集有关,暂时不会执行这个if里面的逻辑,主要用于复用节点
  const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps;
  if (nextDep !== undefined && nextDep.dep === dep) {
    sub.depsTail = nextDep;
    return;
  }
  ...
  return linkNewDep(dep, sub, nextDep, currentDep);
}

linkNewDep会创建一个newLink节点,用于关联dep和sub

function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
): Link {
  const newLink: Link = {
    dep,
    sub,
    nextDep,
    prevSub: undefined,
    nextSub: undefined,
  };
  // 没有depsTail,表示currentDep不存在,表示这是一个新的sub,那么sub1的deps就指向dep1
  if (depsTail === undefined) {
    sub.deps = newLink;
  } else {
    depsTail.nextDep = newLink;
  }
  // 如果当前的dep没有订阅,那么dep1的subs指向第一个订阅sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
  return newLink;
}

第一次linkNewDep后的依赖收集如下

image.png

开始收集count2了,又进行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep2, sub1, undefined, dep1.depsTail)

function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> dep1-> depsTail的newLink
  if (depsTail === undefined) {
    // 不会执行
    sub.deps = newLink;
  } else {
    // 这次执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的dep2没有被订阅,那么dep2的subs指向第一个订阅sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    // 不会执行
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

第二次linkNewDep后的依赖收集如下 image.png

第一个effect就依赖收集完成了,现在准备开始第二个effect的依赖收集,根据effect的源码,我们知道会创建一个sub2的订阅,现在的依赖关系图如下图所示,就单纯的多了个sub2

effect(function fn2() {
  console.log(`effect2-> count1 is: ${count1()}`);
  console.log(`effect2-> count2 is: ${count2()}`);
});

image.png

执行fn2,正式进行依赖收集

  • 访问count1(),同样依次执行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep1, sub2, undefined, undefined)
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> undefined
  if (depsTail === undefined) {
    // 这次执行这个
    sub.deps = newLink;
  } else {
    // 不会执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的dep1已经被订阅,subs指向newLink-sub->sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    // 执行这个
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

这次依赖收集后,最新的关系图如下: image.png

  • 访问count2(),同样依次执行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep2, sub2, undefined, dep1.depsTail)
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> dep1-> depsTail的newLink
  if (depsTail === undefined) {
    // 不会执行
    sub.deps = newLink;
  } else {
    // 这次执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的已经被sub1订阅了
  if (dep.subs === undefined) {
    // 不会执行
    dep.subs = newLink;
  } else {
    // 这次执行这个
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

至此所有的依赖收集都完成了。 image.png

下面这是一个3x3的依赖收集关系图,头都画大了🤯

3x3-init.png

快速了解浏览器原理及工作流程

作者 Jenlybein
2025年4月5日 22:36

浏览器原理及工作流程


前言

当我们在地址栏输入网址并按下回车键,到最终页面完整呈现在屏幕上,这看似简单的操作背后隐藏着一系列精密的网络通信和浏览器处理流程。

浏览器工作流程大致分为以下步骤:DNS 解析 —— HTTP 请求资源 —— 解析 HTML 创建 DOM 树 —— 解析 CSS 创建 CSSOM —— 执行 Javascript —— 创建 Render 树 —— 布局Layout(重排)—— 绘制 Painting(重绘)

image.png 本文将详细探讨学习浏览器的原理及工作流程,助力前端开发。

一、导航

导航是加载网页的第一步。它指的是当用户通过点击一个链接在浏览器地址栏中写下一个网址提交一个表格等方式请求一个网页时发生的过程。

DNS 解析

浏览器访问网站的第一步是定位网页静态资源所在的服务器位置。

当我们输入example.com这样的域名时,实际上需要找到对应的IP地址才能建立连接。这个转换过程就是通过**DNS(Domain Name System)**查询完成的。如果我们以前从未访问过这个网站,就必须进行域名系统(DNS)查询。

DNS服务器相当于互联网的"电话簿",存储着域名与IP地址的映射关系。全球分布着600多台DNS根服务器,共同构成了这个分布式数据库系统。

DNS 查询实际是浏览器与这些DNS服务器的其中一个进行对话,要求找出与example.com 名称相对应的IP地址并返回。若查找失败,就会向浏览器发送错误信息。

DNS 查询只发生在我们第一次访问一个网站时(浏览器未储存该域名的解析记录)。查询后 IP 地址可能会被缓存一段时间,所以下次访问同一个网站会更快,因为不需要进行 DNS 查询。

image.png

TCP 连接

浏览器获取网站的 IP 地址后,将尝试通过 TCP (Transmission Control Protocol,传输控制协议) 三次握手(也称为 SYN-SYN-ACK,或者更准确的说是 SYN、SYN-ACK、ACK,因为 TCP 有三个消息传输,用于协商和启动两台计算机之间的TCP 会话),与持有资源的服务器建立连接。

TCP三次握手过程

  1. SYN:客户端发送SYN=1, Seq=X的同步报文(客户端测试服务端接收能力)
  2. SYN-ACK:服务器回应SYN=1, ACK=X+1, Seq=Y (服务端确认客户端发送能力,服务端测试客户端接收能力)
  3. ACK:客户端发送ACK=Y+1, Seq=Z(客户端确认服务端发送能力,客户端确认服务端接收能力,服务端确认客户端接收能力)

三次握手原因

  1. 确认双方的收发能力:通过往返通信验证客户端和服务器都能正常发送和接收数据
  2. 防止历史连接干扰:避免因网络延迟导致的旧连接请求突然到达服务器而产生错误

TCP协议特点

  • 面向连接的可靠传输
  • 提供流量控制和拥塞控制机制
  • 保证数据顺序和完整性

image.png

TLS 协商

对于通过 HTTPS 建立的安全连接,需要进行另一次握手。

**TLS(Transport Layer Security,传输层安全协议)**是SSL(Secure Sockets Layer)的继任者,是一种加密协议,设计用于在计算机网络上提供通信安全。

它位于传输层之上、应用层之下,为HTTP(HTTPS)、SMTP、FTP等应用层协议提供安全保障。

这种握手(TLS协商)决定了哪个密码将被用于加密通信,验证服务器,并在开始实际的数据传输之前建立一个安全的连接。

TLS 握手步骤

  1. 客户端 hello。浏览器向服务器发送一条信息,其中包含:
    • 客户端支持的最高TLS版本
    • 密码套件,即客户端支持的所有加密算法组合
    • 客户端随机数即一串随机字节(32字节,4字节时间戳+28字节随机数) 。
  2. 服务器 hello 和证书。服务器发回一条信息,其中包含:
    • 确定双方都将使用的TLS版本
    • 服务器SSL证书完整的证书链(从叶证书到可信根证书)
    • 服务器选择的密码套件
    • 服务器随机数
  3. 浏览器进行证书认证。通过以下步骤,浏览器就可以确定服务器就是访问目标。
    1. 检查证书有效期与吊销状态。
    2. 向颁发证书的机构核实服务器的 SSL 证书。
  4. 密钥交换
    1. 客户端生成46字节随机数,称为预主密钥(Pre-Master Secret),从服务器的 SSL 证书上获取公钥进行加密,之后发送给服务器。
    2. 服务器用私钥解密预主密钥
    3. 客户端和服务器各自用PRF(伪随机函数算法)独立计算得到主密钥主密钥=PRF(预主密钥,mastersecret,客户端随机数+服务端随机数)主密钥 = PRF(预主密钥,'mastersecret',客户端随机数+服务端随机数)
  5. 会话密钥生成
    • 客户端和服务器各自用PRF独立计算会话密钥会话密钥=PRF(主密钥,keyexpansion,客户端随机数+服务端随机数)会话密钥 = PRF(主密钥,'keyexpansion',客户端随机数+服务端随机数)
    • 根据服务器在服务器 hello中选定密码套件处理密钥,最终确定密钥。
  6. 客户端完成。浏览器向服务器发送一个消息,说它已经完成。
  7. 服务器完成。服务器向浏览器发送一个消息,表示它也完成了。
  8. 安全对称加密实现。握手完成,通信可以继续使用会话密钥。

现在可以开始从服务器请求和接收数据了。

image.png

二、获取资源

浏览器获取资源的方式主要靠 HTTP 通信。

在我们与服务器建立安全连接后,浏览器将发送一个初始的 HTTP GET 请求。首先,浏览器将请求页面的 HTML 文件。

URI

URI 是统一资源识别符的缩写,用于识别互联网上的抽象或物理资源,如网站或电子邮件地址等资源。一个 URI 最多可以有 5 个部分。

示例:https://example.com/users/user?name=Alice#address

组成部分 示例值 说明
scheme https: 使用的协议
authority example.com 域名
path users/user 资源路径
query name=Alice 请求参数
fragment address 资源片段标识

HTTP 请求

HTTP(Hypertext Transfer Protocol,超文本传输协议) 是一个获取资源的协议,如HTML文件。它是网络上任何数据交换的基础,它是一个客户 - 服务器协议,这意味着请求是由接收者发起的,通常是网络浏览器。

请求方法 - POST, GET, PUT, PATCH, DELETE 等

HTTP 头字段 - 是浏览器和服务器在每个 HTTP 请求和响应中发送和接收的字符串列表(它们通常对终端用户是不可见的)。在请求的情况下,它们包含关于要获取的资源或请求资源的浏览器的更多信息。

HTTP 响应

一旦服务器收到请求,它将对其进行处理并回复一个 HTTP 响应。在响应的正文中,我们可以找到所有相关的响应头和我们请求的HTML文档的内容

状态代码 - 例如:200、400、401、504网关超时等(我们的目标是 200 状态代码,因为它告诉我们一切正常,请求是成功的)

响应头字段 - 保存关于响应的额外信息,如它的位置或提供它的服务器。

相应体 - 包含返回的内容

TCP 慢启动和拥塞算法

TCP 慢启动 是一种平衡网络连接速度的算法。

  1. 慢启动
    • 初始传输14kb或更小的数据包
    • 逐步增加传输量直至达到阈值
    • 确保网络连接不被过载
  2. 拥塞控制
    • 从服务器接收到每个数据包后,通过以 ACK 消息响应,监测网络状况。
    • 由于连接容量有限,若服务器发送太多数据包太快,它们将被丢弃。
    • 当数据包丢失时(表现为客户端不相应ACK),服务端自动降低传输速率。
    • 监控发送的数据包和 ACK 消息的流,动态调整传输速率以维持稳定流量。

三、HTML 解析

在向服务器发出初始请求后,浏览器收到 HTML 资源(第一块数据)的响应。 现在浏览器的工作就是开始解析数据。

解析是指将程序分析并转换为运行时环境实际可以运行的内部格式。换句话说,将代码(HTML、CSS)转换为浏览器可以使用的内容。

解析将由浏览器引擎完成(不要与浏览器的 Javascript 引擎混淆)。

浏览器引擎是每个主要浏览器的核心组件,它的主要作用是结合结构 (HTML) 和样式 (CSS),以便它可以在我们的屏幕上绘制网页。 它还负责找出哪些代码片段是交互式的。 我们不应将其视为一个单独的软件,而应将其视为更大软件(在我们的例子中为浏览器)的一部分。

HTML 解析涉及两个步骤:词法分析树构造

词法分析阶段

词法分析器将 HTML 文本分解为基本语法单元(token),类似于将英文句子分解为单词的过程。分析结果包含以下类型的token:

  • DOCTYPE声明
  • 开始标签(<tag>
  • 结束标签(</tag>
  • 自闭合标签(<tag/>
  • 属性名
  • 属性值
  • 注释内容
  • 文本内容
  • 文件结束标记

image.png

DOM 树构建阶段

创建第一个 token 后,树构建开始。 这实质上是基于先前解析的标签创建树状结构(称为文档对象模型)。**DOM 树(Document Object Model)**描述了 HTML 文档的内容。

基于词法分析产生的 token 序列,解析器构建文档对象模型(DOM)树:

  1. <html>元素作为文档根节点
  2. 元素间的嵌套关系形成父子节点结构
  3. 深度越大的DOM树需要更长的构建时间

image.png

实际上,DOM 比该模式中看到的更复杂,此处保持简单以便更好地理解。

此构建阶段是可重入的,这意味着在处理一个 token 时,分词器可能会恢复,导致在第一个 token 处理完成之前触发并处理更多 token。从字节到创建 DOM,整个过程如下所示:

3-3.webp

  • 非阻塞资源:异步加载,不中断解析
    • 如图像
  • 阻塞资源:暂停解析直至加载完成
    • 如CSS 样式表、<head> 部分添加的 Javascrpt 文件、从 CDN 添加的字体

解析器从上到下逐行工作。 当解析器遇到非阻塞资源时,浏览器会向服务器请求资源并继续解析。 若它遇到阻塞资源,解析器将停止执行,直到所有这些阻塞资源都被下载。

这解释了为何建议以下操作:将<script> 标签置于 HTML 文件的末尾,或<script> 标签置于 <head> 标签时添加 defer 或 async 属性( async 允许在下载脚本后立即执行异步操作,而 defer 只允许在整个文档被解析后执行。)。

预加载器

现代浏览器采用预加载器优化策略,作为处理阻塞资源的一种方式,尤其是脚本:

  1. 当遇到阻塞脚本时,第二个轻量级预加载器扫描后续 HTML 查找需要检索的资源(样式表、脚本等)。
  2. 预加载器在后台将找到的需检索的资源提前发送请求。

预加载器的作用:在主 HTML 解析器遇到阻塞资源时,它们可能已经被下载(如果这些资源已经被缓存,则跳过此步骤)。

优势:显著减少资源等待时间、充分利用网络空闲带宽、对缓存资源实现快速加载。

四、解析 CSS

解析完 HTML 之后,轮到解析 **CSS(Cascading Style Sheets)**代码,并构建 CSSOM 树(CSS 对象模型)。

就像从 HTML 构建 DOM 一样,从 CSS 构建 CSSOM 被认为是一个 渲染阻塞 过程。

词法分析和构建 CSSOM

与 HTML 解析类似,CSS 解析从词法分析开始:

  1. 字节流解码:将网络字节转换为UTF-8字符
  2. Token生成:识别@规则、选择器、声明块等语法单元
  3. 规则集构建:将相关规则分组存储
  4. 样式计算:处理继承与层叠逻辑

CSS 规则是从右到左阅读的,若有代码: section p { color: blue; }, 浏览器将先找页面上的所有 p 标签,然后它会查看这些 p 标签中是否有一个 section 标签作为父标签。

样式计算

浏览器按特定顺序处理样式规则,越往后的样式会覆盖之前的样式(级联规则处理):

  1. 继承机制:浏览器从适用于节点的最通用规则开始,子元素继承可继承属性
    • 例如:如果某节点是 body 元素的子节点,该节点继承所有 body 可继承样式
  2. 默认样式:应用浏览器默认样式表
  3. 作者样式:处理开发者定义的样式
  4. 重要声明:应用!important规则
  5. 内联样式:处理元素style属性
选择器匹配

优先级按以下规则计算(从高到低):

  1. 内联样式(1000分)
  2. ID选择器(100分/个)
  3. 类/属性/伪类(10分/个)
  4. 元素/伪元素(1分/个)

假设我们有下面的 HTML 和 CSS:

body {
  font-size: 16px;
  color: white;
} 
h1 {
  font-size: 32px;
}
section {
  color: tomato;
}
section .mainTitle {
  margin-left: 5px
}
div {
  font-size: 20px;
}
div p {
  font-size:  8px;
  color: yellow;
}

这段代码的 CSSOM 看起来像这样:

image.png

五、执行 Javascript

在 Javascript 代码资源获取后,浏览器会调用 Javascript 引擎对代码进行编译。

Javascript 引擎

JavaScript引擎是现代Web技术的核心组件,它将高级JavaScript代码转换为机器可执行的指令。(不要与浏览器引擎混淆)。 根据浏览器的不同,JS 引擎可以有不同的名称和不同的工作方式。

引擎名称 所属浏览器/环境 开发语言 显著特性
V8 Chrome, Node.js, Edge C++ 分层编译架构
JavaScriptCore Safari C++ 低级字节码优化
SpiderMonkey Firefox C++/Rust 多阶段编译管道
Chakra Legacy C++ 后台JIT编译

Javascript 引擎有 3 种工作方式:编译解释即时编译( JIT Compilation )

  • 编译:将用高级语言编写的代码一次性转换为机器代码。
  • 解释:逐行检查 Javascript 代码并立即执行。
  • 即时编译( JIT Compilation ):即时编译是给定语言的解释器的一个特性,它试图同时利用编译和解释。 在 JIT 编译中,代码在执行时(在运行时)被编译。 可以说源代码是动态转换为机器代码的。 较新版本的 Javascript 使用这种类型的代码执行。

事实上,尽管 Javascript 是一种解释型语言(它不需要编译),但如今大多数浏览器都会使用 JIT 编译来运行代码,而不是纯粹的解释型语言。

JIT 编译的一个很重要的方面就是将源代码编译成当前正在运行的机器的机器码指令。 这意味着生成的机器代码是针对正在运行的机器的 CPU 架构进行了优化。

Javascript 处理

当 Javascript 代码进入 Javascript 引擎时,它首先被解析。 这意味着代码被读取,并且在这种情况下,代码被转换为称为抽象语法树 (AST) 的数据结构。 代码将被拆分成与语言相关的部分(如 functionconst 关键字),然后所有这些部分将构建抽象语法树。

构建 AST 后,它会被翻译成机器代码并立即执行,因为现代 Javascript 使用即时编译。 这段代码的执行将由 Javascript 引擎完成,利用称为“调用堆栈”的东西。

六、创建可访问(无障碍)树

除了我们一直在讨论的所有这些树(DOM、CSSOM 和 AST)之外,浏览器还构建了可访问(无障碍)树(Accessibility Tree,ACT)

可访问性树是浏览器基于DOM构建的语义化表示结构,它充当了网页内容与辅助技术之间的桥梁。与渲染树不同,ACT专注于内容的语义表达而非视觉呈现。在未构建 ACT 之前,屏幕阅读器无法访问内容。

一般而言,残疾用户可以并且确实在使用具有各种辅助技术的网页。 他们使用屏幕阅读器、放大镜、眼动追踪、语音命令等。 为了让这些技术发挥作用,它们需要能够访问页面的内容。 由于他们无法直接读取 DOM,因此 ACT 开始发挥作用。

构建可访问的Web应用不仅是道德责任,在许多地区更是法律要求。通过深入理解可访问性树的工作原理,开发者可以创建出真正包容的数字产品,服务全球13亿残障人士。记住:优秀的无障碍实现应该像氧气一样 —— 最好的设计是那些不被注意到却始终存在的设计。

七、渲染树

**渲染树(Render Tree)**是浏览器将 DOM 和 CSSOM 合并后生成的中间表示,它决定了哪些内容最终会显示在屏幕上。渲染树的目的是确保页面内容以正确的顺序绘制元素。 它将作为在屏幕上显示像素的绘画过程的输入。

构建过程遵循以下步骤:

  1. 节点筛选:从 DOM 树的根部开始遍历DOM树,找到需渲染节点,忽略以下节点:
    • 不可见元素(<meta><script>等)
    • 被CSS隐藏的节点(display: none
  2. 样式匹配:为每个可见节点查找CSSOM中的对应规则
  3. 树结构生成:组合形成包含内容与样式的渲染树

image.png

布局(重排)阶段

渲染树包含有关显示哪些节点及其计算样式的信息,但不包含每个节点的尺寸或位置。

接下来需要做的是计算这些节点在设备视口(浏览器窗口内)内的确切位置及其大小。 这个阶段称为布局或重排。浏览器在渲染树的根部开始这个过程并遍历它。

浏览器通过以下步骤计算渲染树节点的几何信息:

  1. 视口尺寸计算:基于viewport元标签
  2. 从根节点开始遍历:计算每个节点的精确位置和尺寸
  3. 盒模型生成:确定margin/border/padding/content区域
  4. 坐标系建立:转换为屏幕绝对坐标

布局步骤不只发生一次,每次更改 DOM 中影响页面布局的某些内容时,都会触发重排。

触发重排的常见操作:

操作类型 示例 性能影响
几何属性变更 修改width/height
位置属性变更 改变position/float
内容变化 文本增减/图片尺寸变化 中-高
样式查询 offsetTop/getComputedStyle 潜在触发

绘画(重绘)阶段

在浏览器决定哪些节点需要可见并计算出它们在视口中的位置后,就可以开始在屏幕上绘制它们(渲染像素)。 这个阶段也被称为光栅化阶段,浏览器将在布局阶段计算的每个盒子信息转换为屏幕上的实际像素。

就像布局阶段一样,绘画阶段也不只发生一次,每次改变屏幕上元素的任何外观时都会触发重绘。

绘制顺序

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 子节点
  5. 轮廓

分层和合成

绘画意味着浏览器需要将元素的每个视觉部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换元素(如按钮和图像),并且需要超快地完成。

为了确保重绘可以比初始绘制更快地完成,屏幕上的绘图通常被分解成几层,绘画完成后进行合成。

通过合理利用分层和合成,可以显著提升复杂页面的渲染性能,尤其在动画和滚动场景下效果明显。但使用分层时,需占用更多的内存来获得支持。

分层(Layers) :浏览器将页面的各个部分分解为多个独立的图层,每个图层包含页面的特定部分(如文本、图片、动画元素等)。

合成(Composition) :合成是浏览器将多个图层按正确顺序在称为合成器线程的单独线程中合并为最终屏幕图像的过程。

当文档的各个部分绘制在不同的层中并相互重叠时,合成是必要的,以确保它们以正确的顺序绘制到屏幕上并且内容被正确呈现。

image.png

为了找出哪些元素需要在哪一层,主线程遍历布局树并创建层树。 默认情况下只有一层(这些层的实现方式因浏览器而异),但我们可以找到会触发重绘的元素,并为每个元素创建一个单独的层。 这样重绘不应应用于整个页面,而且此过程将可以使用到 GPU。

如果我们想向浏览器提示某些元素应该在一个单独的层上,我们可以使用 will-change CSS 属性。

触发分层的常见属性:

属性 示例 说明
transform: 3D translateZ(0)rotateY(45deg) 3D变换会触发独立图层
will-change will-change: transform 提示浏览器元素即将变化
opacity(动画中) opacity: 0.5 → 1 透明度变化可能触发分层
<video>/<canvas> 视频、Canvas元素 默认独立图层
position: fixed 固定定位元素 避免滚动时重绘

关键区别

阶段 触发条件 性能成本 优化目标
重排 修改布局(如宽度、位置) 减少DOM操作,批量更新
重绘 修改外观(如颜色、透明度) 使用CSS硬件加速属性
合成 图层变换(如移动、缩放) 优先使用transform/opacity

参考文章

🔬 深度解析:前端异步模型的本质机制与工程落点

作者 DoraBigHead
2025年4月5日 22:23

一、前言:你以为的“异步”,可能只是“异象”

很多人觉得异步模型是指 setTimeout、Promise、async/await 的执行顺序问题。但我们要讨论的是:

  • 异步是如何调度的?
  • 谁在管理这些任务?
  • 什么是“事件循环”?它真的存在吗?
  • 浏览器或 Node.js 是如何实现这套模型的?

二、宏观认知:JS 的执行环境不是 JS 自己

❗ JS 本身不支持异步!

JS 引擎(如 V8)只有一件事:执行 JS 代码。
一旦你调用异步 API(如 setTimeout),其实是调用了宿主提供的功能,比如:

API 实际由谁实现
setTimeout 浏览器定时器线程 / libuv
fetch 浏览器网络线程
fs.readFile Node.js IO 线程池
Promise 本身 JS 引擎调度(V8 内部)

关键点:JavaScript 是语言,异步能力是运行时赋予的。


三、事件循环:不存在的“循环结构”

“事件循环”这个名字很容易让人误解成 while(true) 那种循环,其实它是调度协议,不是 JS 代码结构。

🔄 它是如何调度的?

浏览器(或 Node.js)维护多个任务队列,遵循如下规则:

  1. 执行一个宏任务(macro task)
  2. 清空所有微任务(micro tasks)
  3. 处理渲染或 I/O
  4. 重复上述流程

四、任务源(Task Source) & 执行优先级

按照 WhatWG HTML 标准,任务来源大致如下:

类型 属于哪种任务队列 优先级
setTimeout 宏任务
Promise.then 微任务
queueMicrotask 微任务
requestAnimationFrame 渲染前回调 特殊
MessageChannel 宏任务(消息任务) 中等
fetch().then 微任务
mutationObserver 微任务

五、微任务队列的本质:V8 实现分析

🔍 microtask checkpoint

在 V8(以及 SpiderMonkey、JavaScriptCore)中,每次执行完一个任务后,都会进入一个叫:

MicrotaskCheckpoint 的阶段

这个阶段里,会:

  • 检查是否注册了微任务;
  • 依次同步执行这些任务(FIFO);
  • 若微任务中又注册新微任务,会继续直到清空为止;
  • 若抛出错误,也会进入 host 的错误处理机制。
// V8 调度伪代码
RunOneTask() {
  ExecuteMacroTask();
  RunMicrotasks(); // microtask checkpoint
  MaybeRender();
}

🌊 微任务是“嵌套执行”的

这就是为什么你可以写出“递归注册微任务”的代码:

Promise.resolve().then(function foo() {
  console.log('tick');
  Promise.resolve().then(foo);
});

输出是无限刷屏,因为每个微任务执行后注册一个新的微任务。


六、await 背后到底发生了什么?

📖 编译产物 VS 执行策略

async function run() {
  await sleep(1000);
  console.log('after');
}

其实会被编译成:

function run() {
  return sleep(1000).then(() => {
    console.log('after');
  });
}

但 V8 的执行策略是:

  1. 遇到 await,保存当前执行上下文(ExecutionContext)
  2. 立即返回控制权
  3. 将后续代码注册为微任务,等待 Promise 解析完成
  4. 由微任务调度器恢复执行上下文并接着跑

👉 所以说 await 并不会阻塞线程,它只是“中断后注册续接任务”,让出 CPU 控制权。


七、真正的异步线程:Web Worker / libuv threadpool

💡 Web Worker(浏览器)

  • 属于浏览器提供的真正多线程
  • 没有 DOM 权限(沙箱模型)
  • 可以并行执行 heavy task,不阻塞 UI

🧵 Node.js 的 libuv threadpool

  • Node 本身是单线程事件循环
  • 但 IO 会被调度进 threadpool(最多 4 线程,可调)
  • 通过事件机制通知主线程执行回调

这才是现代 JS 环境里“真·并行”的部分,Event Loop 并不意味着整个程序只有一个线程。


八、你从没思考过但必须知道的深度问题

❓ 为什么 setTimeout(fn, 0) 不是立即执行?

→ 它只是“最快加入下一个宏任务队列”,而不是立即抢执行栈。

❓ 微任务为什么必须先于下一个宏任务?

→ 因为这样才不会出现状态残留,比如 .then() 中变更了值,必须马上被消费。

❓ requestAnimationFrame 为什么总是最后执行?

→ 它是专为视觉帧刷新设计,每次浏览器准备渲染前才会调用一次。插在事件循环之后,重绘之前。


九、工程落地中的异步细节

⏱ 如何避免 setTimeout 造成节奏不一致?

const frameTime = 1000 / 60;
let last = Date.now();

setTimeout(function loop() {
  const now = Date.now();
  const delta = now - last;
  last = now;
  logic(delta);
  setTimeout(loop, frameTime - (delta % frameTime));
}, frameTime);

用于实现和 requestAnimationFrame 类似的帧稳定调度


🔚 总结:事件循环不是 JS 的语法,是宿主环境的调度协议

异步模型 ≠ Promise
异步模型 = JS 执行模型 + 调度协议 + 宿主 API + 多线程协作机制

掌握异步模型,不能靠死记执行顺序,而是要:

  • 看标准(HTML、ECMA、WHATWG)
  • 看实现(V8、libuv、Chromium)
  • 看调度机制(任务队列、微任务、渲染帧)

学习笔记:企业级Git代码规范与协作指南💖

2025年4月5日 21:15

企业级Git代码规范与协作指南

微信图片_20250405211246.jpg

一、分支管理规范

(一)核心分支体系

分支类型 基线来源 功能场景 合并流向 环境对应 存活周期
master - 生产环境稳定版本 仅接收release/hotfix PRO 永久
develop master 最新开发基线(含已修复BUG) 接收feature/test DEV 永久
feature/* develop 新功能开发(功能模块维度) 合并回develop - 功能开发周期
test develop 测试环境功能验证 接收feature合并 FAT 版本测试周期
release/* test 预发布环境验收 合并到master UAT 版本发布周期
hotfix/* master 紧急生产问题修复 合并到master/develop - 修复验证周期

(二)分支命名规则

  1. 语义化前缀feature/login_module(功能模块) hotfix/order_payment(紧急修复) release/v2.3.0(版本号)
  2. 生命周期管理: 功能分支开发完成后执行git branch -d feature/xxx清理

二、提交规范体系

1. 提交类型(Type)

类型 适用场景 示例
feat 新功能开发 feat(user): 新增第三方登录
fix BUG修复 fix(payment): 修复金额计算错误
refactor 重构代码(不改变功能) refactor(api): 优化接口层结构
perf 性能优化 perf(image): 压缩静态资源
test 测试用例变更 test(utils): 增加日期函数测试
chore 构建/依赖变更 chore: 升级webpack至v5
docs 文档变更 docs: 补充接口文档
2. 提交内容控制
  • 原子化提交:每个commit仅完成单一功能修改
  • 强制验证:通过pre-commit钩子检查代码规范
# 修改最近提交
git commit --amend -m "feat: 完善用户权限校验逻辑" 

三、环境治理策略

(一)环境与分支映射

环境标识 对应分支 访问权限 核心用途
DEV develop 开发人员 日常联调/单元测试
FAT test 测试团队 功能验收测试
UAT release/* 产品/客户 用户验收测试
PRO master 全体用户 生产环境运行

(二)分支保护机制

  1. master分支

    • 强制Code Review(至少2人)
    • 要求通过CI流水线(单元测试+代码扫描)
    • 禁止Force Push
  2. release分支

    • 仅允许从test分支合并
    • 触发自动化回归测试套件

四、最佳实践建议

  1. Git Flow可视化: 使用git log --graph --oneline查看分支拓扑结构

  2. 自动化治理: 配置Husky+Commitlint实现提交信息规范检查

  3. Code Review原则

    • 单次PR不超过500行变更
    • 聚焦业务逻辑而非代码风格(由工具保障)
    • 采用「三段式评审法」:架构设计 → 代码实现 → 异常处理

实施价值:某电商平台采用该规范后,代码冲突率降低63%,生产事故减少42%,功能交付周期缩短28%。建议团队结合SonarQube等代码质量平台形成完整治理闭环。

vue2源码记录(2)

作者 醋醋
2025年4月5日 21:11

组件化思想

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  // context.$options._base 在 initGlobalAPI 中定义, 就是 Vue 本身
  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  // 组件局部注册
  if (isObject(Ctor)) {
    // 相当于调用 Vue.extend,得到一个构造器
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  // 如果不是一个函数,返回错误,组件定义有问题
  if (typeof Ctor !== "function") {
    if (process.env.NODE_ENV !== "production") {
      warn(`Invalid Component definition: ${String(Ctor)}`, context);
    }
    return;
  }

  // 对异步组件的处理
  // async component
  let asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.

      // 是创建一个注释节点vnode
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }

  data = data || {};

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 构造器配置合并
  resolveConstructorOptions(Ctor);

  // transform component v-model data into props & events
  // 组件的 v-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot;
    data = {};
    if (slot) {
      data.slot = slot;
    }
  }

  // 安装一些组件的钩子
  // install component management hooks onto the placeholder node
  installComponentHooks(data);

  // return a placeholder vnode
  const name = Ctor.options.name || tag;
  // 创建组件 VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode);
  }

  return vnode;
}

构造子类构造函数

把编写的对象组件转变为一个构造函数

  // context.$options._base 在 initGlobalAPI 中定义, 就是 Vue 本身
  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  // 组件局部注册
  if (isObject(Ctor)) {
    // 相当于调用 Vue.extend,得到一个构造器
    Ctor = baseCtor.extend(Ctor);
  }

extend方法


  // 使用 Vue 构造器,创建一个“子类”,该子类同样支持进一步的扩展
  // 扩展时可以传递一些默认配置,就像 Vue 也会有一些默认配置
  // 默认配置如果和基类有冲突则会进行选项合并(mergeOptions)
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid

    // 判断缓存中有没有存在,有就直接使用
    // 比如:多次调用 Vue.extend 传入同一个配置项(extendOptions),这时就会启用该缓存
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 校验组件名
      validateComponentName(name)
    }

    // 定义 Sub 构造函数,和 Vue 构造函数一致
    const Sub = function VueComponent (options) {
      // 里面也是和 Vue 构造函数一样,使用 this._init 进行初始化
      this._init(options)
    }
    // 通过寄生组合继承 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 将 Vue 的配置合并到自己的配置里
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // 将 props 代理到 Sub.prototype._props 对象上
    // 在组件内可以通过 this._props 的方式访问
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 将 computed 代理到 Sub.prototype 对象上
    // 在组件内可以通过 this.computed[key] 的方式访问
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 定义组件的 extend、mixin、use,允许在 Sub 基础上再进一步构造子类
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    // 定义 component、filter、directive 三个静态方法
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })

    // enable recursive self-lookup
    // 如果组件设置了 name 属性,将自己注册到自己的 components 选项中,这也是递归组件的原理
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 把继承后的 Sub 缓存,好处: 当下次创建 Sub 时,发现已有,就直接使用
    cachedCtors[SuperId] = Sub
    return Sub
  }

采用继承的方式把对象转换为继承自Vue的构造器Sub并返回,做了一些额外的处理比如name,props,computed等,并且进行了缓存。 实例化Sub时,会执行this._init进行Vue实例的初始化。

安装组件钩子函数

VNode在patch流程中对外暴露了各种时机的钩子函数,方便做一些额外的事情。

// 将componentVNodeHooks 钩子函数合并到组件data.hook中
function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {});
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i];
    const existing = hooks[key];
    const toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}

内置的hooks

const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      // keep-alive 包裹的组件走这里
      const mountedNode: any = vnode; // work around flow
      // 只调用 prepatch 更新实例属性
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // createComponentInstanceForVnode 会 new Vue 构造组件实例并赋值到 componentInstance
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      // 挂载组件-因为在执行_init时,没有el,所以组件自己接管了$mount过程
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions;
    const child = (vnode.componentInstance = oldVnode.componentInstance);
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },

  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode;
    // 首次渲染,执行的是 mounted 钩子,不会去执行 updated 钩子
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true;
      callHook(componentInstance, "mounted");
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance);
      } else {
        activateChildComponent(componentInstance, true /* direct */);
      }
    }
  },

  destroy(vnode: MountedComponentVNode) {
    const { componentInstance } = vnode;
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent(componentInstance, true /* direct */);
      }
    }
  },
};

实例化vnode

const name = Ctor.options.name || tag;
  // 创建组件 VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );
  return vnode;

实例化一个vnode并返回,和普通元素的vnode不同,组件的vnode没有children。

patch中createComponent


  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data // 这个是 组件的 VNodeData
    if (isDef(i)) {
       // isReactivated 用来判断组件是否缓存。
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 执行组件初始化的内部钩子 init
        i(vnode, false /* hydrating */ )
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // 插入顺序:先子后父
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

vnode是组件VNode时,i就是init钩子,init钩子函数调用 createComponentInstanceForVnode创建Vue实例。


export function createComponentInstanceForVnode(
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  // parent:是 activeInstance, 在 lifecycle 中
  parent: any // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent,
  };
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 执行 vue 子组件实例化
  return new vnode.componentOptions.Ctor(options);
}

vnode.componentOptions.Ctor是构造子类构造函数的Sub,相当于new Sub(options).在执行实例的初始化方法时,组件则会调用initInternalComponent方法。


// 组件的配置合并
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

组件$mount方法之后执行的render方法中,将当前组件渲染的vnode的parent执行父VNode_parentVnode,建立父子关系。在update方法中,

vm._vnode.parent === vm.$vnode

在$mount之前会调用initLifecycle(vm)方法。

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

vm.$parent用来保留当前vm的父实例,将当前vm存储到父实例的$children中。在update过程中,使用prevActiveInstance保留父实例。在完成固件patch之后,如果组件patch过程中又创建了子组件,DOM的插入顺序是先子后父。

总结:

组件化创建三个关键的步骤是:构造子类构造函数、安装组件钩子函数和实例化vnode。因为组件被转化为继承自Vue的子类构造函数,在初始化过程中也是调用_init方法,经过render方法创建虚拟DOM,通过patch和createElm创建真实DOM,和使用普通元素有一些区别,特别是父子关系的处理,在DOM插入时深度遍历,递归调用,先子后父。

npm、Yarn、pnpm Workspace 对比

作者 EricXJ
2025年4月5日 20:56

npm、Yarn、pnpm Workspace 对比

一、核心机制差异

1. 依赖存储架构对比

# 项目结构
monorepo/
├─ package.json
└─ packages/
   ├─ lib1/package.json
   └─ lib2/package.json
包管理器 node_modules 结构 示例场景(安装 lodash)
npm 提升到根目录(hoisting) 所有包共享根目录的 包
Yarn 选择性提升(通过 nohoist 配置) 部分依赖保留在子包 node_modules
pnpm 虚拟存储 + 硬链接(.pnpm 目录) 所有包共享全局存储的硬链接

2. 符号链接实现

# 查看 lib1 的真实依赖路径
npm ls lodash         # → ../../node_modules/lodash (npm/Yarn)
pnpm ls lodash        # → .pnpm/lodash@4.17.21/node_modules/lodash (pnpm)
特性 npm/Yarn pnpm
链接类型 软链接(symlink) 硬链接 + 符号链接组合
跨磁盘支持 ❌(硬链接限制)
修改同步 实时双向同步 写时复制(CoW)机制

二、关键命令差异

1. 多包操作命令

# 在所有子包运行 build 命令
npm run build --workspaces       # npm
yarn workspaces foreach run build # Yarn
pnpm -r run build                # pnpm

# 过滤特定包
npm run dev --workspace=lib1     # npm
yarn workspace lib1 run dev      # Yarn
pnpm --filter lib1 run dev       # pnpm

2. 依赖安装差异

# 为所有子包安装 lodash
npm install lodash -ws           # npm(v7+)
yarn add lodash -W               # Yarn(根目录安装)
pnpm add lodash -r               # pnpm(递归安装)

# 添加跨包依赖(lib1 依赖 lib2)
cd packages/lib1
npm install ../lib2              # 自动生成 "lib2": "file:../lib2"
yarn add ../lib2                 # 同上
pnpm add ../lib2                 # 生成 workspace: 协议

三、幽灵依赖防御对比

场景示例

// packages/lib1/index.js
import _ from 'lodash' // 但未在 package.json 声明依赖
包管理器 结果 防御机制
npm ✅ 正常运行 无,依赖提升导致可访问
Yarn ⚠️ 部分失败 非提升依赖会报错
pnpm ❌ 立即报错 严格隔离,未声明依赖无法访问
# pnpm 的错误信息
Error: Cannot find module 'lodash'
  Require stack:
  - /monorepo/packages/lib1/index.js

四、特殊场景处理

1. 混合公私依赖

# 私有包(未发布)与公有包的混合使用
# npm/Yarn 需要手动配置
"dependencies": {
  "public-lib": "^1.0.0",
  "private-lib": "file:../private-lib"
}

# pnpm 自动处理
"dependencies": {
  "public-lib": "workspace:*",
  "private-lib": "workspace:../private-lib"
}

2. 依赖版本冲突

# 包A需要 lodash@4.17,包B需要 lodash@4.18
# npm/Yarn 的 node_modules 结构:
node_modules/
  └─ lodash(4.18)
  └─ packageA/node_modules/lodash(4.17# pnpm 的存储结构:
.pnpm/
  ├─ lodash@4.17.0/
  ├─ lodash@4.18.0/
  └─ store(硬链接)

六、选择决策流程图

graph TD
    A[需要 Monorepo?] --> B{项目规模}
    B -->|小型项目| C[选择 npm Workspace]
    B -->|中型项目| D[pnpm + 基础脚本]
    B -->|大型企业级| E[Yarn + Turborepo]
    
    A --> F{关键需求}
    F -->|磁盘空间敏感| G[pnpm]
    F -->|生态兼容性优先| H[npm]
    F -->|现有 Yarn 项目迁移| I[Yarn Workspace]

总结:Workspace 的本质差异

维度 npm Yarn pnpm
设计哲学 渐进式增强 平稳过渡 颠覆式创新
适用场景 简单 Monorepo 混合依赖管理 大型 Monorepo
核心优势 生态兼容性 配置灵活性 性能与存储效率
学习曲线 平缓 中等 较陡峭

选择时需注意:Workspace 是工具链的起点而非终点,真正的 Monorepo 需要配合 Turborepo/Nx 等工具实现完整能力链。

Vue.js 3 渐进式实现之响应式系统——第五节:分支切换与 cleanup

2025年4月5日 20:41

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数

分支切换与 cleanup

上一节中我们把依赖收集和触发的逻辑分别封装到 track 和 trigger 函数中,提升可读性和灵活性。

这一节我们来解决分支切换可能会产生遗留副作用函数的问题。

思路

分支切换

const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */})

effect(function effectFn() {
    document.body.innerText = obj.ok ? obj.text : 'not'
})

在 effectFn 函数内部存在一个三元表达式,根据字段 obj.ok 值的不同会执行不同的代码分支,这就是所谓的分支切换。

遗留的副作用函数

拿上面这段代码来说,obj.ok 初始值为 true 时, effectFn 函数会 读取 obj.ok 和 obj.text 两个值,此时 effectFn 与响应式数据之间建立的联系如下: 分支切换前.png

然而当 obj.ok 的值变为 false, effectFn 内发生分支切换,此时 effectFn 函数只会读取 obj.ok,理想情况下 effectFn 不该被 obj.text 字段所对应的依赖集合收集: 分支切换后.png

通俗一点解释就是,当 obj.ok 为 false 时,effectFn 已经不读取 obj.text 了, obj.text 怎么变都跟 effectFn 没关系。然而我们现在实现的响应式系统,副作用函数一旦被某个字段的依赖集合收集就没法删除,因此哪怕现在 effectFn 和 obj.text 已经没关系了,当 obj.text 的值变化时, effectFn 还是会重新运行一次,这显然没必要。

cleanup

解决这个问题的思路很简单:每当副作用函数执行时,先把它从所有与之关联的依赖集合中删除,在执行的过程中重新进行一次依赖收集,把副作用函数重新添加到与之关联的依赖集合中。

解释

副作用函数执行一次之后读取了哪些依赖,就是这个副作用函数当前状态(分支)下真正需要的依赖。

还是上面那个例子,当 obj.ok 从 true 切换为 false,会触发 effectFn 重新执行一次。此次执行过程中 effectFn 只读取了 obj.ok 而没有读取 obj.text,那就说明当前状态下的 effectFn 只依赖了 obj.ok,只有 obj.ok 变化时才需要重新执行,至于 obj.text 爱怎么变怎么变跟它没关系。

代码实现层面需要解决的问题
  1. 目前我们的响应式系统直接收集副作用函数本身,依赖触发也是直接执行副作用函数。而现在在依赖触发时,我们需要先把副作用函数从相关联的依赖集合中删除,然后再运行并进行一次依赖收集。因此收集到集合中的不能再是副作用函数本身,而是需要再包装一个函数,包含上述依赖删除和再收集的逻辑。
  2. 副作用函数本身需要能知道哪些依赖集合收集了它,因此副作用函数也需要收集依赖集合。
    副作用函数对依赖集合的收集.png

代码

// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
    // 包装真实副作用函数的函数,包含清除和再收集逻辑
    const effectFn = () => {
        // 调用 cleanup 完成清除工作
        cleanup(effectFn)
    
        // 在 effectFn 里,activeEffect 被赋值为它自己。
        // 而 activeEffect 只要不为空,就会执行依赖收集(见 track 函数第一句 if)。
        // 因此 effectFn 每次执行都会重新进行一次依赖收集,并且被收集的副作用函数就是它自己
        activeEffect = effectFn
        // 在 fn 这次执行中,effectFn 会作为副作用函数被收集到 fn 读取了的依赖的集合中
        fn()
    }

    // effctFn.deps 数组用来储存该副作用函数的所有依赖集合
    effectFn.deps = []
    // 执行副作用函数
    effectFn()
}

function cleanup(effectFn) {
    // 遍历 deps 数组
    effectFn.deps.forEach( deps => {
        // 把副作用函数从集合中删除
        deps.delete(effectFn)
    })

    // 重置副作用函数的依赖集合,因为之后要再重新收集一次的
    effectFn.deps.length = 0
}

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { text: 'hellow world' }
// 对原始数据的代理
const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
        // 把副作用函数收集到桶中
        track(traget, key)
        // 返回属性值
        return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        trigger(target, key)
        // 返回 true 代表设置操作成功
        return true
    }
})

// track 函数 在 get 拦截函数中被调用,用来追踪副作用函数
function track(target, key) {
    // 没有 activeEffect 直接 return
    if (!activeEffect) return terget[key]

    // 根据 target 从“桶”中取得 depsMap,也是Map类型:key --> effects
    let depsMap = bucket.get(target)
    // 如果不存在 depsMap,就新建一个 Map 并与 target 关联
    if (!depsMap) {
        depsMap = new Map()
        bucket.set(target, depsMap)
    }

    // 再根据 key 从 depsMap 中取得 deps。
    // deps是一个 Set 类型,储存所有与当前 key 相关联的副作用函数
    let deps = depsMap.get(key)
    // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }

    // 最后将当前激活的副作用函数添加到“桶”里
    deps.add(activeEffect)

    // 也将这个集合添加到副作用函数的 deps 数组中
    activeEffect.deps.push(deps)
}

// trigger 函数 在 set 拦截函数中被调用,用来触发更新
function trigger(target, key) {
    const depsMap = bucket.get(target)
    // 如果这个对象没有被追踪的依赖,没有需要重新运行的副作用函数,直接 return
    if (!depsMap) return

    const effects = depsMap.get(key)
    // 如果这个对象的这个key没有被追踪的依赖,没有需要重新运行的副作用函数,啥也不干
    // 否则就把 effects 中的函数依次执行
    const effectsToRun = new Set(effects)
    // 为了避免无限循环
    effectsToRun.forEach(effectFn => effectFn())
}

已实现

通过副作用函数每次执行前先把它从所有与之关联的依赖集合中删除,然后在执行过程中重新进行一次依赖收集,我们解决了副作用函数内有代码分支时分支切换可能会产生遗留的副作用函数的问题。

缺陷/待实现

effect 是可能发生嵌套的,而我们现在的响应式系统还不支持这一点。

下一节我们将详细阐述什么情况下 effect 会发生嵌套调用,以及如何支持这一功能。

打破常规认知:重新认识 CSS 层叠上下文

作者 bug_kada
2025年4月5日 19:31

在网页开发中,CSS 层叠样式上下文(Stacking Context)扮演着极为关键的角色,它决定了页面元素在 Z 轴方向上的显示顺序,直接影响着用户看到的页面视觉效果。接下来,让我们深入探究 CSS 层叠样式上下文的奥秘。

层叠上下文基础概念

层叠上下文可以理解为 HTML 中的一个三维概念,它就像一个 “容器”,其中包含的元素按照特定规则在 Z 轴方向(垂直于屏幕)进行层叠排列。每个层叠上下文都是独立的,内部元素的层叠顺序不会影响到其他层叠上下文的元素。

触发层叠上下文的条件

在 CSS 中,有多种方式可以触发层叠上下文的创建:

  1. 根元素( <html> :浏览器会默认将根元素作为一个层叠上下文,它是整个页面层叠上下文的基础。所有其他层叠上下文都在它的 “管辖” 范围内。
  1. 设置了 position 属性且值不为 static ,同时设置了 z-index 属性:例如:
.element {
    position: relative;
    z-index: 1;
}

在上述代码中,.element元素因为设置了position: relative和z-index: 1,创建了自己的层叠上下文。在这个层叠上下文中,该元素及其子元素将按照特定规则进行层叠排列。

  1. 设置了 opacity 属性且值小于 1
.transparent - element {
    opacity: 0.5;
}

当一个元素的opacity值小于 1 时,它会创建一个新的层叠上下文。这意味着该元素及其子元素将在一个独立的层叠环境中进行排序,即使它的父元素没有创建层叠上下文。

  1. 设置了 transform 属性且值不为 none
.transformed - element {
    transform: scale(1.2);
}

任何设置了非none值的transform属性的元素,都会成为一个层叠上下文的根元素。这在实现元素的动画效果(如缩放、旋转、平移等)时非常有用,因为不同的层叠上下文可以确保动画元素的显示顺序不会被其他元素干扰。

层叠顺序规则

在一个层叠上下文中,元素按照以下顺序进行层叠排列(从后往前,即从离用户最远到最近):

  1. 背景和边框(层叠上下文的背景和边框) :每个层叠上下文都有自己的背景和边框,它们处于最底层。例如,一个包含多个元素的层叠上下文,其背景颜色或背景图片会在所有内部元素的后面显示。
  1. z-index 值的元素:具有负z-index值的元素会出现在背景和边框之上,但在其他普通元素之下。例如:
.negative - z - index {
    position: relative;
    z - index: -1;
}

假设在一个页面中有多个元素,其中一个元素设置了上述样式,那么它会被其他具有正z-index值或默认z-index(auto)的元素覆盖。

  1. 块级元素(按文档流顺序) :在普通的文档流中,块级元素按照它们在 HTML 文档中出现的顺序进行层叠。后面出现的块级元素会覆盖前面出现的块级元素。例如:
<div style="width: 200px; height: 200px; background - color: blue;"></div>
<div style="width: 100px; height: 100px; background - color: red;"></div>

在上述代码中,红色的<div>会覆盖蓝色的<div>,因为红色<div>在 HTML 文档中位于蓝色<div>之后。

  1. 浮动元素:浮动元素会覆盖块级元素,但会被具有正z-index值的元素覆盖。比如:
.float - element {
    float: left;
    width: 150px;
    height: 150px;
    background - color: green;
}

如果页面中既有块级元素又有浮动元素,浮动元素会在块级元素之上显示,但如果有其他元素设置了正z-index值,那么这些元素会覆盖浮动元素。

  1. 行内元素:行内元素在块级元素和浮动元素之上,但在设置了正z-index值的元素之下。例如:
<span style="background - color: yellow;">行内元素</span>

这个黄色背景的行内元素会显示在普通块级元素和浮动元素之上,但如果有元素设置了正z-index值,它会被这些元素覆盖。

  1. z-index: auto 的定位元素:这些元素按照它们在文档中的顺序进行层叠,与普通块级元素类似。例如:
.positioned - auto {
    position: relative;
    z - index: auto;
}

如果有多个这样的定位元素,后面的元素会覆盖前面的元素。

  1. z-index 值的元素:具有正z-index值的元素会显示在最上面,值越大,越靠近用户。例如:
.high - z - index {
    position: relative;
    z - index: 10;
}

一个设置了较高正z-index值的元素会覆盖页面中其他具有较低z-index值的元素。

层叠上下文与父元素的关系

正如前文提到的,z-index的值会受到父元素的影响。当一个元素处于某个层叠上下文中时,它的z-index值决定了它在该层叠上下文内部的层叠顺序,但不会影响到其他层叠上下文。如果父元素的层叠上下文设置不当,子元素的z-index可能无法达到预期效果。例如:

<div style="position: relative; z - index: 1;">
    <div style="position: relative; z - index: 10;">子元素</div>
</div>
<div style="position: relative; z - index: 2;">另一个元素</div>

在上述代码中,虽然第一个<div>中的子元素设置了z-index: 10,但由于它的父元素z-index为 1,小于另一个<div>z-index: 2,所以这个子元素会被另一个<div>覆盖。这就像儿子(子元素)再厉害(设置了高z-index),也得在老爹(父元素的层叠上下文)的 “势力范围” 内行事。

实际应用案例

制作下拉菜单

在网页设计中,下拉菜单是一个常见的交互组件。通过层叠上下文,可以很好地实现下拉菜单的显示效果。例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        nav {
            position: relative;
        }
        nav ul {
            list-style - type: none;
            margin: 0;
            padding: 0;
        }
        nav ul li {
            position: relative;
            display: inline - block;
        }
        nav ul li a {
            display: block;
            padding: 10px;
            background - color: #333;
            color: white;
            text - decoration: none;
        }
        nav ul li ul {
            display: none;
            position: absolute;
            top: 100%;
            left: 0;
            background - color: #444;
            z - index: 1;
        }
        nav ul li:hover ul {
            display: block;
        }
    </style>
</head>
<body>
    <nav>
        <ul>
            <li><a href="#">首页</a></li>
            <li><a href="#">产品</a>
                <ul>
                    <li><a href="#">产品1</a></li>
                    <li><a href="#">产品2</a></li>
                </ul>
            </li>
            <li><a href="#">关于我们</a></li>
        </ul>
    </nav>
</body>
</html>

在这个例子中,下拉菜单的子菜单(<ul>元素)通过设置position: absolutez-index: 1创建了自己的层叠上下文。当鼠标悬停在父菜单项上时,子菜单显示出来,并且由于其z-index值,会覆盖页面上其他元素(如背景等),从而实现了下拉菜单的效果。

图片轮播图中的层叠效果

图片轮播图也是层叠上下文的一个典型应用场景。通过合理设置z-index值,可以控制图片的显示顺序和切换效果。例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <style>
       .slider {
            position: relative;
            width: 500px;
            height: 300px;
            overflow: hidden;
        }
       .slider img {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z - index: 0;
            opacity: 0;
            transition: opacity 0.5s ease - in - out;
        }
       .slider img.active {
            z - index: 1;
            opacity: 1;
        }
    </style>
</head>
<body>
    <div class="slider">
        <img src="image1.jpg" alt="图片1" class="active">
        <img src="image2.jpg" alt="图片2">
        <img src="image3.jpg" alt="图片3">
    </div>
    <script>
        const images = document.querySelectorAll('.slider img');
        let currentIndex = 0;
        function showNextImage() {
            images[currentIndex].classList.remove('active');
            currentIndex = (currentIndex + 1) % images.length;
            images[currentIndex].classList.add('active');
        }
        setInterval(showNextImage, 3000);
    </script>
</body>
</html>

在这个图片轮播图中,通过 JavaScript 控制图片的active类名切换。当一个图片具有active类时,它的z-index值变为 1,显示在最前面,而其他图片z-index值为 0,处于隐藏状态(通过opacity: 0实现)。通过这种方式,实现了图片的轮流显示效果。

总结

CSS 层叠样式上下文是网页开发中一个非常重要的概念,它直接影响着页面元素的显示顺序和视觉效果。了解触发层叠上下文的条件、层叠顺序规则以及层叠上下文与父元素的关系,并通过实际应用案例加以巩固,能够帮助开发者更好地控制页面布局,实现各种复杂的交互效果。在日常开发中,合理运用层叠上下文,避免z-index滥用导致的性能问题,将有助于提升网页的质量和用户体验。

Visual Studio Code 发布王炸更新:Agent 模式上线,支持 MCP 协议!

作者 Captaincc
2025年4月5日 23:54

Visual Studio Code 2025 年 3 月更新(版本 1.99):Agent 模式上线,MCP 支持开启!

2025 年 4 月 5 日,Visual Studio Code(VSCode)正式发布了 2025 年 3 月的更新(版本 1.99)。本次更新带来了多项激动人心的新功能,包括 Agent 模式 的正式上线、对 Model Context Protocol(MCP) 的支持,以及通过自定义 API 访问大型语言模型的预览功能。以下是本次更新的详细解读,助力开发者更好地利用 VSCode 提升工作效率。

一、Agent 模式正式上线:AI 驱动的智能开发体验

本次更新的最大亮点是 Agent 模式 正式在 VSCode Stable 版本中上线。开发者可以通过设置 chat.agent.enabled 启用该功能,并在 Chat 视图的模式选择器中找到它。

Agent 模式的核心功能

Agent 模式通过集成 AI 模型,帮助开发者与外部工具、应用程序和数据源进行交互。它支持以下场景:

文件操作:自动生成、编辑代码或笔记本文件。

数据交互:访问数据库或通过网络获取数据。

上下文增强:结合项目上下文,提供更精准的代码建议。

例如,开发者可以在 Chat 窗口中输入提示,Agent 模式会调用相关工具,生成符合需求的代码片段或执行复杂任务。

配置与使用

Agent 模式支持通过 Model Context Protocol(MCP) 服务器扩展功能。配置方式包括:

在用户设置、远程设置或 .code-workspace 文件中调整 mcp 部分。

在工作区的 .vscode/mcp.json 文件中定义 MCP 服务器。

官方文档提供了详细的配置指南,开发者可以根据需求进一步定制 Agent 模式的体验。

二、支持 Model Context Protocol(MCP):AI 工具集成更灵活

VSCode 在本次更新中新增了对 Model Context Protocol(MCP) 的支持,这一协议为 AI 模型与外部工具的交互提供了标准化方式。

MCP 的价值

MCP 协议允许 AI 模型更高效地发现和使用外部资源,例如:

访问项目文件和上下文。

调用外部 API 获取数据。

与调试工具或语言服务器协作。

通过 MCP,开发者可以构建更智能的开发环境。例如,Rust 扩展可以利用 MCP 和语言模型,为代码重命名提供更智能的建议。

如何启用 MCP?

开发者可以通过配置 MCP 服务器来启用该功能。官方推荐参考 GitHub 上的 mcp-labs 项目(github.com/thangchung/…

三、预览功能:自定义 API 访问大型语言模型

VSCode 团队推出了一项预览功能:开发者可以通过自己的 API 访问大型语言模型。这一功能为新模型的快速集成提供了便利。

功能详情

灵活性提升:开发者可以直接通过 API 调用最新的语言模型,无需等待官方支持。

适用范围:目前仅限 GitHub Copilot Pro 和免费用户使用,Business 和 Enterprise 用户需等待后续支持。

这一功能的推出意味着开发者可以更快速地体验新模型。例如,当新的语言模型发布时,你可以直接通过 API 访问并集成到开发流程中。

未来计划

根据 GitHub Copilot 的更新路线图,未来将支持更多模型(如 o1 和 Gemini),进一步丰富 AI 辅助编码的选项。

四、其他重要改进

除了核心功能,本次更新还包含多项实用改进:

  1. 敏捷的更新节奏

VSCode 团队保持了高效的开发节奏:

每日发布:每天修复 bug 并添加预览功能。

月度整合:次月发布正式版,统一更新内容。

这种快速迭代让开发者能够及时体验新功能,同时为团队提供宝贵的反馈。

  1. GitHub Copilot 免费计划

VSCode 推出了 GitHub Copilot 免费计划,包含:

每月 2000 次代码补全(约 80 次/工作日)。

每月 50 次聊天请求。

支持 GPT-4o 和 Claude 3.5 Sonnet 模型。

对于需要更多功能的用户,Copilot Pro 计划提供无限制访问和更多模型选择。

  1. 笔记本编辑增强

VSCode 改进了笔记本编辑体验,支持与代码文件相同的编辑和 Agent 模式。开发者可以通过设置 nbformat_minor 为 5 来更新现有笔记本,启用 AI 驱动的编辑功能。

  1. 语言模型 API 增强

VSCode 扩展 API 提供了更强大的语言模型支持,例如:

使用 LanguageModelChatMessage 创建提示。

通过 @vscode/prompt-tsx 以 TSX 语法声明提示。

这些工具帮助开发者更高效地构建 AI 驱动的扩展。

五、总结与展望

Visual Studio Code 的 2025 年 3 月更新(版本 1.99)为开发者带来了更智能、更灵活的开发体验。Agent 模式的正式上线让 AI 辅助编码更进一步,MCP 协议的支持为工具集成提供了新可能,而自定义 API 访问大型语言模型的预览功能则为未来创新铺平了道路。

对于希望尝试新功能的开发者,建议通过官方文档深入了解 Agent 模式和 MCP 的配置方法。同时,VSCode 团队欢迎用户反馈,共同推动产品迭代。

未来,随着更多模型的集成和功能的扩展,VSCode 无疑将继续引领开发者工具的发展方向。你对本次更新有何期待?欢迎在评论区分享你的看法!

参考链接

完整更新内容:t.co/OW1aHIRYxO

VSCode 官方网站:code.visualstudio.com

MCP 相关资源:github.com/thangchung/…

昨天 — 2025年4月5日掘金 前端

vue自定义指令的几个注意点

2025年4月5日 22:29

Vue 的自定义指令提供了一种直接操作 DOM 的低级机制,但使用时需注意以下关键点:


1. ‌生命周期钩子的触发时机

指令的钩子函数在不同阶段触发,需根据需求选择合适的钩子。

  • ‌**bind**‌:指令首次绑定到元素时调用(元素未插入 DOM)。
  • ‌**inserted**‌:元素插入父节点后调用(适合访问 DOM)。
  • ‌**update**‌:组件更新时调用(可能发生在子组件更新前)。
  • ‌**componentUpdated**‌:组件及子组件更新后调用。
  • ‌**unbind**‌:指令与元素解绑时调用(清理资源)。

示例:自动聚焦输入框
在 inserted 钩子中操作 DOM,确保元素已渲染:

Vue.directive('focus', {
  inserted(el) {
    el.focus(); // 正确:元素已插入 DOM
  }
});
// 使用:<input v-focus>

2. ‌指令参数的动态获取

通过 binding 对象获取参数、修饰符和值,支持响应式更新。

  • ‌**binding.value**‌:指令的绑定值(如 v-dir="value")。
  • ‌**binding.arg**‌:指令的参数(如 v-dir:arg)。
  • ‌**binding.modifiers**‌:修饰符对象(如 v-dir.modifier)。

示例:动态调整元素颜色
根据参数和修饰符设置颜色:

Vue.directive('color', {
  bind(el, binding) {
    const color = binding.arg || 'text'; // 参数决定颜色类型
    const isImportant = binding.modifiers.important;
    el.style[color] = binding.value + (isImportant ? ' !important' : '');
  }
});
// 使用:<div v-color:background.important="'#f00'"></div>

3. ‌避免副作用与内存泄漏

在 unbind 钩子中清理事件监听、定时器等资源。

示例:点击外部关闭浮层
添加全局点击事件,解绑时移除:

Vue.directive('click-outside', {
  bind(el, binding) {
    el._clickHandler = (e) => {
      if (!el.contains(e.target)) binding.value();
    };
    document.addEventListener('click', el._clickHandler);
  },
  unbind(el) {
    document.removeEventListener('click', el._clickHandler); // 必须清理
    delete el._clickHandler;
  }
});
// 使用:<div v-click-outside="closeMenu"></div>

4. ‌指令与组件的关系

指令应聚焦于 DOM 操作,复杂逻辑建议封装为组件。

适用场景对比‌:

  • 指令‌:DOM 操作(如聚焦、动画、防抖)。
  • 组件‌:数据驱动、可复用的 UI 模块(如表单、弹窗)。

示例:按钮防抖指令
封装高频点击的防抖逻辑:

Vue.directive('debounce', {
  bind(el, binding) {
    let timer;
    el._debounceHandler = () => {
      clearTimeout(timer);
      timer = setTimeout(() => binding.value(), 500);
    };
    el.addEventListener('click', el._debounceHandler);
  },
  unbind(el) {
    el.removeEventListener('click', el._debounceHandler);
  }
});
// 使用:<button v-debounce="submitForm">提交</button>

5. ‌响应式更新的处理

使用 update 或 componentUpdated 响应数据变化。

示例:元素尺寸监听
通过 ResizeObserver 监听元素尺寸变化:

Vue.directive('resize', {
  bind(el, binding) {
    el._resizeObserver = new ResizeObserver(entries => {
      binding.value(entries.contentRect);
    });
    el._resizeObserver.observe(el);
  },
  unbind(el) {
    el._resizeObserver.disconnect();
  }
});
// 使用:<div v-resize="handleResize"></div>

总结

  • 生命周期选择‌:根据 DOM 操作需求选择钩子(如 inserted 替代 bind)。
  • 参数处理‌:通过 binding 对象动态获取指令参数。
  • 资源清理‌:在 unbind 中移除事件、观察者等。
  • 职责分离‌:指令处理原生 DOM 操作,组件处理业务逻辑。
  • 响应式支持‌:通过 update 或 componentUpdated 响应数据变化。

合理使用自定义指令,可高效解决特定场景的 DOM 操作问题,同时保持代码的可维护性。

uniapp与React Native/vue 的简单对比

2025年4月5日 22:30

1. 在UniApp中如何通过条件编译实现多平台代码适配?

关键代码示例‌:

// 仅H5平台生效
// #ifdef H5
console.log("H5平台特定逻辑");
// #endif

// 仅微信小程序生效
// #ifdef MP-WEIXIN
wx.request({...});
// #endif

// 多平台共用代码
function commonLogic() { ... }

与React Native的差异‌:

  1. 语法机制‌:

    • UniApp使用注释指令(如#ifdef)实现条件编译,代码在编译时按平台剔除无关内容。

    • React Native通过运行时判断Platform.OS实现平台分支逻辑:

      if (Platform.OS === 'ios') { ... }
      
  2. 编译产物‌:

    • UniApp为每个平台生成独立包,体积更小。
    • React Native所有平台代码打包在一起,运行时动态选择逻辑。

2. 图片懒加载自定义组件设计

核心实现‌:

<!-- lazy-image.vue -->
<template>
  <image :src="loaded ? realSrc : placeholder" @load="handleLoad" />
</template>

<script>
export default {
  props: {
    realSrc: String,
    placeholder: { type: String, default: '/placeholder.png' }
  },
  data() {
    return { loaded: false };
  },
  mounted() {
    // #ifdef H5
    const observer = new IntersectionObserver(entries => {
      if (entries.isIntersecting) {
        this.loaded = true;
        observer.disconnect();
      }
    });
    observer.observe(this.$el);
    // #endif

    // #ifdef MP-WEIXIN
    const observer = wx.createIntersectionObserver();
    observer.relativeToViewport().observe('', res => {
      if (res.intersectionRatio > 0) this.loaded = true;
    });
    // #endif
  }
}
</script>

平台适配要点‌:

  • H5使用标准IntersectionObserver API
  • 微信小程序使用wx.createIntersectionObserver
  • 通过条件编译隔离平台逻辑

3. UniApp中Vuex与Composition API的差异

Vuex实现‌:

// store.js
export default new Vuex.Store({
  state: { count: 0 },
  mutations: { increment: state => state.count++ }
})

// 页面使用
import { mapState, mapMutations } from 'vuex'
export default {
  computed: mapState(['count']),
  methods: mapMutations(['increment'])
}

与Composition API对比‌:

  1. 状态管理方式‌:

    • Vuex:集中式存储,强调单一数据源
    • Composition API:分散式状态,可通过reactive()创建响应式对象
  2. 代码组织‌:

    • Vuex需要严格定义state/mutations/actions
    • Composition API更灵活,使用setup()自由组合逻辑
  3. 调试支持‌:

    • Vuex有devtools时间旅行调试
    • Composition API需自行实现调试工具

4. uni.request封装与Axios对比

封装示例‌:

const http = {
  request(options) {
    return new Promise((resolve, reject) => {
      uni.request({
        url: 'https://api.example.com' + options.url,
        method: options.method || 'GET',
        data: options.data,
        header: { 'X-Token': uni.getStorageSync('token') },
        success: res => res.statusCode === 200 ? resolve(res.data) : reject(res),
        fail: reject
      })
    })
  }
}

// 使用
http.request({ url: '/user', method: 'POST', data: { name: 'John' } })

与Axios对比‌:

特性 uni.request Axios
运行环境 多平台(小程序/H5等) 主要浏览器/Node
拦截器 需手动实现 内置拦截器机制
自动JSON转换 部分平台需要手动处理 自动转换请求/响应数据
取消请求 不支持 支持CancelToken
适配器扩展 无法扩展 支持自定义适配器

5. 页面跳转传参与React Router对比

UniApp实现‌:

// 传递参数
const complexData = { user: { id: 1, tags: ['a', 'b'] } }
uni.navigateTo({
  url: `/pages/detail?payload=${encodeURIComponent(JSON.stringify(complexData))}`
})

// 接收参数
onLoad(options) {
  const data = JSON.parse(decodeURIComponent(options.payload))
}

与React Router差异‌:

  1. 参数传递方式‌:

    • UniApp:强制URL字符串传输,需手动序列化

    • React Router:

      history.push('/detail', { secretData: 123 }) // 通过state传递
      
  2. 安全性‌:

    • UniApp参数暴露在URL中
    • React Router可通过内存state传递敏感数据
  3. 数据类型支持‌:

    • UniApp只能传递可序列化数据
    • React Router支持任意对象(包括循环引用等特殊结构)
❌
❌