阅读视图

发现新文章,点击刷新页面。

React Native之Android端Fabric 架构源码分析(下)

React Native之Android端Fabric 架构源码分析(下)

由于单章字数限制,这里将本文拆成上下两篇,上篇《React Native之Android端Fabric 架构源码分析(上)》

Fabric 组件

声明组件

要开发一个Fabric 组件,首先需要在TS代码中声明规范。我们使用npx create-react-native-library@latest创建一个Fabric组件库,然后参考根据官方文档的示例添加如下内容:

import type {
  CodegenTypes,
  HostComponent,
  ViewProps,
} from 'react-native';
import {codegenNativeComponent} from 'react-native';

type WebViewScriptLoadedEvent = {
  result: 'success' | 'error';
};

export interface NativeProps extends ViewProps {
  sourceURL?: string;
  onScriptLoaded?: CodegenTypes.BubblingEventHandler<WebViewScriptLoadedEvent> | null;
}

export default codegenNativeComponent<NativeProps>(
  'CustomWebView',
) as HostComponent<NativeProps>;

这里主要有三部分:

  • WebViewScriptLoadedEvent 是一种支持的数据类型,用于存储事件需要从原生代码传递到 JavaScript 的数据。

  • NativeProps 定义了可以设置的组件属性。

  • codegenNativeComponent 允许我们为自定义组件生成代码,并为组件定义一个与原生实现相匹配的名称。

其中,对codegenNativeComponent函数进行分析,源码react-native/packages/react-native/Libraries/Utilities/codegenNativeComponent.js:

// 如果这个函数运行,说明视图配置没有在构建时通过 `GenerateViewConfigJs.js` 生成。
// 因此我们需要使用 `requireNativeComponent` 从视图管理器获取视图配置。
// `requireNativeComponent` 在 Bridgeless 模式下不可用。
// 例如:如果 `codegenNativeComponent` 不是从以 NativeComponent.js 结尾的文件中调用,
// 这个函数就会在运行时执行。
function codegenNativeComponent<Props: {...}>(
  componentName: string,
  options?: NativeComponentOptions,
): NativeComponentType<Props> {
  if (global.RN$Bridgeless === true && __DEV__) {
    console.warn(
      `Codegen didn't run for ${componentName}. This will be an error in the future. Make sure you are using @react-native/babel-preset when building your JavaScript code.`,
    );
  }
  // 确定基础组件名称
  let componentNameInUse =
    options && options.paperComponentName != null
      ? options.paperComponentName
      : componentName;

  // 省略部分代码......

  return (requireNativeComponent<Props>(
    // $FlowFixMe[incompatible-type]
    componentNameInUse,
  ): HostComponent<Props>);
}

根据注释可知,在新架构中,这个函数几乎没有实际意义,因为新架构是通过GenerateViewConfigJs.js 来生成实际的视图配置。

我们通过搜索codegenNativeComponent字符串,很容易定位到react-native/packages/babel-plugin-codegen/index.js文件,这里babel-plugin-codegen包就是检测codegenNativeComponent调用,解析TypeScript/Flow类型定义的工具包:

module.exports = function ({parse, types: t}) {
  return {
    pre(state) {
      this.code = state.code;
      this.filename = state.opts.filename;
      this.defaultExport = null;
      this.commandsExport = null;
      this.codeInserted = false;
    },

    // 省略部分代码.....
      ExportDefaultDeclaration(path, state) {
        if (isCodegenDeclaration(path.node.declaration)) {
          this.defaultExport = path;
        }
      },

      // 程序退出时进行替换
      Program: {
        exit(path) {
          if (this.defaultExport) {
            // 1. 生成ViewConfig代码
            const viewConfig = generateViewConfig(this.filename, this.code);
            // 2. 解析为AST
            const ast = parse(viewConfig, {
              babelrc: false,
              browserslistConfigFile: false,
              configFile: false,
            });

            // 3. 完全替换原始导出
            this.defaultExport.replaceWithMultiple(ast.program.body);

            if (this.commandsExport != null) {
              this.commandsExport.remove();
            }

            this.codeInserted = true;
          }
        },
      },
    },
  };
};


function generateViewConfig(filename /*: string */, code /*: string */) {
  // 解析TypeScript/Flow类型
  const schema = parseFile(filename, code);
  // 提取组件信息
  const libraryName = basename(filename).replace(
    /NativeComponent\.(js|ts)$/,
    '',
  );
  // 调用Codegen生成器
  return RNCodegen.generateViewConfig({
    libraryName,
    schema,
  });
}

function isCodegenDeclaration(declaration) {
  if (!declaration) {
    return false;
  }

  if (
    declaration.left &&
    declaration.left.left &&
    declaration.left.left.name === 'codegenNativeComponent'
  ) {
    return true;
  } else if (
    declaration.callee &&
    declaration.callee.name &&
    declaration.callee.name === 'codegenNativeComponent'
  ) {
    return true;
  } 
  // 省略......
  return false;
}

继续跟踪RNCodegen.generateViewConfig的实现,源码react-native/packages/react-native-codegen/src/generators/RNCodegen.js

const generateViewConfigJs = require('./components/GenerateViewConfigJs.js');


module.exports = {
  allGenerators: ALL_GENERATORS,
  // 省略部分代码......
  generateViewConfig({
    libraryName,
    schema,
  }: Pick<LibraryOptions, 'libraryName' | 'schema'>): string {
    schemaValidator.validate(schema);

    const result = generateViewConfigJs
      .generate(libraryName, schema)
      .values()
      .next();

    if (typeof result.value !== 'string') {
      throw new Error(`Failed to generate view config for ${libraryName}`);
    }

    return result.value;
  },
};

最终是调用的GenerateViewConfigJs.js中的generate,源码react-native/packages/react-native-codegen/src/generators/components/GenerateViewConfigJs.js

module.exports = {
  generate(libraryName: string, schema: SchemaType): FilesOutput {
    try {
      const fileName = `${libraryName}NativeViewConfig.js`;
      const imports: Set<string> = new Set();

      const moduleResults = Object.keys(schema.modules)
        .map(moduleName => {
          const module = schema.modules[moduleName];
          if (module.type !== 'Component') {
            return;
          }

          const {components} = module;

          return Object.keys(components)
            .map((componentName: string) => {
              const component = components[componentName];

              if (component.paperComponentNameDeprecated) {
                imports.add(UIMANAGER_IMPORT);
              }

              const replacedTemplate = ComponentTemplate({
                componentName,
                paperComponentName: component.paperComponentName,
                paperComponentNameDeprecated:
                  component.paperComponentNameDeprecated,
              });
      // 省略部分代码...... 

      return new Map([[fileName, replacedTemplate]]);
    } catch (error) {
      console.error(`\nError parsing schema for ${libraryName}\n`);
      console.error(JSON.stringify(schema));
      throw error;
    }
  },
};

这里我们根据ComponentTemplate中的模板,大概就能还原出生成的代码是什么样子:

// 输入:
// libraryName: "MyComponent"
// schema: { 解析后的TypeScript/Flow类型信息 }

// 输出:完整的JavaScript代码字符串


'use strict';

const {UIManager} = require("react-native")

let nativeComponentName = 'MyComponent';

export const __INTERNAL_VIEW_CONFIG = {
  uiViewClassName: 'MyComponent',
  bubblingEventTypes: {},
  directEventTypes: {},
  validAttributes: {
    opacity: true,
    backgroundColor: { process: require('react-native/Libraries/StyleSheet/processColor').default },
    // ... 其他属性
  }
};

export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);

这里最关键的就是最后一行,它将开发者编写的Fabric 组件规范进行了代码替换:

// 开发者编写
const CustomView = codegenNativeComponent<Props>('CustomView');

// Babel插件替换后
const CustomView = NativeComponentRegistry.get('CustomView', () => __INTERNAL_VIEW_CONFIG);

需要注意,babel-plugin-codegen工具并不像Codegen工具,会生成实际的代码文件。它是对AST语法树进行的动态修改和替换,也就是说它修改的是内存中的语法树,并不会写文件。

现在来重点追踪NativeComponentRegistry.get的实现,首先是导出位置react-native/packages/react-native/index.js

  get NativeComponentRegistry() {
    return require('./Libraries/NativeComponent/NativeComponentRegistry');
  },

定位到方法实现react-native/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js

/**
 * 获取一个可以被 React Native 渲染的 `NativeComponent`。
 *
 * 提供的 `viewConfigProvider` 可能会被调用和使用,也可能不会,
 * 这取决于 `setRuntimeConfigProvider` 是如何配置的。
 */
export function get<Config: {...}>(
  name: string,
  viewConfigProvider: () => PartialViewConfig,
): HostComponent<Config> {
  // 注册ViewConfig到全局注册表
  ReactNativeViewConfigRegistry.register(name, () => {
    // 这里的回调函数会在React需要组件配置时被调用
    const {native, verify} = getRuntimeConfig?.(name) ?? {
      native: !global.RN$Bridgeless,   // 关键:新架构检测
      verify: false,
    };

    let viewConfig: ViewConfig;
    if (native) {
      // 旧架构:原生ViewManager
      viewConfig =
        getNativeComponentAttributes(name) ??
        createViewConfig(viewConfigProvider());
    } else {
      // 新架构:优先静态ViewConfig
      viewConfig =
        createViewConfig(viewConfigProvider()) ??
        getNativeComponentAttributes(name);
    }
    // 省略部分代码......
    return viewConfig;
  });

  // $FlowFixMe[incompatible-type] `NativeComponent` 实际上是字符串!
  return name;
}

继续跟踪ReactNativeViewConfigRegistry.register实现。源码react-native/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js

const viewConfigCallbacks = new Map<string, ?() => ViewConfig>();


export function register(name: string, callback: () => ViewConfig): string {
  // 省略部分代码......
  viewConfigCallbacks.set(name, callback);
  return name;
}

这里基本上就是将返回ViewConfig的闭包给存了起来。

查找组件

JS层

在前面启动渲染一节我们知道了启动渲染的最终调用是JS层的AppRegistry.runApplication方法。沿着这条线,我们来分析一下JS层的组件加载与处理流程。源码react-native/packages/react-native/Libraries/ReactNative/AppRegistry.js

import * as AppRegistry from './AppRegistryImpl';

// 省略部分代码......
global.RN$AppRegistry = AppRegistry;

registerCallableModule('AppRegistry', AppRegistry);

export {AppRegistry};

继续跟踪源码react-native/packages/react-native/Libraries/ReactNative/AppRegistryImpl.js

const runnables: Runnables = {};


/**
 * Loads the JavaScript bundle and runs the app.
 *
 * See https://reactnative.dev/docs/appregistry#runapplication
 */
export function runApplication(
  appKey: string,
  appParameters: AppParameters,
  displayMode?: number,
): void {
  if (appKey !== 'LogBox') {
    const logParams = __DEV__ ? ` with ${JSON.stringify(appParameters)}` : '';
    const msg = `Running "${appKey}"${logParams}`;
    console.log(msg);
  }

  SceneTracker.setActiveScene({name: appKey});
  runnables[appKey](appParameters, coerceDisplayMode(displayMode));
}


/**
 * Registers an app's root component.
 *
 * See https://reactnative.dev/docs/appregistry#registercomponent
 */
export function registerComponent(
  appKey: string,
  componentProvider: ComponentProvider,
  section?: boolean,
): string {
  const scopedPerformanceLogger = createPerformanceLogger();
  runnables[appKey] = (appParameters, displayMode) => {
    const renderApplication = require('./renderApplication').default;
    renderApplication(
      componentProviderInstrumentationHook(
        componentProvider,
        scopedPerformanceLogger,
      ),
      appParameters.initialProps,
      appParameters.rootTag,
      wrapperComponentProvider && wrapperComponentProvider(appParameters),
      rootViewStyleProvider && rootViewStyleProvider(appParameters),
      appParameters.fabric,
      scopedPerformanceLogger,
      appKey === 'LogBox', // is logbox
      appKey,
      displayMode,
    );
  };
  if (section) {
    sections[appKey] = runnables[appKey];
  }
  return appKey;
}

可以看到,runApplication调用的是runnables对象中注册的闭包。而在我们React Native JS层的Bundle包中,首先就需要调用AppRegistry.registerComponent(appName, () => App)完成最根组件的注册。所以runApplication方法中调用的闭包,就是在registerComponent中注册的闭包。

继续跟踪renderApplication方法的实现,源码react-native/packages/react-native/Libraries/ReactNative/renderApplication.js

export default function renderApplication<Props: Object>(
  RootComponent: React.ComponentType<Props>,
  initialProps: Props,
  rootTag: any,
  WrapperComponent?: ?React.ComponentType<any>,
  rootViewStyle?: ?ViewStyleProp,
  fabric?: boolean,
  scopedPerformanceLogger?: IPerformanceLogger,
  isLogBox?: boolean,
  debugName?: string,
  displayMode?: ?DisplayModeType,
  useOffscreen?: boolean,
) {

  const performanceLogger = scopedPerformanceLogger ?? GlobalPerformanceLogger;

  // 构建React元素树
  // 外层:PerformanceLoggerContext.Provider - 提供性能监控上下文
  // 中层:AppContainer - React Native的根容器组件
  // 内层:RootComponent - 用户注册的应用组件(如App.js)
  let renderable: React.MixedElement = (
    <PerformanceLoggerContext.Provider value={performanceLogger}>
      <AppContainer
        rootTag={rootTag}
        fabric={fabric}
        WrapperComponent={WrapperComponent}
        rootViewStyle={rootViewStyle}
        initialProps={initialProps ?? Object.freeze({})}
        internal_excludeLogBox={isLogBox}>
        <RootComponent {...initialProps} rootTag={rootTag} />
      </AppContainer>
    </PerformanceLoggerContext.Provider>
  );

  // 开发模式调试包装
  if (__DEV__ && debugName) {
    const RootComponentWithMeaningfulName = getCachedComponentWithDebugName(
      `${debugName}(RootComponent)`,
    );
    renderable = (
      <RootComponentWithMeaningfulName>
        {renderable}
      </RootComponentWithMeaningfulName>
    );
  }

  //  实验性离屏渲染支持
  if (useOffscreen && displayMode != null) {
    // $FlowFixMe[incompatible-type]
    // $FlowFixMe[prop-missing]
    // $FlowFixMe[missing-export]
    const Activity: ActivityType = React.unstable_Activity;

    renderable = (
      <Activity
        mode={displayMode === DisplayMode.VISIBLE ? 'visible' : 'hidden'}>
        {renderable}
      </Activity>
    );
  }

  // 我们希望在使用 Fabric 时始终启用 concurrentRoot 功能
  const useConcurrentRoot = Boolean(fabric);

  // 省略部分性能日志打印......

  // 进入React渲染系统
  Renderer.renderElement({
    element: renderable,
    rootTag,
    useFabric: Boolean(fabric),
    useConcurrentRoot,
  });
}

继续跟踪Renderer.renderElement方法实现。源码react-native/packages/react-native/Libraries/ReactNative/RendererImplementation.js

export function renderElement({
  element,
  rootTag,
  useFabric,
  useConcurrentRoot,
}: {
  element: React.MixedElement,
  rootTag: number,
  useFabric: boolean,
  useConcurrentRoot: boolean,
}): void {
  if (useFabric) {
    if (cachedFabricRender == null) {
      // 获取实际渲染器
      cachedFabricRender = getFabricRenderer().render;
    }

    cachedFabricRender(element, rootTag, null, useConcurrentRoot, {
      onCaughtError,
      onUncaughtError,
      onRecoverableError,
    });
  } else {
    // 省略旧架构......
  }
}

function getFabricRenderer(): ReactFabricType {
  if (cachedFabricRenderer == null) {
    cachedFabricRenderer = require('../Renderer/shims/ReactFabric').default;
  }
  return cachedFabricRenderer;
}

继续跟踪react-native/packages/react-native/Libraries/Renderer/shims/ReactFabric.js

import {BatchedBridge} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

import type {ReactFabricType} from './ReactNativeTypes';

let ReactFabric: ReactFabricType;

if (__DEV__) {
  ReactFabric = require('../implementations/ReactFabric-dev');
} else {
  ReactFabric = require('../implementations/ReactFabric-prod');
}

global.RN$stopSurface = ReactFabric.stopSurface;

if (global.RN$Bridgeless !== true) {
  BatchedBridge.registerCallableModule('ReactFabric', ReactFabric);
}

export default ReactFabric;

这里根据开发环境选择了不同的渲染器。开发环境的渲染器体积更大,包含了许多调试、性能日志等信息,而生产环境的移除了注释和空白,变量名被压缩代码经过优化,减少包体积。

继续跟踪生产环境的渲染器,因为代码更加简洁,可更好的聚焦于调用的链路和流程。源码react-native/packages/react-native/Libraries/Renderer/implementations/ReactFabric-prod.js

exports.render = function (
  element,
  containerTag,
  callback,
  concurrentRoot,
  options
) {
  var root = roots.get(containerTag);
  if (!root) {
    // 创建新的FiberRootNode
    // 省略部分代码......

    initializeUpdateQueue(concurrentRoot);
    roots.set(containerTag, root);
  }
  // 启动渲染 
  updateContainer(element, root, null, callback);
  // 返回渲染后的公共实例引用
  a: if (((element = root.current), element.child))
    switch (element.child.tag) {
      case 27:
      case 5:
        element = getPublicInstance(element.child.stateNode);
        break a;
      default:
        element = element.child.stateNode;
    }
  else element = null;
  return element;
};

function updateContainer(element, container, parentComponent, callback) {
  parentComponent = container.current;
  // / 获取更新优先级
  var lane = requestUpdateLane(parentComponent);
  null === container.context
    ? (container.context = emptyContextObject)
    : (container.pendingContext = emptyContextObject);

  // 创建更新对象
  container = createUpdate(lane);
  container.payload = { element: element };
  callback = void 0 === callback ? null : callback;
  null !== callback && (container.callback = callback);

  // 将更新加入队列
  element = enqueueUpdate(parentComponent, container, lane);

  // 调度更新
  null !== element &&
    (scheduleUpdateOnFiber(element, parentComponent, lane),
    entangleTransitions(element, parentComponent, lane));
  return lane;
}

以上调用都比较简单,这里的核心是scheduleUpdateOnFiber方法,它负责调度更新。由于代码量较大,我们后面只追求核心逻辑,将省略大部分代码:

function scheduleUpdateOnFiber(root, fiber, lane) {
  // 省略......

  // 标记更新并触发调度
  markRootUpdated$1(root, lane);
  if (/* 不在渲染中 */) {
      ensureRootIsScheduled(root);                    // 确保根节点被调度
      if (/* 同步更新 */) {
        flushSyncWorkAcrossRoots_impl(0, !0);         // 立即执行同步工作
      }
   }
}


function flushSyncWorkAcrossRoots_impl(syncTransitionLanes, onlyLegacy) {
  if (!isFlushingWork && mightHavePendingSyncWork) {
    isFlushingWork = !0;
    do {
      var didPerformSomeWork = !1;
      // 遍历所有根节点执行同步工作
      for (var root = firstScheduledRoot; null !== root; ) {
        if (!onlyLegacy || 0 === root.tag)
          //省略......
          // 常规同步更新 
          performSyncWorkOnRoot(root, JSCompiler_inline_result));

        root = root.next;
      }
    } while (didPerformSomeWork);
    isFlushingWork = !1;
  }
}


function performSyncWorkOnRoot(root, lanes) {
  if (flushPendingEffects()) return null;
  performWorkOnRoot(root, lanes, !0);
}


function performWorkOnRoot(root$jscomp$0, lanes, forceSync) {
   // 选择同步或并发渲染
   var shouldTimeSlice = (!forceSync && /* 条件判断 */);
   var exitStatus = shouldTimeSlice
      ? renderRootConcurrent(root, lanes)            // 并发渲染
      : renderRootSync(root, lanes, !0);             // 同步渲染

   // 省略......
}

继续追踪同步渲染renderRootSync方法实现:

function renderRootSync(root, lanes, shouldYieldForPrerendering) {
   // 省略......

   // 同步工作循环
   workLoopSync();
   // 省略......
}


function workLoopSync() {
  for (; null !== workInProgress; ) performUnitOfWork(workInProgress);
}


function performUnitOfWork(unitOfWork) {
  // 处理单个Fiber节点
  var next = beginWork(unitOfWork.alternate, unitOfWork, entangledRenderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  null === next ? completeUnitOfWork(unitOfWork) : (workInProgress = next);
}


function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;
  do {
    // 省略......
    unitOfWork = completedWork.return;
    var next = completeWork(
      completedWork.alternate,
      completedWork,
      entangledRenderLanes
    );
    if (null !== next) {
      workInProgress = next;
      return;
    }
    completedWork = completedWork.sibling;
    if (null !== completedWork) {
      workInProgress = completedWork;
      return;
    }
    workInProgress = completedWork = unitOfWork;
  } while (null !== completedWork);
  0 === workInProgressRootExitStatus && (workInProgressRootExitStatus = 5);
}

最终是调用的completeWork方法完成渲染工作:

function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case 28:
    case 16:
    case 15:
    case 0:
    case 11:
    case 7:
    case 8:
    case 12:
    case 9:
    case 14:
      return bubbleProperties(workInProgress), null;
    case 1:
      return bubbleProperties(workInProgress), null;
    case 3:
      // 省略......
      // return  
    case 26:
    case 27:
    case 5:
      popHostContext(workInProgress);
      var type = workInProgress.type;
      if (null !== current && null != workInProgress.stateNode)
        // 省略......
      else {
        // 首次挂载
        if (!newProps) {  
          if (null === workInProgress.stateNode)
            throw Error(
              "We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue."
            );
          bubbleProperties(workInProgress);
          return null;
        }
        current = rootInstanceStackCursor.current;
        renderLanes = nextReactTag;
        nextReactTag += 2;
        // 获取ViewConfig
        type = getViewConfigForType(type);
        var updatePayload = ReactNativePrivateInterface.createAttributePayload(
          newProps,
          type.validAttributes
        );
        // 创建原生节点
        current = {
          node: createNode(
            renderLanes,
            type.uiViewClassName,  //  使用ViewConfig中的原生类名
            current.containerTag,
            updatePayload,
            workInProgress
          ),
          canonical: {
            nativeTag: renderLanes,
            viewConfig: type,       // 保存ViewConfig引用
            currentProps: newProps,
            internalInstanceHandle: workInProgress,
            publicInstance: null,
            publicRootInstance: current.publicInstance
          }
        };
        workInProgress.flags |= 8;
        appendAllChildren(current, workInProgress, !1, !1);
        workInProgress.stateNode = current;
      }
      bubbleProperties(workInProgress);
      workInProgress.flags &= -16777217;
      return null;
    // 省略部分代码......
  }
}

此方法中的case都是整数,为了弄清楚其含义,我们可以查看React 仓库中的Tag定义。源码位于 ReactWorkTags.js

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3; // 宿主树的根节点。它可以嵌套在另一个节点内部
export const HostPortal = 4; // 一个子树。它可以是通往不同渲染器的入口点
export const HostComponent = 5;
// 省略......

很显然,我们需要查看的是React Native的Tag类型,也就是HostComponent,它的值是5,因此对应到completeWork中的处理代码就是我截取的这部分。这段代码中有一个关键的方法,就是getViewConfigForType,查看其定义,显示为getViewConfigForType = ReactNativePrivateInterface.ReactNativeViewConfigRegistry.get

可看到,这里就完全与前面声明组件一节最后分析到的ReactNativeViewConfigRegistry.js对应上了,我们前面分析的是ViewConfig的注册,现在来看一下get方法:

/**
 * 获取指定视图的配置。如果这是第一次使用该视图,此配置将从UIManager延迟加载
 */
export function get(name: string): ViewConfig {
  // 从viewConfigs Map中查找已缓存的ViewConfig
  let viewConfig = viewConfigs.get(name);
  if (viewConfig == null) {
    // 获取该组件的配置生成回调函数
    const callback = viewConfigCallbacks.get(name);
    if (typeof callback !== 'function') {
      // 省略日志......
    }
    viewConfig = callback();
    invariant(viewConfig, 'View config not found for component `%s`', name);

    processEventTypes(viewConfig);
    viewConfigs.set(name, viewConfig);

    // 配置设置后清除回调,这样我们就不会在注册过程中掩盖任何错误。
    viewConfigCallbacks.set(name, null);
  }
  return viewConfig;
}

通过ViewConfig,可以得到uiViewClassName,也就是声明的组件名称,我们继续查看原生节点创建的方法:

var _nativeFabricUIManage = nativeFabricUIManager,
  createNode = _nativeFabricUIManage.createNode,          


 createNode(
   renderLanes,
   type.uiViewClassName,  //  使用ViewConfig中的原生类名
   current.containerTag,
   updatePayload,
   workInProgress
 )

createNode方法来自于全局对象nativeFabricUIManager,通过变量名就知道,这个应该是来自于JSI定义的对象,代码不在JS层。

C++ 层

继续在C++中搜索nativeFabricUIManager,定位到源码react-native/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp

void UIManagerBinding::createAndInstallIfNeeded(
    jsi::Runtime& runtime,
    const std::shared_ptr<UIManager>& uiManager) {
  auto uiManagerModuleName = "nativeFabricUIManager";

  auto uiManagerValue =
      runtime.global().getProperty(runtime, uiManagerModuleName);
  if (uiManagerValue.isUndefined()) {
    // 全局命名空间中没有该绑定的实例;我们需要创建、安装并返回它
    auto uiManagerBinding = std::make_shared<UIManagerBinding>(uiManager);
    auto object = jsi::Object::createFromHostObject(runtime, uiManagerBinding);
    runtime.global().setProperty(
        runtime, uiManagerModuleName, std::move(object));
  }
}

可见这里的nativeFabricUIManager是一个jsi::HostObject对象,我们要查找其createNode方法,直接查看UIManagerBindingget方法:

jsi::Value UIManagerBinding::get(
    jsi::Runtime& runtime,
    const jsi::PropNameID& name) {
  auto methodName = name.utf8(runtime);

  // 将 shared_ptr<UIManager> 转换为原始指针
  // 为什么这样做?原因如下:
  // 1) UIManagerBinding 强引用(strongly retains)UIManager。
  //    JS VM 通过 JSI 强引用 UIManagerBinding。
  //    这些函数是 JSI 函数,只能通过 JS VM 调用;如果 JS VM 被销毁,
  //    这些函数无法执行,这些 lambda 也不会执行。
  // 2) UIManager 只有在所有对它的引用都被释放后才会被析构,包括
  //    UIManagerBinding。这只有在 JS VM 被析构时才会发生。因此,原始指针是安全的。
  //
  // 即使这样做是安全的,为什么不直接使用 shared_ptr 作为额外的保险呢?
  // 1) 在不需要的情况下使用 shared_ptr 或 weak_ptr
  // 是一种性能劣化(pessimisation)。
  //    在这种情况下,它会执行更多指令,但不会带来任何额外价值。
  // 2) 这些 lambda 的确切释放时机和方式很复杂。向它们添加 shared_ptr 会导致
  //    UIManager 可能存活更长时间,这是不必要的、复杂的认知负担。
  // 3) 有强烈怀疑认为,从这些 C++ lambda 中保留 UIManager(这些 lambda 被
  //    JSI 持有的对象所保留),在 Scheduler 和 JS VM 析构时导致了一些崩溃。
  //    如果 C++ 语义导致这些 lambda 在 JS VM 被析构后一个 CPU
  //    时钟周期(或更久) 才被释放,就可能发生这种情况。
  UIManager* uiManager = uiManager_.get();

  // Semantic: Creates a new node with given pieces.
  if (methodName == "createNode") {
    auto paramCount = 5;
    return jsi::Function::createFromHostFunction(
        runtime,
        name,
        paramCount,
        [uiManager, methodName, paramCount](
            jsi::Runtime& runtime,
            const jsi::Value& /*thisValue*/,
            const jsi::Value* arguments,
            size_t count) -> jsi::Value {
          try {
            validateArgumentCount(runtime, methodName, paramCount, count);

            auto instanceHandle =
                instanceHandleFromValue(runtime, arguments[4], arguments[0]);
            if (!instanceHandle) {
              react_native_assert(false);
              return jsi::Value::undefined();
            }

            return valueFromShadowNode(
                runtime,
                uiManager->createNode(
                    tagFromValue(arguments[0]),
                    stringFromValue(runtime, arguments[1]),
                    surfaceIdFromValue(runtime, arguments[2]),
                    RawProps(runtime, arguments[3]),
                    std::move(instanceHandle)),
                true);
          } catch (const std::logic_error& ex) {
            LOG(FATAL) << "logic_error in createNode: " << ex.what();
          }
        });
  }
  // 省略部分代码......

  return jsi::Value::undefined();
}

继续跟踪react-native/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp中的createNode实现:

std::shared_ptr<ShadowNode> UIManager::createNode(
    Tag tag,                  // 节点标签
    const std::string& name,  // 组件名称
    SurfaceId surfaceId,
    RawProps rawProps,        // 原始属性
    InstanceHandle::Shared instanceHandle) const {
  TraceSection s("UIManager::createNode", "componentName", name);

  // 根据组件名称获取对应的ComponentDescriptor
  auto& componentDescriptor = componentDescriptorRegistry_->at(name);
  auto fallbackDescriptor =
      componentDescriptorRegistry_->getFallbackComponentDescriptor();

  PropsParserContext propsParserContext{surfaceId, *contextContainer_};

  // 创建ShadowNodeFamily,用于管理同一组件的不同实例
  auto family = componentDescriptor.createFamily(
      {.tag = tag,
       .surfaceId = surfaceId,
       .instanceHandle = std::move(instanceHandle)});

  // 解析和克隆属性
  const auto props = componentDescriptor.cloneProps(
      propsParserContext, nullptr, std::move(rawProps));
  // 创建初始状态
  const auto state = componentDescriptor.createInitialState(props, family);

  // 创建ShadowNode
  auto shadowNode = componentDescriptor.createShadowNode(
      ShadowNodeFragment{
          .props = fallbackDescriptor != nullptr &&
                  fallbackDescriptor->getComponentHandle() ==
                      componentDescriptor.getComponentHandle()
              ? componentDescriptor.cloneProps(
                    propsParserContext,
                    props,
                    RawProps(folly::dynamic::object("name", name)))
              : props,
          .children = ShadowNodeFragment::childrenPlaceholder(),
          .state = state,
      },
      family);

  if (delegate_ != nullptr) {
    delegate_->uiManagerDidCreateShadowNode(*shadowNode);
  }
  if (leakChecker_) {
    leakChecker_->uiManagerDidCreateShadowNodeFamily(family);
  }

  return shadowNode;
}

可以看到,这里通过componentDescriptorRegistry_来查找Fabric 组件描述对象,至于注册的地方,下一节注册组件会专门分析。

这里还有一点要注意,createShadowNode方法只是创建了虚拟的ShadowNode,并没有创建真正的原生视图。ShadowNode是Fabric中的虚拟DOM节点,用于布局计算。也就是说当完成布局和diff计算后,会生成MountItem指令。

Kotlin层

在前面启动渲染一节中,我们有分析到createViewUnsafe方法,回顾一下安卓原生组件的查找:

ViewManager viewManager = mViewManagerRegistry.get(componentName);

跟踪源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.kt

  @Synchronized
  public fun get(className: String): ViewManager<*, *> {
    // 1. Try to get the manager without the prefix.
    viewManagersMap[className]?.let {
      return it
    }

    // 2. Try to get the manager with the RCT prefix.
    val rctViewManagerName = "RCT$className"
    viewManagersMap[rctViewManagerName]?.let {
      return it
    }

    if (viewManagerResolver != null) {

      // 1. Try to get the manager without the prefix.
      val resolvedManager = getViewManagerFromResolver(className)
      if (resolvedManager != null) {
        return resolvedManager
      }

      // 2. Try to get the manager with the RCT prefix.
      val rctResolvedManager = getViewManagerFromResolver(rctViewManagerName)
      if (rctResolvedManager != null) {
        return rctResolvedManager
      }

      throw IllegalViewOperationException(
          "Can't find ViewManager '$className' nor '$rctViewManagerName' in ViewManagerRegistry, " +
              "existing names are: ${viewManagerResolver.getViewManagerNames()}"
      )
    }

    throw IllegalViewOperationException("No ViewManager found for class $className")
  }

  private fun getViewManagerFromResolver(className: String): ViewManager<*, *>? {
    val viewManager = viewManagerResolver?.getViewManager(className)
    if (viewManager != null) {
      viewManagersMap[className] = viewManager
    }
    return viewManager
  }

新架构的情况下,是通过getViewManagerFromResolver方法来查找,其中viewManagerResolver的类型是BridgelessViewManagerResolver,它是一个内部类,定义在ReactInstance.kt文件中:

private class BridgelessViewManagerResolver(
      private val reactPackages: List<ReactPackage>,
      private val context: BridgelessReactContext,
  ) : ViewManagerResolver {
    private val lazyViewManagerMap: MutableMap<String, ViewManager<*, *>> = HashMap()

    override fun getViewManager(viewManagerName: String): ViewManager<*, *>? {
      // 从懒加载包中查找
      val viewManager = getLazyViewManager(viewManagerName)
      if (viewManager != null) {
        return viewManager
      }
      // 如果通过延迟加载在所有 React 包中都找不到视图管理器,则回退到默认实现:立即初始化所有视图管理器
      return eagerViewManagerMap[viewManagerName]
    }

    private lateinit var _eagerViewManagerMap: Map<String, ViewManager<*, *>>

    @get:Synchronized
    val eagerViewManagerMap: Map<String, ViewManager<*, *>>
      get() {
        if (::_eagerViewManagerMap.isInitialized) {
          return _eagerViewManagerMap
        }

        val viewManagerMap: MutableMap<String, ViewManager<*, *>> = HashMap()
        for (reactPackage in reactPackages) {
          if (reactPackage is ViewManagerOnDemandReactPackage) {
            continue
          }

          val viewManagersInPackage = reactPackage.createViewManagers(context)
          for (viewManager in viewManagersInPackage) {
            // TODO(T173624687): Should we throw/warn when the same view manager name is registered
            // twice?
            viewManagerMap[viewManager.name] = viewManager
          }
        }

        _eagerViewManagerMap = viewManagerMap
        return viewManagerMap
      }

    @Synchronized
    fun getLazyViewManager(viewManagerName: String): ViewManager<*, *>? {
      // 先查缓存 
      if (lazyViewManagerMap.containsKey(viewManagerName)) {
        return lazyViewManagerMap[viewManagerName]
      }

      // 缓存未命中则遍历所有 reactPackages,调用 createViewManager(name) 创建
      for (reactPackage in reactPackages) {
        if (reactPackage is ViewManagerOnDemandReactPackage) {
          val viewManager = reactPackage.createViewManager(context, viewManagerName)
          if (viewManager != null) {
            // TODO(T173624687): Should we throw/warn when the same view manager name is registered
            // twice?
            lazyViewManagerMap[viewManagerName] = viewManager
            return viewManager
          }
        }
      }

      return null
    }

    // 省略部分代码......
  }

注册组件

JS工具

React Native新架构使用了很多代码生成工具,以致于成了黑箱操作,这对于Turbo Module和Fabric组件的注册流程造成了理解上的困难。为此,我们不得不研究这些CLI工具。首先研究@react-native-community/cli工具,源码 cli。这里我们使用的版本是v20.0.2

查看源码cli/packages/cli-config-android/src/config/index.ts

export function dependencyConfig(
  root: string,
  userConfig: AndroidDependencyParams | null = {},
): AndroidDependencyConfig | null {
  if (userConfig === null) {
    return null;
  }

  const src = userConfig.sourceDir || findAndroidDir(root);

  if (!src) {
    return null;
  }

  const sourceDir = path.join(root, src);
  const manifestPath = userConfig.manifestPath
    ? path.join(sourceDir, userConfig.manifestPath)
    : findManifest(sourceDir);
  const buildGradlePath = findBuildGradle(sourceDir, '');
  const isPureCxxDependency =
    userConfig.cxxModuleCMakeListsModuleName != null &&
    userConfig.cxxModuleCMakeListsPath != null &&
    userConfig.cxxModuleHeaderName != null &&
    !manifestPath &&
    !buildGradlePath;

  if (!manifestPath && !buildGradlePath && !isPureCxxDependency) {
    return null;
  }

  let packageImportPath = null,
    packageInstance = null;

  if (!isPureCxxDependency) {
    const packageName =
      userConfig.packageName || getPackageName(manifestPath, buildGradlePath);
    const packageClassName = findPackageClassName(sourceDir);

    /**
     * This module has no package to export
     */
    if (!packageClassName) {
      return null;
    }

    packageImportPath =
      userConfig.packageImportPath ||
      `import ${packageName}.${packageClassName};`;

    packageInstance = userConfig.packageInstance || `new ${packageClassName}()`;
  }

  const buildTypes = userConfig.buildTypes || [];
  const dependencyConfiguration = userConfig.dependencyConfiguration;
  const libraryName =
    userConfig.libraryName || findLibraryName(root, sourceDir);
  const componentDescriptors =
    userConfig.componentDescriptors || findComponentDescriptors(root);
  let cmakeListsPath = userConfig.cmakeListsPath
    ? path.join(sourceDir, userConfig.cmakeListsPath)
    : path.join(sourceDir, 'build/generated/source/codegen/jni/CMakeLists.txt');
  const cxxModuleCMakeListsModuleName =
    userConfig.cxxModuleCMakeListsModuleName || null;
  const cxxModuleHeaderName = userConfig.cxxModuleHeaderName || null;
  let cxxModuleCMakeListsPath = userConfig.cxxModuleCMakeListsPath
    ? path.join(sourceDir, userConfig.cxxModuleCMakeListsPath)
    : null;

  if (process.platform === 'win32') {
    cmakeListsPath = cmakeListsPath.replace(/\\/g, '/');
    if (cxxModuleCMakeListsPath) {
      cxxModuleCMakeListsPath = cxxModuleCMakeListsPath.replace(/\\/g, '/');
    }
  }

  return {
    sourceDir,
    packageImportPath,
    packageInstance,
    buildTypes,
    dependencyConfiguration,
    libraryName,
    componentDescriptors,
    cmakeListsPath,
    cxxModuleCMakeListsModuleName,
    cxxModuleCMakeListsPath,
    cxxModuleHeaderName,
    isPureCxxDependency,
  };
}

dependencyConfig函数非常重要,它主要用于生成example/android/build/generated/autolinking/autolinking.json文件。autolinking.json文件我们在前面的TurboModule的文章已经介绍过了,其中的信息是指导代码生成的关键。这里重点关注componentDescriptors字段的生成,因为它是Fabric组件代码生成的起始。

组件开发者通常并不会在react-native.config.js文件中主动配置componentDescriptors字段,一般都是默认值,那么就要查看一下findComponentDescriptors函数的实现逻辑了,源码cli/packages/cli-config-android/src/config/findComponentDescriptors.ts

export function findComponentDescriptors(packageRoot: string) {
  let jsSrcsDir = null;
  try {
    const packageJson = fs.readFileSync(
      path.join(packageRoot, 'package.json'),
      'utf8',
    );
    jsSrcsDir = JSON.parse(packageJson).codegenConfig.jsSrcsDir;
  } catch (error) {
    // no jsSrcsDir, continue with default glob pattern
  }
  const globPattern = jsSrcsDir
    ? `${jsSrcsDir}/**/*{.js,.jsx,.ts,.tsx}`
    : '**/*{.js,.jsx,.ts,.tsx}';
  const files = glob.sync(globPattern, {
    cwd: unixifyPaths(packageRoot),
    onlyFiles: true,
    ignore: ['**/node_modules/**'],
  });
  const codegenComponent = files
    .map((filePath) =>
      fs.readFileSync(path.join(packageRoot, filePath), 'utf8'),
    )
    .map(extractComponentDescriptors)
    .filter(Boolean);

  // Filter out duplicates as it happens that libraries contain multiple outputs due to package publishing.
  // TODO: consider using "codegenConfig" to avoid this.
  return Array.from(new Set(codegenComponent as string[]));
}

这里主要逻辑其实是从package.json中获取到jsSrcsDir配置,从而定位到源码所在目录。我们关注的是codegenComponent的生成,此处是通过读取每个源文件内容,然后调用 extractComponentDescriptors 提取组件描述符。继续查看cli/packages/cli-config-android/src/config/extractComponentDescriptors.ts

const CODEGEN_NATIVE_COMPONENT_REGEX =
  /codegenNativeComponent(<.*>)?\s*\(\s*["'`](\w+)["'`](,?[\s\S]+interfaceOnly:\s*(\w+))?/m;

export function extractComponentDescriptors(contents: string) {
  const match = contents.match(CODEGEN_NATIVE_COMPONENT_REGEX);
  if (!(match?.[4] === 'true') && match?.[2]) {
    return `${match[2]}ComponentDescriptor`;
  }
  return null;
}

到这里就很清楚了,扫描所有源码,使用正则找到我们在声明组件一节中提到的export default codegenNativeComponent<NativeProps>('CustomviewView');这行代码,这里的正则就是匹配关键字codegenNativeComponent。这里的match?.[2]就是提前出组件名,也就是我们示例中的CustomviewView。最终返回的字符串是拼接后的,这里就是CustomviewViewComponentDescriptor。也就是说componentDescriptors字段的默认值是CustomviewViewComponentDescriptor

接下来我们回顾前文《Android端TurboModule分析》,其中提到但略过的React Gradle脚本的configureCodegen方法:

  private fun configureCodegen(
      project: Project,
      localExtension: ReactExtension,
      rootExtension: PrivateReactExtension,
      isLibrary: Boolean,
  ) {
    // 首先,我们需要设置 Codegen 的输出目录
    val generatedSrcDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/source/codegen")

    // 我们为 jsRootDir(JS根目录)指定默认值(约定)。
    // 对于 App 来说是根文件夹(即 Gradle 项目的 ../../)
    // 对于 Library 来说是包文件夹(即 Gradle 项目的 ../)
    if (isLibrary) {
      localExtension.jsRootDir.convention(project.layout.projectDirectory.dir("../"))
    } else {
      localExtension.jsRootDir.convention(localExtension.root)
    }

    // 我们创建任务以从 JS 文件生成 Schema
    val generateCodegenSchemaTask =
        project.tasks.register(
            "generateCodegenSchemaFromJavaScript",
            GenerateCodegenSchemaTask::class.java,
        ) { it ->
          it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
          it.codegenDir.set(rootExtension.codegenDir)
          it.generatedSrcDir.set(generatedSrcDir)
          it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)

          // 我们在配置阶段读取 package.json,以便正确地填充此任务的 `jsRootDir` @Input 属性
          // 以及 onlyIf 条件。因此,parsePackageJson 应该在这个 lambda 表达式内部被调用。
          val packageJson = findPackageJsonFile(project, rootExtension.root)
          val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }

          val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
          val includesGeneratedCode =
              parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
          if (jsSrcsDirInPackageJson != null) {
            it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
          } else {
            it.jsRootDir.set(localExtension.jsRootDir)
          }
          it.jsInputFiles.set(
              project.fileTree(it.jsRootDir) { tree ->
                tree.include("**/*.js")
                tree.include("**/*.jsx")
                tree.include("**/*.ts")
                tree.include("**/*.tsx")

                tree.exclude("node_modules/**/*")
                tree.exclude("**/*.d.ts")
                // 我们希望排除 build 目录,以避免在执行规避检查时选中它们
                tree.exclude("**/build/**/*")
              }
          )

          val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
          it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
        }

    // 我们创建任务以从 Schema 生成 Java 代码
    val generateCodegenArtifactsTask =
        project.tasks.register(
            "generateCodegenArtifactsFromSchema",
            GenerateCodegenArtifactsTask::class.java,
        ) { task ->
          task.dependsOn(generateCodegenSchemaTask)
          task.reactNativeDir.set(rootExtension.reactNativeDir)
          task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
          task.generatedSrcDir.set(generatedSrcDir)
          task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
          task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
          task.libraryName.set(localExtension.libraryName)
          task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)

          // 请注意,appNeedsCodegen 会触发在配置阶段读取 package.json,
          // 因为我们需要填充此任务的 onlyIf 条件。
          // 因此,appNeedsCodegen 需要在这个 lambda 表达式内部被调用
          val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
          val packageJson = findPackageJsonFile(project, rootExtension.root)
          val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
          val includesGeneratedCode =
              parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
          task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
        }

    // 我们更新 Android 配置以包含生成的源码
    // 这相当于以下的 DSL:
    //
    // android { sourceSets { main { java { srcDirs += "$generatedSrcDir/java" } } } }
    if (isLibrary) {
      project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext
        ->
        ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile)
      }
    } else {
      project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
          ext ->
        ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile)
      }
    }

    // `preBuild` 是 AGP 自动注册的基础任务之一
    // 这将在编译整个项目之前调用 Codegen
    project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask)
  }

configureCodegen 是 React Native Gradle 插件的核心方法,负责配置 Fabric/TurboModule 新架构的代码生成(Codegen)流程。该方法在 Gradle 配置阶段执行,设置两个关键任务和 Android 源码集集成。

其流程可以分为五个阶段:

  1. 设置输出目录和 JS 根目录

  2. 创建 Schema 生成任务

  3. 创建代码生成任务

  4. 集成到 Android 构建系统

  5. 挂载到构建生命周期

这里概括一下整个流程:

1. Gradle 配置阶段
   ├─> 设置 generatedSrcDir = "build/generated/source/codegen"
   ├─> 设置 jsRootDir 默认值(Library: "../", App: root)
   ├─> 读取 package.json(配置阶段)
   │   ├─> 检查 codegenConfig.jsSrcsDir
   │   └─> 检查 codegenConfig.includesGeneratedCode
   ├─> 注册 generateCodegenSchemaTask
   │   ├─> 配置 Node.js 环境
   │   ├─> 设置 JS 输入文件(**/*.{js,jsx,ts,tsx})
   │   └─> 设置 onlyIf 条件
   ├─> 注册 generateCodegenArtifactsTask
   │   ├─> 依赖 generateCodegenSchemaTask
   │   └─> 设置 onlyIf 条件
   ├─> 配置 Android SourceSets
   │   └─> 添加 generatedSrcDir/java 到 main SourceSet
   └─> 建立 preBuild 依赖关系

2. Gradle 执行阶段(运行 ./gradlew build)
   ├─> preBuild 任务执行
   │   └─> generateCodegenArtifactsTask 执行
   │         ├─> generateCodegenSchemaTask 执行
   │         │     ├─> 扫描 JS/TS 文件
   │         │     └─> 生成 schema.json
   │         └─> 根据 schema.json 生成 Java/C++ 代码
   └─> compileJava 编译生成的代码

这里的两个关键任务分别是generateCodegenSchemaFromJavaScriptgenerateCodegenArtifactsFromSchema。前者从 JS/TS 生成 Schema JSON文件,后者从 Schema 生成 Java/C++ 代码。

Schema JSON主要是JS/TS的接口描述,生成路径是Your Project/node_modules/Third-party Lib/android/build/generated/source/codegen/schema.json,可以自行查看,这里我们重点关注generateCodegenArtifactsFromSchema,源码react-native/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt

abstract class GenerateCodegenArtifactsTask : Exec() {
  @get:Internal abstract val reactNativeDir: DirectoryProperty
  @get:Internal abstract val generatedSrcDir: DirectoryProperty
  @get:InputFile abstract val packageJsonFile: RegularFileProperty   // package.json 文件路径
  @get:Input abstract val nodeWorkingDir: Property<String>
  @get:Input abstract val nodeExecutableAndArgs: ListProperty<String>
  @get:Input abstract val codegenJavaPackageName: Property<String>
  @get:Input abstract val libraryName: Property<String>

  @get:InputFile
  val generatedSchemaFile: Provider<RegularFile> = generatedSrcDir.file("schema.json")

  @get:OutputDirectory val generatedJavaFiles: Provider<Directory> = generatedSrcDir.dir("java")
  @get:OutputDirectory val generatedJniFiles: Provider<Directory> = generatedSrcDir.dir("jni")

  override fun exec() {
    val (resolvedLibraryName, resolvedCodegenJavaPackageName) = resolveTaskParameters()
    setupCommandLine(resolvedLibraryName, resolvedCodegenJavaPackageName)
    super.exec()
  }

  internal fun resolveTaskParameters(): Pair<String, String> {
    val parsedPackageJson =
        if (packageJsonFile.isPresent && packageJsonFile.get().asFile.exists()) {
          JsonUtils.fromPackageJson(packageJsonFile.get().asFile)
        } else {
          null
        }
    val resolvedLibraryName = parsedPackageJson?.codegenConfig?.name ?: libraryName.get()
    val resolvedCodegenJavaPackageName =
        parsedPackageJson?.codegenConfig?.android?.javaPackageName ?: codegenJavaPackageName.get()
    return resolvedLibraryName to resolvedCodegenJavaPackageName
  }

  internal fun setupCommandLine(libraryName: String, codegenJavaPackageName: String) {
    val workingDir = File(nodeWorkingDir.get())
    commandLine(
        windowsAwareCommandLine(
            *nodeExecutableAndArgs.get().toTypedArray(),
            reactNativeDir.file("scripts/generate-specs-cli.js").get().asFile.cliPath(workingDir),
            "--platform",
            "android",
            "--schemaPath",
            generatedSchemaFile.get().asFile.cliPath(workingDir),
            "--outputDir",
            generatedSrcDir.get().asFile.cliPath(workingDir),
            "--libraryName",
            libraryName,
            "--javaPackageName",
            codegenJavaPackageName,
        )
    )
  }
}

该类主要用于执行外部 Node.js 命令,依据 schema.json文件,生成 Java/JNI 代码。需要注意一下,这里libraryNamecodegenJavaPackageName的取值,代码中是通过resolveTaskParameters方法提取出来的。回顾一下package.json文件的配置,大概是以下结构:

{
  "codegenConfig": {
    "name": "MyLibrary",
    "android": {
      "javaPackageName": "com.example.mylibrary"
    }
  }
}

那么libraryName就是"MyLibrary"codegenJavaPackageName就是"com.example.mylibrary"

现在再还原一下命令,其实就是以下形式:

node <reactNativeDir>/scripts/generate-specs-cli.js \
  --platform android \
  --schemaPath <generatedSrcDir>/schema.json \
  --outputDir <generatedSrcDir> \
  --libraryName <resolvedLibraryName> \
  --javaPackageName <resolvedCodegenJavaPackageName>

我们定位到该js文件react-native/packages/react-native/scripts/generate-specs-cli.js

const executor = require('./codegen/generate-specs-cli-executor');
// 省略......

function main() {
  executor.execute(
    // $FlowFixMe[prop-missing]
    argv.platform,
    // $FlowFixMe[prop-missing]
    argv.schemaPath,
    // $FlowFixMe[prop-missing]
    argv.outputDir,
    // $FlowFixMe[prop-missing]
    argv.libraryName,
    // $FlowFixMe[prop-missing]
    argv.javaPackageName,
    // $FlowFixMe[prop-missing]
    argv.libraryType,
  );
}

main();

继续跟踪react-native/packages/react-native/scripts/codegen/generate-specs-cli-executor.js

const utils = require('./codegen-utils');

const GENERATORS /*: {[string]: {[string]: $ReadOnlyArray<string>}} */ = {
  all: {
    android: ['componentsAndroid', 'modulesAndroid', 'modulesCxx'],
    ios: ['componentsIOS', 'modulesIOS', 'modulesCxx'],
  },
  components: {
    android: ['componentsAndroid'],
    ios: ['componentsIOS'],
  },
  modules: {
    android: ['modulesAndroid', 'modulesCxx'],
    ios: ['modulesIOS', 'modulesCxx'],
  },
};

function generateSpecFromInMemorySchema(
  platform /*: string */,
  schema /*: string */,
  outputDirectory /*: string */,
  libraryName /*: string */,
  packageName /*: string */,
  libraryType /*: string */,
  useLocalIncludePaths /*: boolean */,
) {
  validateLibraryType(libraryType);
  createOutputDirectoryIfNeeded(outputDirectory, libraryName);
  const includeGetDebugPropsImplementation =
    libraryName.includes('FBReactNativeSpec'); //only generate getDebugString for React Native Core Components
  utils.getCodegen().generate(
    {
      libraryName,
      schema,
      outputDirectory,
      packageName,
      assumeNonnull: platform === 'ios',
      useLocalIncludePaths,
      includeGetDebugPropsImplementation,
    },
    {
      generators: GENERATORS[libraryType][platform],
    },
  );

  if (platform === 'android') {
    // Move all components C++ files to a structured jni folder for now.
    // Note: this should've been done by RNCodegen's generators, but:
    // * the generators don't support platform option yet
    // * this subdir structure is Android-only, not applicable to iOS
    const files = fs.readdirSync(outputDirectory);
    const jniOutputDirectory = `${outputDirectory}/jni/react/renderer/components/${libraryName}`;
    fs.mkdirSync(jniOutputDirectory, {recursive: true});
    files
      .filter(f => f.endsWith('.h') || f.endsWith('.cpp'))
      .forEach(f => {
        fs.renameSync(`${outputDirectory}/${f}`, `${jniOutputDirectory}/${f}`);
      });
  }
}

这里核心是调用utils.getCodegen().generate来执行代码生成逻辑,但是这里有一个传参需要注意一下,generators: GENERATORS[libraryType][platform],我们观察GENERATORS的定义就会发现,这里的参数配置正好对应package.json中的codegen配置,由于我们现在研究的是Fabric组件注册,那么这里的参数应该是componentsAndroid

继续查找generate方法的实现,源码react-native/packages/react-native/scripts/codegen/codegen-utils.js

/**
* 用于抽象实际代码生成过程的包装器。 
* 之所以需要这个包装器,是因为在 Sandcastle 中运行测试时,并非所有环境都像通常那样设置。 
* 例如,`@react-native/codegen` 库就不存在。 
*
* 借助这个包装器,我们可以模拟代码生成器的 getter 方法,使其返回一个自定义对象,该对象模拟了 Codegen 接口。 
*
* @return 一个可以为新架构生成代码的对象。 
*/
function getCodegen() /*: $FlowFixMe */ {
  let RNCodegen;
  try {
    // $FlowFixMe[cannot-resolve-module]
    RNCodegen = require('../../packages/react-native-codegen/lib/generators/RNCodegen.js');
  } catch (e) {
    // $FlowFixMe[cannot-resolve-module]
    RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen.js');
  }
  if (!RNCodegen) {
    throw new Error('RNCodegen not found.');
  }
  return RNCodegen;
}

继续跟踪react-native/packages/react-native-codegen/lib/generators/RNCodegen.js

const LIBRARY_GENERATORS = {
  descriptors: [
    generateComponentDescriptorCpp.generate,
    generateComponentDescriptorH.generate,
  ],
  events: [generateEventEmitterCpp.generate, generateEventEmitterH.generate],
  states: [generateStateCpp.generate, generateStateH.generate],
  props: [
    generateComponentHObjCpp.generate,
    generatePropsCpp.generate,
    generatePropsH.generate,
    generatePropsJavaInterface.generate,
    generatePropsJavaDelegate.generate,
  ],
  // TODO: Refactor this to consolidate various C++ output variation instead of forking per platform.
  componentsAndroid: [
    // JNI/C++ files
    generateComponentDescriptorH.generate,
    generateComponentDescriptorCpp.generate,
    generateEventEmitterCpp.generate,
    generateEventEmitterH.generate,
    generatePropsCpp.generate,
    generatePropsH.generate,
    generateStateCpp.generate,
    generateStateH.generate,
    generateShadowNodeCpp.generate,
    generateShadowNodeH.generate,
    // Java files
    generatePropsJavaInterface.generate,
    generatePropsJavaDelegate.generate,
  ],
  componentsIOS: [
    generateComponentDescriptorH.generate,
    generateComponentDescriptorCpp.generate,
    generateEventEmitterCpp.generate,
    generateEventEmitterH.generate,
    generateComponentHObjCpp.generate,
    generatePropsCpp.generate,
    generatePropsH.generate,
    generateStateCpp.generate,
    generateStateH.generate,
    generateShadowNodeCpp.generate,
    generateShadowNodeH.generate,
  ],
  modulesAndroid: [
    generateModuleJniCpp.generate,
    generateModuleJniH.generate,
    generateModuleJavaSpec.generate,
  ],
  modulesCxx: [generateModuleH.generate],
  modulesIOS: [generateModuleObjCpp.generate],
  tests: [generateTests.generate],
  'shadow-nodes': [
    generateShadowNodeCpp.generate,
    generateShadowNodeH.generate,
  ],
};


module.exports = {
  allGenerators: ALL_GENERATORS,
  generate(
    {
      libraryName,
      schema,
      outputDirectory,
      packageName,
      assumeNonnull,
      useLocalIncludePaths,
      includeGetDebugPropsImplementation = false,
      libraryGenerators = LIBRARY_GENERATORS,
    },
    {generators, test},
  ) {
    schemaValidator.validate(schema);
    const defaultHeaderPrefix = 'react/renderer/components';
    const headerPrefix =
      useLocalIncludePaths === true
        ? ''
        : `${defaultHeaderPrefix}/${libraryName}/`;
    function composePath(intermediate) {
      return path.join(outputDirectory, intermediate, libraryName);
    }
    const componentIOSOutput = composePath(
      useLocalIncludePaths === true ? '' : defaultHeaderPrefix,
    );
    const modulesIOSOutput = composePath('./');
    const outputFoldersForGenerators = {
      componentsIOS: componentIOSOutput,
      modulesIOS: modulesIOSOutput,
      descriptors: outputDirectory,
      events: outputDirectory,
      props: outputDirectory,
      states: outputDirectory,
      componentsAndroid: outputDirectory,
      modulesAndroid: outputDirectory,
      modulesCxx: outputDirectory,
      tests: outputDirectory,
      'shadow-nodes': outputDirectory,
    };
    const generatedFiles = [];
    for (const name of generators) {
      for (const generator of libraryGenerators[name]) {
        generator(
          libraryName,
          schema,
          packageName,
          assumeNonnull,
          headerPrefix,
          includeGetDebugPropsImplementation,
        ).forEach((contents, fileName) => {
          generatedFiles.push({
            name: fileName,
            content: contents,
            outputDir: outputFoldersForGenerators[name],
          });
        });
      }
    }
    return checkOrWriteFiles(generatedFiles, test);
  },
  // 省略......
};

可以看到,这里for (const name of generators)遍历的generators就是我们前面强调过的GENERATORS参数处理。那么此时name的值就是componentsAndroid

既然确定了参数,那么从libraryGenerators中遍历出来的generator就是以下这些生成器:

  componentsAndroid: [
    // JNI/C++ files
    generateComponentDescriptorH.generate,
    generateComponentDescriptorCpp.generate,
    generateEventEmitterCpp.generate,
    generateEventEmitterH.generate,
    generatePropsCpp.generate,
    generatePropsH.generate,
    generateStateCpp.generate,
    generateStateH.generate,
    generateShadowNodeCpp.generate,
    generateShadowNodeH.generate,
    // Java files
    generatePropsJavaInterface.generate,
    generatePropsJavaDelegate.generate,
  ],

这里先研究一下generateComponentDescriptorH.generategenerateComponentDescriptorCpp.generate

源码react-native/packages/react-native-codegen/src/generators/components/GenerateComponentDescriptorH.js

const FileTemplate = ({
  libraryName,
  componentDefinitions,
  headerPrefix,
}: {
  libraryName: string,
  componentDefinitions: string,
  headerPrefix: string,
}) => `
/**
 * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 * ${'@'}generated by codegen project: GenerateComponentDescriptorH.js
 */

#pragma once

${IncludeTemplate({headerPrefix, file: 'ShadowNodes.h'})}
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook::react {

${componentDefinitions}

void ${libraryName}_registerComponentDescriptorsFromCodegen(
  std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);

} // namespace facebook::react
`;

const ComponentDefinitionTemplate = ({className}: {className: string}) =>
  `
using ${className}ComponentDescriptor = ConcreteComponentDescriptor<${className}ShadowNode>;
`.trim();

// 省略部分代码......

这里我们只简单看一下用于头文件生成的字符串模版。libraryName参数取值我们上面已经分析过了,来自package.json中的配置项。那么还有一个关键参数className的取值需要弄清楚。实际上这里的componentDefinitionsclassName都来自于schema.json。具体看一下className生成逻辑:

    const componentDefinitions = Object.keys(schema.modules)
      .map(moduleName => {
        const module = schema.modules[moduleName];
        if (module.type !== 'Component') {
          return;
        }

        const {components} = module;
        // No components in this module
        if (components == null) {
          return null;
        }

        return Object.keys(components)
          .map(componentName => {
            if (components[componentName].interfaceOnly === true) {
              return;
            }

            return ComponentDefinitionTemplate({className: componentName});
          })
          .join('\n');
      })
      .filter(Boolean)
      .join('\n');

    const replacedTemplate = FileTemplate({
      libraryName,
      componentDefinitions,
      headerPrefix: headerPrefix ?? '',
    });

可以看到,实际上是在遍历schema.json中的components字段。在声明组件一节已经创建了Demo工程,现在构建项目生成schema.json,查看相关内容:

{
  "libraryName": "",
  "modules": {
    "CustomWebView": {
      "type": "Component",
      "components": {
        "CustomWebView": {
          "extendsProps": [
            {
              "type": "ReactNativeBuiltInType",
              "knownTypeName": "ReactNativeCoreViewProps"
            }
          ],
          "events": [省略......],
          "props": [省略......],
          "commands": []
        }
      }
    }
  }
}

那么className就是componentName,也就是自定义的组件名CustomWebView

C++ 层

弄清楚代码生成的逻辑之后,接下来我们可以直接查看生成的文件内容,主要是ComponentDescriptors.h

#pragma once

#include <react/renderer/components/CustomviewViewSpec/ShadowNodes.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook::react {

using CustomWebViewComponentDescriptor = ConcreteComponentDescriptor<CustomWebViewShadowNode>;

void CustomviewViewSpec_registerComponentDescriptorsFromCodegen(
  std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);
}

ComponentDescriptors.cpp

#include <react/renderer/components/CustomviewViewSpec/ComponentDescriptors.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook::react {

void CustomviewViewSpec_registerComponentDescriptorsFromCodegen(
  std::shared_ptr<const ComponentDescriptorProviderRegistry> registry) {
registry->add(concreteComponentDescriptorProvider<CustomWebViewComponentDescriptor>());
}

}

头文件定义了一个类型别名CustomWebViewComponentDescriptor,然后在实现文件中注册了这个Provider。我们看一下concreteComponentDescriptorProvider函数的实现,源码react-native/packages/react-native/ReactCommon/react/renderer/componentregistry/ComponentDescriptorProvider.h:

/*
* 为给定的 `ComponentDescriptor` 类创建一个 `ComponentDescriptorProvider`
*/
template <typename ComponentDescriptorT>
ComponentDescriptorProvider concreteComponentDescriptorProvider()
{
  static_assert(
      std::is_base_of<ComponentDescriptor, ComponentDescriptorT>::value,
      "ComponentDescriptorT must be a descendant of ComponentDescriptor");

  return {
      ComponentDescriptorT::ConcreteShadowNode::Handle(),
      ComponentDescriptorT::ConcreteShadowNode::Name(),
      nullptr,
      &concreteComponentDescriptorConstructor<ComponentDescriptorT>};
}

返回值是一个ComponentDescriptorProvider类型实例,继续跟踪一下类定义:

/*
 * 提供了一种统一的方式来构造特定存储的 `ComponentDescriptor` 类的实例。 
 * C++ 不允许创建指向构造函数的指针,因此我们必须使用这样的数据结构来操作一组类。
 *
 * 注意:某些组件的 `handle` 和 `name` 的实际值取决于 `flavor`。 
 * 如果使用给定的 `flavor` 通过 `constructor` 对象实例化后,
 * Provider暴露的 `handle` 和 `name` 值与预期值相同,则该提供者有效。 
 */
class ComponentDescriptorProvider final {
 public:
  ComponentHandle handle;
  ComponentName name;
  ComponentDescriptor::Flavor flavor;
  ComponentDescriptorConstructor *constructor;
};

这里大量使用了C++模版,要想查看真正的handle和name值,需要当实际的模版类型中查找。这里先查看源码react-native/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h

namespace facebook::react {

/*
 * Default template-based implementation of ComponentDescriptor.
 * Use your `ShadowNode` type as a template argument and override any methods
 * if necessary.
 */
template <typename ShadowNodeT>
class ConcreteComponentDescriptor : public ComponentDescriptor {
  static_assert(std::is_base_of<ShadowNode, ShadowNodeT>::value, "ShadowNodeT must be a descendant of ShadowNode");

  using SharedShadowNodeT = std::shared_ptr<const ShadowNodeT>;

 public:
  using ConcreteShadowNode = ShadowNodeT;

  // 省略代码......
} // namespace facebook::react

可以看到,ConcreteShadowNode实际上只是一个类型别名,具体的要看模版的实际参数,那么ConcreteShadowNode::Handle就相当于CustomWebViewShadowNode::Handle。这里CustomWebViewShadowNode也是自动生成的代码,我们直接查看android/app/build/generated/source/codegen/jni/react/renderer/components/CustomviewViewSpec/ShadowNodes.h

/*
 * `ShadowNode` for <CustomWebView> component.
 */
using CustomWebViewShadowNode = ConcreteViewShadowNode<
    CustomWebViewComponentName,
    CustomWebViewProps,
    CustomWebViewEventEmitter,
    CustomWebViewState>;

继续查看android/app/build/generated/source/codegen/jni/react/renderer/components/CustomviewViewSpec/ShadowNodes.cpp

#include <react/renderer/components/CustomviewViewSpec/ShadowNodes.h>

namespace facebook::react {

extern const char CustomWebViewComponentName[] = "CustomWebView";

} 

现在跟踪一下react-native/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h

namespace facebook::react {

/*
 * Template for all <View>-like classes (classes which have all same props
 * as <View> and similar basic behaviour).
 * For example: <Paragraph>, <Image>, but not <Text>, <RawText>.
 */
template <
    const char *concreteComponentName,
    typename ViewPropsT = ViewProps,
    typename ViewEventEmitterT = ViewEventEmitter,
    typename StateDataT = StateData>
  requires(std::is_base_of_v<ViewProps, ViewPropsT>)
class ConcreteViewShadowNode : public ConcreteShadowNode<
                                   concreteComponentName,
                                   YogaLayoutableShadowNode,
                                   ViewPropsT,
                                   ViewEventEmitterT,
                                   StateDataT> {
  // 省略代码......
};

} // namespace facebook::react

ConcreteViewShadowNode类中并未实现HandleName方法,继续查看父类react-native/packages/react-native/ReactCommon/react/renderer/core/ConcreteShadowNode.h

namespace facebook::react {

/*
 * Base templace class for all `ShadowNode`s which connects exact `ShadowNode`
 * type with exact `Props` type.
 * `ConcreteShadowNode` is a default implementation of `ShadowNode` interface
 * with many handy features.
 */
template <
    ComponentName concreteComponentName,
    typename BaseShadowNodeT,
    typename PropsT,
    typename EventEmitterT = EventEmitter,
    typename StateDataT = StateData>
class ConcreteShadowNode : public BaseShadowNodeT {

 protected:
  using ShadowNode::props_;
  using ShadowNode::state_;

 public:
  using BaseShadowNodeT::BaseShadowNodeT;
  // 省略......

  static ComponentName Name()
  {
    return ComponentName(concreteComponentName);
  }

  static ComponentHandle Handle()
  {
    return ComponentHandle(concreteComponentName);
  }

  // 省略......
};

} // namespace facebook::react

到这里就很清晰了,Name和Handle方法返回值内部是持有的相同的concreteComponentName,而这个模版参数,根据前面的传参,实际是就是

CustomWebViewComponentName,也就是"CustomWebView"

扫描Fabric 组件库,生成代码的逻辑其实已经很清楚了,最后只剩下一个问题,真正调用注册的代码在哪里?事实上,安卓中并未真正通过CustomviewViewSpec_registerComponentDescriptorsFromCodegen函数去注册,而是使用了autolinking机制。这部分在《Android端TurboModule分析》一文有详细介绍了,可以去回顾一下GenerateAutolinkingNewArchitecturesFileTask脚本的分析,其中生成的信息源就来自我们前面费了半天劲分析的autolinking.json中的

现在来看一下Gradle脚本生成的autolinking.cpp

#include "autolinking.h"
#include <CustomviewViewSpec.h>
#include <react/renderer/components/CustomviewViewSpec/ComponentDescriptors.h>

namespace facebook {
namespace react {

std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
auto module_CustomviewViewSpec = CustomviewViewSpec_ModuleProvider(moduleName, params);
if (module_CustomviewViewSpec != nullptr) {
return module_CustomviewViewSpec;
}
  return nullptr;
}

std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {

  return nullptr;
}

void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
providerRegistry->add(concreteComponentDescriptorProvider<CustomWebViewComponentDescriptor>());
  return;
}

} // namespace react
} // namespace facebook

这里生成了autolinking_registerProviders方法,这才是真正注册组件的地方。而此处的代码是由Gradle脚本生成,其中的关键信息就来自autolinking.json中的componentDescriptors字段,也就是前面我们费了半天劲才分析出该字段的默认值的地方。整个React Native的代码生成其实是有些混乱的,会生成一些并不会使用的代码,对理解产生干扰。

关于autolinking_registerProviders函数的调用链,在前面的文章也分析涉及过,这里再回顾一下调用流程,源码react-native/packages/react-native/ReactAndroid/cmake-utils/default-app-setup/OnLoad.cpp

namespace facebook::react {

void registerComponents(
    std::shared_ptr<const ComponentDescriptorProviderRegistry> registry) {
  // 自定义 Fabric 组件放在这里。您可以在此处注册来自您的应用程序或第三方库的自定义组件
  // providerRegistry->add(concreteComponentDescriptorProvider<
  //        MyComponentDescriptor>());

  // We link app local components if available
#ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION
  REACT_NATIVE_APP_COMPONENT_REGISTRATION(registry);
#endif

  // And we fallback to the components autolinked
  autolinking_registerProviders(registry);
}

// 省略部分代码......

} // namespace facebook::react

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
  return facebook::jni::initialize(vm, [] {
    facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
        &facebook::react::cxxModuleProvider;
    facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
        &facebook::react::javaModuleProvider;
    facebook::react::DefaultComponentsRegistry::
        registerComponentDescriptorsFromEntryPoint =
            &facebook::react::registerComponents;
  });
}

可见,注册的起点也是JNI_OnLoad函数。

Kotlin层

使用npx create-react-native-library@latest工具创建一个Fabric组件库时,会生成一些模版代码,其中包括我们上面提到的CustomWebViewManager类,现在我们来看一下CustomWebViewPackage类:

class CustomWebViewPackage : ReactPackage {
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
    val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
    viewManagers.add(CustomWebViewManager())
    return viewManagers
  }

  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
    return emptyList()
  }
}

这里createViewManagers方法会返回一个ViewManager的实例列表。根据我们在《ReactNative新架构之Android端TurboModule机制完全解析》一文中分析GeneratePackageListTask任务的结果,我们知道最终会生成一个PackageList文件,其中会注入每个三方TurboModule或Fabric组件包中的ReactPackage实现类。

现在来查看一下我们示例工程生成的example/android/app/build/generated/autolinking/src/main/java/com/facebook/react/PackageList.java文件:

public class PackageList {
  private Application application;
  private ReactNativeHost reactNativeHost;
  private MainPackageConfig mConfig;

  // 省略部分代码......

  public ArrayList<ReactPackage> getPackages() {
    return new ArrayList<>(Arrays.<ReactPackage>asList(
      new MainReactPackage(mConfig),
      new CustomWebViewPackage()
    ));
  }
}

回顾一下本文开头的初始化部分,我们提到过以下代码

fabricUIManager =
    FabricUIManager(context, ViewManagerRegistry(viewManagerResolver), eventBeatManager)

现在回顾一下《React Native新架构之Android端初始化源码分析》一文,在ReactInstance类构造时,有如下初始化逻辑:

    viewManagerResolver = BridgelessViewManagerResolver(reactPackages, context)

    ComponentNameResolverBinding.install(
        unbufferedRuntimeExecutor,
        object : ComponentNameResolver {
          override val componentNames: Array<String>
            get() {
              val viewManagerNames = viewManagerResolver.getViewManagerNames()
              if (viewManagerNames.isEmpty()) {
                FLog.e(TAG, "No ViewManager names found")
                return arrayOf()
              }
              return viewManagerNames.toTypedArray<String>()
            }
        },
    )

结合上一节查找组件 kotlin层实现的分析,整个流程都十分清晰了。

Win11 如何打开 IE11 浏览器

大家好,我是前端架构师,关注微信公众号【@程序员大卫】免费领取前端精品资料。

前言

有时候在调试老项目时,仍然需要使用 IE11 浏览器。但在 Windows 11 中,直接打开 IE 时,系统通常会自动跳转到 Microsoft Edge,导致无法真正进入 IE11。

下面教你一种简单、有效的方法,在 Win11 中直接打开 IE11。

1. 搜索并打开 cmd

在开始菜单中搜索 cmd,然后点击打开。

2. 粘贴并执行下面的脚本

将以下命令粘贴到 cmd 中并回车执行:

powershell -command "$pf86=[Environment]::GetFolderPath('ProgramFilesX86');$s=New-Object -ComObject WScript.Shell;$sc=$s.CreateShortcut(\"$env:USERPROFILE\Desktop\Internet Explorer.lnk\");$sc.TargetPath=\"$pf86\Internet Explorer\iexplore.exe\";$sc.Arguments='about:blank -Embedding';$sc.IconLocation=\"$pf86\Internet Explorer\iexplore.exe, 0\";$sc.Save()"

这段脚本会在桌面创建一个 IE 浏览器的快捷方式

3. 双击桌面的 IE 图标即可打开

执行完成后,桌面会出现一个 Internet Explorer 图标,双击它即可成功打开 IE11 浏览器。

React Native之Android端Fabric 架构源码分析(上)

React Native之Android端Fabric 架构源码分析(上)

前言

Fabric 是 React Native 新架构的 UI 渲染系统,现在我们就来深入分析其源码。

本文基于React Native 0.83版本源码进行分析。

初始化

《React Native新架构之Android端初始化源码分析》一文已经提过Fabric的初始化部分,现在回顾一下:

 // ReactInstance.kt   


    val eventBeatManager = EventBeatManager()
    fabricUIManager =
        FabricUIManager(context, ViewManagerRegistry(viewManagerResolver), eventBeatManager)

    // 在 Fabric 初始化之前需要完成的其他初始化操作。
    DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)

    val binding = FabricUIManagerBinding()
    binding.register(
        getBufferedRuntimeExecutor(),
        getRuntimeScheduler(),
        fabricUIManager,
        eventBeatManager,
        componentFactory,
    )

    // 初始化 FabricUIManager
    fabricUIManager.initialize()

这里EventBeatManager 是 一个基于观察者模式的 Fabric 架构的 "节拍控制器",它利用 Android 原生的帧回调机制,协调并驱动 C++ 层的事件向 JS 层高效、有序地流动。

现在重点看一FabricUIManager的构造方法做了什么react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

  public FabricUIManager(
      ReactApplicationContext reactContext,
      ViewManagerRegistry viewManagerRegistry,
      BatchEventDispatchedListener batchEventDispatchedListener) {
    // 初始化帧回调 
    mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext);
    mReactApplicationContext = reactContext;
    // 初始化挂载管理器 
    mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor);
    // 初始化挂载指令调度器 
    mMountItemDispatcher =
        new MountItemDispatcher(mMountingManager, new MountItemDispatchListener());
    // 初始化事件分发器 
    mEventDispatcher = new FabricEventDispatcher(reactContext, new FabricEventEmitter(this));
    // 持有批处理事件监听器
    mBatchEventDispatchedListener = batchEventDispatchedListener;
    // 注册生命周期监听
    mReactApplicationContext.addLifecycleEventListener(this);
    // 注册组件回调 
    mViewManagerRegistry = viewManagerRegistry;
    mReactApplicationContext.registerComponentCallbacks(viewManagerRegistry);
  }

这段代码,首先创建一个派生自 Choreographer.FrameCallback的回调,这是 Fabric 渲染的“心脏”。它会注册到 Android 的 Choreographer,在每一帧垂直同步(VSync)信号到来时被调用。它负责驱动 MountItemDispatcher 来执行挂载操作(即实际的 View 更新)。

随后创建了一个挂载管理器MountingManager,它是实际操作 Android View 的管理者(执行 createView, updateProps 等)。接着创建了挂载指令调度器MountItemDispatcher,它负责管理挂载指令(MountItem)的队列,决定它们是在当前线程同步执行还是推入队列等待下一帧执行。当一批指令分发完成后,它会收到回调。这主要用于通知监听器,告诉它们“UI 已经更新了,你们可以进行下一帧动画计算了”。

接下来又创建了事件分发器FabricEventDispatcher,它负责将 Android 原生事件(如 Touch, Scroll)发送给 JavaScript。它的参数FabricEventEmitter是一个实现了 RCTEventEmitter 接口的类,它内部持有 C++ 层的引用(通过 JNI),是 Java 事件通往 C++ Fabric 核心的入口。

以上这写类基本上构成了一套UI系统的核心处理。如果大家需要更深入分析React Native UI系统,那么这些类就是研究的重点。在构造方法的最后,注册了生命周期监听,这是为了让 FabricUIManager 能够感知 Activity/Host 的 onResumeonPauseonDestroy。尤其是在 onHostResume 时恢复 UI 挂载循环,在 onHostPause 时暂停,以节省资源并避免在后台更新 UI。

最后注册组件回调,主要是用于当系统内存不足时,ViewManagerRegistry 可以收到通知并释放缓存。

现在继续分析初始化流程FabricUIManagerBinding的创建,源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt

@DoNotStrip
@SuppressLint("MissingNativeLoadLibrary")
internal class FabricUIManagerBinding : HybridClassBase() {
  init {
    initHybrid()
  }

  private external fun initHybrid()

  external fun setPixelDensity(pointScaleFactor: Float)

  private external fun installFabricUIManager(
      runtimeExecutor: RuntimeExecutor,
      runtimeScheduler: RuntimeScheduler,
      uiManager: FabricUIManager,
      eventBeatManager: EventBeatManager,
      componentsRegistry: ComponentFactory,
  )


  fun register(
      runtimeExecutor: RuntimeExecutor,
      runtimeScheduler: RuntimeScheduler,
      fabricUIManager: FabricUIManager,
      eventBeatManager: EventBeatManager,
      componentFactory: ComponentFactory,
  ) {
    fabricUIManager.setBinding(this)
    installFabricUIManager(
        runtimeExecutor,
        runtimeScheduler,
        fabricUIManager,
        eventBeatManager,
        componentFactory,
    )
    setPixelDensity(getDisplayMetricDensity())
  }

  // 省略部分代码......
}

该对象的构造,主要是调用initHybrid方法。关于initHybrid的机制,我们在前面的文章已经做了详细分析,这里就不再重复解释。

这里的FabricUIManagerBinding是 React Native Fabric 架构在 Android 端的 "核心启动器" 和 "跨语言胶水层"。它的主要作用是初始化 Fabric 的 C++ 核心组件,并建立 Java、C++ 和 JavaScript 三者之间的通信桥梁。当该对象被创建时,立即调用了其register方法。在这个方法中,主要是调用了installFabricUIManager,它将 C++ 层的 Fabric API 绑定到 JavaScript 运行时(Runtime)。这使得 JavaScript 可以直接通过 JSI 调用 C++ 接口(如 createNode, cloneNode, appendChild),实现同步且高效的 UI 操作。这里还有一个重要的操作,即setPixelDensity,将 Android 设备的屏幕像素密度(Density)传递给 React Native 的 C++ 核心层(Fabric/Yoga),用于统一布局单位。

最后,分析一下FabricUIManagerinitialize做了什么事:

  public void initialize() {
    // 注册事件批处理监听
    mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener);

    // 启用 Fabric 日志与性能监控
    if (ReactNativeFeatureFlags.enableFabricLogs()) {
      mDevToolsReactPerfLogger = new DevToolsReactPerfLogger();
      mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER);

      ReactMarker.addFabricListener(mDevToolsReactPerfLogger);
    }

    // 启用新旧架构互操作
    if (ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) {
      InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext);
      mReactApplicationContext.internal_registerInteropModule(
          RCTEventEmitter.class, interopEventEmitter);
    }
  }

这里首先是将 mBatchEventDispatchedListener(即 EventBeatManager)注册到事件分发器中。这是 “心跳” 连接的关键一步。当 Android 原生事件(如 Touch)被成批分发时,会通知 EventBeatManager,进而触发 C++ 层的 tick(),驱动 Fabric 渲染管线刷新。没有这一步,JavaScript 可能永远收不到事件更新。

接下来是性能监控相关的处理,开启需依赖enableFabricLogs的值,这是排查 Fabric 性能问题(如掉帧、白屏)和调试渲染流程的“开关”。

最后是启用新旧架构互操作的处理,这是 React Native 平滑迁移到新架构的重要兼容层,确保老代码在新架构下也能工作。

C++层

以上FabricUIManagerBinding提供了很多Native方法,我们在此重点分析一下installFabricUIManager。源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp

void FabricUIManagerBinding::installFabricUIManager(
    jni::alias_ref<JRuntimeExecutor::javaobject> runtimeExecutorHolder,
    jni::alias_ref<JRuntimeScheduler::javaobject> runtimeSchedulerHolder,
    jni::alias_ref<JFabricUIManager::javaobject> javaUIManager,
    EventBeatManager* eventBeatManager,
    ComponentFactory* componentsRegistry) {
  TraceSection s("FabricUIManagerBinding::installFabricUIManager");

  enableFabricLogs_ = ReactNativeFeatureFlags::enableFabricLogs();

  if (enableFabricLogs_) {
    LOG(WARNING)
        << "FabricUIManagerBinding::installFabricUIManager() was called (address: "
        << this << ").";
  }

  std::unique_lock lock(installMutex_);

  // 创建 C++ MountingManager (
  auto globalJavaUiManager = make_global(javaUIManager);
  mountingManager_ =
      std::make_shared<FabricMountingManager>(globalJavaUiManager);

  std::shared_ptr<const ContextContainer> contextContainer =
      std::make_shared<ContextContainer>();

  auto runtimeExecutor = runtimeExecutorHolder->cthis()->get();

  auto runtimeScheduler = runtimeSchedulerHolder->cthis()->get().lock();
  // 如果存在 RuntimeScheduler(通常都存在),则包装 runtimeExecutor。
  // 这意味着所有通过此 executor 提交的 JS 任务都会经过 RuntimeScheduler 调度,从而支持优先级和任务取消。
  if (runtimeScheduler) {
    runtimeExecutor =
        [runtimeScheduler](
            std::function<void(jsi::Runtime & runtime)>&& callback) {
          runtimeScheduler->scheduleWork(std::move(callback));
        };
    contextContainer->insert(
        RuntimeSchedulerKey, std::weak_ptr<RuntimeScheduler>(runtimeScheduler));
  }

  // 创建 EventBeat 工厂 
  EventBeat::Factory eventBeatFactory =
      [eventBeatManager, &runtimeScheduler, globalJavaUiManager](
          std::shared_ptr<EventBeat::OwnerBox> ownerBox)
      -> std::unique_ptr<EventBeat> {
    return std::make_unique<AndroidEventBeat>(
        std::move(ownerBox),
        eventBeatManager,
        *runtimeScheduler,
        globalJavaUiManager);
  };

  contextContainer->insert("FabricUIManager", globalJavaUiManager);

  // 组装 Scheduler 工具箱
  auto toolbox = SchedulerToolbox{};
  toolbox.contextContainer = contextContainer;
  toolbox.componentRegistryFactory = componentsRegistry->buildRegistryFunction;

  // TODO: (T132338609) runtimeExecutor 应该在主 bundle eval 之后执行 lambda 表达式,
  // 而 bindingsInstallExecutor 应该在之前执行。
  toolbox.bridgelessBindingsExecutor = std::nullopt;
  toolbox.runtimeExecutor = runtimeExecutor;

  toolbox.eventBeatFactory = eventBeatFactory;

  // 启动 Fabric 核心
  animationDriver_ = std::make_shared<LayoutAnimationDriver>(
      runtimeExecutor, contextContainer, this);
  scheduler_ =
      std::make_shared<Scheduler>(toolbox, animationDriver_.get(), this);
}

这个方法是一个 "组装车间":

  1. 它接收来自 Android (Java) 的原材料(UIManager, EventBeatManager)。

  2. 它接收来自 JS Runtime 的驱动器(RuntimeExecutor)。

  3. 它将这些零件组装成 C++ 的核心部件(MountingManager, AndroidEventBeat)。

  4. 最后,它启动了 Fabric 的引擎 —— Scheduler

总结

下面是对整个初始化流程的概述

ReactInstance 初始化
           │
           │
           └─► Fabric 初始化
                    │
                    ├─► ViewManagerResolver 创建 (收集 ReactPackage 中的 ViewManager)
                    │
                    ├─► ViewManagerRegistry 创建
                    │
                    ├─► FabricUIManager 创建
                    │
                    ├─► FabricUIManagerBinding 创建
                    │
                    └─► binding.register() ──► 触发 C++ 层初始化

启动渲染

回顾一下Android 的初始化流程一文:

// ReactInstance.kt


 /**
   * 渲染一个 React Native surface.
   *
   * @param surface 要渲染的 [com.facebook.react.interfaces.fabric.ReactSurface] 对象
   */
  @ThreadConfined("ReactHost")
  fun startSurface(surface: ReactSurfaceImpl) {
    // 省略部分代码......
    val view = surface.view

    if (surface.isRunning) {
      // Surface 已经在运行(预渲染过),只需附加 View
      fabricUIManager.attachRootView(surface.surfaceHandler, view)
    } else {
      fabricUIManager.startSurface(surface.surfaceHandler, surface.context, view)
    }
  }

之前并未深入fabricUIManager.startSurface方法的调用,现在来分析一下:

// FabricUIManager.java

  public void startSurface(
      final SurfaceHandlerBinding surfaceHandler,
      final Context context,
      final @Nullable View rootView) {
    final int rootTag =
        rootView instanceof ReactRoot
            ? ((ReactRoot) rootView).getRootViewTag()
            : ReactRootViewTagGenerator.getNextRootViewTag();

    ThemedReactContext reactContext =
        new ThemedReactContext(
            mReactApplicationContext, context, surfaceHandler.getModuleName(), rootTag);
    mMountingManager.startSurface(rootTag, reactContext, rootView);
    Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null");
    mBinding.startSurfaceWithSurfaceHandler(rootTag, surfaceHandler, rootView != null);
  }

此方法主要做了两件事,首先是调用MountingManagerstartSurface启动 Surface,接着调用了FabricUIManagerBinding的Native方法startSurfaceWithSurfaceHandler在C++层启动 Surface。先查看react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt

  /**
    * 启动 Surface 但不附加视图。对该 Surface 执行的所有视图操作都将被排队,直到视图被附加为止。
    */
  @AnyThread
  fun startSurface(
      surfaceId: Int,
      reactContext: ThemedReactContext?,
      rootView: View?,
  ): SurfaceMountingManager {
    // 创建一个新的 SurfaceMountingManager 实例,负责管理特定 Surface 的视图挂载操作
    val surfaceMountingManager =
        SurfaceMountingManager(
            surfaceId,
            jsResponderHandler,  // JS 响应处理器(处理触摸事件响应)
            viewManagerRegistry,
            rootViewManager,
            mountItemExecutor,   // 挂载项执行器
            checkNotNull(reactContext),
        )

    // 理论上这里可能存在竞态条件,如果 addRootView 从不同线程被调用两次,
    // 虽然这种情况(可能)极不可能发生,而且很可能是一个错误。
    // 这个防止竞态条件的逻辑是从旧代码继承而来的,我们不知道在实际中是否真的会发生
    // 所以,我们现在记录软异常。这在调试模式下会崩溃,但在生产环境中不会。
    surfaceIdToManager.putIfAbsent(surfaceId, surfaceMountingManager)
    if (surfaceIdToManager[surfaceId] !== surfaceMountingManager) {
      logSoftException(
          TAG,
          IllegalStateException(
              "Called startSurface more than once for the SurfaceId [$surfaceId]"
          ),
      )
    }

    mostRecentSurfaceMountingManager = surfaceIdToManager[surfaceId]

    if (rootView != null) {
      surfaceMountingManager.attachRootView(rootView, reactContext)
    }

    return surfaceMountingManager
  }

此方法主要内容是创建SurfaceMountingManager,然后调用attachRootView方法。现在继续跟踪attachRootView方法,源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java

  public void attachRootView(View rootView, ThemedReactContext themedReactContext) {
    mThemedReactContext = themedReactContext;
    addRootView(rootView);
  }

  private void addRootView(@NonNull final View rootView) {
    if (isStopped()) {
      return;  // 检查 Surface 是否已停止
    }

    mTagToViewState.put(mSurfaceId, new ViewState(mSurfaceId, rootView, mRootViewManager, true));

    // 在 UI 线程上执行根视图设置
    Runnable runnable =
        new GuardedRunnable(Assertions.assertNotNull(mThemedReactContext)) {
          @Override
          public void runGuarded() {
            // 自从调用`addRootView`以来,CPU 一直在运行,因此从理论上讲,界面可能已经在此处停止渲染了。
            if (isStopped()) {
              return;
            }
            // 省略部分日志打印......

            // 设置根视图 ID 
            rootView.setId(mSurfaceId);

            if (rootView instanceof ReactRoot) {
              ((ReactRoot) rootView).setRootViewTag(mSurfaceId);
            }

            executeMountItemsOnViewAttach();

            // 通过在调用 `executeMountItemsOnViewAttach` 之后执行此操作,
            // 我们可以确保在处理此队列时安排的任何操作也会被添加到队列中,
            // 而不是通过 `MountItemDispatcher` 中的队列立即进行处理。
            mRootViewAttached = true;
          }
        };

    // 确保在 UI 线程执行
    if (UiThreadUtil.isOnUiThread()) {
      runnable.run();
    } else {
      UiThreadUtil.runOnUiThread(runnable);
    }
  }

这里的实现核心是封装了一个Runnable,即一个任务,且这个任务必须在安卓的UI线程执行。继续跟踪executeMountItemsOnViewAttach方法,查看任务的内容:

  private final Queue<MountItem> mOnViewAttachMountItems = new ArrayDeque<>();


  @UiThread
  @ThreadConfined(UI)
  private void executeMountItemsOnViewAttach() {
    mMountItemExecutor.executeItems(mOnViewAttachMountItems);
  }

可以看到,该方法就是在调用挂载项执行器不断的执行挂载项队列。这里的挂载项执行器是在创建MountingManager时传入的,回到FabricUIManager源码查看实现:

  private final MountingManager.MountItemExecutor mMountItemExecutor =
      new MountingManager.MountItemExecutor() {
        @Override
        public void executeItems(Queue<MountItem> items) {
          // 从技术上讲,在调度程序创建之前就可以访问这个执行器,但如果真的发生这种情况,那就说明出了非常严重的问题。
          mMountItemDispatcher.dispatchMountItems(items);
        }
      };

继续跟踪react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.kt中的实现:

  @UiThread
  @ThreadConfined(UI)
  fun dispatchMountItems(mountItems: Queue<MountItem?>) {
    while (!mountItems.isEmpty()) {
      val item = requireNotNull(mountItems.poll()) { "MountItem should not be null" }
      try {
        item.execute(mountingManager)
      } catch (e: RetryableMountingLayerException) {
        // 省略已弃用的逻辑......
      }
    }
  }

此处核心逻辑是从队列取出一个 MountItem并执行它的execute方法。至于MountItem接口,它有许多实现类,其中最核心的是IntBufferBatchMountItem实现,我们可以大致浏览一下它的execute方法主要做些什么。源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.kt

  override fun execute(mountingManager: MountingManager) {
    val surfaceMountingManager = mountingManager.getSurfaceManager(surfaceId)
    if (surfaceMountingManager == null) {
      return
    }
    if (surfaceMountingManager.isStopped) {
      return
    }
    if (ReactNativeFeatureFlags.enableFabricLogs()) {
      FLog.d(TAG, "Executing IntBufferBatchMountItem on surface [%d]", surfaceId)
    }

    beginMarkers("mountViews")
    var i = 0
    var j = 0
    while (i < intBufferLen) {
      val rawType = intBuffer[i++]
      val type = rawType and INSTRUCTION_FLAG_MULTIPLE.inv()
      val numInstructions =
          (if ((rawType and INSTRUCTION_FLAG_MULTIPLE) != 0) intBuffer[i++] else 1)

      val args = arrayOf("numInstructions", numInstructions.toString())

      for (k in 0 until numInstructions) {
        when (type) {
          INSTRUCTION_CREATE -> {    
            val componentName = (objBuffer[j++] as String?).orEmpty()
            val fabricComponentName =
                FabricNameComponentMapping.getFabricComponentName(componentName)
            // 创建视图
            surfaceMountingManager.createView(
                fabricComponentName,
                intBuffer[i++],
                objBuffer[j++] as ReadableMap?,
                objBuffer[j++] as StateWrapper?,
                objBuffer[j++] as EventEmitterWrapper?,
                intBuffer[i++] == 1,
            )
          }
          // 删除视图
          INSTRUCTION_DELETE -> surfaceMountingManager.deleteView(intBuffer[i++]) 
          INSTRUCTION_INSERT -> {
            val tag = intBuffer[i++]
            val parentTag = intBuffer[i++]
            // 插入视图到父视图
            surfaceMountingManager.addViewAt(parentTag, tag, intBuffer[i++])
          }
          // 从父视图移除
          INSTRUCTION_REMOVE ->
              surfaceMountingManager.removeViewAt(intBuffer[i++], intBuffer[i++], intBuffer[i++])
           // 更新属性
          INSTRUCTION_UPDATE_PROPS ->
              surfaceMountingManager.updateProps(intBuffer[i++], objBuffer[j++] as ReadableMap?)
          // 更新状态
          INSTRUCTION_UPDATE_STATE ->
              surfaceMountingManager.updateState(intBuffer[i++], objBuffer[j++] as StateWrapper?)

          // 更新布局
          INSTRUCTION_UPDATE_LAYOUT -> {
            val reactTag = intBuffer[i++]
            val parentTag = intBuffer[i++]
            val x = intBuffer[i++]
            val y = intBuffer[i++]
            val width = intBuffer[i++]
            val height = intBuffer[i++]
            val displayType = intBuffer[i++]
            val layoutDirection = intBuffer[i++]
            surfaceMountingManager.updateLayout(
                reactTag,
                parentTag,
                x,
                y,
                width,
                height,
                displayType,
                layoutDirection,
            )
          }
          // 省略部分代码......
          else -> {
            throw IllegalArgumentException(
                "Invalid type argument to IntBufferBatchMountItem: $type at index: $i"
            )
          }
        }
      }
    }
    endMarkers()
  }

可以看到,IntBufferBatchMountItem包含批量视图操作指令,它使用 int 数组和 object 数组优化性能,减少 JNI 调用,并且支持多种视图操作:创建、删除、插入、移除、更新属性/状态/布局等。

接下来我们跟踪一下surfaceMountingManager.createView的实现,源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java

 @UiThread
  public void createView(
      @NonNull String componentName,
      int reactTag,
      @Nullable ReadableMap props,
      @Nullable StateWrapper stateWrapper,
      @Nullable EventEmitterWrapper eventEmitterWrapper,
      boolean isLayoutable) {
    if (isStopped()) {
      return;
    }
    ViewState viewState = getNullableViewState(reactTag);
    if (viewState != null && viewState.mView != null) {
      return;
    }

    createViewUnsafe(
        componentName, reactTag, props, stateWrapper, eventEmitterWrapper, isLayoutable);
  }

  /**
   * 执行视图创建操作,但不进行任何安全检查。您必须在调用此方法之前确保安全性(参见现有调用位置)
   */
  @UiThread
public void createViewUnsafe(
      @NonNull String componentName,
      int reactTag,
      @Nullable ReadableMap props,
      @Nullable StateWrapper stateWrapper,
      @Nullable EventEmitterWrapper eventEmitterWrapper,
      boolean isLayoutable) {
    Systrace.beginSection(
        Systrace.TRACE_TAG_REACT,
        "SurfaceMountingManager::createViewUnsafe(" + componentName + ")");
    try {
      ReactStylesDiffMap propMap = new ReactStylesDiffMap(props);

      ViewState viewState = new ViewState(reactTag);
      viewState.mCurrentProps = propMap;
      viewState.mStateWrapper = stateWrapper;
      viewState.mEventEmitter = eventEmitterWrapper;
      mTagToViewState.put(reactTag, viewState);

      if (isLayoutable) {
        ViewManager viewManager = mViewManagerRegistry.get(componentName);
        // View Managers are responsible for dealing with inital state and props.
        viewState.mView =
            viewManager.createView(
                reactTag, mThemedReactContext, propMap, stateWrapper, mJSResponderHandler);
        viewState.mViewManager = viewManager;
      }
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT);
    }
  }

这里是根据组件名通过mViewManagerRegistry来查找ViewManager。具体注册逻辑,我们在后面的注册组件一节分析。

继续跟踪一下viewManager.createView方法的实现,源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java

  /** 创建一个了解 props 和 state 的视图。 */
  public @NonNull T createView(
      int reactTag,
      @NonNull ThemedReactContext reactContext,
      @Nullable ReactStylesDiffMap props,
      @Nullable StateWrapper stateWrapper,
      JSResponderHandler jsResponderHandler) {
    T view = createViewInstance(reactTag, reactContext, props, stateWrapper);
    if (view instanceof ReactInterceptingViewGroup) {
      ((ReactInterceptingViewGroup) view).setOnInterceptTouchEventListener(jsResponderHandler);
    }
    return view;
  }

  /**
   * 子类应该返回一个适当类型的新 View 实例。这是一个可选方法,它会为您调用 createViewInstance。
   * 如果您需要在创建视图时使用 props 或 state,请重写它。
   *
   * 如果您重写此方法,您*必须*确保正确处理 updateProperties、view.setId、
   * addEventEmitters 和 updateState/updateExtraData!
   *
   * @param reactTag 应该设置为视图实例 ID 的 reactTag
   * @param reactContext 用于初始化视图实例的 ReactContext
   * @param initialProps 视图实例的初始 props
   * @param stateWrapper 视图实例的初始 state
   */
  protected @NonNull T createViewInstance(
      int reactTag,
      @NonNull ThemedReactContext reactContext,
      @Nullable ReactStylesDiffMap initialProps,
      @Nullable StateWrapper stateWrapper) {
    T view = null;
    @Nullable Stack<T> recyclableViews = getRecyclableViewStack(reactContext.getSurfaceId(), true);
    if (recyclableViews != null && !recyclableViews.empty()) {
      T recyclableView = recyclableViews.pop();

      // 当视图回收未对所有组件启用时,可回收视图仍可能附加到不可回收视图。
      // 这保证了回收视图已从其父视图中移除。
      if (ReactNativeFeatureFlags.enableViewRecycling() && recyclableView.getParent() != null) {
        ((ViewGroup) recyclableView.getParent()).removeView(recyclableView);
      }

      view = recycleView(reactContext, recyclableView);
    } else {
      view = createViewInstance(reactContext);
    }
    view.setId(reactTag);
    addEventEmitters(reactContext, view);
    if (initialProps != null) {
      updateProperties(view, initialProps);
    }
    // 仅在 Fabric 架构中存在;但在 Fabric 中始终存在。
    if (stateWrapper != null) {
      Object extraData = updateState(view, initialProps, stateWrapper);
      if (extraData != null) {
        updateExtraData(view, extraData);
      }
    }
    return view;
  }

 /**
   * 子类应该返回一个适当类型的新 View 实例。
   */
  protected abstract @NonNull T createViewInstance(@NonNull ThemedReactContext reactContext);

   /**
   * 子类可以重写此方法以在给定 View 上安装自定义事件发射器。
   * 如果您的视图需要向 JS 发送除基本触摸事件之外的事件(例如滚动事件),
   * 您可能想要重写此方法。
   */
  protected void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull T view) {}

 /**
   * 子类可以实现此方法以接收从 {@link ReactShadowNode#onCollectExtraUpdates} 中
   * 相应 {@link ReactShadowNode} 实例排队的可选额外数据。
   *
   * 由于 CSS 布局步骤和 UI 更新可以在设置 x/y/width/height 之外的单独线程中执行,
   * 这是从 CSS 节点向原生视图对应部分传递额外数据的推荐且线程安全的方式。
   *
   * <p>TODO T7247021: Replace updateExtraData with generic update props mechanism after D2086999
   */
  public abstract void updateExtraData(@NonNull T view, Object extraData);

 /**
   * 子类可以实现此方法以接收在此组件类型的所有实例之间共享的状态更新。
   */
  public @Nullable Object updateState(
      @NonNull T view, ReactStylesDiffMap props, StateWrapper stateWrapper) {
    return null;
  }

这里比较重要的是createViewInstance方法,子类必须重写它,用于创建自定义View。现在查看一下我们创建的自定义Fabric 组件包中自动生成的模版代码:

@ReactModule(name = CustomWebViewManager.NAME)
class CustomWebViewManager : SimpleViewManager<CustomWebView>(),
  CustomWebViewManagerInterface<CustomWebView> {
  private val mDelegate: ViewManagerDelegate<CustomWebView>

  init {
    mDelegate = CustomWebViewManagerDelegate(this)
  }

  override fun getDelegate(): ViewManagerDelegate<CustomWebView>? {
    return mDelegate
  }

  override fun getName(): String {
    return NAME
  }

  public override fun createViewInstance(context: ThemedReactContext): CustomWebView {
    return CustomWebView(context)
  }

  @ReactProp(name = "sourceURL")
  override fun setSourceURL(view: CustomWebView?, sourceURL: String?) {
    // Implement the logic to handle the sourceURL property
  }

  companion object {
    const val NAME = "CustomWebView"
  }
}

可以看到,CustomWebViewManager实际上就是ViewManager的子孙类,其实现了createViewInstance方法,返回自定义的View实例。

这条线的跟踪已经足够深入了,关于原生UI组件的具体布局渲染就不继续了,先到此为止。

C++ 层

FabricUIManagerstartSurface方法中还有一个Native方法mBinding.startSurfaceWithSurfaceHandler未分析,源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp

void FabricUIManagerBinding::startSurfaceWithSurfaceHandler(
    jint surfaceId,
    jni::alias_ref<SurfaceHandlerBinding::jhybridobject> surfaceHandlerBinding,
    jboolean isMountable) {

  // SurfaceHandler配置 
  // 从JNI包装中提取C++ SurfaceHandler对象
  const auto& surfaceHandler =
      surfaceHandlerBinding->cthis()->getSurfaceHandler();
  surfaceHandler.setSurfaceId(surfaceId);
  // 根据是否有View设置显示模式
  surfaceHandler.setDisplayMode(
      isMountable != 0 ? DisplayMode::Visible : DisplayMode::Suspended);

  // 获取Fabric调度器
  auto scheduler = getScheduler();
  if (!scheduler) {
    LOG(ERROR)
        << "FabricUIManagerBinding::startSurfaceWithSurfaceHandler: scheduler disappeared";
    return;
  }

  // 将SurfaceHandler注册到调度器中。调度器负责管理渲染和布局
  scheduler->registerSurface(surfaceHandler);

  auto mountingManager = getMountingManager("startSurfaceWithSurfaceHandler");
  if (mountingManager != nullptr) {
    // 通知MountingManager, Surface开始启动
    // MountingManager负责将C++的UI操作转换为Android原生View操作
    mountingManager->onSurfaceStart(surfaceId);
  }

  // 启动SurfaceHandler 
  surfaceHandler.start();

  // 如果启用布局动画,设置动画驱动器
  if (ReactNativeFeatureFlags::enableLayoutAnimationsOnAndroid()) {
    surfaceHandler.getMountingCoordinator()->setMountingOverrideDelegate(
        animationDriver_);
  }

  {
    std::unique_lock lock(surfaceHandlerRegistryMutex_);
    // 将SurfaceHandler添加到注册表中,便于后续管理(停止、更新等)
    surfaceHandlerRegistry_.emplace(
        surfaceId, jni::make_weak(surfaceHandlerBinding));
  }
}

这个方法是新架构Surface启动的关键桥梁,它体现了新架构的核心特点:通过JSI直接从C++调用JS。

  • 向上:接收Java层的启动请求

  • 向下:触发C++层的Surface启动流程

  • 横向:协调Scheduler、MountingManager等各个子系统

这里重点关注一下surfaceHandler.start()的实现,源码react-native/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp:

void SurfaceHandler::start() const noexcept {
  std::unique_lock lock(linkMutex_);
  // 省略断言......

  auto parameters = Parameters{};
  {
    std::shared_lock parametersLock(parametersMutex_);
    parameters = parameters_;
  }

  // 创建ShadowTree 
  auto shadowTree = std::make_unique<ShadowTree>(
      parameters.surfaceId,
      parameters.layoutConstraints,
      parameters.layoutContext,
      *link_.uiManager,
      *parameters.contextContainer);

  // 将ShadowTree指针保存到link_中,供后续操作使用
  link_.shadowTree = shadowTree.get();

  if (!parameters.moduleName.empty()) {
    // 启动Surface
    link_.uiManager->startSurface(
        std::move(shadowTree),
        parameters.moduleName,
        parameters.props,
        parameters_.displayMode);
  } else {
    // 创建空Surface,仅用于预渲染
    link_.uiManager->startEmptySurface(std::move(shadowTree));
  }
  // 将状态从Registered更新为Running
  link_.status = Status::Running;
  // 应用显示模式
  applyDisplayMode(parameters.displayMode);
}

此方法是Fabric架构中渲染流程的起点,负责创建ShadowTree并启动UI渲染流程。这里的ShadowTree就是Fabric中的虚拟DOM树,用于布局计算和渲染。

继续跟踪uiManager->startSurface的实现,源码react-native/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp:

void UIManager::startSurface(
    ShadowTree::Unique&& shadowTree,
    const std::string& moduleName,
    const folly::dynamic& props,
    DisplayMode displayMode) const noexcept {
  TraceSection s("UIManager::startSurface");

  // ShadowTree注册 
  auto surfaceId = shadowTree->getSurfaceId();
  shadowTreeRegistry_.add(std::move(shadowTree));

  // 委托通知
  // 使用访问者模式安全访问已注册的ShadowTree
  shadowTreeRegistry_.visit(
      surfaceId, [delegate = delegate_](const ShadowTree& shadowTree) {
        if (delegate != nullptr) {
          // 通知UIManager的委托对象Surface已启动
          delegate->uiManagerDidStartSurface(shadowTree);
        }
      });

  // 异步调用JS层
  runtimeExecutor_([=](jsi::Runtime& runtime) {
    TraceSection s("UIManager::startSurface::onRuntime");
    // 在JS线程上异步执行
    AppRegistryBinding::startSurface(
        runtime, surfaceId, moduleName, props, displayMode);
  });
}

此方法是Fabric架构中连接C++渲染系统和JS应用层的关键桥梁方法,负责注册ShadowTree并触发JS应用启动。

继续跟踪AppRegistryBinding::startSurface。源码react-native/packages/react-native/ReactCommon/react/renderer/uimanager/AppRegistryBinding.cpp:

/* static */ void AppRegistryBinding::startSurface(
    jsi::Runtime& runtime,
    SurfaceId surfaceId,
    const std::string& moduleName,
    const folly::dynamic& initialProps,
    DisplayMode displayMode) {
  TraceSection s("AppRegistryBinding::startSurface");

  // 构建参数对象
  jsi::Object parameters(runtime);
  parameters.setProperty(runtime, "rootTag", surfaceId);
  parameters.setProperty(
      runtime, "initialProps", jsi::valueFromDynamic(runtime, initialProps));
  parameters.setProperty(runtime, "fabric", true);

  // 获取全局AppRegistry 
  // 访问JS全局对象
  auto global = runtime.global();
  // 查找RN$AppRegistry(在AppRegistry.js中设置)
  auto registry = global.getProperty(runtime, "RN$AppRegistry");
  if (!registry.isObject()) {
    throw std::runtime_error(
        "AppRegistryBinding::startSurface failed. Global was not installed.");
  }

  // 获取runApplication方法对象
  auto method = std::move(registry).asObject(runtime).getPropertyAsFunction(
      runtime, "runApplication");

  // 调用JS方法:runApplication
  method.call(
      runtime,
      {jsi::String::createFromUtf8(runtime, moduleName),
       std::move(parameters),
       jsi::Value(runtime, displayModeToInt(displayMode))});
}

这里的重点是就是JS层的runApplication方法调用,可以说这就是JS层的入口方法。此处的JSI C++调用等价于以下JS代码:

AppRegistry.runApplication(
  "RNTesterApp",           // moduleName
  {
    rootTag: 1,            // surfaceId
    initialProps: {...},   // 初始属性
    fabric: true           // 新架构标记
  },
  0                        // displayMode
);

AppRegistryBinding::startSurface方法是新架构调用链的终点,负责通过JSI直接调用JS层的AppRegistry.runApplication,启动React Native应用。

渲染调度

接下来我们探索一下前面提到的mOnViewAttachMountItems队列:

// SurfaceMountingManager.java

  @UiThread
  @ThreadConfined(UI)
  public void scheduleMountItemOnViewAttach(MountItem item) {
    mOnViewAttachMountItems.add(item);
  }

经过搜索,可以定位到scheduleMountItemOnViewAttach调用处:

// MountItemDispatcher.kt

  private fun getAndResetViewCommandMountItems(): List<DispatchCommandMountItem>? =
      drainConcurrentItemQueue(viewCommandMountItems)

  private fun getAndResetMountItems(): List<MountItem>? = drainConcurrentItemQueue(mountItems)

  private fun getAndResetPreMountItems(): List<MountItem>? = drainConcurrentItemQueue(preMountItems)

  private fun executeOrEnqueue(item: MountItem) {
    if (mountingManager.isWaitingForViewAttach(item.getSurfaceId())) {
      // Surface 还未准备好,将任务加入等待队列
      val surfaceMountingManager: SurfaceMountingManager =
          mountingManager.getSurfaceManagerEnforced(
              item.getSurfaceId(),
              "MountItemDispatcher::executeOrEnqueue",
          )
      surfaceMountingManager.scheduleMountItemOnViewAttach(item)
    } else {
      // Surface 已准备好,直接执行
      item.execute(mountingManager)
    }
  }


  /*
   * 按以下顺序执行视图命令、预挂载项和挂载项:
   * 1. 视图命令
   * 2. 预挂载项  
   * 3. 常规挂载项
   *
   * 如果 `viewCommandMountItemsToDispatch` 和 `mountItemsToDispatch` 都为空,则不执行任何操作。
   * 除了 `tryDispatchMountItems` 之外,任何地方都不应直接调用此方法。
   */
  @UiThread
  @ThreadConfined(UI)
  private fun dispatchMountItems() {
    batchedExecutionTime = 0
    runStartTime = SystemClock.uptimeMillis()
    // 初始化和获取待分发项
    val viewCommandMountItemsToDispatch = getAndResetViewCommandMountItems()
    val mountItemsToDispatch = getAndResetMountItems()

    if (mountItemsToDispatch == null && viewCommandMountItemsToDispatch == null) {
      return
    }

    itemDispatchListener.willMountItems(mountItemsToDispatch)

    // 定义视图命令分发函数
    val dispatchViewCommand: (command: DispatchCommandMountItem) -> Unit = { command ->
      if (ReactNativeFeatureFlags.enableFabricLogs()) {
        printMountItem(command, "dispatchMountItems: Executing viewCommandMountItem")
      }
      try {
        executeOrEnqueue(command)
      } catch (e: RetryableMountingLayerException) {
        // 省略......
      } catch (e: Throwable) {
        // 省略......
      }
    }

    // 执行ViewCommand

    // 作为优化,首先执行所有 ViewCommand
    // 这应该是:
    // 1) 高性能的:ViewCommand 通常是 SetNativeProps 的替代品,我们一直希望它尽可能"同步"。
    // 2) 更安全的:ViewCommand 本质上与树提交/差异/挂载过程断开连接。
    // JS 令式地排队这些命令。
    //    如果 JS 已经排队了一个命令,可以合理地假设时间过得越久, 视图消失的可能性就越大。
    //    因此,通过提前执行 ViewCommand,我们实际上应该避免一类错误/故障。
    viewCommandMountItemsToDispatch?.let { commands ->
      for (command in commands) {
        dispatchViewCommand(command)
      }

      Systrace.endSection(Systrace.TRACE_TAG_REACT)
    }

    // 执行PreMountItem
    // 如果有 MountItem 要分发,我们确保所有"预挂载项"首先执行
    getAndResetPreMountItems()?.let { preMountItems ->
      for (preMountItem in preMountItems) {
        if (ReactNativeFeatureFlags.enableFabricLogs()) {
          printMountItem(preMountItem, "dispatchMountItems: Executing preMountItem")
        }
        executeOrEnqueue(preMountItem)
      }
      Systrace.endSection(Systrace.TRACE_TAG_REACT)
    }

    // 执行常规 MountItem 
    mountItemsToDispatch?.let { items ->
      val batchedExecutionStartTime = SystemClock.uptimeMillis()

      for (mountItem in items) {
        if (ReactNativeFeatureFlags.enableFabricLogs()) {
          printMountItem(mountItem, "dispatchMountItems: Executing mountItem")
        }

        val command = mountItem as? DispatchCommandMountItem
        if (command != null) {
          dispatchViewCommand(command)
          continue
        }

        try {
          executeOrEnqueue(mountItem)
        } catch (e: Throwable) {
          // 省略......
        }
      }
      batchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime
    }

    itemDispatchListener.didMountItems(mountItemsToDispatch)
  }

 /**
  * 尝试分发 MountItems。如果出现异常,我们将重试 10 次后放弃。
  */
  fun tryDispatchMountItems() {
    // 如果我们已经在分发中,不要重入。在 Android 的 Fabric 中可能经常发生重入,
    // 因为来自挂载层的 `updateState` 会导致挂载项被同步分发。我们想要 
    //    1)确保在这些情况下不重入,但 2)仍然同步执行这些排队的指令。
    // 这是一个相当粗暴的工具,但我们可能没有更好的选择,因为我们真的不想执行任何乱序的操作。
    if (inDispatch) {
      return
    }

    inDispatch = true

    try {
      dispatchMountItems()
    } finally {
      // 即使抛出异常也要在运行 dispatchMountItems 后清理
      inDispatch = false
    }

    // 无论我们是否实际分发了任何内容,我们都会调用 didDispatchMountItems,
    // 因为 NativeAnimatedModule 依赖于此来执行可能已调度的任何动画
    itemDispatchListener.didDispatchMountItems()
  }

可以看到,scheduleMountItemOnViewAttach仅被executeOrEnqueue方法调用。只有在Surface 还未准备好时,才将任务加入等待队列。否则,直接在executeOrEnqueue中执行挂载项。

这里executeOrEnqueue方法在dispatchMountItems方法中有三次调用,分别对应着三个队列。这三个队列存在执行顺序:

  1. ViewCommand 优先执行:提高性能和安全性(存储视图命令,如 scrollTo、focus等,这些是来自 JS 的命令式调用,与视图树的提交/差异/挂载过程分离)

  2. PreMountItem 次之:为后续挂载做准备(存储预挂载项,主要是视图预分配操作。这是性能优化,尽可能提前完成工作)

  3. 常规 MountItem 最后:执行实际的视图操作(存储常规挂载项,包含主要的视图操作。这是视图树更新的核心操作)

有个地方需要注意,就是drainConcurrentItemQueue方法。此方法是一次性清空队列,将结果转为一个List。之所以这样做,是为了将累积的所有挂载项一次性取出进行批处理,避免在执行过程中队列继续增长导致的不一致性。这里的三个队列都调用了此方法返回列表。

接下来,根据注释可知,dispatchMountItems方法只应该被tryDispatchMountItems调用。而tryDispatchMountItems方法主要做了一个防止重入的处理。继续搜索该方法,发现只有两处调用,且都在FabricUIManager.java中:

private class DispatchUIFrameCallback extends GuardedFrameCallback {
    private volatile boolean mIsMountingEnabled = true;
    @ThreadConfined(UI)
    private boolean mShouldSchedule = false;
    @ThreadConfined(UI)
    private boolean mIsScheduled = false;

    private DispatchUIFrameCallback(ReactContext reactContext) {
      super(reactContext);
    }

    @UiThread
    @ThreadConfined(UI)
    private void schedule() {
      if (!mIsScheduled && mShouldSchedule) {
        mIsScheduled = true;
        ReactChoreographer.getInstance()
            .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this);
      }
    }
    // 省略部分代码......

    @Override
    @UiThread
    @ThreadConfined(UI)
    public void doFrameGuarded(long frameTimeNanos) {
      mIsScheduled = false;
      if (!mIsMountingEnabled) {
        return;
      }

      if (mDestroyed) {
        return;
      }

      // 通过 C++ 驱动所有动画。
      // 这里存在获取/设置 `mDriveCxxAnimations` 的竞态条件,但这应该无关紧要;
      // 调用 mBinding 方法是安全的,除非 mBinding 对象已被销毁。
      if ((mDriveCxxAnimations || ReactNativeFeatureFlags.cxxNativeAnimatedEnabled())
          && mBinding != null) {
        mBinding.driveCxxAnimations();
      }

      if (mBinding != null) {
        mBinding.drainPreallocateViewsQueue();
      }

      try {
        // 首先,在 frameTimeNanos 时间内执行尽可能多的预挂载项。
        // 如果不是所有预挂载项都被执行,可能发生以下情况:
        //   1. 如果 MountItemDispatcher 中有视图命令或挂载项:执行剩余的预挂载项。
        //   2. 如果没有视图命令或挂载项,等待下一帧。
        mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
        mMountItemDispatcher.tryDispatchMountItems();
      } catch (Exception ex) {
        FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex);
        mIsMountingEnabled = false;
        throw ex;
      } finally {
        schedule();
      }

      mSynchronousEvents.clear();
    }
  }

先看doFrameGuarded这处调用,这里正是一开始分析初始化流程时提到的基于 Android Choreographer帧回调处理。

  • 监听 VSync 信号

  • 在每帧开始时触发回调

  • 按优先级顺序执行回调队列

    1. mBinding.driveCxxAnimations()- C++ 动画

    2. mBinding.drainPreallocateViewsQueue()- C++ 预分配队列

    3. dispatchPreMountItems(frameTimeNanos)- Java 预挂载项

    4. tryDispatchMountItems() - 常规挂载项

doFrameGuarded这处调用就是正常的帧驱动渲染流程,确保 UI 更新与屏幕刷新同步。

我们再看另一处调用:

  /**
   * 此方法将 UI 操作直接添加到 UI 线程队列中。以使用 {@link ReactChoreographer.CallbackType} 来强制执行顺序。
   * 此方法应仅在提交新渲染树后调用。
   */
  @SuppressWarnings("unused")
  @AnyThread
  @ThreadConfined(ANY)
  private void scheduleMountItem(
      @Nullable final MountItem mountItem,
      int commitNumber,
      long commitStartTime,
      long diffStartTime,
      long diffEndTime,
      long layoutStartTime,
      long layoutEndTime,
      long finishTransactionStartTime,
      long finishTransactionEndTime,
      int affectedLayoutNodesCount) {
    // 当 Binding.cpp 在提交阶段调用 scheduleMountItems 时,它总是使用 BatchMountItem 调用。
    // 没有其他地方使用 BatchMountItem 调用此方法,并且 Binding.cpp 只使用 BatchMountItem 调用 scheduleMountItems。
    long scheduleMountItemStartTime = SystemClock.uptimeMillis();
    boolean isBatchMountItem = mountItem instanceof BatchMountItem;
    boolean shouldSchedule = false;

    // 判断是否为批量挂载项
    if (isBatchMountItem) {
      BatchMountItem batchMountItem = (BatchMountItem) mountItem;
      Assertions.assertNotNull(batchMountItem, "BatchMountItem is null");
      shouldSchedule = !batchMountItem.isBatchEmpty();
    } else {
      shouldSchedule = mountItem != null;
    }
    // 省略性能记录......

    // 通知监听器
    // 在同步渲染的情况下,这可能在 UI 线程上被调用。否则,
    // 它应该几乎总是在 JS 线程上被调用。
    for (UIManagerListener listener : mListeners) {
      listener.didScheduleMountItems(this);
    }

    // 调度执行
    if (shouldSchedule) {
      Assertions.assertNotNull(mountItem, "MountItem is null");
      // 将 MountItem 添加到分发器队列
      mMountItemDispatcher.addMountItem(mountItem);
      // 判断是否是UI线程,如果是UI线程则立即执行,实现低延迟渲染
      if (UiThreadUtil.isOnUiThread()) {
        Runnable runnable =
            new GuardedRunnable(mReactApplicationContext) {
              @Override
              public void runGuarded() {
                mMountItemDispatcher.tryDispatchMountItems();
              }
            };
        runnable.run();
      }
    }

    // 在锁外和同步挂载完成执行后发布标记
    if (isBatchMountItem) {
      // 省略部分代码......
    }
  }

可以看到,此方法是由C++层调用,在其中实现了双路径执行渲染策略。当前如果在UI线程调用,则会立即执行渲染。反之,只是通过mMountItemDispatcher.addMountItem(mountItem)将 MountItem 添加到队列,等待帧回调处理:

  // MountItemDispatcher.kt

  fun addMountItem(mountItem: MountItem) {
    mountItems.add(mountItem)
  }

C++ 层

现在来研究一下C++层触发scheduleMountItem的流程,源码react-native/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp

jsi::Value UIManagerBinding::get(
    jsi::Runtime& runtime,
    const jsi::PropNameID& name) {
  auto methodName = name.utf8(runtime);

  UIManager* uiManager = uiManager_.get();
  // 省略部分代码......

  if (methodName == "completeRoot") {
    auto paramCount = 2;
    return jsi::Function::createFromHostFunction(
        runtime,
        name,
        paramCount,
        [uiManager, methodName, paramCount](
            jsi::Runtime& runtime,
            const jsi::Value& /*thisValue*/,
            const jsi::Value* arguments,
            size_t count) -> jsi::Value {
          validateArgumentCount(runtime, methodName, paramCount, count);

          auto runtimeSchedulerBinding =
              RuntimeSchedulerBinding::getBinding(runtime);
          auto surfaceId = surfaceIdFromValue(runtime, arguments[0]);

          auto shadowNodeList = shadowNodeListFromValue(runtime, arguments[1]);
          uiManager->completeSurface(
              surfaceId,
              shadowNodeList,
              {.enableStateReconciliation = true,
               .mountSynchronously = false,
               .source = ShadowTree::CommitSource::React});

          return jsi::Value::undefined();
        });
  }
  // 省略部分代码......

  return jsi::Value::undefined();
}

继续跟踪completeSurface方法实现,源码react-native/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp

void UIManager::completeSurface(
    SurfaceId surfaceId,
    const ShadowNode::UnsharedListOfShared& rootChildren,
    ShadowTree::CommitOptions commitOptions) {
  TraceSection s("UIManager::completeSurface", "surfaceId", surfaceId);

  shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) {
    auto result = shadowTree.commit(
        [&](const RootShadowNode& oldRootShadowNode) {
          return std::make_shared<RootShadowNode>(
              oldRootShadowNode,
              ShadowNodeFragment{
                  .props = ShadowNodeFragment::propsPlaceholder(),
                  .children = rootChildren,
              });
        },
        commitOptions);

    if (result == ShadowTree::CommitStatus::Succeeded) {
      // It's safe to update the visible revision of the shadow tree immediately
      // after we commit a specific one.
      lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision(
          surfaceId, shadowTree.getCurrentRevision().rootShadowNode);
    }
  });
}

这里的核心是ShadowTree::commit方法的调用,实参是个闭包。它主要用于计算 Diff,生成 MountingTransaction。继续跟踪源码react-native/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp

CommitStatus ShadowTree::commit(
    const ShadowTreeCommitTransaction& transaction,
    const CommitOptions& commitOptions) const {
  [[maybe_unused]] int attempts = 0;

  if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
    while (attempts < MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING) {
      auto status = tryCommit(transaction, commitOptions);
      if (status != CommitStatus::Failed) {
        return status;
      }
      attempts++;
    }

    {
      std::unique_lock lock(commitMutexRecursive_);
      return tryCommit(transaction, commitOptions);
    }
  } else {
    // 循环尝试提交,直到成功或达到最大尝试次数
    while (true) {
      attempts++;

      auto status = tryCommit(transaction, commitOptions);
      if (status != CommitStatus::Failed) {
        return status;
      }

      // After multiple attempts, we failed to commit the transaction.
      // Something internally went terribly wrong.
      react_native_assert(attempts < 1024);
    }
  }
}

CommitStatus ShadowTree::tryCommit(
    const ShadowTreeCommitTransaction& transaction,
    const CommitOptions& commitOptions) const {
  TraceSection s("ShadowTree::commit");

  auto telemetry = TransactionTelemetry{};
  telemetry.willCommit();

  CommitMode commitMode;
  auto oldRevision = ShadowTreeRevision{};
  auto newRevision = ShadowTreeRevision{};

  {
    // Reading `currentRevision_` in shared manner.
    SharedLock lock = sharedCommitLock();
    commitMode = commitMode_;
    oldRevision = currentRevision_;
  }

  const auto& oldRootShadowNode = oldRevision.rootShadowNode;

  // 1. 执行 transaction,生成新的 RootShadowNode
  auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);

  if (!newRootShadowNode) {
    return CommitStatus::Cancelled;
  }

  // 2. 状态协调(State Reconciliation)
  if (commitOptions.enableStateReconciliation) {
    auto updatedNewRootShadowNode =
        progressState(*newRootShadowNode, *oldRootShadowNode);
    if (updatedNewRootShadowNode) {
      newRootShadowNode =
          std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
    }
  }

  // 3. 调用 delegate 的 shadowTreeWillCommit hook
  newRootShadowNode = delegate_.shadowTreeWillCommit(
      *this, oldRootShadowNode, newRootShadowNode, commitOptions);

  if (!newRootShadowNode) {
    return CommitStatus::Cancelled;
  }

  // Layout nodes.
  std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
  affectedLayoutableNodes.reserve(1024);

  telemetry.willLayout();
  telemetry.setAsThreadLocal();

  // 4. 布局计算
  newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
  telemetry.unsetAsThreadLocal();
  telemetry.didLayout(static_cast<int>(affectedLayoutableNodes.size()));

  {
    // Updating `currentRevision_` in unique manner if it hasn't changed.
    UniqueLock lock = uniqueCommitLock();

    if (currentRevision_.number != oldRevision.number) {
      return CommitStatus::Failed;
    }

    auto newRevisionNumber = currentRevision_.number + 1;

    {
      std::scoped_lock dispatchLock(EventEmitter::DispatchMutex());
      updateMountedFlag(
          currentRevision_.rootShadowNode->getChildren(),
          newRootShadowNode->getChildren(),
          commitOptions.source);
    }

    telemetry.didCommit();
    telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));

    // Seal the shadow node so it can no longer be mutated
    // Does nothing in release.
    newRootShadowNode->sealRecursive();

    newRevision = ShadowTreeRevision{
        .rootShadowNode = std::move(newRootShadowNode),
        .number = newRevisionNumber,
        .telemetry = telemetry};
    // 5. 更新 currentRevision_
    currentRevision_ = newRevision;
  }

  // 6. 发送布局事件
  emitLayoutEvents(affectedLayoutableNodes);

  // 7. 关键:如果 commitMode 是 Normal,调用 mount
  if (commitMode == CommitMode::Normal) {
    mount(std::move(newRevision), commitOptions.mountSynchronously);
  }

  return CommitStatus::Succeeded;
}


void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously)
    const {
  // 1. 将新的 revision 推送到 MountingCoordinator
  mountingCoordinator_->push(std::move(revision));
  // 2. 调用 delegate 的 shadowTreeDidFinishTransaction
  delegate_.shadowTreeDidFinishTransaction(
      mountingCoordinator_, mountSynchronously);
}

注意,ShadowTreeRevision 表示 Shadow Tree 的一个已提交快照(版本)。由三个组成部分:

  • rootShadowNode:整个 Shadow Tree 的根节点,包含完整的树结构和所有子节点

  • number:版本号,从 0(INITIAL_REVISION)开始递增,每次 commit 创建新 revision 时递增。主要用于检测并发冲突和版本追踪

  • telemetry:性能数据(commit 时间、layout 时间、影响的节点数等

MountingCoordinator 使用 revision 来计算新旧树之间的差异,生成需要执行的视图操作。

继续跟踪shadowTreeDidFinishTransaction的实现:

// UIManager.cpp

void UIManager::shadowTreeDidFinishTransaction(
    std::shared_ptr<const MountingCoordinator> mountingCoordinator,
    bool mountSynchronously) const {
  TraceSection s("UIManager::shadowTreeDidFinishTransaction");

  if (delegate_ != nullptr) {
    delegate_->uiManagerDidFinishTransaction(
        std::move(mountingCoordinator), mountSynchronously);
  }
}

继续跟踪uiManagerDidFinishTransaction实现,源码react-native/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp

void Scheduler::uiManagerDidFinishTransaction(
    std::shared_ptr<const MountingCoordinator> mountingCoordinator,
    bool mountSynchronously) {
  TraceSection s("Scheduler::uiManagerDidFinishTransaction");

  if (delegate_ != nullptr) {
    // 除了 Android 平台之外,此操作在所有平台上均无效,
    // 因为在 Android 平台上我们需要观察每个事务才能正确地进行挂载。
    delegate_->schedulerDidFinishTransaction(mountingCoordinator);

    if (!mountSynchronously) {
      auto surfaceId = mountingCoordinator->getSurfaceId();

      runtimeScheduler_->scheduleRenderingUpdate(
          surfaceId,
          [delegate = delegate_,
           mountingCoordinator = std::move(mountingCoordinator)]() {
            delegate->schedulerShouldRenderTransactions(mountingCoordinator);
          });
    } else {
      delegate_->schedulerShouldRenderTransactions(mountingCoordinator);
    }
  }
}

继续跟踪schedulerShouldRenderTransactions实现:

// FabricUIManagerBinding.cpp


void FabricUIManagerBinding::schedulerShouldRenderTransactions(
    const std::shared_ptr<const MountingCoordinator>& mountingCoordinator) {
  auto mountingManager =
      getMountingManager("schedulerShouldRenderTransactions");
  if (!mountingManager) {
    return;
  }
  if (ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid()) {
    auto mountingTransaction = mountingCoordinator->pullTransaction(
        /* willPerformAsynchronously = */ true);
    if (mountingTransaction.has_value()) {
      auto transaction = std::move(*mountingTransaction);
      mountingManager->executeMount(transaction);
    }
  } else {
    std::vector<MountingTransaction> pendingTransactions;

    {
      // 保留锁以访问待处理事务,但不要执行挂载操作,因为该方法可能会再次调用此方法。
      //
      // 当挂载管理器同步触发状态更新时(例如从 UI 线程提交时),此方法可能会被重入调用。
      // 这是安全的,因为我们已经将同一surface ID 的所有事务合并到待处理事务列表中的单个事务中,
      // 因此操作不会乱序执行。
      std::unique_lock<std::mutex> lock(pendingTransactionsMutex_);
      pendingTransactions_.swap(pendingTransactions);
    }

    for (auto& transaction : pendingTransactions) {
      mountingManager->executeMount(transaction);
    }
  }
}

到这里,最重要的方法是executeMount。该方法负责将 C++ 层的 Shadow Tree 变更(MountingTransaction)转换为 Java 层的视图操作指令,并调度到 UI 线程执行。

具体说,就是将 C++ 的 ShadowViewMutation 转换为 Java 的 MountItem,并序列化传递给 Java 层。该方法的实现非常长,这里仅摘要一部分。源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp

void FabricMountingManager::executeMount(
    const MountingTransaction& transaction) {
   // 省略......

    // 遍历 mutations,将 ShadowViewMutation(Create/Delete/Insert/Remove/Update)转换为 CppMountItem
    for (const auto& mutation : mutations) {
      auto parentTag = mutation.parentTag;
      const auto& oldChildShadowView = mutation.oldChildShadowView;
      const auto& newChildShadowView = mutation.newChildShadowView;
      auto& mutationType = mutation.type;
      auto& index = mutation.index;

      bool isVirtual = mutation.mutatedViewIsVirtual();
      switch (mutationType) {
        case ShadowViewMutation::Create: {
          bool shouldCreateView =
              !allocatedViewTags.contains(newChildShadowView.tag);

          if (shouldCreateView) {
            cppCommonMountItems.push_back(
                CppMountItem::CreateMountItem(newChildShadowView));
            allocatedViewTags.insert(newChildShadowView.tag);
          }
          break;
        }
        case ShadowViewMutation::Remove: {
          if (!isVirtual) {
            cppCommonMountItems.push_back(
                CppMountItem::RemoveMountItem(
                    parentTag, oldChildShadowView, index));
          }
          break;
        }
        case ShadowViewMutation::Delete: {
          (maintainMutationOrder ? cppCommonMountItems : cppDeleteMountItems)
              .push_back(CppMountItem::DeleteMountItem(oldChildShadowView));
          if (allocatedViewTags.erase(oldChildShadowView.tag) != 1) {
            LOG(ERROR) << "Emitting delete for unallocated view "
                       << oldChildShadowView.tag;
          }
          break;
        }
        // 省略代码......

        default: {
          break;
        }
      }
    }
  }

  // 现在我们已经掌握了所有必要的信息,包括挂载项的顺序,因此可以准确地知道需要分配多少空间。
  auto [batchMountItemIntsSize, batchMountItemObjectsSize] = computeBufferSizes(
      cppCommonMountItems,
      cppDeleteMountItems,
      cppUpdatePropsMountItems,
      cppUpdateStateMountItems,
      cppUpdatePaddingMountItems,
      cppUpdateLayoutMountItems,
      cppUpdateOverflowInsetMountItems,
      cppUpdateEventEmitterMountItems);

  static auto scheduleMountItem = JFabricUIManager::javaClassStatic()
                                      ->getMethod<void(
                                          JMountItem::javaobject,
                                          jint,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jint)>("scheduleMountItem");

  // 省略

  // 调用 JFabricUIManager.createIntBufferBatchMountItem() 创建 Java 对象
  // 将序列化数据传递给 Java 层
  static auto createMountItemsIntBufferBatchContainer =
      JFabricUIManager::javaClassStatic()
          ->getMethod<jni::alias_ref<JMountItem>(
              jint, jintArray, jni::jtypeArray<jobject>, jint)>(
              "createIntBufferBatchMountItem");
  auto batch = createMountItemsIntBufferBatchContainer(
      javaUIManager_,
      surfaceId,
      // If there are no items, we pass a nullptr instead of passing the
      // object through the JNI
      batchMountItemIntsSize > 0 ? buffer.ints : nullptr,
      batchMountItemObjectsSize > 0 ? buffer.objects.get() : nullptr,
      revisionNumber);

  auto finishTransactionEndTime = telemetryTimePointNow();

  // 调度到 UI 线程,将 BatchMountItem 发送到 Java 层
  scheduleMountItem(
      javaUIManager_,
      batch.get(),
      telemetry.getRevisionNumber(),
      telemetryTimePointToMilliseconds(telemetry.getCommitStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getDiffStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getDiffEndTime()),
      telemetryTimePointToMilliseconds(telemetry.getLayoutStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getLayoutEndTime()),
      telemetryTimePointToMilliseconds(finishTransactionStartTime),
      telemetryTimePointToMilliseconds(finishTransactionEndTime),
      telemetry.getAffectedLayoutNodesCount());

  env->DeleteLocalRef(buffer.ints);
}

可以看到,在此方法中反射调用了上层的scheduleMountItem,与我们前面分析的结果对接上了。

总的来说,该方法充当 C++ Shadow Tree 与 Java 原生视图之间的桥梁,将 React 的声明式变更转换为 Android 视图操作指令,并调度到 UI 线程执行,实现从虚拟树到真实视图的映射。

由于单章字数限制,这里将本文拆成上下两篇,下篇《React Native之Android端Fabric 架构源码分析(下)》

《实时渲染》第2章-图形渲染管线-2.2应用程序阶段

实时渲染

2. 图形渲染管线

2.2 应用程序阶段

开发人员可以完全控制应用程序阶段发生的事情,因为它通常在CPU上执行。因此,开发人员可以完全决定其实现,然后对其进行修改以提高性能。此处的更改也会影响后续阶段的性能。例如,应用程序阶段算法或设置可以减少要渲染的三角形数量。

综上所述,一些应用程序工作可以由GPU执行,使用称为计算着色器的单独模式。此模式将GPU视为高度并行的通用处理器,忽略其专门用于渲染图形的特殊功能。

在应用程序阶段结束时,要渲染的几何图形被馈送到几何图形处理阶段。这些是渲染图元,即点、线和三角形,它们最终可能会出现在屏幕上(或正在使用的任何输出设备)。这是应用阶段最重要的任务。

该阶段基于软件实现的结果是它没有被划分为子阶段,几何处理、光栅化和像素处理阶段也是如此1。但是,为了提高性能,此阶段通常在多个处理器内核上并行执行。在CPU设计中,这被称为超标量构造,因为它能够在同一阶段同时执行多个进程。第18.5节介绍了使用多个处理器内核的各种方法。

在这个阶段通常实施的一个过程是碰撞检测。在检测到两个物体之间发生碰撞后,可以生成响应并将其发送回碰撞物体以及力反馈设备。应用程序阶段也是处理来自其他来源的输入的地方,例如键盘、鼠标或头戴式显示器。根据这个输入,可以采取几种不同的动作。加速算法,例如特定的剔除算法(第19章),也在这里实现,以及管道的其余部分无法处理的任何其他内容。

Footnotes

  1. 由于CPU本身的流水线规模要小得多,您可以说应用程序阶段被进一步细分为几个流水线阶段,但这在这里无关紧要。

《实时渲染》第2章-图形渲染管线-2.1架构

实时渲染

2. 图形渲染管线

链条的坚固程度取决于它最薄弱的环节。 --匿名者

本章介绍实时图形渲染的核心组件,即图形渲染管线,也简称为“管线”。管线的主要功能是通过给定虚拟相机、三维对象、光源等,生成或渲染二维图像。因此,渲染管线是实时渲染的基础工具。使用管线的过程如图2.1所示。图像中对象的位置和形状由它们的几何形状、环境特征以及相机在该环境中的位置决定。对象的外观受材料属性、光源、纹理(应用于表面的图像)和着色方程的影响。

转存失败,建议直接上传图片文件

图2.1. 在左图中,一个虚拟相机位于金字塔的顶端(四条线会聚的地方)。仅渲染视图体积内的图元。对于透视渲染的图像(如这里的情况),视图体积是一个平截头体(frustum,复数为frusta),即具有矩形底部的截棱锥。右图显示了相机“看到”的内容。请注意,左侧图像中的红色甜甜圈形状不在右侧的渲染中,因为它位于视锥体之外。此外,左图中扭曲的蓝色棱镜被剪裁在平截头体的顶平面上。

我们将解释渲染管线的不同阶段,重点是功能而不是实现。应用这些阶段的相关细节将在后面的章节中介绍。

2.1 架构

在现实世界中,管线的概念以许多不同的形式表现出来,从工厂装配线到快餐厨房。它也适用于图形渲染。管线由几个阶段组成[715],每个阶段执行一个更大任务的一部分。

每一个流水线阶段都是并行执行,但是其都依赖于前一阶段的结果。理想情况下,一个非流水线系统然后被分成n个流水线级,可以提供n倍的加速。这种性能的提高是使用流水线的主要原因。例如,可以由一系列人快速准备大量三明治——一个准备面包,另一个添加肉,另一个添加浇头。每个人都将结果传递给排队的下一个人,然后立即开始制作下一个三明治。如果每个人需要20秒来完成他们的任务,那么每20秒一个三明治的最大速度是可能的,每分钟三个。流水线阶段并行执行,但它们会暂停,直到最慢的阶段完成其任务。例如,假设肉类添加阶段变得更加复杂,需要30秒。现在可以达到的最佳速度是每分钟两个三明治。对于这个特定的管线,肉阶段是瓶颈,因为它决定了整个生产的速度。据说浇头阶段在等待肉类阶段完成期间饿死了(顾客也是如此)。

这种管线结构也可以在实时计算机图形的上下文中找到。实时渲染管线粗略划分为四个主要阶段——应用程序、几何处理、光栅化和像素处理——如图2.2所示。渲染管线引擎用于实时计算机图形应用程序,其核心正是这种结构;因此这种管线结构是后续章节讨论的重要基础。这些阶段中的每一个通常本身就是一个管道,这意味着它由几个子阶段组成。我们区分此处显示的功能阶段及其实现结构。一个功能阶段有一个特定的任务要执行,但没有指定任务在管道中的执行方式。一个给定的实现可以将两个功能阶段合并为一个单元或使用可编程内核执行,同时它将另一个更耗时的功能阶段划分为几个硬件单元。

转存失败,建议直接上传图片文件

图2.2. 渲染管线的基本结构,包括四个阶段:应用程序、几何处理、光栅化和像素处理。这些阶段中的每一个本身都可以是一个流水线,如几何处理阶段下方所示,或者一个阶段可以(部分)并行化,如像素处理阶段下方所示。在这个例子中,应用程序阶段是一个单一的进程,但这个阶段也可以是流水线或并行的。请注意,光栅化阶段会查找图元内部的像素,例如三角形。

渲染速度可以用每秒帧数(FPS)来表示,即每秒渲染的图像数量。也可以用赫兹(Hz)来表示,它只是1/秒的表示法,即更新频率。仅说明渲染图像所需的时间(以毫秒(ms)为单位)也很常见。生成图像的时间通常会有所不同,具体取决于每帧期间执行的计算的复杂性。每秒帧数用于表示特定帧的速率或一段时间内的平均性能。赫兹用于硬件,例如设置为固定速率的显示器。

顾名思义,应用程序阶段由应用程序驱动,因此通常在通用CPU上运行的软件中实现。这些CPU通常包括能够并行处理多个执行线程的多个内核。这使CPU能够有效地运行由应用程序阶段负责的各种任务。一些传统上在CPU上执行的任务包括碰撞检测、全局加速算法、动画、物理模拟等,具体取决于应用程序的类型。下一个主要阶段是几何处理,它处理变换、投影和所有其他类型的几何处理。此阶段计算要绘制的内容、应如何绘制以及应在何处绘制。几何阶段通常在包含许多可编程内核和固定操作硬件的图形处理单元(GPU)上执行。光栅化阶段通常将三个顶点作为输入,形成一个三角形,并找到该三角形内的所有像素,然后将这些像素转发到下一个阶段。最后,像素处理阶段对每个像素执行一个程序以确定其颜色,并可能执行深度测试以查看它是否可见。它还可以执行逐像素操作,例如将新计算的颜色与先前的颜色混合。光栅化和像素处理阶段也完全在GPU上处理。所有这些阶段及其内部管道将在接下来的四节中讨论。有关GPU如何处理这些阶段的更多详细信息,请参见第3章

还在无脑 .map().filter()?实测改用 Iterator Helpers 后,内存占用降低了 99%

别再把所有东西都转成数组了:Iterator Helpers 性能实测

今天看到一篇文章,标题很直接:Stop turning everything into arrays。作者的观点是,JavaScript 里我们习惯了用 .map().filter().slice() 这种链式调用,看着很优雅,但其实每一步都在创建新数组,做了很多不必要的工作。

我一开始也半信半疑,毕竟这种写法用了这么多年,真的有那么大问题吗?于是我写了个测试脚本,跑了几组对比实验,结果还挺出乎意料的。

问题在哪

先说传统数组方法的问题。假设你有个常见场景:从一个大列表里筛选出符合条件的数据,做点转换,然后只取前 10 条显示。

const visibleItems = items
  .filter(item => item.active)
  .map(item => ({ id: item.id, doubled: item.value * 2 }))
  .slice(0, 10);

这代码看着没毛病,但实际执行时:

  1. filter 遍历整个数组,创建一个新数组
  2. map 再遍历一遍,又创建一个新数组
  3. slice 最后从结果里取前 10 条,又创建一个新数组

如果 items 有 10 万条数据,你可能只需要 10 条,但前面两步已经把 10 万条全处理完了。这就是"急切求值"(eager evaluation)的问题——不管你最后用不用,先把活全干了再说。

Iterator Helpers 是什么

Iterator Helpers 是 JavaScript 新加的特性,提供了一套"惰性求值"(lazy evaluation)的方法。关键区别是:

  • 传统数组方法:每一步都立即执行,创建中间数组
  • Iterator Helpers:只描述要做什么,真正需要数据时才执行

用法上也很直观:

const visibleItems = items
  .values()              // 转成 iterator
  .filter(item => item.active)
  .map(item => ({ id: item.id, doubled: item.value * 2 }))
  .take(10)              // 只取 10 条
  .toArray();            // 最后转回数组

核心差异在于:

  1. items.values() 返回的是 iterator,不是数组
  2. 每一步只是在"描述"操作,不会立即执行
  3. take(10) 告诉它只需要 10 条,处理到 10 条就停
  4. toArray() 才真正触发执行,而且只处理需要的数据

实测对比

我写了个测试脚本,从时间空间两个维度进行对比测试。每组场景的时间测试重复 10 次取平均值,内存测试使用 Node.js 的 process.memoryUsage() API 测量堆内存增长。

场景 1:过滤 + 转换 + 取前 10 项

数据规模:100,000 条

// 传统数组方法
dataset
  .filter(item => item.active)
  .map(item => ({ id: item.id, doubled: item.value * 2 }))
  .slice(0, 10);

// Iterator Helpers
dataset
  .values()
  .filter(item => item.active)
  .map(item => ({ id: item.id, doubled: item.value * 2 }))
  .take(10)
  .toArray();

结果

维度 传统数组方法 Iterator Helpers 性能提升
时间 1.545ms 0.019ms 98.77%
内存 3.38 MB 0.01 MB 99.75%

这个结果很夸张,Iterator Helpers 在时间上快了 80 多倍,内存使用更是只有传统方法的 0.3%。原因很简单:传统方法处理了所有 10 万条数据并创建了 2 个中间数组(filter 和 map 各一个),而 Iterator Helpers 找到 10 条就停了,完全不创建中间数组。

场景 2:嵌套数据扁平化

数据规模:10,000 个父项,每个包含 10 个子项(共 100,000 条子数据)

// 传统数组方法
dataset
  .flatMap(parent => parent.children)
  .filter(child => child.value > 50)
  .slice(0, 20);

// Iterator Helpers
dataset
  .values()
  .flatMap(parent => parent.children)
  .filter(child => child.value > 50)
  .take(20)
  .toArray();

结果

维度 传统数组方法 Iterator Helpers 性能提升
时间 3.716ms 0.018ms 99.53%
内存 4.03 MB 0.01 MB 99.73%

flatMap 这种场景更明显,因为传统方法要先把所有嵌套数据(10 万条子数据)展平成一个大数组,再过滤,再切片,创建了 2 个中间数组。Iterator Helpers 直接在展平的过程中就能提前终止,内存使用几乎可以忽略不计。

场景 3:查找第一个匹配项

数据规模:1,000,000 条(目标在第 800,000 条)

// 传统数组方法
dataset.filter(item => item.id === targetId)[0];

// Iterator Helpers
dataset.values().find(item => item.id === targetId);

结果

维度 传统数组方法 Iterator Helpers 性能变化
时间 6.587ms 8.713ms -32.27% ⚠️
内存 0.01 MB 14.80 MB -194800% ⚠️

这个场景很有意思,Iterator Helpers 在时间和空间上都更差了。我分析了一下原因:

  1. 时间慢的原因:目标在数据集后半部分(第 80 万条),两者都要遍历大部分数据。filter 是原生实现,优化得很好;而 Iterator Helpers 的 find 每次迭代的开销更大
  2. 内存多的原因:这个测试结果有点反直觉,可能是因为 iterator 本身的内部状态占用了内存,而且 GC 时机不同导致测量偏差

这说明 Iterator Helpers 不是银弹,在"需要遍历大部分数据"的场景下,传统方法可能更好。

场景 4:复杂链式调用

数据规模:50,000 条

// 传统数组方法
dataset
  .filter(item => item.active)
  .map(item => ({ ...item, doubled: item.value * 2 }))
  .filter(item => item.doubled > 500)
  .map(item => ({ id: item.id, final: item.doubled + 100 }))
  .slice(0, 15);

// Iterator Helpers
dataset
  .values()
  .filter(item => item.active)
  .map(item => ({ ...item, doubled: item.value * 2 }))
  .filter(item => item.doubled > 500)
  .map(item => ({ id: item.id, final: item.doubled + 100 }))
  .take(15)
  .toArray();

结果

维度 传统数组方法 Iterator Helpers 性能提升
时间 1.421ms 0.028ms 98.02%
内存 5.33 MB 0.01 MB 99.75%

链式调用越多,传统方法创建的中间数组就越多。这个场景有 4 次操作(filter → map → filter → map),传统方法创建了 4 个中间数组,总共占用 5.33 MB 内存;而 Iterator Helpers 一个中间数组都没创建,内存使用几乎为零。

什么时候用 Iterator Helpers

根据测试结果,我总结了几个适合用 Iterator Helpers 的场景:

1. 只需要前 N 项

这是最明显的优势场景。无限滚动、分页加载、虚拟列表这些场景都适合。

// 虚拟列表只渲染可见的 20 条
const visibleRows = allRows
  .values()
  .filter(isInViewport)
  .take(20)
  .toArray();

2. 流式数据处理

处理 API 分页、SSE 流、WebSocket 消息这些场景,Iterator Helpers 配合 async iterator 很好用:

async function* fetchPages() {
  let page = 1;
  while (true) {
    const res = await fetch(`/api/items?page=${page++}`);
    if (!res.ok) return;
    yield* await res.json();
  }
}

// 只拉取需要的数据,不会一次性加载所有分页
const firstTen = await fetchPages()
  .filter(isValid)
  .take(10)
  .toArray();

3. 复杂的数据管道

如果你的数据处理链路很长,有多次 filtermapflatMap,用 Iterator Helpers 能避免创建一堆中间数组。

const result = data
  .values()
  .filter(step1)
  .map(step2)
  .flatMap(step3)
  .filter(step4)
  .take(100)
  .toArray();

什么时候不用

Iterator Helpers 也不是万能的,这几种情况还是老老实实用数组:

1. 需要随机访问

Iterator 是单向的,不能 items[5] 这样直接取某一项。如果你需要随机访问,还是得用数组。

2. 数据量很小

如果就几十条数据,用 Iterator Helpers 反而增加了复杂度,传统数组方法更简单直接。

3. 需要多次遍历

Iterator 只能遍历一次,如果你需要对同一份数据做多次不同的处理,还是先 toArray() 转成数组再说。

const iter = data.values().filter(x => x > 10);

// ❌ 第二次遍历会返回空,因为 iterator 已经消费完了
const first = iter.take(5).toArray();
const second = iter.take(5).toArray(); // []

// ✅ 先转数组,再多次使用
const filtered = data.values().filter(x => x > 10).toArray();
const first = filtered.slice(0, 5);
const second = filtered.slice(5, 10);

兼容性

Iterator Helpers 在现代浏览器和 Node.js 22+ 都已经支持了。如果你的项目还要兼容老版本,可以用 core-js 这类 polyfill。

可以在 Can I Use 查看详细的兼容性数据。

一些坑

1. Iterator 不是数组

这是最容易踩的坑。Iterator 没有 length[index] 这些属性,也不能直接 console.log 看内容。

const iter = [1, 2, 3].values();

console.log(iter.length);  // undefined
console.log(iter[0]);      // undefined
console.log(iter);         // Object [Array Iterator] {}

// 要看内容,得先转数组
console.log([...iter]);    // [1, 2, 3]

2. reduce 不是惰性的

大部分 Iterator Helpers 都是惰性的,但 reduce 是个例外,它必须遍历所有数据才能得出结果。

// reduce 会立即消费整个 iterator
const sum = data.values().reduce((acc, x) => acc + x, 0);

3. 调试不方便

因为是惰性求值,你不能在中间步骤打断点看数据。如果要调试,可以在关键步骤插入 toArray() 转成数组再看。

const result = data
  .values()
  .filter(step1)
  .toArray()  // 调试用,看看 filter 后的结果
  .values()
  .map(step2)
  .take(10)
  .toArray();

总结

Iterator Helpers 不是要替代数组,而是给了我们另一个选择。核心思路就一句话:如果你不需要整个数组,就别创建它

从实测结果看:

  • 在"取前 N 项"这类场景下,时间和空间开销都能降低 90%+
  • 在"需要遍历大部分数据"的场景下,传统方法可能更快(场景 3 是个反例)
  • 数据规模越大,Iterator Helpers 的优势越明显
  • 内存优势尤其突出:传统方法创建的中间数组会占用大量内存,而 Iterator Helpers 几乎不占用额外内存

我个人的使用建议是:

  1. 默认还是用数组方法,简单直接,不容易出错
  2. 遇到性能瓶颈时,看看是不是"只需要部分数据却处理了全部",如果是,试试 Iterator Helpers
  3. 写数据管道时,如果链路很长,用 Iterator Helpers 能让代码更清晰,也能避免不必要的内存分配
  4. 内存敏感场景,比如处理大数据集、移动端应用等,Iterator Helpers 能显著降低内存压力

完整测试代码

下面是完整的性能测试代码,包含时间空间两个维度的测试。你可以复制到本地跑一下:

/**
 * Iterator Helpers vs Array Methods 性能对比测试
 * 
 * 测试维度:
 * 1. 时间开销(执行时间)
 * 2. 空间开销(内存使用)
 * 
 * 测试场景:
 * 1. 大数据集过滤 + 转换 + 取前 N 项
 * 2. 嵌套数据扁平化
 * 3. 查找第一个匹配项
 * 4. 复杂链式调用
 */

// ============ 工具函数 ============

function generateLargeDataset(size) {
  return Array.from({ length: size }, (_, i) => ({
    id: i,
    value: Math.random() * 1000,
    category: ['A', 'B', 'C', 'D'][i % 4],
    active: i % 3 === 0
  }));
}

function benchmark(name, fn, iterations = 1) {
  const times = [];
  
  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    const end = performance.now();
    times.push(end - start);
  }
  
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
  const min = Math.min(...times);
  const max = Math.max(...times);
  
  console.log(`\n${name}:`);
  console.log(`  平均: ${avg.toFixed(3)}ms`);
  console.log(`  最小: ${min.toFixed(3)}ms`);
  console.log(`  最大: ${max.toFixed(3)}ms`);
  
  return avg;
}

function memoryBenchmark(name, fn) {
  // 强制垃圾回收(需要 Node.js 启动时加 --expose-gc 参数)
  if (global.gc) {
    global.gc();
  }
  
  const memBefore = process.memoryUsage();
  fn();
  const memAfter = process.memoryUsage();
  
  const heapUsedDiff = (memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024;
  
  console.log(`  内存增长: ${heapUsedDiff.toFixed(2)} MB`);
  
  return heapUsedDiff;
}

// ============ 测试场景 1: 过滤 + 转换 + 取前 N 项 ============

console.log('='.repeat(60));
console.log('测试场景 1: 大数据集过滤 + 转换 + 取前 10 项');
console.log('数据规模: 100,000 条');
console.log('='.repeat(60));

const dataset1 = generateLargeDataset(100000);

// 传统数组方法 - 时间
const arrayMethodTime1 = benchmark('传统数组方法 (filter + map + slice) - 时间', () => {
  const result = dataset1
    .filter(item => item.active)
    .map(item => ({ id: item.id, doubled: item.value * 2 }))
    .slice(0, 10);
  
  return result;
}, 10);

// 传统数组方法 - 内存
const arrayMethodMem1 = memoryBenchmark('传统数组方法 (filter + map + slice) - 内存', () => {
  const result = dataset1
    .filter(item => item.active)
    .map(item => ({ id: item.id, doubled: item.value * 2 }))
    .slice(0, 10);
  
  return result;
});

// Iterator Helpers - 时间
const iteratorHelpersTime1 = benchmark('Iterator Helpers (filter + map + take) - 时间', () => {
  const result = dataset1
    .values()
    .filter(item => item.active)
    .map(item => ({ id: item.id, doubled: item.value * 2 }))
    .take(10)
    .toArray();
  
  return result;
}, 10);

// Iterator Helpers - 内存
const iteratorHelpersMem1 = memoryBenchmark('Iterator Helpers (filter + map + take) - 内存', () => {
  const result = dataset1
    .values()
    .filter(item => item.active)
    .map(item => ({ id: item.id, doubled: item.value * 2 }))
    .take(10)
    .toArray();
  
  return result;
});

const timeImprovement1 = ((arrayMethodTime1 - iteratorHelpersTime1) / arrayMethodTime1 * 100).toFixed(2);
const memImprovement1 = ((arrayMethodMem1 - iteratorHelpersMem1) / arrayMethodMem1 * 100).toFixed(2);
console.log(`\n性能提升: 时间 ${timeImprovement1}%, 内存 ${memImprovement1}%`);

// ============ 测试场景 2: 嵌套数据扁平化 ============

console.log('\n' + '='.repeat(60));
console.log('测试场景 2: 嵌套数据扁平化 + 过滤 + 取前 20 项');
console.log('数据规模: 10,000 个父项,每个包含 10 个子项');
console.log('='.repeat(60));

const dataset2 = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  children: Array.from({ length: 10 }, (_, j) => ({
    childId: `${i}-${j}`,
    value: Math.random() * 100
  }))
}));

// 传统数组方法 - 时间
const arrayMethodTime2 = benchmark('传统数组方法 (flatMap + filter + slice) - 时间', () => {
  const result = dataset2
    .flatMap(parent => parent.children)
    .filter(child => child.value > 50)
    .slice(0, 20);
  
  return result;
}, 10);

// 传统数组方法 - 内存
const arrayMethodMem2 = memoryBenchmark('传统数组方法 (flatMap + filter + slice) - 内存', () => {
  const result = dataset2
    .flatMap(parent => parent.children)
    .filter(child => child.value > 50)
    .slice(0, 20);
  
  return result;
});

// Iterator Helpers - 时间
const iteratorHelpersTime2 = benchmark('Iterator Helpers (flatMap + filter + take) - 时间', () => {
  const result = dataset2
    .values()
    .flatMap(parent => parent.children)
    .filter(child => child.value > 50)
    .take(20)
    .toArray();
  
  return result;
}, 10);

// Iterator Helpers - 内存
const iteratorHelpersMem2 = memoryBenchmark('Iterator Helpers (flatMap + filter + take) - 内存', () => {
  const result = dataset2
    .values()
    .flatMap(parent => parent.children)
    .filter(child => child.value > 50)
    .take(20)
    .toArray();
  
  return result;
});

const timeImprovement2 = ((arrayMethodTime2 - iteratorHelpersTime2) / arrayMethodTime2 * 100).toFixed(2);
const memImprovement2 = ((arrayMethodMem2 - iteratorHelpersMem2) / arrayMethodMem2 * 100).toFixed(2);
console.log(`\n性能提升: 时间 ${timeImprovement2}%, 内存 ${memImprovement2}%`);

// ============ 测试场景 3: 查找第一个匹配项 ============

console.log('\n' + '='.repeat(60));
console.log('测试场景 3: 查找第一个匹配项(匹配项在数据集后半部分)');
console.log('数据规模: 1,000,000 条');
console.log('='.repeat(60));

const dataset3 = generateLargeDataset(1000000);
const targetId = 800000;

// 传统数组方法 - 时间
const arrayMethodTime3 = benchmark('传统数组方法 (filter + [0]) - 时间', () => {
  const result = dataset3
    .filter(item => item.id === targetId)[0];
  
  return result;
}, 10);

// 传统数组方法 - 内存
const arrayMethodMem3 = memoryBenchmark('传统数组方法 (filter + [0]) - 内存', () => {
  const result = dataset3
    .filter(item => item.id === targetId)[0];
  
  return result;
});

// Iterator Helpers - 时间
const iteratorHelpersTime3 = benchmark('Iterator Helpers (find) - 时间', () => {
  const result = dataset3
    .values()
    .find(item => item.id === targetId);
  
  return result;
}, 10);

// Iterator Helpers - 内存
const iteratorHelpersMem3 = memoryBenchmark('Iterator Helpers (find) - 内存', () => {
  const result = dataset3
    .values()
    .find(item => item.id === targetId);
  
  return result;
});

const timeImprovement3 = ((arrayMethodTime3 - iteratorHelpersTime3) / arrayMethodTime3 * 100).toFixed(2);
const memImprovement3 = ((arrayMethodMem3 - iteratorHelpersMem3) / arrayMethodMem3 * 100).toFixed(2);
console.log(`\n性能提升: 时间 ${timeImprovement3}%, 内存 ${memImprovement3}%`);

// ============ 测试场景 4: 多次链式调用 ============

console.log('\n' + '='.repeat(60));
console.log('测试场景 4: 复杂链式调用(filter + map + filter + map + take)');
console.log('数据规模: 50,000 条');
console.log('='.repeat(60));

const dataset4 = generateLargeDataset(50000);

// 传统数组方法 - 时间
const arrayMethodTime4 = benchmark('传统数组方法 (多次链式调用) - 时间', () => {
  const result = dataset4
    .filter(item => item.active)
    .map(item => ({ ...item, doubled: item.value * 2 }))
    .filter(item => item.doubled > 500)
    .map(item => ({ id: item.id, final: item.doubled + 100 }))
    .slice(0, 15);
  
  return result;
}, 10);

// 传统数组方法 - 内存
const arrayMethodMem4 = memoryBenchmark('传统数组方法 (多次链式调用) - 内存', () => {
  const result = dataset4
    .filter(item => item.active)
    .map(item => ({ ...item, doubled: item.value * 2 }))
    .filter(item => item.doubled > 500)
    .map(item => ({ id: item.id, final: item.doubled + 100 }))
    .slice(0, 15);
  
  return result;
});

// Iterator Helpers - 时间
const iteratorHelpersTime4 = benchmark('Iterator Helpers (多次链式调用) - 时间', () => {
  const result = dataset4
    .values()
    .filter(item => item.active)
    .map(item => ({ ...item, doubled: item.value * 2 }))
    .filter(item => item.doubled > 500)
    .map(item => ({ id: item.id, final: item.doubled + 100 }))
    .take(15)
    .toArray();
  
  return result;
}, 10);

// Iterator Helpers - 内存
const iteratorHelpersMem4 = memoryBenchmark('Iterator Helpers (多次链式调用) - 内存', () => {
  const result = dataset4
    .values()
    .filter(item => item.active)
    .map(item => ({ ...item, doubled: item.value * 2 }))
    .filter(item => item.doubled > 500)
    .map(item => ({ id: item.id, final: item.doubled + 100 }))
    .take(15)
    .toArray();
  
  return result;
});

const timeImprovement4 = ((arrayMethodTime4 - iteratorHelpersTime4) / arrayMethodTime4 * 100).toFixed(2);
const memImprovement4 = ((arrayMethodMem4 - iteratorHelpersMem4) / arrayMethodMem4 * 100).toFixed(2);
console.log(`\n性能提升: 时间 ${timeImprovement4}%, 内存 ${memImprovement4}%`);

// ============ 总结 ============

console.log('\n' + '='.repeat(60));
console.log('总结');
console.log('='.repeat(60));
console.log(`
场景 1 (过滤+转换+取前N项):
  时间提升: ${timeImprovement1}%
  内存提升: ${memImprovement1}%

场景 2 (嵌套数据扁平化):
  时间提升: ${timeImprovement2}%
  内存提升: ${memImprovement2}%

场景 3 (查找第一个匹配项):
  时间提升: ${timeImprovement3}%
  内存提升: ${memImprovement3}%

场景 4 (复杂链式调用):
  时间提升: ${timeImprovement4}%
  内存提升: ${memImprovement4}%

结论:
- Iterator Helpers 在需要"取前 N 项"的场景下优势明显
- 时间和空间开销都能显著降低
- 数据规模越大,提升越显著
- 惰性求值避免了不必要的中间数组创建
- 在查找场景下,Iterator Helpers 可以提前终止,避免遍历整个数组
`);

console.log('\n提示: 运行时加上 --expose-gc 参数可以获得更准确的内存测试结果');
console.log('命令: node --expose-gc iterator-helpers-benchmark.js');

实际运行输出

在我的环境(Node.js 22+)下,使用 node --expose-gc iterator-helpers-benchmark.js 运行上面的代码,得到以下结果:

============================================================
测试场景 1: 大数据集过滤 + 转换 + 取前 10 项
数据规模: 100,000 条
============================================================

传统数组方法 (filter + map + slice) - 时间:
  平均: 1.545ms
  最小: 0.920ms
  最大: 2.993ms
  内存增长: 3.38 MB

Iterator Helpers (filter + map + take) - 时间:
  平均: 0.019ms
  最小: 0.003ms
  最大: 0.126ms
  内存增长: 0.01 MB

性能提升: 时间 98.77%, 内存 99.75%

============================================================
测试场景 2: 嵌套数据扁平化 + 过滤 + 取前 20 项
数据规模: 10,000 个父项,每个包含 10 个子项
============================================================

传统数组方法 (flatMap + filter + slice) - 时间:
  平均: 3.716ms
  最小: 2.263ms
  最大: 8.936ms
  内存增长: 4.03 MB

Iterator Helpers (flatMap + filter + take) - 时间:
  平均: 0.018ms
  最小: 0.004ms
  最大: 0.120ms
  内存增长: 0.01 MB

性能提升: 时间 99.53%, 内存 99.73%

============================================================
测试场景 3: 查找第一个匹配项(匹配项在数据集后半部分)
数据规模: 1,000,000 条
============================================================

传统数组方法 (filter + [0]) - 时间:
  平均: 6.587ms
  最小: 5.605ms
  最大: 11.103ms
  内存增长: 0.01 MB

Iterator Helpers (find) - 时间:
  平均: 8.713ms
  最小: 7.893ms
  最大: 9.545ms
  内存增长: 14.80 MB

性能提升: 时间 -32.27%, 内存 -194800.20%

============================================================
测试场景 4: 复杂链式调用(filter + map + filter + map + take)
数据规模: 50,000 条
============================================================

传统数组方法 (多次链式调用) - 时间:
  平均: 1.421ms
  最小: 0.840ms
  最大: 2.797ms
  内存增长: 5.33 MB

Iterator Helpers (多次链式调用) - 时间:
  平均: 0.028ms
  最小: 0.005ms
  最大: 0.220ms
  内存增长: 0.01 MB

性能提升: 时间 98.02%, 内存 99.75%

============================================================
总结
============================================================

场景 1 (过滤+转换+取前N项):
  时间提升: 98.77%
  内存提升: 99.75%

场景 2 (嵌套数据扁平化):
  时间提升: 99.53%
  内存提升: 99.73%

场景 3 (查找第一个匹配项):
  时间提升: -32.27%
  内存提升: -194800.20%

场景 4 (复杂链式调用):
  时间提升: 98.02%
  内存提升: 99.75%

结论:
- Iterator Helpers 在需要"取前 N 项"的场景下优势明显
- 时间和空间开销都能显著降低
- 数据规模越大,提升越显著
- 惰性求值避免了不必要的中间数组创建
- 在查找场景下,Iterator Helpers 可以提前终止,避免遍历整个数组

提示: 运行时加上 --expose-gc 参数可以获得更准确的内存测试结果
命令: node --expose-gc iterator-helpers-benchmark.js

从输出可以看到:

场景 1-2、4("取前 N 项"类场景)

  • 时间提升:98%+
  • 内存提升:99%+
  • Iterator Helpers 在时间和空间上都有压倒性优势

场景 3(查找匹配项)

  • 时间下降:32%(Iterator Helpers 更慢)
  • 内存异常:这个结果看起来有问题,可能是 GC 时机导致的测量误差
  • 这个场景不适合用 Iterator Helpers

关键发现

  1. 内存优势极其明显:在适合的场景下,Iterator Helpers 的内存使用只有传统方法的 0.3%
  2. 中间数组是大头:场景 4 创建了 4 个中间数组,占用 5.33 MB;Iterator Helpers 几乎为零
  3. 不是所有场景都适用:场景 3 证明了 Iterator Helpers 不是银弹

如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):

qwen/gemini/claude - cli 原理学习网站

coding-cli-guide

全栈项目(适合学习现代技术栈):

  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB

VS Code 插件

  • vscode-ai-commit - 一键生成 commit message,支持 Conventional Commits,兼容任何 OpenAI 格式接口

回溯算法:选→钻→退→删,掌握穷举的艺术

回溯算法:选→钻→退→删,掌握穷举的艺术

回溯算法是算法领域的核心思想之一,尤其在处理「穷举所有可能解」的问题时堪称"神器"。本文将从核心思路出发,通过"选一个数→钻到底→退回来→删掉这个数→选下一个数→再钻到底"这个固定节奏,带你彻底掌握回溯算法。

一、回溯核心原理:选→钻→退→删的固定节奏

一句话记牢回溯的执行过程

选一个数→钻到底→退回来→删掉这个数→选下一个数→再钻到底,直到所有数都试完,最后收集所有符合条件的路径。

选了就往下钻,走不通就退回来擦脚印,换条路再试。很多人一开始都会被递归的多层调用绕晕,但只要抓住 push(选数)→ 递归(钻深层)→ pop(擦脚印) 这个固定节奏,再结合剪枝提前止损,所有回溯题都能套模板解决~

核心四要素

任何回溯问题都能拆解为以下4个核心部分:

要素 作用 示例(组合总和)
路径 已经做出的选择(比如选了哪些数) path = [2,3](表示选了2和3)
选择列表 当前步骤可选择的选项(比如还能选哪些数) nums = [2,3,6,7],已选2则可选3/6/7
终止条件 什么时候停止探索(找到解/走到底) 路径和等于目标值,或路径和超过目标值
剪枝 提前排除无效路径(核心优化) 路径和超过目标值,直接停止当前路径

关键理解

  • 递归是"往下钻":每选一个数,就递归调用backtrack,相当于"往下走一步"
  • return 是"往回退":触发终止条件(找到解/和超了),就结束当前递归,回到上一层
  • pop 是"擦脚印":回到上一层后,必须执行path.pop(),把刚才选的数删掉,才能"换另一个数选"
  • for 循环是"选岔路":每一层的 for 循环,就是在当前位置选不同的数(岔路),试完一条再试下一条

通用模板

// 回溯核心函数
function backtrack(路径, 选择列表, 其他参数) {
  // 1. 终止条件:找到解,记录结果并返回
  if (满足终止条件) {
    结果列表.push(路径的拷贝); // 注意:要拷贝,否则会被后续修改
    return;
  }

  // 2. 遍历所有可选选项
  for (const 选项 of 选择列表) {
    // 剪枝:提前排除无效选项(关键优化)
    if (选项无效) continue;

    // 3. 做选择:把当前选项加入路径(选数)
    路径.push(选项);

    // 4. 递归探索:基于当前选择,继续往下走(钻到底)
    backtrack(路径, 新的选择列表, 其他参数);

    // 5. 撤销选择(回溯核心):回到上一步,换选项(退回来→删掉这个数)
    路径.pop();
  }
}

// 主函数
function solveProblem(参数) {
  const 结果列表 = []; // 存储所有符合条件的解
  backtrack([], 初始选择列表, 初始参数); // 初始路径为空
  return 结果列表;
}

二、入门示例:组合总和(可视化理解)

为了让你快速理解回溯的核心节奏,我们先从组合总和这个经典入门题入手,通过可视化打印完整展示「选→探→撤」的全过程。

题目描述

给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合(数字可以无限制重复被选取)。

  • 示例:输入 candidates = [2,3,7]target = 7,输出 [[2,2,3],[7]]

完整代码实现

/**
 * 组合总和:找所有和为目标值的组合(可视化打印版)
 * @param {number[]} candidates - 候选数组
 * @param {number} target - 目标和
 * @returns {number[][]} - 所有符合条件的组合
 */
function combinationSum(candidates, target) {
  const result = []; // 存储最终结果
  candidates.sort((a, b) => a - b); // 排序便于剪枝
  let recursionLevel = 0; // 标记递归层级,用于可视化缩进

  // 回溯函数:path=当前路径,sum=当前路径和,start=起始索引(避免重复组合)
  function backtrack(path, sum, start) {
    // 层级+1,生成缩进(每级2个空格)
    recursionLevel++;
    const indent = '  '.repeat(recursionLevel - 1);
    const levelTag = `【层级${recursionLevel}】`;

    // 🟢 调用阶段:打印当前层级初始状态
    console.log(
      `${indent}🟢 ${levelTag} 调用阶段 → path=${JSON.stringify(path)}, sum=${sum}, start=${start}`
    );

    // 终止条件1:路径和等于目标值,记录结果
    if (sum === target) {
      result.push([...path]); // 拷贝路径,避免后续修改
      console.log(
        `${indent}${levelTag} 找到有效组合 → ${JSON.stringify(path)},result=${JSON.stringify(result)}`
      );
      // 层级-1,准备返回
      recursionLevel--;
      console.log(`${indent}🔴 ${levelTag} 返回阶段 → 找到解,回到上一层`);
      return;
    }
    // 终止条件2:路径和超过目标值,直接返回(剪枝)
    if (sum > target) {
      console.log(`${indent}🚫 ${levelTag} 剪枝 → sum=${sum} > target=${target},直接返回`);
      recursionLevel--;
      console.log(`${indent}🔴 ${levelTag} 返回阶段 → 剪枝返回,回到上一层`);
      return;
    }

    // 遍历选择列表(从start开始,避免重复组合)
    for (let i = start; i < candidates.length; i++) {
      const num = candidates[i];
      console.log(`${indent}🔍 ${levelTag} 遍历i=${i} → 尝试选数字${num},当前sum=${sum}`);

      // 排序剪枝:当前数+已选和>目标值,后续数更大,无需继续
      if (sum + num > target) {
        console.log(
          `${indent}🚫 ${levelTag} 排序剪枝 → ${sum}+${num}=${sum + num} > ${target},break循环`
        );
        break;
      }

      // 做选择:加入当前数(选数)
      path.push(num);
      console.log(
        `${indent}${levelTag} 选数字${num} → path=${JSON.stringify(path)}, sum=${sum + num}`
      );

      // 🟡 暂停阶段:调用下一层,当前层级暂停
      console.log(`${indent}🟡 ${levelTag} 暂停阶段 → 调用下一层backtrack,等待返回`);
      // 递归探索:数字可重复选,所以start仍为i(钻到底)
      backtrack(path, sum + num, i);
      console.log(`${indent}🔴 ${levelTag} 返回阶段 → 从下一层返回,继续执行`);

      // 撤销选择:回溯核心(退回来→删掉这个数)
      path.pop();
      console.log(
        `${indent}🔵 ${levelTag} 撤销阶段 → 撤销数字${num} → path=${JSON.stringify(path)}, sum=${sum}`
      );
    }

    // 层级-1,准备返回
    recursionLevel--;
    console.log(`${indent}🔴 ${levelTag} 返回阶段 → for循环结束,回到上一层`);
  }

  // 初始调用:空路径、和为0、从索引0开始
  console.log('========== 开始执行组合总和 ==========');
  backtrack([], 0, 0);
  console.log('========== 执行结束 ==========');
  return result;
}

// 测试:输入 [2,3,7], 7 → 输出 [[2,2,3],[7]]
console.log('最终结果:', combinationSum([2, 3, 7], 7));

各个图标的含义:

  • 🟢 调用阶段:进入递归时的状态
  • 🔍 遍历阶段:尝试选择数字
  • ✅ 选择阶段:成功选择数字
  • 🟡 暂停阶段:调用下一层递归
  • 🔴 返回阶段:从下一层返回
  • 🔵 撤销阶段:删除数字(擦脚印)
  • 🚫 剪枝阶段:提前终止无效路径

控制台输出示例:

========== 开始执行组合总和 ==========
🟢 【层级1】 调用阶段 → path=[], sum=0, start=0
🔍 【层级1】 遍历i=0 → 尝试选数字2,当前sum=0
✅ 【层级1】 选数字2 → path=[2], sum=2
🟡 【层级1】 暂停阶段 → 调用下一层backtrack,等待返回
  🟢 【层级2】 调用阶段 → path=[2], sum=2, start=0
  🔍 【层级2】 遍历i=0 → 尝试选数字2,当前sum=2
  ✅ 【层级2】 选数字2 → path=[2,2], sum=4
  🟡 【层级2】 暂停阶段 → 调用下一层backtrack,等待返回
    🟢 【层级3】 调用阶段 → path=[2,2], sum=4, start=0
    🔍 【层级3】 遍历i=0 → 尝试选数字2,当前sum=4
    ✅ 【层级3】 选数字2 → path=[2,2,2], sum=6
    🟡 【层级3】 暂停阶段 → 调用下一层backtrack,等待返回
      🟢 【层级4】 调用阶段 → path=[2,2,2], sum=6, start=0
      🔍 【层级4】 遍历i=0 → 尝试选数字2,当前sum=6
      🚫 【层级4】 排序剪枝 → 6+2=8 > 7,break循环
      🔴 【层级4】 返回阶段 → for循环结束,回到上一层
    🔴 【层级3】 返回阶段 → 从下一层返回,继续执行
    🔵 【层级3】 撤销阶段 → 撤销数字2 → path=[2,2], sum=4
    🔍 【层级3】 遍历i=1 → 尝试选数字3,当前sum=4
    ✅ 【层级3】 选数字3 → path=[2,2,3], sum=7
    🟡 【层级3】 暂停阶段 → 调用下一层backtrack,等待返回
      🟢 【层级4】 调用阶段 → path=[2,2,3], sum=7, start=1
      ✅ 【层级4】 找到有效组合 → [2,2,3],result=[[2,2,3]]
      🔴 【层级4】 返回阶段 → 找到解,回到上一层
    🔴 【层级3】 返回阶段 → 从下一层返回,继续执行
    🔵 【层级3】 撤销阶段 → 撤销数字3 → path=[2,2], sum=4
    🔍 【层级3】 遍历i=2 → 尝试选数字7,当前sum=4
    🚫 【层级3】 排序剪枝 → 4+7=11 > 7,break循环
    🔴 【层级3】 返回阶段 → for循环结束,回到上一层
  🔴 【层级2】 返回阶段 → 从下一层返回,继续执行
  🔵 【层级2】 撤销阶段 → 撤销数字2 → path=[2], sum=2
  🔍 【层级2】 遍历i=1 → 尝试选数字3,当前sum=2
  ✅ 【层级2】 选数字3 → path=[2,3], sum=5
  🟡 【层级2】 暂停阶段 → 调用下一层backtrack,等待返回
    🟢 【层级3】 调用阶段 → path=[2,3], sum=5, start=1
    🔍 【层级3】 遍历i=1 → 尝试选数字3,当前sum=5
    🚫 【层级3】 排序剪枝 → 5+3=8 > 7,break循环
    🔴 【层级3】 返回阶段 → for循环结束,回到上一层
  🔴 【层级2】 返回阶段 → 从下一层返回,继续执行
  🔵 【层级2】 撤销阶段 → 撤销数字3 → path=[2], sum=2
  🔍 【层级2】 遍历i=2 → 尝试选数字7,当前sum=2
  🚫 【层级2】 排序剪枝 → 2+7=9 > 7,break循环
  🔴 【层级2】 返回阶段 → for循环结束,回到上一层
🔴 【层级1】 返回阶段 → 从下一层返回,继续执行
🔵 【层级1】 撤销阶段 → 撤销数字2 → path=[], sum=0
🔍 【层级1】 遍历i=1 → 尝试选数字3,当前sum=0
✅ 【层级1】 选数字3 → path=[3], sum=3
🟡 【层级1】 暂停阶段 → 调用下一层backtrack,等待返回
  🟢 【层级2】 调用阶段 → path=[3], sum=3, start=1
  🔍 【层级2】 遍历i=1 → 尝试选数字3,当前sum=3
  ✅ 【层级2】 选数字3 → path=[3,3], sum=6
  🟡 【层级2】 暂停阶段 → 调用下一层backtrack,等待返回
    🟢 【层级3】 调用阶段 → path=[3,3], sum=6, start=1
    🔍 【层级3】 遍历i=1 → 尝试选数字3,当前sum=6
    🚫 【层级3】 排序剪枝 → 6+3=9 > 7,break循环
    🔴 【层级3】 返回阶段 → for循环结束,回到上一层
  🔴 【层级2】 返回阶段 → 从下一层返回,继续执行
  🔵 【层级2】 撤销阶段 → 撤销数字3 → path=[3], sum=3
  🔍 【层级2】 遍历i=2 → 尝试选数字7,当前sum=3
  🚫 【层级2】 排序剪枝 → 3+7=10 > 7,break循环
  🔴 【层级2】 返回阶段 → for循环结束,回到上一层
🔴 【层级1】 返回阶段 → 从下一层返回,继续执行
🔵 【层级1】 撤销阶段 → 撤销数字3 → path=[], sum=0
🔍 【层级1】 遍历i=2 → 尝试选数字7,当前sum=0
✅ 【层级1】 选数字7 → path=[7], sum=7
🟡 【层级1】 暂停阶段 → 调用下一层backtrack,等待返回
  🟢 【层级2】 调用阶段 → path=[7], sum=7, start=2
  ✅ 【层级2】 找到有效组合 → [7],result=[[2,2,3],[7]]
  🔴 【层级2】 返回阶段 → 找到解,回到上一层
🔴 【层级1】 返回阶段 → 从下一层返回,继续执行
🔵 【层级1】 撤销阶段 → 撤销数字7 → path=[], sum=0
🔴 【层级1】 返回阶段 → for循环结束,回到上一层
========== 执行结束 ==========
最终结果: [ [ 2, 2, 3 ], [ 7 ] ]

执行流程解析

candidates = [2,3,7], target = 7 为例:

  1. 选2 → path=[2], sum=2 → 钻到底(递归)
  2. 选2 → path=[2,2], sum=4 → 钻到底(递归)
  3. 选2 → path=[2,2,2], sum=6 → 钻到底(递归)
  4. 选2 → sum=8 > 7,剪枝,退回来
  5. 删掉2 → path=[2,2], sum=6 → 选下一个数3
  6. 选3 → path=[2,2,3], sum=7 ✅ 找到解,退回来
  7. 删掉3 → path=[2,2], sum=6 → 选下一个数7
  8. 剪枝,退回来 → path=[2], sum=2 → 继续尝试...
  9. 最终收集到 [[2,2,3],[7]]

三、回溯算法常见题型及解题方法

1. 组合问题

核心特征
  • 不考虑元素顺序,每个组合唯一(如[2,3][3,2]算同一个)
  • start参数控制"不回头选",避免生成重复组合
  • 数字可重复选(组合总和)/不可重复选(组合),仅需调整start参数(重复选传i,不重复选传i+1
LeetCode 题目详解
39. 组合总和

题目描述:

给定一个无重复元素的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。

candidates 中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7。注意 2 可以使用多次。
7 也是一个候选, 7 = 7。
所以这两种组合是唯一的答案。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

解决方案:

/**
 * 39. 组合总和
 * @param {number[]} candidates - 无重复元素的候选数组
 * @param {number} target - 目标和
 * @returns {number[][]} 所有符合条件的组合
 */
function combinationSum(candidates, target) {
  const res = [];
  candidates.sort((a, b) => a - b); // 排序便于剪枝

  function backtrack(path, sum, start) {
    // 终止条件:找到有效解
    if (sum === target) {
      res.push([...path]);
      return;
    }
    // 剪枝:和超过目标值
    if (sum > target) return;

    for (let i = start; i < candidates.length; i++) {
      const num = candidates[i];
      // 排序剪枝:后续数更大,无需继续
      if (sum + num > target) break;

      // 选数
      path.push(num);
      // 钻到底:数字可重复选,所以start仍为i
      backtrack(path, sum + num, i);
      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 0, 0);
  return res;
}

易错点:

  • 忘记排序导致剪枝失效
  • start参数传错(重复选传i,不重复选传i+1
  • 忘记拷贝路径([...path]

40. 组合总和 II

题目描述:

给定一个候选人编号的集合 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次

**注意:**解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

解决方案:

/**
 * 40. 组合总和 II
 * @param {number[]} candidates - 可能包含重复元素的候选数组
 * @param {number} target - 目标和
 * @returns {number[][]} 所有符合条件的组合(不重复)
 */
function combinationSum2(candidates, target) {
  const res = [];
  candidates.sort((a, b) => a - b); // 排序便于剪枝和去重

  function backtrack(path, sum, start) {
    // 终止条件:找到有效解
    if (sum === target) {
      res.push([...path]);
      return;
    }
    // 剪枝:和超过目标值
    if (sum > target) return;

    for (let i = start; i < candidates.length; i++) {
      const num = candidates[i];

      // 去重剪枝:跳过重复元素(关键!)
      // i > start 确保同一层不选重复数字,但不同层可以选
      if (i > start && candidates[i] === candidates[i - 1]) {
        continue;
      }

      // 排序剪枝
      if (sum + num > target) break;

      // 选数
      path.push(num);
      // 钻到底:数字不可重复选,所以start传i+1
      backtrack(path, sum + num, i + 1);
      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 0, 0);
  return res;
}

易错点:

  • 去重逻辑错误:应该是i > start而不是i > 0(允许不同层选相同数字)
  • 忘记排序导致去重失效

77. 组合

题目描述:

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按任何顺序返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

解决方案:

/**
 * 77. 组合
 * @param {number} n - 范围 [1, n]
 * @param {number} k - 组合长度
 * @returns {number[][]} 所有可能的k个数的组合
 */
function combine(n, k) {
  const res = [];

  function backtrack(path, start) {
    // 终止条件:路径长度等于k
    if (path.length === k) {
      res.push([...path]);
      return;
    }

    // 剪枝:剩余可选数字不够
    // 还需要选 k - path.length 个数,从start到n还有 n - start + 1 个数
    // 如果 n - start + 1 < k - path.length,则无法凑够k个数
    if (n - start + 1 < k - path.length) return;

    for (let i = start; i <= n; i++) {
      // 选数
      path.push(i);
      // 钻到底:不重复选,所以start传i+1
      backtrack(path, i + 1);
      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 1);
  return res;
}

易错点:

  • 范围错误:应该是[1, n],循环条件是i <= n而不是i < n
  • 忘记剪枝优化导致超时

216. 组合总和 III

题目描述:

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字最多使用一次

返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

解决方案:

/**
 * 216. 组合总和 III
 * @param {number} k - 组合长度
 * @param {number} n - 目标和
 * @returns {number[][]} 所有符合条件的组合
 */
function combinationSum3(k, n) {
  const res = [];

  function backtrack(path, sum, start) {
    // 终止条件1:找到有效解(长度和和都满足)
    if (path.length === k && sum === n) {
      res.push([...path]);
      return;
    }
    // 终止条件2:长度已满但和不满足,或和已超
    if (path.length === k || sum >= n) return;

    // 剪枝:剩余可选数字不够
    if (9 - start + 1 < k - path.length) return;

    for (let i = start; i <= 9; i++) {
      // 剪枝:当前数加上已选和超过目标值
      if (sum + i > n) break;

      // 选数
      path.push(i);
      // 钻到底:数字不可重复选,所以start传i+1
      backtrack(path, sum + i, i + 1);
      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 0, 1);
  return res;
}

2. 排列问题

核心特征
  • 考虑元素顺序,每个排列唯一(如[1,2][2,1]算不同排列)
  • used数组控制"不重复选",循环从0开始(允许选任意未选过的数字)
  • 含重复数字的排列(全排列II)需增加"去重剪枝"
LeetCode 题目详解
46. 全排列

题目描述:

给定一个不含重复数字的数组 nums,返回其所有可能的全排列。你可以按任意顺序返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

解决方案:

/**
 * 46. 全排列
 * @param {number[]} nums - 不含重复数字的数组
 * @returns {number[][]} 所有全排列
 */
function permute(nums) {
  const res = [];
  const len = nums.length;
  const used = new Array(len).fill(false); // 标记哪些数字已使用

  function backtrack(path) {
    // 终止条件:路径长度等于数组长度
    if (path.length === len) {
      res.push([...path]);
      return;
    }

    // 遍历所有可选数字(排列从0开始,允许选任意未选过的数字)
    for (let i = 0; i < len; i++) {
      // 剪枝:跳过已选数字
      if (used[i]) continue;

      // 选数
      used[i] = true;
      path.push(nums[i]);

      // 钻到底
      backtrack(path);

      // 退回来→删掉这个数
      path.pop();
      used[i] = false;
    }
  }

  backtrack([]);
  return res;
}

易错点:

  • 忘记使用used数组导致重复选同一个数字
  • 循环应该从0开始,不是从start开始(排列需要考虑所有位置)

47. 全排列 II

题目描述:

给定一个可包含重复数字的序列 nums按任意顺序返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

解决方案:

/**
 * 47. 全排列 II
 * @param {number[]} nums - 可能包含重复数字的数组
 * @returns {number[][]} 所有不重复的全排列
 */
function permuteUnique(nums) {
  const res = [];
  const len = nums.length;
  const used = new Array(len).fill(false);
  nums.sort((a, b) => a - b); // 排序便于去重

  function backtrack(path) {
    // 终止条件:路径长度等于数组长度
    if (path.length === len) {
      res.push([...path]);
      return;
    }

    for (let i = 0; i < len; i++) {
      // 剪枝1:跳过已选数字
      if (used[i]) continue;

      // 剪枝2:去重(关键!)
      // 如果当前数字和前一个数字相同,且前一个数字未被使用,则跳过
      // 这确保相同数字按顺序使用,避免重复排列
      if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) {
        continue;
      }

      // 选数
      used[i] = true;
      path.push(nums[i]);

      // 钻到底
      backtrack(path);

      // 退回来→删掉这个数
      path.pop();
      used[i] = false;
    }
  }

  backtrack([]);
  return res;
}

易错点:

  • 去重逻辑错误:应该是!used[i - 1]而不是used[i - 1]
    • !used[i - 1]:前一个相同数字未被使用,说明我们在同一层尝试重复数字,应该跳过
    • used[i - 1]:前一个相同数字已被使用,说明我们在不同层,可以使用

3. 子集问题

核心特征
  • 找出所有可能的子集(包括空集)
  • start参数控制不回头选,无需严格终止条件(每次递归都记录路径)
  • 含重复数字的子集(子集II)需增加"去重剪枝"
LeetCode 题目详解
78. 子集

题目描述:

给你一个整数数组 nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。

解集不能包含重复的子集。你可以按任意顺序返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

解决方案:

/**
 * 78. 子集
 * @param {number[]} nums - 不含重复元素的数组
 * @returns {number[][]} 所有子集
 */
function subsets(nums) {
  const res = [];

  function backtrack(path, start) {
    // 每次递归都记录路径(无需严格终止条件)
    res.push([...path]);

    for (let i = start; i < nums.length; i++) {
      // 选数
      path.push(nums[i]);

      // 钻到底:不重复选,所以start传i+1
      backtrack(path, i + 1);

      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 0);
  return res;
}

易错点:

  • 忘记在每次递归开始时记录路径(子集问题需要在每个节点都记录,不只是叶子节点)

90. 子集 II

题目描述:

给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

解决方案:

/**
 * 90. 子集 II
 * @param {number[]} nums - 可能包含重复元素的数组
 * @returns {number[][]} 所有不重复的子集
 */
function subsetsWithDup(nums) {
  const res = [];
  nums.sort((a, b) => a - b); // 排序便于去重

  function backtrack(path, start) {
    // 每次递归都记录路径
    res.push([...path]);

    for (let i = start; i < nums.length; i++) {
      // 去重剪枝:跳过重复元素(关键!)
      // i > start 确保同一层不选重复数字,但不同层可以选
      if (i > start && nums[i] === nums[i - 1]) {
        continue;
      }

      // 选数
      path.push(nums[i]);

      // 钻到底:不重复选,所以start传i+1
      backtrack(path, i + 1);

      // 退回来→删掉这个数
      path.pop();
    }
  }

  backtrack([], 0);
  return res;
}

易错点:

  • 去重逻辑和组合总和II相同,但容易忘记排序

四、回溯算法核心优化:剪枝

剪枝是提升回溯效率的关键,常见剪枝策略:

  1. 提前终止:如组合总和中"路径和超过目标值直接返回"
  2. 排序剪枝:先排序候选数组,遇到"当前值+已选和>目标值"时,后续值更大,直接break
  3. 重复剪枝:如子集II/全排列II中,跳过重复元素(if (i > start && nums[i] === nums[i-1]) continue
  4. 约束剪枝:如N皇后中,提前判断位置是否合法,不合法则跳过
  5. 数量剪枝:如组合问题中,剩余可选数字不够时直接返回

五、总结

  1. 核心思想:回溯 = 深度优先遍历 + 状态回退 + 剪枝,核心是"选→探→撤"
  2. 固定节奏push(选数)→ 递归(钻深层)→ pop(擦脚印),抓住这个节奏就能解决所有回溯题
  3. 模板复用
    • 组合/子集:用start参数避免重复,循环从start开始
    • 排列:用used数组避免重复选,循环从0开始
  4. 优化关键:剪枝是减少无效递归的核心,能让回溯效率提升一个量级

回溯算法看似复杂,但只要抓住"选→钻→退→删"这个固定节奏和"剪枝优化"这个核心,就能轻松应对各类回溯问题。建议从简单的组合问题入手,逐步过渡到排列、子集问题,多写多练就能融会贯通。

依赖自动导入:unplugin-auto-import 完整配置指南

unplugin-auto-import 完整配置指南

目录


简介

unplugin-auto-import 是一个 Vite/Webpack/Rollup 插件,用于自动导入 API,无需手动编写 import 语句。支持 Vue、Vue Router、Pinia 等库,以及自定义工具函数。

主要优势:

  • ✅ 减少重复代码,无需手动导入
  • ✅ 提升开发效率
  • ✅ 支持 TypeScript 类型提示
  • ✅ 支持 ESLint 配置

安装

npm install -D unplugin-auto-import
# 或
yarn add -D unplugin-auto-import
# 或
pnpm add -D unplugin-auto-import

核心配置选项

1. imports - 自动导入库的 API

作用: 指定要自动导入的库或包的 API。

案例 1.1:字符串形式(最简单)
AutoImport({
  imports: [
    'vue',           // 自动导入 ref, computed, onMounted 等
    'vue-router',    // 自动导入 useRoute, useRouter 等
    'pinia',         // 自动导入 defineStore, storeToRefs 等
  ],
})

使用效果:

<script setup>
// 不需要 import,直接使用
const count = ref(0)
const route = useRoute()
const store = defineStore('user', { ... })
</script>
案例 1.2:对象形式(精确控制)
AutoImport({
  imports: [
    'vue',
    // 只导入指定的 API
    {
      'vue-router': ['useRoute', 'useRouter'], // 只导入这两个
    },
    {
      '@vueuse/core': [
        'useMouse',
        'useFetch',
        'useLocalStorage',
      ],
    },
    {
      'axios': [
        ['default', 'axios'], // 导入默认导出并重命名为 axios
      ],
    },
    {
      'lodash-es': [
        'debounce',
        'throttle',
        ['default', '_'], // 默认导出重命名为 _
      ],
    },
  ],
})

使用效果:

<script setup>
// 自动导入指定的 API
const { x, y } = useMouse()
const { data } = useFetch('/api/data')
const debouncedFn = debounce(() => {}, 300)
const throttledFn = throttle(() => {}, 300)
</script>
案例 1.3:带别名的导入
AutoImport({
  imports: [
    {
      'vue': [
        'ref',
        ['computed', 'computedRef'], // 导入 computed 并重命名为 computedRef
        ['onMounted', 'onComponentMount'], // 导入 onMounted 并重命名
      ],
    },
  ],
})

使用效果:

<script setup>
const count = ref(0)
const double = computedRef(() => count.value * 2) // 使用别名
onComponentMount(() => { // 使用别名
  console.log('mounted')
})
</script>

2. dirs - 自动导入自定义目录

作用: 自动导入项目内部目录下的模块(工具函数、composables 等)。

案例 2.1:基本用法
AutoImport({
  dirs: [
    './src/utils',        // 导入 utils 目录下的所有导出
    './src/composables',  // 导入 composables 目录下的所有导出
  ],
})

项目结构:

src/
  utils/
    format.js      // export const formatDate = ...
    validate.js    // export const validateEmail = ...
  composables/
    useAuth.js     // export const useAuth = ...
    useTable.js    // export const useTable = ...

使用效果:

<script setup>
// 自动从 utils 和 composables 导入
const formatted = formatDate(new Date())
const isValid = validateEmail('test@example.com')
const { user, login } = useAuth()
const { data, loading } = useTable()
</script>
案例 2.2:使用 Glob 模式(支持子目录)
AutoImport({
  dirs: [
    './src/utils/**',           // 包含所有子目录
    './src/composables/**',     // 包含所有子目录
    './src/hooks/**/*.ts',      // 只匹配 .ts 文件
  ],
})
案例 2.3:带配置的对象形式
AutoImport({
  dirs: [
    './src/utils',
    {
      // 指定目录
      dir: './src/composables',
      // 是否生成类型声明(默认 true)
      types: true,
      // 文件匹配模式
      pattern: ['**/*.{ts,js}'],
      // 排除某些文件
      ignore: ['**/*.test.ts', '**/*.spec.ts'],
    },
  ],
})
案例 2.4:只导入特定文件
AutoImport({
  dirs: [
    './src/utils/format.js',    // 只导入这个文件
    './src/utils/validate.js', // 只导入这个文件
  ],
})

⚠️ 注意事项:

  • dirs 只能自动导入命名导出export constexport function
  • 不能导入默认导出export default),除非使用 defaultExportByFilename

3. resolvers - 动态解析器

作用: 动态判断某些标识符应该从哪个模块导入(常用于 UI 库)。

案例 3.1:使用 Element Plus Resolver
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

AutoImport({
  resolvers: [
    ElementPlusResolver({
      // 自动导入样式
      importStyle: 'sass', // 或 'css', false
    }),
  ],
})

使用效果:

<script setup>
// 自动导入 ElMessage, ElMessageBox 等
ElMessage.success('成功')
ElMessageBox.confirm('确定?')
</script>
案例 3.2:使用 Vant Resolver
import { VantResolver } from 'unplugin-vue-components/resolvers'

AutoImport({
  resolvers: [
    VantResolver(),
  ],
})

使用效果:

<script setup>
// 自动导入 Vant 的函数式 API
showToast('提示')
showDialog({ message: '内容' })
showNotify({ message: '通知' })
</script>
案例 3.3:自定义 Resolver
AutoImport({
  resolvers: [
    // 自定义 resolver 函数
    (name) => {
      // 如果函数名以 use 开头,从 @/composables 导入
      if (name.startsWith('use')) {
        return {
          name: name,
          from: `@/composables/${name}`,
        }
      }
      // 如果函数名以 $ 开头,从 @/utils 导入
      if (name.startsWith('$')) {
        return {
          name: name.slice(1), // 去掉 $ 前缀
          from: `@/utils/${name.slice(1)}`,
        }
      }
    },
  ],
})

使用效果:

<script setup>
// useAuth 会自动从 @/composables/useAuth 导入
const { user } = useAuth()

// $format 会自动从 @/utils/format 导入(实际导入的是 format)
const date = $format(new Date())
</script>
案例 3.4:多个 Resolver 组合
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

AutoImport({
  resolvers: [
    ElementPlusResolver(),
    AntDesignVueResolver(),
    // 自定义 resolver
    (name) => {
      if (name === 'myCustomFunction') {
        return { name, from: '@/utils/custom' }
      }
    },
  ],
})

4. include - 包含文件

作用: 指定哪些文件会被插件处理。

案例 4.1:基本用法
AutoImport({
  include: [
    /\.[tj]sx?$/,     // .ts, .tsx, .js, .jsx
    /\.vue$/,         // .vue
    /\.vue\?vue/,     // .vue?vue (SFC)
  ],
})
案例 4.2:只处理特定文件
AutoImport({
  include: [
    /\.vue$/,                    // 只处理 .vue 文件
    /src\/views\/.*\.ts$/,       // 只处理 views 目录下的 .ts 文件
  ],
})
案例 4.3:排除测试文件
AutoImport({
  include: [
    /\.[tj]sx?$/,
    /\.vue$/,
  ],
  exclude: [
    /\.test\.[tj]sx?$/,  // 排除测试文件
    /\.spec\.[tj]sx?$/,  // 排除测试文件
    /node_modules/,      // 排除 node_modules
  ],
})

5. exclude - 排除文件

作用: 排除不需要处理的文件。

案例 5.1:排除特定文件
AutoImport({
  exclude: [
    /node_modules/,           // 排除 node_modules
    /\.test\.[tj]sx?$/,      // 排除测试文件
    /\.spec\.[tj]sx?$/,      // 排除测试文件
    /dist/,                  // 排除构建目录
    /\.d\.ts$/,              // 排除类型声明文件
  ],
})
案例 5.2:排除特定目录
AutoImport({
  exclude: [
    /node_modules/,
    /src\/components\/legacy/, // 排除旧组件目录
    /src\/utils\/deprecated/,  // 排除废弃工具目录
  ],
})

6. dts - 类型声明文件

作用: 生成 TypeScript 类型声明文件,提供类型提示。

案例 6.1:启用类型声明(默认位置)
AutoImport({
  dts: true, // 默认生成到根目录的 auto-imports.d.ts
})
案例 6.2:指定类型声明文件路径
AutoImport({
  dts: './src/auto-imports.d.ts', // 指定生成位置
})
案例 6.3:禁用类型声明
AutoImport({
  dts: false, // 不生成类型声明文件
})

生成的文件示例:

/* eslint-disable */
/* prettier-ignore */
// Generated by unplugin-auto-import
export {}
declare global {
  const ElMessage: typeof import('element-plus')['ElMessage']
  const computed: typeof import('vue')['computed']
  const onMounted: typeof import('vue')['onMounted']
  const ref: typeof import('vue')['ref']
  const useRoute: typeof import('vue-router')['useRoute']
  // ...
}

tsconfig.json 中引入:

{
  "include": [
    "src/auto-imports.d.ts"
  ]
}

7. eslintrc - ESLint 配置

作用: 生成 ESLint 全局变量配置,避免 no-undef 错误。

案例 7.1:基本配置
AutoImport({
  eslintrc: {
    enabled: true,                              // 启用
    filepath: './.eslintrc-auto-import.json',  // 生成文件路径
    globalsPropValue: true,                     // 设置为全局变量
  },
})

生成的 .eslintrc-auto-import.json

{
  "globals": {
    "ElMessage": "readonly",
    "computed": "readonly",
    "onMounted": "readonly",
    "ref": "readonly",
    "useRoute": "readonly"
  }
}

.eslintrc.js 中引入:

module.exports = {
  extends: [
    './.eslintrc-auto-import.json', // 引入自动生成的配置
  ],
}
案例 7.2:自定义配置
AutoImport({
  eslintrc: {
    enabled: true,
    filepath: './.eslintrc-auto-import.json',
    globalsPropValue: 'readonly', // 或 'writable'
  },
})

8. defaultExportByFilename - 按文件名导入

作用: 如果目录下的文件是默认导出,按文件名自动导入。

案例 8.1:启用按文件名导入
AutoImport({
  dirs: ['./src/composables'],
  defaultExportByFilename: true, // 启用
})

文件结构:

src/composables/
  useAuth.js      // export default function useAuth() {}
  useTable.js     // export default function useTable() {}

使用效果:

<script setup>
// 自动从文件名推断导入
const { user } = useAuth()    // 从 useAuth.js 导入
const { data } = useTable()   // 从 useTable.js 导入
</script>

9. vueTemplate - Vue 模板支持

作用: 在 Vue 模板中也能使用自动导入的 API(实验性功能)。

案例 9.1:启用模板支持
AutoImport({
  vueTemplate: true, // 在模板中也能使用自动导入的函数
})

使用效果:

<template>
  <!-- 在模板中直接使用 -->
  <div>{{ formatDate(date) }}</div>
  <button @click="showMessage('Hello')">点击</button>
</template>

10. injectAtEnd - 注入位置

作用: 控制自动导入语句的注入位置。

案例 10.1:在文件末尾注入
AutoImport({
  injectAtEnd: true, // 在文件末尾注入 import(默认 false,在文件开头)
})

完整配置示例

Vite 配置示例

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      // 1. 自动导入库的 API
      imports: [
        'vue',
        'vue-router',
        'pinia',
        {
          '@vueuse/core': ['useMouse', 'useFetch'],
          'lodash-es': ['debounce', 'throttle'],
        },
      ],
      
      // 2. 自动导入自定义目录
      dirs: [
        './src/utils/**',
        './src/composables/**',
      ],
      
      // 3. 动态解析器
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
        // 自定义 resolver
        (name) => {
          if (name.startsWith('use')) {
            return { name, from: `@/composables/${name}` }
          }
        },
      ],
      
      // 4. 包含的文件
      include: [
        /\.[tj]sx?$/,
        /\.vue$/,
        /\.vue\?vue/,
      ],
      
      // 5. 排除的文件
      exclude: [
        /node_modules/,
        /\.test\.[tj]sx?$/,
        /\.spec\.[tj]sx?$/,
      ],
      
      // 6. 类型声明文件
      dts: './src/auto-imports.d.ts',
      
      // 7. ESLint 配置
      eslintrc: {
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true,
      },
      
      // 8. 按文件名默认导出
      defaultExportByFilename: false,
      
      // 9. Vue 模板支持(实验性)
      vueTemplate: false,
      
      // 10. 注入位置
      injectAtEnd: false,
    }),
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Webpack 配置示例

const AutoImport = require('unplugin-auto-import/webpack')

module.exports = {
  plugins: [
    AutoImport({
      imports: ['vue', 'vue-router'],
      dts: true,
    }),
  ],
}

配置选项总结表

选项 类型 默认值 作用 常用值
imports Array<String | Object> [] 自动导入库的 API ['vue', 'vue-router']
dirs Array<String | Object> [] 自动导入自定义目录 ['./src/utils/**']
resolvers Array<Function> [] 动态解析标识符来源 [ElementPlusResolver()]
include Array<Regex | String> [/\.vue$/, /\.[tj]sx?$/] 包含的文件模式 [/\.vue$/, /\.[tj]sx?$/]
exclude Array<Regex | String> [/node_modules/] 排除的文件模式 [/node_modules/]
dts Boolean | String false 类型声明文件 true'./src/auto-imports.d.ts'
eslintrc Object { enabled: false } ESLint 配置 { enabled: true }
defaultExportByFilename Boolean false 按文件名导入 true
vueTemplate Boolean false 模板支持 false
injectAtEnd Boolean false 注入位置 false

常见问题

Q1: 为什么我的自定义函数没有被自动导入?

A: 检查以下几点:

  1. 确保函数是命名导出export constexport function),不是默认导出
  2. 检查 dirs 配置的路径是否正确
  3. 确保文件在 include 范围内,不在 exclude 范围内
  4. 如果使用默认导出,启用 defaultExportByFilename: true

Q2: TypeScript 类型提示不工作?

A:

  1. 确保 dts 配置已启用
  2. tsconfig.jsoninclude 中添加生成的类型声明文件
  3. 重启 IDE/编辑器

Q3: ESLint 报 no-undef 错误?

A:

  1. 启用 eslintrc.enabled: true
  2. .eslintrc.js 中引入生成的配置文件
  3. 重启 ESLint 服务

Q4: 如何只导入部分 API?

A: 使用对象形式的 imports 配置:

imports: [
  {
    'vue': ['ref', 'computed'], // 只导入这两个
  },
]

Q5: 如何排除某些文件不被处理?

A: 使用 exclude 配置:

exclude: [
  /\.test\.[tj]sx?$/,  // 排除测试文件
  /node_modules/,      // 排除 node_modules
]

Q6: 自定义工具函数如何自动导入?

A:

  1. 将函数改为命名导出
  2. 配置 dirs 指向函数所在目录
  3. 使用 Glob 模式支持子目录:'./src/utils/**'

最佳实践

  1. 命名导出优先:自定义函数使用命名导出,便于自动导入
  2. 类型声明文件:始终启用 dts,提供类型提示
  3. ESLint 配置:启用 eslintrc,避免 no-undef 错误
  4. 精确导入:使用对象形式的 imports,只导入需要的 API
  5. 目录结构:合理组织 utilscomposables 等目录
  6. 性能优化:使用 includeexclude 限制处理范围

相关链接


更新日志

  • v0.16.0+: 支持 defaultExportByFilename 选项
  • v0.15.0+: 改进 dirs 的 Glob 模式支持
  • v0.10.0+: 添加 vueTemplate 实验性功能

文档版本: 1.0.0
最后更新: 2024年

关于ruyi生产正常,开发异常的问题记录

问题描述

因公司需要,使用的环境是ruyi的前端部分,采用vue2+element-ui

1.当以开发环境进入页面时,第一次正常,在刷新后会白屏,或者卡在加载页面,控制台抛出错误 2.关于样式异常问题

Uncaught SyntaxError: Unexpected token '<'

但进入生产环境访问,一切正常,刷新后也依然正常。

原因

问题就出在Uncaught SyntaxError上,这是js的语法错误。例如

const 

上面代码会抛出


const 
      

SyntaxError: Unexpected end of input

当点击抛出部分进入具体错误地方,会进入html页面,错误就出在这。

graph TD
浏览器请求获取html页面 --> 浏览器解析html -->解析script与link标签

问题就出在解析解析script与link标签上

总所周知道script的标签使用

<script src="xxxx">

这时候浏览器发请求拿取到js问题,但如果拿取到的并非js文件,那自然会有SyntaxError错,本人就遇到这种情况。生产正常的原因在于ng配置对了,开发环境配置错了。因为开发环境也有类似于 try_files uriuri uri/ /xxx/index.html;的配置逻辑,既无论请求什么如果请求不到了就返回/xxx/index.html 但js文件和css文件是实际存在的,是请求的到的,理论上是不应该返回html

本人具体前端错误配置了啥忘了,因为vue-cli在配置文件中需要重启才生效,

export default new Router({
  mode: 'history', // 去掉url中的#
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes,
  base: '/yyy', // 当路由配置base 到时候进入http://xxx:xx/yyy才会展示页面。http://xxx:xx会空白
  
  
  
  对应的vite/vue-cli都得配置
  publicPath: process.env.NODE_ENV === "production" ? "/yyy/" : "/",
  既生产环境需要添加nbbz 开发环境不需要添加,如果生产不编写/yyy/会导致生产上有问题
})

关于样式问题

在ruyi样式问题当中/srccomponents/ThemePicker 这个文件当中的有个setTheme函数,当中的

   if (!this.chalk) {
        const url = `${process.env.VUE_APP_BASE_URL}/styles/theme-chalk/index.css`
        await this.getCSSString(url, 'chalk')
      }
生产
VUE_APP_BASE_URL = '/yyy'
开发
VUE_APP_BASE_URL = 'http://localhost:8090'

styles/theme-chalk/index.css这是手动请求,不是浏览器请求的css。文件文件在public/styles下面 当以上路由在生产当中配置了base 请求头前面也需要带上,否则会报404

而开发环境需要带http://localhost:服务端口 。因为开发的时候没有自动携带前面的ip

20 周年之际!jQuery 4.0 正式发布!轻装上阵

2006 年 1 月 14 日,John Resig 发布了名为 jQuery 的 JavaScript 库。

至今已经整整过去了 20 年!

你还记得第一次在项目中使用 jQuery 的场景吗?

在那个浏览器兼容性让人头疼,DOM 操作繁琐复杂的时代,jQuery 凭借着“Write less, do more”的理念,几乎是那个时代网站开发的标配。

后来,前端框架层出不穷,React、Vue、Angular 各领风骚,但 jQuery 依然在全球数百万网站上默默工作着。

如今, jQuery 4.0.0 正式版发布。

这也是 jQuery 近 10 年来的首个主要版本,标志着 jQuery 正式踏上了现代化转型之路。

让我们一起来看看 jQuery 4.0 都做了哪些更新。

1. 彻底告别 IE

jQuery 4.0 不再支持 IE 10 及更早版本。IE 11 预计在 5.0 版本移除。

同时也停止了一些老旧浏览器的支持,这使得 jQuery 代码更清爽,文件体积更小,性能提升显著。

2. 安全大升级

jQuery 4.0 引入了对 Trusted Types 的支持,jQuery 内部会自动通过 TrustedHTML 封装字符串,避免被 CSP 拦截,大大降低了网站被黑客攻击的风险。

3. 架构现代化

jQuery 源码从 AMD 迁移到了 ES Modules,这意味着更好的模块化开发体验,并为未来拆分功能打下基础。

4. API 精简

jQuery 4.0 移除了 15 个废弃的 API,这些函数要么是内部使用,要么已经有了原生的替代方案。

5. jQuery 的全新定位

有人可能会问:现在前端框架这么发达,jQuery 还有存在的必要吗?

答案是肯定的!

jQuery 不是要重新成为前端主角,而是在它适应的场景中继续发光发热。

6. 最后

jQuery 4.0 不是一次“重生”,而是一次面向现代 Web 的断舍离。它抛弃了历史包袱,拥抱了安全标准,清理了冗余代码,做了工程化升级。

20 年前,jQuery 改变了 Web 开发的方式;20 年后,它选择了与时俱进。

对于用过 jQuery 的老程序员来说,虽然我们已经习惯了 Vue、React 的思维模式,但看到 jQuery 的这次蜕变,依然会心潮澎湃。

因为这是我们青春岁月里最美好的代码记忆。

我整理了一份 Vue 性能优化指南(给AI用的)

为什么做这个

说实话,这个项目是我自己用的。

工作这几年,遇到的性能问题基本都是类似的坑:接口瀑布流、bundle 越来越大、响应式乱用。每次踩完坑修好了,过段时间换个项目又踩一遍。

后来想着,干脆整理一份文档,自己查方便,也能给 AI 编码助手看(我现在用 Claude Code),这样审代码的时候能提前发现问题。

整理完发现,好像也可以分享出来,说不定有人也遇到过这些问题。

我踩过的坑

这几年写 Vue 项目(Vue 2/3 + Nuxt 都有),踩过不少坑:

接口请求变成瀑布流 一个 await 接一个 await,明明能并行的请求硬是串行了。用户抱怨页面慢,一查发现 3 个接口排队等了 750ms。

bundle 体积失控 每次加需求就往里塞代码,没人关心打包结果。等到首屏白屏 3 秒了,才发现 JavaScript 已经 300KB+。

响应式系统滥用 大对象直接 ref(),上千条商品数据,每个字段都变成响应式。渲染一卡一卡的,还以为是组件写得不好。

这些问题不是什么高深的优化,就是基本功。但忙起来就容易忽略,等出问题再改成本就高了。

怎么说呢,优化要分轻重

我发现很多人(包括以前的我)做性能优化会搞错重点。

举个例子:页面有 600ms 的请求等待时间,结果花一周优化 computed 缓存。首屏加载了 300KB 的 JavaScript,结果去优化循环少跑几次。

其实应该先解决大问题:

  1. 先干掉请求瀑布流 - 能并行就并行,该预加载就预加载
  2. 再砍 bundle 体积 - 代码分割、动态导入、tree-shaking
  3. 然后才是组件和响应式优化 - 减少不必要的渲染

我按这个思路把规则分成了 10 个类别,从 CRITICALLOW,总共 46 条。先把影响大的问题解决了,那些微优化可以慢慢来。

里面有什么

10 个类别,46 条规则:

  • 消除异步瀑布流(CRITICAL)
  • 包体积优化(CRITICAL)
  • 服务端性能(HIGH)
  • 客户端数据获取(HIGH)
  • 响应式系统优化(MEDIUM-HIGH)
  • 渲染性能(MEDIUM)
  • Vue 2 特定优化(MEDIUM)
  • Vue 3 特定优化(MEDIUM)
  • JavaScript 性能(LOW-MEDIUM)
  • 高级模式(LOW)

每条规则的格式:

  • 影响等级(CRITICAL / HIGH / MEDIUM / LOW)
  • 错误示例(我以前写过的错误代码)
  • 正确示例(后来改成什么样)
  • Vue 2/3 兼容性说明

举几个我踩过的坑

坑 1:不需要的 await 也在阻塞代码

以前写过这样的代码:

async function handleRequest(userId: string, skipProcessing: boolean) {
  // 即使 skipProcessing=true,也会等待 userData
  const userData = await fetchUserData(userId)

  if (skipProcessing) {
    // 立即返回,但前面已经浪费时间等待了
    return { skipped: true }
  }

  // 只有这个分支使用 userData
  return processUserData(userData)
}

问题是,即使 skipProcessing=true,还是会去请求 userData。白白浪费时间。

后来改成这样:

async function handleRequest(userId: string, skipProcessing: boolean) {
  if (skipProcessing) {
    return { skipped: true }
  }

  // 只在需要时才获取数据
  const userData = await fetchUserData(userId)
  return processUserData(userData)
}

其实很简单,但之前就是没注意到。

坑 2:大对象别直接用 ref

1000 条商品数据,每条 10+ 个字段,以前直接 ref()

<script setup lang="ts">
import { ref } from 'vue'

// 1000 个商品,每个商品 10+ 字段,全部变成响应式
const products = ref<Product[]>([])

async function loadProducts() {
  products.value = await fetchProducts()
  // Vue 会递归遍历所有对象,添加响应式代理
}
</script>

渲染的时候卡得要命。后来发现应该用 shallowRef

<script setup lang="ts">
import { shallowRef } from 'vue'

// 只有数组本身是响应式的,内部对象保持普通对象
const products = shallowRef<Product[]>([])

async function loadProducts() {
  // 替换整个数组触发更新,无需深度响应式
  products.value = await fetchProducts()
}
</script>

shallowRef 只让数组本身响应式,内部对象保持普通对象。更新时替换整个数组就能触发响应,省了大量性能开销。

几个真实案例(我遇到过的)

案例 1:别对同一个数组循环多次

之前接手一个项目,发现同一个商品列表循环了 5 次:

// 错误:5 次独立遍历
const discounted = products.filter(p => p.discount > 0)
const inStock = products.filter(p => p.stock > 0)
const featured = products.filter(p => p.featured)
const totalValue = products.reduce((sum, p) => sum + p.price, 0)
const avgPrice = totalValue / products.length

看着就难受。后来改成一次循环:

// 正确:一次遍历
const stats = products.reduce((acc, product) => {
  if (product.discount > 0) acc.discounted.push(product)
  if (product.stock > 0) acc.inStock.push(product)
  if (product.featured) acc.featured.push(product)
  acc.totalValue += product.price
  return acc
}, { discounted: [], inStock: [], featured: [], totalValue: 0 })

const avgPrice = stats.totalValue / products.length

商品少的时候看不出来,数据一多性能差距就很明显了。

案例 2:独立的请求不要排队

用户详情页,三个互不依赖的接口,结果在串行调用:

// 错误:串行:总耗时 = 300ms + 200ms + 250ms = 750ms
const user = await fetchUser(userId)          // 300ms
const posts = await fetchUserPosts(userId)    // 200ms
const comments = await fetchUserComments(userId) // 250ms

改成并行之后:

// 正确:并行:总耗时 = max(300ms, 200ms, 250ms) = 300ms
const [user, posts, comments] = await Promise.all([
  fetchUser(userId),
  fetchUserPosts(userId),
  fetchUserComments(userId)
])

总耗时从 750ms 降到 300ms,页面快了一半多。这种优化投入产出比最高。

案例 3:长列表用 CSS content-visibility

1000+ 条评论的页面,初始渲染很慢:

<!-- 错误:所有评论立即渲染 -->
<div v-for="comment in comments" :key="comment.id">
  <CommentCard :comment="comment" />
</div>

后来加上 content-visibility

<!-- 正确:浏览器跳过屏幕外的渲染 -->
<div
  v-for="comment in comments"
  :key="comment.id"
  class="comment-item"
>
  <CommentCard :comment="comment" />
</div>

<style>
.comment-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 200px;
}
</style>

浏览器会跳过屏幕外的渲染,初始加载快了 5-10 倍,滚动也流畅多了。这个 CSS 属性真的好用。

怎么用

直接看

# 克隆仓库
git clone https://github.com/ursazoo/vue-best-practices.git

# 安装依赖
npm install

# 构建 AGENTS.md
npm run build

克隆下来,直接看 rules/ 目录下的规则文件。每个文件都是独立的,包含问题说明、代码示例和解决方案。

也可以看构建好的 AGENTS.md,把所有规则整合在一起,方便搜索。

集成到 AI 编码助手

如果你也在用 Claude Code、Cursor 这类 AI 工具写代码,可以集成进去:

npx add-skill vue-best-practices

AI 审查代码的时候,如果发现性能问题(比如请求瀑布流、过度响应式),会参考这些规则给出优化建议。我现在就是这么用的,挺方便。

Vue 2 还是 Vue 3

都支持。每条规则都标注了版本兼容性:

  • Vue 2 & 3 通用:基础的性能优化技巧
  • Vue 3 Only:用了 <script setup>shallowRefSuspense 等新特性
  • Vue 2 Only:针对 Vue 2 的特定优化(比如 Object.freeze()

老项目也能用,新项目能用得更充分。

项目地址

GitHub: github.com/ursazoo/vue…

欢迎贡献

这是个开源项目。如果你在生产环境踩过坑、有更好的优化方案,欢迎提 Issue 或 PR。

特别是:

  • 实际项目中遇到的性能问题
  • 现有规则的改进建议
  • Vue/Nuxt 新版本的优化技巧

项目信息

  • 46 条规则,10 个类别
  • 按影响程度排序(先解决大问题)
  • 支持 Vue 2/3 和 Nuxt
  • 适配 AI 编码助手

希望对你有帮助。

我的2025:只靠爱发电的开源能走多远?

前言

你是否也在想:纯粹的热情能否支撑一个长期可维护的开源项目?内容创作、维护开源项目、上架应用,哪一项最耗时却最重要?把兴趣变成产品,这条路能走多远?带着这些疑问,我开始了我2025年的复盘旅程。

2025年,我的重点就是维护我的开源项目,因为它,我的个人业余时间几乎就没有了,面对朋友的质疑,单靠爱发电,没有盈利,是否能长期继续坚持下去?

2025 年,我把写作、开源、鸿蒙应用上线当作了全年的主线,我的目的是把技术沉淀变成能被大家直接使用的东西:更好的文档、更实用的组件、更易上手的示例,以及一款沉浸式演示应用:uViewPro(跨平台 UI 组件库)

回想这一年过得忙忙碌碌,从年初立下的 flag 到年底的收获,每一步都走得踏实(很累)。写作上我在三个平台积累了大量内容,开源项目 uView Pro 从零到几百 Star,开源鸿蒙应用也顺利上架。

回头看看,这一年不仅学习到了很多东西,也认识到了很多朋友,这波不亏!

希望 2026 年能继续这个节奏,把更多想法变成现实。

一. 开篇:这一年,很忙碌

今年总结下来,我主要做了三件事,可以总结为如下:

  1. 持续写技术文章:在公众号、掘金、CSDN这些平台上输出内容,分享开发经验和踩坑记录。
  2. 维护开源组件库:把 uView Pro 这个项目从零做到现在的规模,让更多开发者用起来顺手。
  3. 首款鸿蒙应用上线:把演示应用适配鸿蒙系统并成功上架,验证跨平台开发的效果。

这三件事说起来简单,但每件都耗费了我大量的时间和精力。写作要保证质量和频率,开源项目不仅要修复、迭代、上新功能,还要处理各种 issuePR,鸿蒙应用上架还要跟审核机制斗智斗勇(审核驳回近 10 次,我真是打不死的小强)。

但正是这些挑战,让我这一年过得非常非常充实。

1. 为什么是这三件事?

其实这跟我的工作经历有关,我从 2015 年开始接触前端和移动端,从最基础的 HTML、CSS 开始了解,后来慢慢接触到 Vue.js,Angular.js,React.js 三大框架。工作的这几年,陆陆续续做过电商网站、后台管理系统、原生应用、微信小程序等等,每个项目都让我长了不少见识,也踩了不少坑。

后来,uni-app 生态越来越火,我身边很多朋友都在用它开发项目。我们公司内部也有考量,后来经过最终选型,选定了 uView UI 为主要UI框架,从此 uni-app + uView UI 成为了我开发移动端应用的合作搭子。

2. 忙碌背后的收获

说到底,2025 年对我来说,就是一个"把想法变成现实"的过程。从抽象的概念,到具体的代码,到最终的产品,每一步都让我更踏实,也更期待2026年的到来。

这一年我不仅思想上变得通达了,思维方式也变了很多。以前总觉得一个人埋头苦干就行了,现在明白了分享和交流的重要性。

技术这条路,本来就该一起走。

二. 内容创作:公众号、掘金、CSDN

写作这事,说起来简单,但真要坚持下来其实挺不容易的,我从 2016 年开始写技术文章,期间是断断续续的在维护,各种原因没能一直坚持下去。

最早写文章的时候,纯粹是为了记录自己踩的坑。记得第一次写技术博客,手抖了半天,才憋出几百字,发出去后还忐忑不安,生怕被同行笑话。没想到居然有人点赞,有时还被评论说帮到了他。

慢慢地,我发现写作不仅能帮到别人,还能让自己思路更清晰,很多时候,写文章的过程就是梳理知识的过程。你以为自己懂了,但真要写出来,才发现还有不少模糊的地方。

直到 2023 年,才是我写作开始认真的一年,不仅文章数量上去了,质量也比以前好很多。我主要在三个平台发文:掘金、公众号和 CSDN,每个平台都有自己的特色和受众群体。

1. 掘金:我的"主战场"

掘金是我最活跃的平台,从 2023 年开始就在上面写文章,到今年已经积累了 168 篇原创内容,总阅读量超过 81 万+,粉丝也有 1618 个。

截至目前,掘金的数据如下:

  • 文章数:168 篇
  • 阅读量:81万+
  • 关注数:1618

1.png

今年,我也特别重视掘金,那么多平台,我唯独感觉掘金的技术氛围最好,读者质量也高。掘金的用户大都喜欢学习新技术,也愿意分享自己的经验。

2025 年,我在掘金大概发了 25 篇原创文章,内容主要是围绕 uni-app、可视化地图、Vue3、鸿蒙开发这些主题。

2.png

不过掘金也不是没有缺点,有时候一篇非常用心的文章发出去后,算法推荐不太给力,阅读量上不去就挺郁闷的。有时候可能随便写一篇,阅读量却很可观,有些不理解。但总体来说,掘金还是我最舒服的写作环境。

今年参加金石计划征文活动,连续三次获得优秀作者,今年(去年)的财富都是掘金给的,但是今年金石计划就举办了3期,所以比之去年相差很大:

15.png

2. 公众号:开始用心经营的阵地

公众号“前端梦工厂”,是我去年年底开始认真经营的。说实话,一开始我对公众号没抱太大期望,因为之前发的几篇文章,效果一般,阅读量惨不忍睹。有时候文章发出去,才几个阅读,几个点赞,心里还是挺失落的。

但是幸好没放弃,目前公众号写作算是也坚持下来了,虽然文章数量不多,但是质量都很高,最近官方给不断的加流量,越来越多的人关注了。

截至目前,公众号的数据如下:

  • 原创内容:63
  • 总用户数:1900+

3.png

4.png

3. CSDN:稳扎稳打的平台

CSDN 是我最早开始写博客的平台,从 2015 年就开始了。现在已经积累了 258 篇文章,阅读量 60万+,粉丝 9394 个。

虽然数据比其他平台耀眼,但感觉这里的用户粘性并不好,一篇文章的阅读量一般都在 1000+ 左右,但是却没有评论,为什么?目前 CSDN 的内容基本上大都同步于掘金和公众号,但也获得了优质创作者和博客专家称号。

截至目前,CSDN 的数据如下:

  • 文章数:258
  • 阅读量:60万+
  • 关注数:9394

5.png

后续的分享和写作也会以这3个平台为主,将会持续输出更多的文章。

4. 写作这行的酸甜苦辣

写了几年文章,我最大的感受是:坚持真的很重要。刚开始写作的时候,我特别羡慕那些大V,动不动就几万阅读。但后来发现,人家也是日积月累出来的,谁也不是刚开始写就成功。

当然,写作也有苦恼的时候。有时候卡文,写一晚上才憋出几百字;有时候写完发现没人看,心里挺失落的。但每当看到读者说"这篇文章帮到我了",所有的辛苦都值了。

2025 年,我的写作生涯达到了一个小高峰,不仅文章多了,质量也上去了。我相信,2026 年我会写得更好。

写作就像跑马拉松,不能急功近利。

三. 年度开源项目:uView Pro

官网:uviewpro.cn

说起 uView Pro,这绝对是我2025 年最投入的一个项目。从年初的构想到年底的成熟产品,这一路走得真是跌跌撞撞,但也收获满满。

开源让项目不断被验证,但也带来了沟通和维护的成本,自从 uView Pro 开源后,我的个人时间基本就没有了!虽然零零散散的有人赞助,但是目前与我的付出严重不成正比。

13.png

1. 从 uView 到 uView Pro 的华丽转身

因为在移动端开发领域,我一直在用 uni-app,而 UI 组件库一直用 uView UI 1.8.8,框架已经用顺手了,习惯了。

然而最大的问题是它是基于 Vue2 的,而我已经在新项目中全面拥抱 Vue3 了,uView UI 的官方仓库也停止了更新,并没有推出 Vue3 的版本。而也有个人推出的 uView 系的 Vue3 版本,但观其源码,仅是在 Vue2 的基础上做的兼容,并没有真正发挥 TypeScript 的效果,编码体验并不好。

于是我开始思考,能不能基于 uView 1.8.8 做一个 Vue3 版本的?想法一出,我就开始行动了。大概花了两个多月时间,把所有组件用 Vue3 + TypeScript 重写了一遍。这可不是简单的复制粘贴,我把每个组件的 API 都对齐了官方 API,目的是让原先的 Vue2 + uView 1.8.8 的项目可以无缝迁移。

终于在2025年8月4日,我把这个项目开源了,取名叫 uView Pro。说实话,开源的时候心里挺紧张的。但发布后收到的反馈超出了我的预期,很多开发者说这个版本用起来更舒服了。

开源已经5个多月,目前在平台上的数据如下:

Github:

6.png

star-history-2026119.png

Gitee:

7.png

Dcloud 插件市场:

8.png

2. 多端适配的艰难之路

uView Pro 最让我难做的就是多端适配。刚开始我只支持了H5、微信小程序,但后面陆续加了Android、iOS、鸿蒙、支付宝小程序、头条小程序等平台。现在已经实现了真正的"一套代码,多端运行"。

多端适配听起来简单,做起来真不容易。每个平台都有自己的特色,拿 provide/inject 来说,头条小程序并不支持这个 API,我就得想办法用其他方式实现类似的功能。

但这些困难都是值得的,现在 uView Pro 可以在 10 个平台上运行,开发者只需要写一套代码,就能覆盖绝大部分用户。

3. 主题系统和暗黑模式

uView Pro 的主题系统是我最得意的功能之一。传统的组件库,换个皮肤要改一堆 CSS 文件,但 uView Pro 只需要三分钟,就能生成一套全新的主题。

我研究了很多实现方案,最后在文档网站搞出了一个主题生成器。通过配置一些基础颜色,就能自动生成整套主题文件。

暗黑模式也是基于这个系统实现的。现在用户可以一键切换明暗主题,而且过渡效果超级丝滑。

多主题+浅色主题

首页-浅色主题.png

多主题+深色主题

首页-深色主题.png

4. 国际化支持

好多小伙伴反馈说,希望 uView Pro 能支持多语言。之前的 uView UI 官方并没有真正的支持国际化,只能通过一些 prop 传递来修改组件的文案,但这并不够灵活,而且还不能完全覆盖。

uView Pro 一直希望开发者用起来更方便、更顺手,所以早在几个月前,uView Pro 就已经开始了将所有组件 i18n 化的工作。

目前,很高兴地告诉大家:uView Pro全系组件现在都支持国际化了!有了这个功能,开发者可以更简单地让应用支持多种语言,让产品更容易走向全球市场。

image.png

5. 优势总结

目前 uView Pro 具有以下优势:

  • 🚀 彻底重构:基于 Vue3 语法和特性,源码级重构所有组件和工具,非兼容层方案。
  • ⚡ 高性能:充分利用 Vue3 响应式和组合式 API,组件性能和可维护性大幅提升。
  • 🖥️ 多端适配:支持 Android、iOS、微信小程序,持续兼容更多平台。
  • 🌍 国际化(i18n)支持:内置多语言切换,便于多语言项目快速集成与部署。
  • ✨ 易用性强:API 设计现代,文档详尽,开发体验优于传统兼容方案。
  • 🌐 生态完善:内置 80+ 高质量组件和丰富工具库,覆盖主流业务场景。
  • 🎨 多主题定制:通过主题生成工具三分钟实现多套主题定制。
  • 🌙 暗黑模式:支持一键暗黑模式。

四. 我的鸿蒙应用正式上架:在纯血鸿蒙系统上验证可行性

应用体验链接uviewpro.cn/zh/resource…

把组件库做成一款面向开发者的应用,这个想法其实很早就有了。我一直在想,开发者学习一个组件库的时候,最怕的就是"看了文档不知道长什么样,搭了工程又嫌麻烦"。如果能直接在手机上点一点,看看组件的效果,那该多好啊!

2025 年可以说是鸿蒙系统的元年,华为 Pura 80、Mate80 系列等新机都搭载了纯血 HarmonyOS 系统,之前的部分旗舰机型也可以升级到鸿蒙6.0,说明纯血鸿蒙系统已经趋于稳定,到了全面推广的时候

而华为官方还有开发者激励活动,这给了我一个绝佳的机会,去验证 uView Pro 在鸿蒙上的表现。

动画10.gif

1. 从想法到原型:应用的定位

应用的名字就叫"uViewPro(跨平台 UI 组件库)",目标很明确:做一款面向开发者的学习型应用。应用包含三大功能:

  1. 组件演示:把uView Pro的所有组件都做成可交互的demo
  2. 模板示例:提供一些常用的页面模板和布局示例
  3. 学习工具:加入一些代码片段复制、API 查询等实用功能
  4. 成就系统:加入游戏化的学习方式,让学习组件库变得更加有趣和高效。

我希望用户下载了应用后,不需要看文档就能大概了解 uView Pro 能做什么,怎么用。这样就能降低学习成本,让更多开发者愿意试试。

预览图.png

2. 上架历程:审核的那些事

鸿蒙应用上架可比其他平台上线严格多了,可以说比 iOS 审核都要严格的多,我反复修改提交了 10 次才最终审核通过。

第一次提交申请后被拒的原因是:

  1. 功能交互简单,影响用户的总体体验
  2. 横竖屏布局未适配问题,不符合鸿蒙应用UX设计规范。
  3. 未正常适配设备深色模式,不符合鸿蒙应用UX设计规范。

11.png

第2,3个问题都好解决,第一个为主观问题,不好解决!果真,后面所有被拒绝的原因都为第一个,不管应用内容如何丰富,都被拒绝!

12.png

我一度想要放弃!最终通过新增应用功能,申诉,提交工单,提交完整的应用说明和演示视频,最终通过。

应用引导页.png

附一张完整的审核拒绝的截图:

10.png

3. 验证与收获:鸿蒙生态的潜力

通过这个应用,我验证了很多东西:

  1. 组件的真实表现:在真机上测试,发现了一些在模拟器上看不出来的问题
  2. 用户的真实需求:通过用户反馈,我知道了开发者最关心哪些功能
  3. 鸿蒙开发的门槛:虽然比想象中复杂,但生态已经很成熟了

最让我感动的是,有个开发者私信我说:"谢谢你的应用,我通过它学会了 uView Pro,现在已经用在项目里了。"这种直接的反馈让我觉得所有的努力都值了。

2025 年鸿蒙应用的上架,不仅验证了 uView Pro 的技术实力,也让我对鸿蒙生态有了更深的认识。相信随着华为的投入,鸿蒙也会成为 uni-app 开发的重要平台之一。

4. 如何体验?

📱 去鸿蒙应用商店体验

应用名称:  uViewPro(跨平台 UI 组件库)

应用市场:  打开华为应用市场(AppGallery) 搜索 uViewPro 或 跨平台UI组件库

或访问链接uviewpro.cn/zh/resource…

注意:此应用仅在 HarmonyOS 5.0 及以上版本 设备的应用市场中提供。

9.png

五. 比赛经历:高德空间智能开发者大赛

2025 年底的比赛经历让我印象深刻,特别是高德空间智能开发者大赛,从报名到决赛,整个过程都让我学到了很多。

大赛是从去年年底开始的,主题是"空间智能应用创新"。由于时间原因,我做了一个基于位置+轨迹+日记创作的轻量应用,可以帮助用户记录每一段路线、留存沿途灵感,并以图文海报形式分享,并兼容鸿蒙系统。

作品提交后,没想到获得了优胜奖,得到了官方邀请,于2026年1月9日参加了决赛现场,在现场,见到了很多优秀的开发者,大家分享了自己的作品和技术思路。虽然比赛时间很短暂,但它让我在 2026 年的开头就有了满满的动力。

所以,技术这条路,不仅要埋头苦干,还要抬头看路。参加比赛就是一种很好的方式,既能锻炼技能,又能认识同行。

合影.png

六. 2026 年计划

2025 年过得特别匆忙,但 2026 年我想让自己更加自由高效一点。基于这一年的经验,我对明年有了更清晰的规划。主要是三个方向,但每个方向都有更具体的目标。

  1. 持续创作,打造个人品牌:在掘金、公众号、CSDN继续发布高质量文章。计划每个月至少发2篇原创文章,内容不仅包括技术教程,还会分享更多行业洞察和个人成长经历。希望能把"前端梦工厂"公众号做成一个有影响力的技术媒体,粉丝数量早日突破 5000

  2. 继续打磨开源框架uView Pro:这是重中之重。基于大多数用户的诉求,计划增加更多实用组件。多端适配要更完善,特别关注鸿蒙和 iOS 端的体验。国际化支持也要加强,争取支持更多语言。

  3. 打造 uView Pro X,支持uni-app x:计划基于 uView Pro 打造一个新项目,支持最新的技术栈。不仅要兼容现有的功能,充分发挥 uni-app x 的优势,这将会是一个大项目,需要投入不少时间和精力。

希望 2026 年能找到工作和生活的更好平衡,既能高效工作,又能享受生活。

七. 感谢

感谢每一位朋友在 2025 年的支持,感谢开源贡献者和用户,感谢每个提出建议的人,尤其感谢 uView Pro 的赞助者,让我有了不断维护的动力!

开源发布 🚀 | 解决 Vue Hero 动画的最后一块拼图:完美支持 v-show!

背景

在前段时间我实现了一个Vue指令,用于实现元素的跨页面动画效果: 【Hero动画】用一个指令实现Vue跨路由/组件动画

但有个遗憾一直没解决:不支持v-show指令。

最近终于有时间了,决定攻克这个技术难题,让 Hero 动画更加完整!

为什么v-show这么棘手🤔

v-if / 路由切换

v-if路由切换的情况下,使用指令的mountedbeforeUnmount钩子非常方便,只需要在挂载时注册Hero元素,在卸载前执行过渡动画即可。

// 这种很简单:挂载时注册,卸载时执行动画
const heroAnimationDirective: Directive<HTMLElement, HeroAnimationProps> = {
  mounted(el, { value }) {
    el.dataset.heroId = value.heroId;
  },
  beforeUnmount(el, { value }) { 
    heroAnimation(el, value);
  }
};

v-show 触发的变化

v-show通过display属性控制显示/隐藏,没有卸载过程,只能通过beforeUpdateupdated钩子来监听元素的变化。 核心难点:如何区分是v-show触发的显示变化,还是其他响应式数据的变化?

解决方案思路

所以我们只能手动判断是否是v-show触发的变化,只有在display属性变化时,才执行过渡动画。 大致实现步骤:

  1. mounted钩子中,将相同heroId的元素注册到一个集合中,标记为v-show组合。
  2. updated钩子中,判断display状态,从而判断是否是v-show触发的变化。

1-1.png

实现

注册Hero元素

我们先定义一个Map,用于存储heroId和对应的v-show元素集合。 并且实现注册和注销函数。

// 元素映射表 用于v-show 元素对的匹配
const heroMap = new Map<string, Set<HTMLElement>>();

/**
 * 注册Hero元素
 * @param el Hero元素
 * @param heroId Hero ID
 */
function registerHero(el: HTMLElement, heroId: string) {
  if (!heroMap.has(heroId)) {
    heroMap.set(heroId, new Set());
  }
  heroMap.get(heroId)?.add(el);
}

/**
 * 注销Hero元素
 * @param el Hero元素
 * @param heroId Hero ID
 */
function unregisterHero(el: HTMLElement, heroId: string) {
  const set = heroMap.get(heroId);
  if (set) {
    set.delete(el);
    if (set.size === 0) heroMap.delete(heroId);
  }
}

除此之外,我们还需要在元素都挂载好之后,来验证每个heroId是否有且只有2个v-show元素。

/**
 * 验证Hero元素对是否匹配
 * @param heroId Hero ID
 */
function validatePair(heroId: string) {
  const set = heroMap.get(heroId);
  if (set) {
    if (set.size === 2) {
      set.forEach(el => {
        const display = getComputedStyle(el).display;
        (el as any).__isVShowPair = true;
        (el as any).__wasHidden = display === 'none';
        // 记录原始display属性
        display !== 'none' && ((el as any).__originDisplay = display);
      });
    } else if (set?.size < 2) {
      set.forEach(el => (el as any).__isVIfPair = true);
      heroMap.delete(heroId);
    } else {
      console.error(`Hero ID "${heroId}" 有 ${set.size} 个元素,预期 2 个`);
    }
  }
}

再在指令处调用方法:

  1. mounted钩子中注册并验证元素对.
  2. updated钩子中判断是否是v-show触发的变化,从而执行过渡动画。
  3. beforeUnmount钩子中注销元素对。
const heroAnimationDirective: Directive<HTMLElement, HeroAnimationProps> = {
  mounted(el, { value }) {
    const heroId = value.heroId;
    el.dataset.heroId = heroId;
    registerHero(el, heroId);

    queueMicrotask(() => validatePair(heroId));
  },
  updated(el, { value }) {
    if (!(el as any).__isVShowPair) return
    const wasHidden = (el as any).__wasHidden;
    const display = getComputedStyle(el).display;
    // 初始display为隐藏的元素触发 避免触发两次
    if (!wasHidden) {
      heroAnimation(el, value);
    }
    // 重新记录隐藏状态
    (el as any).__wasHidden = display === 'none';
    (display !== 'none' && !(el as any).__originDisplay) && ((el as any).__originDisplay = display);
  },
  beforeUnmount(el, { value }) {
    // v-if/路由切换元素触发动画
    if ((el as any).__isVIfPair) {
      heroAnimation(el, value);
    }
    unregisterHero(el, value.heroId);
  }
};

改造动画

因为我们是在updated钩子中执行的动画,这时起始元素display属性已经被改变为none,我们需要先恢复原始值然后再执行动画。

/**
 * 执行元素的动画过渡
 * @param source 起始元素
 * @param props 动画属性
 */
async function heroAnimation(source: HTMLElement, props: HeroAnimationProps) {
  const {
    heroId,
    duration = '1s',
    timingFunction = 'ease',
    delay = '0s',
    position = 'fixed',
    zIndex = 9999,
    container = document.body
  } = props;

  // 容器
  const containerEl: HTMLElement = isRef(container)
    ? container.value ?? document.body
    : typeof container === 'string'
      ? document.querySelector(container) ?? document.body
      : container;
  const containerRect = getRect(containerEl);

  // v-show 标识
  const isVShowPair = (source as any).__isVShowPair;

  // v-show情况下,需要先显示元素,才能获取到正确的位置信息
  if (isVShowPair) {
    source.style.setProperty('display', (source as any).__originDisplay || 'block');
    await nextTick();
  }

  const rect = getRect(source);
  const clone = source.cloneNode(true) as HTMLElement;

  copyStyles(source, clone);
  // v-show 恢复隐藏
  isVShowPair && source.style.setProperty('display', 'none');
  await nextTick();

  let target: HTMLElement | null = null;

  if (isVShowPair) {
    // 从映射表中获取目标元素
    const set = heroMap.get(heroId);
    set && set.forEach(item => item !== source && (target = item));
  } else {
    target = document.querySelector(
      `[data-hero-id="${heroId}"]:not([data-clone]):not([style*="display: none"])`
    ) as HTMLElement;
  }

  if (!target) return;

  ...先前的动画逻辑
}

简单来个页面测试一下

<template>
  <button @click="flag = !flag">触发</button>
  <div class="container">
    <div
      v-show="flag"
      v-hero="animationProps"
      class="box1"
    />
    <div
      v-show="!flag"
      v-hero="animationProps" 
      class="box2"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue-demi';
import type { HeroAnimationProps } from 'vue-hero-cross';

const flag = ref(false)
const boxRef = ref<HTMLDivElement>()

const animationProps: HeroAnimationProps = {
  heroId: 'box',
  duration: '2s',
  position: 'absolute',
  container: '.container'
}
</script>

<style scoped>
.container {
  position: relative;
  width: 500px;
  height: 500px;
  border: 1px solid #000;
  border-radius: 12px;
  overflow: hidden;
}
.box1 {
  position: absolute;
  top: -50px;
  left: -50px;
  width: 200px;
  height: 200px;
  background-color: red;
  border-radius: 12px;
}

.box2 {
  position: absolute;
  bottom: -50px;
  right: -50px;
  width: 300px;
  height: 300px;
  background-color: blue;
  border-radius: 50%;
  transform: rotate(45deg);
}
</style>

看看效果:

1-2.gif

完美触发过渡😀

细节优化

快速切换优化

想到一个场景,如果快速点击按钮/切换路由,会出现什么效果。

1-3.gif

可以看到连点两下按钮后,虽然只有一个动画再执行,但是目标元素已经变化到了最初的蓝色BOX,但是动画的路径却没有变化,这明显是不符合预期的。 预期效果应该是如果目标元素已经变化了,那么动画的路径也应该变化到新的目标位置。 实现步骤:

  1. 当触发动画时,先判断是否存在正在进行的动画。
  2. 如果存在,需要先中断当前动画,然后创建一个新的动画元素。
  3. 新的动画元素需要复制当前动画元素的所有样式
  4. 新元素的位置需要设置为当前动画元素的位置
  5. 最后,新元素作为起始元素,开始新的动画。

我们先定义一个映射表,用于存储当前正在进行的动画元素。

// 正在进行的动画元素映射表
const animatingMap = new Map<string, HTMLElement>();

然后再实现中断当前动画的逻辑。

async function heroAnimation(source: HTMLElement, props: HeroAnimationProps) {
  const {
    heroId,
    duration = '1s',
    timingFunction = 'ease',
    delay = '0s',
    position = 'fixed',
    zIndex = 9999,
    container = document.body
  } = props;

  // 中断动画标识
  let isInterruptedAnimation = false;
  // 容器
  const containerEl: HTMLElement = isRef(container)
    ? container.value ?? document.body
    : typeof container === 'string'
      ? document.querySelector(container) ?? document.body
      : container;
  const containerRect = getRect(containerEl);

  // 存在正在进行的动画,需要中断
  if (animatingMap.has(heroId)) {
    // 当前动画元素
    const animatingEl = animatingMap.get(heroId) as HTMLElement;
    const animatingElStyle = window.getComputedStyle(animatingEl);

    // 克隆当前动画元素,用于新的动画
    const newSource = animatingEl.cloneNode(true) as HTMLElement;
    copyStyles(animatingEl, newSource);
    // copyStyles 函数排除了 left、top 样式,手动计算并设置当前动画元素的位置
    newSource.style.left = animatingElStyle.left;
    newSource.style.top = animatingElStyle.top;
    containerEl.appendChild(newSource);

    // 移除旧的动画元素
    containerEl.removeChild(animatingEl);
    
    source = newSource;
    isInterruptedAnimation = true;
  }

  ...

  copyStyles(source, clone);
  // v-show 恢复隐藏
  isVShowPair && source.style.setProperty('display', 'none');
  // 这时候的source是我们手动添加的 现在需要手动移除
  isInterruptedAnimation && containerEl.removeChild(source);
  await nextTick();

  ...

  containerEl.appendChild(clone);
  // 添加动画元素到映射表
  animatingMap.set(heroId, clone);

  requestAnimationFrame(() => {
    ...

    clone.addEventListener('transitionend', () => {
      ...
      // 动画结束后删除
      animatingMap.delete(heroId);
    }, { once: true });
  })
}

再看看现在的效果:

1-4.gif

这下可以实现移动到新的目标位置了😀。

动画时间优化

但这也带来了一个问题,就是动画时间。 现在中断动画后,当前动画元素过渡到新的目标位置还是需要2秒,但这不符合预期。 我们预想一个场景: 假设一个AB的动画,过渡动画时间是2000ms

  1. 前进的途中,动画播放了750ms,用户再次点击了按钮,那当前动画元素应该回到A位置,而过渡时间就是已播放的750ms
  2. 折返的途中,动画播放了500ms,用户再次点击了按钮,那当前动画元素应该回到B位置,而过渡时间就是总播放时长2000ms减去AB已过渡的250ms得到的1750ms

1-6.png

根据这个逻辑,我们需要多记录几个信息:

  1. 动画当前被重播的次数,以此来判断是前进还是折返
  2. 前进的时长,以此来计算继续前进折返的过渡时间。
  3. 动画开始时间,用于计算已播放时长。

我们修改animatingMap的类型,添加这些属性。 再添加一个方法,用于转换duration为毫秒数。

// 正在进行的动画元素映射表
interface AnimatingInfo {
  el: HTMLElement;
  count: number;
  elapsed: number;
  startTime: number;
}
const animatingMap = new Map<string, AnimatingInfo>();

/**
 * 解析动画时长
 * @param d 时长字符串或数字
 * @returns 时长(毫秒)
 */
function parseDuration(d: string | number): number {
  if (typeof d === 'number') return d
  const match = String(d).match(/^([\d.]+)\s*(s|ms)?$/)
  if (!match) return 1000
  const [, n, unit] = match
  return unit === 's' ? parseFloat(n) * 1000 : parseInt(n, 10)
}

我们再改造heroAnimation函数,来实现动画时间优化。

async function heroAnimation(source: HTMLElement, props: HeroAnimationProps) {
  const {
    heroId,
    duration = '1s',
    timingFunction = 'ease',
    delay = '0s',
    position = 'fixed',
    zIndex = 9999,
    container = document.body
  } = props;

  // 解析时长
  let durationMs = parseDuration(duration);
  ...
  const animatingInfo = animatingMap.get(heroId);
  // 存在正在进行的动画,需要中断
  if (animatingInfo) {
    const timeElapsed = performance.now() - animatingInfo.startTime;
    // 前进 还是 折返
    const isForward = animatingInfo.count % 2 === 0;

    animatingInfo.elapsed = isForward
      ? (animatingInfo.elapsed || 0) - timeElapsed
      : animatingInfo.elapsed + timeElapsed;
    
    durationMs = isForward
      ? durationMs - animatingInfo.elapsed
      : animatingInfo.elapsed;
    
    // 当前动画元素
    const animatingEl = animatingInfo.el;
    const animatingElStyle = window.getComputedStyle(animatingEl);
    ...
  }

  ...

  containerEl.appendChild(clone);
  // 更新动画元素
  const animationData = animatingInfo || {
    el: clone,
    count: 1,
    elapsed: 0,
    startTime: performance.now(),
  }
  if (animatingInfo) {
    animatingInfo.el = clone;
    animatingInfo.count++;
    animatingInfo.startTime = performance.now();
  }
  animatingMap.set(heroId, animationData);

  requestAnimationFrame(() => {
    // 改用转换后的时间
    clone.style.transition = `all ${durationMs}ms ${timingFunction} ${delay}`;
    ...
  });
}

这时我们再看看效果:

1-5.gif

这下动画时间就符合预期了🎉。

源码 和 使用

GitHub仓库

该指令的源码已经上传到github,如果对你有帮助,请点点star⭐: GitHub vue-hero-cross

npm包安装

同时,也发布到了npm,你可以通过npm install vue-hero-cross安装来直接使用: npm vue-hero-cross

🤝 参与贡献

如果你对这个项目感兴趣,欢迎:

  1. 提交 Issue 报告问题或建议。
  2. 提交 PR 添加新功能或修复 Bug。
  3. 在项目中实际使用并反馈体验。
  4. 分享给更多开发者

学习Three.js--材质(Material)

学习Three.js--材质(Material)

前置必读:材质通用规则 & 公共核心参数

一、通用规则

  1. 所有网格类材质MeshXXXMaterial)都是 THREE.Material 的子类,共用一套核心公共参数,无需重复记忆;
  2. 材质的参数均为配置对象({})传参,支持创建后通过 material.参数名 = 值 动态修改;
  3. 材质创建后可以复用给多个物体,能极大节省内存(比如100个立方体用同一个材质,只创建1个即可);
  4. 所有颜色值参数:支持16进制0xffffff、RGB字符串#fff/rgb(255,255,255)THREE.Color对象。

二、 所有材质【公共核心参数】

以下参数适用于 90% 的材质(MeshBasic/MeshLambert/MeshPhong/Standard/Physical 全部包含),一次记忆,全部受用!

{
  color: 0xffffff, // 基础颜色,默认白色
  transparent: false, // 是否开启透明效果,默认关闭 ❗开启透明必须设为true,opacity才生效
  opacity: 1, // 透明度,取值范围 0(完全透明) ~ 1(完全不透明),默认1
  visible: true, // 物体是否可见,true显示/false隐藏,默认true
  side: THREE.FrontSide, // 渲染物体的哪个面,核心取值3种:
  // THREE.FrontSide 只渲染正面(默认) | THREE.BackSide 只渲染背面 | THREE.DoubleSide 双面渲染
  depthWrite: true, // 是否写入深度缓冲区,默认true;透明物体建议设为false,避免透明重叠闪烁
  depthTest: true, // 是否开启深度测试,默认true;关闭后物体可能会穿透其他物体
  wireframe: false, // 是否以线框模式渲染物体,默认false(实体),true则只显示几何体的边框线
  wireframeLinewidth: 1, // 线框的线宽,默认1;❗注意:浏览器对WebGL的线宽支持有限,一般只能到1-2px
  alphaTest: 0, // 透明度裁剪阈值,取值0~1;像素透明度低于该值则不渲染,解决透明边缘锯齿问题
  map: null, // 颜色纹理贴图,传 THREE.Texture 纹理对象,用于给物体贴图片(比如木纹、皮肤)
}

一、网格基础材质 THREE.MeshBasicMaterial

核心特点

不受光照影响、无明暗变化,始终纯色/纹理显示,不需要添加光源就能渲染,性能最高

适用场景

调试几何体、UI元素、纯色简单物体、快速原型开发

完整调用语法

const material = new THREE.MeshBasicMaterial({
  // 公共参数 + 自身专属参数(无专属,全是公共参数)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  alphaTest: 0,
  depthWrite: true
});

专属注意点

  • 该材质无视所有光源,添加AmbientLight/DirectionalLight等都不会有任何效果;
  • wireframe: true 时,能直观看到几何体的顶点和面结构,调试几何体形状的最佳材质

二、网格漫反射材质 THREE.MeshLambertMaterial

核心特点

基于Lambert漫反射光照模型,对光源有响应、有明暗过渡,无高光效果,计算量低

适用场景

哑光粗糙表面(纸张、墙面、布料、水泥、木头、哑光塑料)

完整调用语法

const material = new THREE.MeshLambertMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  //  自身核心专属参数
  emissive: 0x000000, // 自发光颜色,默认黑色(无自发光)
  emissiveIntensity: 1, // 自发光强度,取值0~∞,默认1
  emissiveMap: null, // 自发光纹理贴图,给物体贴发光纹理
  vertexColors: false, // 是否使用顶点颜色,默认false
  flatShading: false, // 是否使用平面着色,默认false(平滑着色)
});

所有参数详细释义(含公共+专属)

  1. 公共参数同上文,不再重复;
  2. emissive:自发光色,不会照亮其他物体,只是让材质自身显示该颜色,比如设为0xff0000,物体会带红色发光效果;
  3. emissiveIntensity:调节自发光的亮度,值越大发光越明显;
  4. flatShading: true:几何体每个面会显示纯色,无平滑过渡,适合做低多边形风格;
  5. vertexColors: true:如果几何体设置了顶点颜色,材质会渲染顶点的颜色渐变。

注意点

  • 必须添加光源,否则物体会显示纯黑色,完全不可见;
  • 无高光参数,无法实现光泽效果,是哑光质感的最优选择。

三、网格高光材质 THREE.MeshPhongMaterial

核心特点

基于Phong高光光照模型,支持「漫反射+镜面高光」,对光源响应,有明显的高光光斑,计算量略高于MeshLambertMaterial

适用场景

有光泽的物体(漆面塑料、陶瓷、湿润的石头、抛光木头、电镀金属)

完整调用语法

const material = new THREE.MeshPhongMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  //  继承MeshLambert的自发光参数
  emissive: 0x000000,
  emissiveIntensity: 1,
  emissiveMap: null,
  flatShading: false,
  //  自身核心高光专属参数(重中之重)
  specular: 0x111111, // 高光颜色,默认浅灰色
  shininess: 30, // 高光的「亮度+范围」,取值0~1000+,默认30
});

高光核心参数释义

  1. specular:决定高光光斑的颜色,比如:
    • 塑料材质:设为0xffffff(白色高光),最真实;
    • 金属材质:设为和color相同的颜色,高光和本体同色;
  2. shininess:核心高光参数,值越小 → 高光范围越大、亮度越低值越大 → 高光范围越小、亮度越高
    • 示例:shininess:5 → 大面积柔和高光;shininess:100 → 小面积刺眼高光。

注意点

  1. 同样必须添加光源,无光源则全黑;
  2. Phong的高光效果是模拟的光泽,不是物理真实的,高光光斑略显生硬,但性能不错,适合简单光泽场景;
  3. 该材质的高光计算在顶点上,低面数几何体的高光会有锯齿。

四、物理基础材质 THREE.MeshStandardMaterial 【默认首选】

核心特点

基于 PBR(Physically Based Rendering)物理渲染,完全遵循真实世界的光照规律,参数少但效果极致真实,计算量适中,Three.js官方推荐的默认首选材质,也是目前最常用的材质!

适用场景

99%的现代3D场景:游戏、产品展示、建筑可视化、电商3D商品、写实模型,能替代所有传统材质(Lambert/Phong)

核心优势

  • 传统材质(Phong/Lambert)是「模拟光照」,PBR是「物理光照」,效果真实度天差地别;
  • 参数语义化,无需调复杂的高光/漫反射,通过「粗糙度+金属度」就能实现所有质感。

完整调用语法

const material = new THREE.MeshStandardMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  alphaTest: 0,
  depthWrite: true,
  //  PBR 核心四大基础参数 (必学!!)
  roughness: 0.5,    // 粗糙度,取值 0(镜面光滑) ~ 1(完全粗糙),默认0.5
  metalness: 0.5,    // 金属度,取值 0(非金属) ~ 1(纯金属),默认0.5
  emissive: 0x000000,// 自发光颜色,默认黑色,无自发光
  emissiveIntensity:1,// 自发光强度,默认1
  //  PBR 进阶纹理贴图(提升真实度)
  roughnessMap: null, // 粗糙度纹理,让物体表面粗糙度有细节变化
  metalnessMap: null, // 金属度纹理,让物体部分区域是金属、部分是非金属
  normalMap: null,    // 法线贴图,模拟物体表面凹凸细节(无需修改几何体)
  aoMap: null,        // 环境遮蔽贴图,模拟物体缝隙/凹陷处的阴影,提升层次感
  displacementMap: null, // 置换贴图,真正修改几何体顶点,实现凹凸效果
  //  特殊物理参数
  clearcoat: 0,       // 清漆层,取值0~1,默认0;模拟车漆/指甲油的表层光泽
  clearcoatRoughness:0,// 清漆层粗糙度,取值0~1,默认0
  transmission: 0,    // 透射率,取值0~1,默认0;模拟玻璃/半透明物体的透光效果
});

PBR 核心参数【必懂必记】(4个)

这是该材质的灵魂,记住取值范围+含义,就能调出所有质感,无任何例外!

  1. roughness (粗糙度) 0~1
    • 0 → 镜面级光滑,反射所有光线,高光极强(比如镜子、抛光金属);
    • 1 → 完全粗糙,无任何高光,纯哑光(比如纸张、水泥);
    • 中间值 → 半光泽(比如塑料、漆面、木头)。
  2. metalness (金属度) 0~1
    • 0 → 非金属材质(塑料、玻璃、石头、布料),反射环境光,高光颜色固定;
    • 1 → 纯金属材质(金、银、铜),反射自身颜色,高光和本体同色;
    • 中间值 → 合金材质(比如不锈钢、黄铜)。
  3. emissive + emissiveIntensity:自发光,不会照亮其他物体,比如霓虹灯、指示灯、发光logo。
  4. 颜色color:金属度0时是「物体本身颜色」;金属度1时,color决定金属的颜色(比如金色0xffd700、银色0xcccccc)。

质感调试万能公式

  • 哑光塑料 → roughness:0.8, metalness:0
  • 镜面塑料 → roughness:0.1, metalness:0
  • 不锈钢 → roughness:0.2, metalness:1, color:0xcccccc
  • 黄金 → roughness:0.1, metalness:1, color:0xffd700
  • 木头 → roughness:0.7, metalness:0, color:0x8b4513
  • 玻璃 → roughness:0, metalness:0, transmission:0.9

五、高级物理材质 THREE.MeshPhysicalMaterial

核心特点

MeshStandardMaterial 的超集,完全兼容其所有参数+方法,在其基础上增加了更精细的物理光学参数,实现极致写实的物理效果,PBR天花板材质!

适用场景

超写实3D场景:汽车车漆、玻璃/水晶、宝石、珍珠、丝绸、拉丝金属、多层镀膜材质

完整调用语法

const material = new THREE.MeshPhysicalMaterial({
  //  完全包含 MeshStandardMaterial 的 所有参数(无需重复写)
  color: 0x00ff00,
  roughness: 0.5,
  metalness: 0.5,
  emissive: 0x000000,
  clearcoat:0,
  transmission:0,
  //  新增的【专属高级参数】(核心!)
  clearcoatRoughness: 0,    // 清漆层粗糙度,0=镜面清漆,1=哑光清漆
  sheen: 0,                 // 光泽层,0~1,默认0;模拟丝绸、天鹅绒、布料的柔和高光
  sheenRoughness: 0.5,      // 光泽层粗糙度,0~1,默认0.5
  sheenColor: 0xffffff,     // 光泽层颜色,默认白色
  iridescence: 0,           // 虹彩/色散,0~1,默认0;模拟珍珠、肥皂泡、CD光盘的彩虹反光
  iridescenceIOR: 1.5,      // 虹彩折射率,默认1.5
  iridescenceThicknessRange: [100, 400], // 虹彩厚度范围,控制彩虹颜色
  anisotropy: 0,            // 各向异性,-1~1,默认0;模拟拉丝金属、头发的方向性高光
  thickness: 0.1,           // 厚度,0~∞,默认0.1;配合transmission使用,模拟玻璃的厚度(厚玻璃颜色更深)
  reflectivity: 1,          // 折射率,1~2.333,默认1;控制玻璃/水的折射效果
});

专属参数核心释义

  1. sheen:丝绸/天鹅绒的核心参数,能实现「反向高光」,布料的质感全靠它;
  2. iridescence:彩虹色散,珍珠、肥皂泡、车漆的金属珠光效果,必调参数;
  3. anisotropy:拉丝金属的核心,比如不锈钢拉丝面板、头发,高光会沿着拉丝方向延伸;
  4. thickness:玻璃厚度,比如厚玻璃杯的边缘会偏绿,薄玻璃则透明,真实度拉满。

注意点

  • 该材质计算量比MeshStandardMaterial略高,但现代浏览器/显卡完全能承载;
  • 能用MeshStandardMaterial实现的效果,就用它;需要极致细节时,再用MeshPhysicalMaterial

六、点材质 THREE.PointsMaterial

核心特点

专门用于 THREE.Points(粒子系统),只能渲染点/粒子,不能渲染网格几何体

适用场景

粒子特效:星空、烟雾、雨滴、雪花、灰尘、点云模型、星空背景

完整调用语法

const material = new THREE.PointsMaterial({
  color: 0xffffff,          // 粒子颜色,默认白色
  size: 1,                  // 粒子大小(像素),默认1
  sizeAttenuation: true,    // 是否开启「近大远小」,默认true;false则所有粒子大小一致
  transparent: true,        // 是否透明,默认false(粒子特效必开)
  opacity: 1,               // 透明度,0~1,默认1
  map: null,                // 粒子纹理贴图,比如用圆形贴图做圆点粒子,用雪花贴图做雪花
  alphaTest: 0.1,           // 透明裁剪,解决粒子边缘锯齿,建议设0.1
  depthWrite: false,        // 关闭深度写入,解决透明粒子重叠时的闪烁问题(必开)
  blending: THREE.AdditiveBlending, // 混合模式,AdditiveBlending=叠加,粒子更亮(适合火焰/星光)
  emissive: 0xffffff,       // 自发光颜色,默认白色
  emissiveIntensity:1,      // 自发光强度
});
// 配套使用:创建粒子系统
const geometry = new THREE.BufferGeometry().setFromPoints( pointsArr ); // 点数组
const points = new THREE.Points( geometry, material );
scene.add(points);

核心参数

  1. size:粒子的像素大小,值越大粒子越大;
  2. sizeAttenuation: true:粒子会像真实物体一样,离相机越近越大,越远越小,必开;
  3. depthWrite: false:透明粒子的必备参数,否则粒子重叠时会出现穿透/闪烁的问题。

七、线材质 「LineBasicMaterial + LineDashedMaterial」

通用特点

专门用于 THREE.Line / THREE.LineSegments(线段/线条几何体),只能渲染线条,不能渲染网格

适用场景

绘制辅助线、坐标轴、边框、电路图、激光线、轨迹线等

7.1 实线材质 THREE.LineBasicMaterial

const material = new THREE.LineBasicMaterial({
  color: 0xffffff,    // 线条颜色,默认白色
  linewidth: 1,       // 线宽(像素),默认1;❗浏览器限制,一般最大只能到2px
  transparent: false, // 是否透明
  opacity: 1,         // 透明度
  dashed: false,      // 是否虚线,默认false(实线)
});

7.2 虚线材质 THREE.LineDashedMaterial【高频使用】

const material = new THREE.LineDashedMaterial({
  color: 0xffffff,    // 线条颜色
  linewidth: 1,       // 线宽
  transparent: false,
  opacity: 1,
  dashSize: 3,        // 【核心】虚线的「实线部分长度」,默认3
  gapSize: 1,         // 【核心】虚线的「空白部分长度」,默认1
  scale: 1,           // 缩放比例,默认1;整体缩放dashSize和gapSize
});
// ❗ 必加:给线段几何体调用 computeLineDistances(),否则虚线不生效!
const lineGeometry = new THREE.BufferGeometry().setFromPoints( pointsArr );
lineGeometry.computeLineDistances(); 
const line = new THREE.Line( lineGeometry, material );
scene.add(line);

注意点

  1. 虚线材质必须调用 geometry.computeLineDistances(),否则会显示为实线,无虚线效果;
  2. linewidth 受WebGL限制,无法设置超大线宽,如需粗线条,建议用网格几何体模拟。

八、精灵材质 THREE.SpriteMaterial

核心特点

专门用于 THREE.Sprite(精灵/广告牌),本质是始终朝向相机的2D平面,不会随视角旋转,永远正面朝向屏幕

适用场景

3D场景中的2D元素:图标、血条、标签、子弹、火焰粒子、雪花粒子、2D精灵特效

完整调用语法

const material = new THREE.SpriteMaterial({
  color: 0xffffff,    // 精灵颜色
  map: null,          // 精灵纹理贴图(核心,一般都用贴图,比如图标图片)
  transparent: true,  // 是否透明,默认false(必开,否则背景是白色)
  opacity: 1,         // 透明度
  rotation: 0,        // 精灵旋转角度(弧度制),默认0;绕中心旋转
  center: new THREE.Vector2(0.5,0.5), // 旋转中心,默认中心(0.5,0.5)
  sizeAttenuation: true, // 是否近大远小,默认true;false则精灵大小固定
  depthWrite: false,  // 关闭深度写入,解决精灵遮挡问题
});
// 配套使用
const sprite = new THREE.Sprite( material );
sprite.scale.set(1,1,1); // 设置精灵大小
scene.add(sprite);

注意点

  • 精灵是2D平面,没有厚度,所以side参数无效;
  • 精灵的大小通过 sprite.scale 控制,不是材质参数。

九、阴影材质 THREE.ShadowMaterial

核心特点

官方正确特性:材质自身完全透明,不接收任何光照,只显示「其他物体投射过来的阴影」

适用场景

地面/平面作为阴影接收器,比如创建一个透明的地面,只显示3D物体的阴影,不显示地面本身,让物体看起来像悬浮在空中,是实现真实阴影的最优方案!

完整调用语法

const material = new THREE.ShadowMaterial({
  opacity: 0.5, // 【唯一核心参数】阴影的透明度,取值0~1,默认0.5
  // 无其他专属参数,公共参数仅支持 transparent/depthWrite
  transparent: true, // 固定为true,无需修改
  depthWrite: false, // 建议关闭,避免阴影闪烁
});

使用注意点

  1. 必须开启光源的阴影投射 + 物体的阴影投射 + 地面的阴影接收
    directionalLight.castShadow = true; // 光源投射阴影
    cube.castShadow = true; // 物体投射阴影
    ground.receiveShadow = true; // 地面接收阴影
    
  2. opacity 越小,阴影越淡;越大,阴影越深。

十、自定义着色器材质 THREE.ShaderMaterial

核心特点

完全自定义顶点着色器(VertexShader)片元着色器(FragmentShader),使用GLSL语言编写,能实现 任何Three.js内置材质做不到的视觉效果,是Three.js的高级核心功能

适用场景

水体、火焰、溶解效果、扭曲变形、玻璃折射、扫描线、像素化、后处理特效等自定义视觉效果

完整调用语法

const material = new THREE.ShaderMaterial({
  //  核心:自定义GLSL着色器代码
  vertexShader: `
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    attribute vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `, // 顶点着色器:控制物体的顶点位置/变形
  fragmentShader: `
    uniform vec3 uColor;
    void main() {
      gl_FragColor = vec4(uColor, 1.0);
    }
  `, // 片元着色器:控制物体的像素颜色/纹理/特效
  //  自定义全局变量(JS传递数据到GLSL,核心!)
  uniforms: {
    uColor: { value: new THREE.Color(0x00ff00) }, // 颜色变量
    uTime: { value: 0 }, // 时间变量,用于做动画
    uTexture: { value: texture }, // 纹理变量
  },
  //  公共材质参数(全部支持)
  transparent: true,
  opacity: 1,
  side: THREE.DoubleSide,
  depthWrite: false,
  wireframe: false,
});

核心参数释义

  1. vertexShader:顶点着色器,运行在GPU上,控制每个顶点的位置、法线、纹理坐标等;
  2. fragmentShader:片元着色器,运行在GPU上,控制每个像素的最终颜色,所有视觉特效都在这里实现;
  3. uniforms:JS和GLSL之间的数据桥梁,可以在JS中动态修改uniforms.uTime.value,GLSL中就能实时获取,实现动画。

注意点

  • Three.js会自动注入内置变量(比如projectionMatrixmodelViewMatrix),无需手动声明;
  • GLSL语言和JS语法不同,需要单独学习基础,入门简单。

十一、原生着色器材质 THREE.RawShaderMaterial

核心特点

THREE.ShaderMaterial 几乎完全一致,唯一的区别是:不会自动注入Three.js的任何内置变量

适用场景

高级自定义着色器,需要完全掌控GLSL代码的所有变量,比如移植外部WebGL着色器、编写极致优化的着色器、实现底层图形学效果

调用语法

const material = new THREE.RawShaderMaterial({
  vertexShader: `
    // ❗ 必须手动声明所有变量,Three.js不自动注入
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    attribute vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 uColor;
    void main() {
      gl_FragColor = vec4(uColor, 1.0);
    }
  `,
  uniforms: { uColor: { value: new THREE.Color(0x00ff00) } },
  transparent: true,
});

核心区别(和ShaderMaterial)

ShaderMaterial:自动注入内置变量,开发效率高,适合绝大多数自定义场景; RawShaderMaterial:无自动注入,完全手动,灵活性最高,适合高级底层开发。


材质学习总结 & 优先级建议(必看)

1. 材质使用优先级(按推荐度排序)

  1. 优先用 MeshStandardMaterial → 99%场景够用,物理真实、参数简单、性能均衡;
  2. 需要极致细节 → 用 MeshPhysicalMaterial
  3. 调试几何体 → 用 MeshBasicMaterial
  4. 简单哑光物体+低性能设备 → 用 MeshLambertMaterial
  5. 简单光泽物体 → 用 MeshPhongMaterial
  6. 粒子/点云 → 用 PointsMaterial
  7. 线条 → 用 LineBasicMaterial/LineDashedMaterial
  8. 2D精灵/图标 → 用 SpriteMaterial
  9. 透明地面+阴影 → 用 ShadowMaterial
  10. 自定义特效 → 用 ShaderMaterial/RawShaderMaterial

2. 核心记忆点

  • 所有网格材质共用一套公共参数,无需重复记忆;
  • PBR材质的核心是「粗糙度+金属度」,记住0~1的取值规则,就能调出所有质感;
  • 材质可以复用,多个物体用同一个材质能极大优化性能;
  • 透明材质建议开启 transparent:true + depthWrite:false,避免闪烁/穿透问题。

css主题theme变量切换实现原理学习记录

大家好,我的开源项目PakePlus可以将网页/Vue/React项目打包为桌面/手机应用并且小于5M只需几分钟,官网地址:pakeplus.com


/* 全局需要根据lang动态修改的样式 */
:root {
--font-size: 16px;
--font-family: Arial, sans-serif;
}




/* 默认dark主题 */
:root[theme='red'] {
--text-color: #f0f6fc;
--themeColor: #fd2d60;
}




/* light主题 */
:root[theme='blue'] {
--text-color: #f0f6fc;
--themeColor: #50a5de;
}

/* light主题 */ :root[theme='blue'] { --text-color: #f0f6fc; --themeColor: #50a5de; }

在开发web端系统的时候,经常需要配置不同的主题来实现不同的样式切换,比如白天和黑夜模式,编辑器的不同颜色主题等,这些的底层原理是什么?

主要就是:

:root[theme='red']

这行代码的含义,可以分解为:

  • :root:伪类选择器,匹配文档的根元素(HTML文档中是 <html> 标签)

  • [theme='red']:属性选择器,匹配具有 theme 属性且值为 "red" 的元素

  • 组合起来:选择 <html theme="red"> 这个元素

作用:创建条件样式规则

相当于说:

"当html元素的theme属性等于'red'时,应用这些CSS变量值"

<!-- 当HTML元素是这样时 -->
<html theme="red">
  <!-- 应用red主题的CSS变量 -->
</html>

<!-- 当HTML元素是这样时 -->
<html theme="blue">
  <!-- 应用blue主题的CSS变量 -->
</html>

JavaScript切换主题

// 切换到红色主题
document.documentElement.setAttribute('theme', 'red');

// 切换到蓝色主题  
document.documentElement.setAttribute('theme', 'blue');

CSS工作原理

/* 当theme="red"时应用这些变量 */
:root[theme='red'] {
  --themeColor: #fd2d60;     /* 红色主题主色 */
  --mainBgImg: url('@/assets/images/redMainBg.png');
}

/* 当theme="blue"时应用这些变量 */
:root[theme='blue'] {
  --themeColor: #50a5de;     /* 蓝色主题主色 */
  --mainBgImg: url('@/assets/images/blueMainBg.png');
}

写法1:属性选择器(你的写法)

:root[theme='red'] {
  --themeColor: #fd2d60;
}

写法2:类名选择器

.theme-red {
  --themeColor: #fd2d60;
}

/* 使用时 */
document.documentElement.className = 'theme-red';

写法3:data属性选择器

:root[data-theme='red'] {
  --themeColor: #fd2d60;
}

/* 使用时 */
document.documentElement.setAttribute('data-theme', 'red');

CSS优先级示例

/* 默认值 */
:root {
  --themeColor: #ccc;  /* 默认灰色 */
}

/* 当有theme属性时覆盖 */
:root[theme] {
  --themeColor: #000;  /* 任何theme属性时变成黑色 */
}

/* 具体theme值时再次覆盖 */
:root[theme='red'] {
  --themeColor: #f00;  /* theme="red"时红色,优先级最高 */
}

/* 优先级顺序:
   :root[theme='red'] > :root[theme] > :root
*/

与普通选择器的区别

/* 普通CSS(一直生效) */
.red-theme {
  color: red;
}

/* 属性选择器(有条件生效) */
[theme='red'] {
  color: red;
}

/* 组合选择器(更精确) */
html[theme='red'] {
  color: red;
}

/* :root选择器(目标根元素) */
:root[theme='red'] {
  color: red;
}

主题切换的完整示例

// 主题切换函数
function switchTheme(themeName) {
  // 1. 修改html元素的theme属性
  document.documentElement.setAttribute('theme', themeName);
  
  // 2. 保存用户偏好
  localStorage.setItem('app-theme', themeName);
  
  // 3. 触发事件通知
  window.dispatchEvent(new CustomEvent('themechange', {
    detail: { theme: themeName }
  }));
}

// 初始化主题
const savedTheme = localStorage.getItem('app-theme') || 'red';
switchTheme(savedTheme);

优点和注意事项

优点

  1. 语义清晰:直接表示"主题",易于理解

  2. 无需额外类名:避免与现有类名冲突

  3. 优先级明确:可以与其他选择器组合使用

注意事项

  1. 属性值区分大小写theme='Red' 和 theme='red' 是不同的

  2. 没有theme属性时的回退:建议添加默认值

  3. 验证属性值:JavaScript中可能需要验证theme值是否有效

建议的改进写法

/* 默认主题(回退) */
:root {
  --themeColor: #fd2d60;  /* 默认红色主题 */
  --mainBgImg: url('@/assets/images/redMainBg.png');
}

/* 显式指定主题 */
:root[theme='red'] {
  /* 可以省略,使用默认值 */
}

:root[theme='blue'] {
  --themeColor: #50a5de;
  --mainBgImg: url('@/assets/images/blueMainBg.png');
}

/* 无效主题时的回退 */
:root[theme]:not([theme='red']):not([theme='blue']) {
  --themeColor: #fd2d60;  /* 回退到红色 */
}

实际应用案例:

前端实现元素叠加

在前端开发中实现元素叠加(Overlapping)是一个非常常见的需求,从简单的文字盖在图片上,到复杂的层叠动画。实现这一效果的方式多种多样,每种方式都有其适用的场景、优缺点以及对布局流的影响。


实现元素叠加的七大核心方式

1. CSS 定位 (Positioning)

这是最经典、使用最广泛的方式。通过脱离文档流,将元素精准放置在另一个元素之上。

  • relative + absolute:父元素设为 relative 建立参考系,子元素设为 absolute 进行偏移。
  • fixed:相对于浏览器窗口叠加,常用于遮照层(Overlay)和全局通知。
  • 特点:完全脱离文档流,不会撑开父元素高度。

2. 负外边距 (Negative Margins)

通过给元素设置负的 margin(通常是 topleft),强行让元素移动并覆盖到前一个元素的空间。

  • 特点:元素依然留在文档流中,会影响后续元素的排列。通常用于微调或创建“破格”设计感。

3. CSS Grid 布局 (网格叠加)

这是现代前端最推荐的叠加方式。通过将多个元素分配到同一个网格单元格(Cell)中实现叠加。

  • 特点不脱离文档流。父容器可以根据所有叠加元素中最高的那一个自动撑开高度,完美解决了 absolute 导致的父容器高度塌陷问题。

4. CSS 转换 (Transforms)

使用 transform: translate(-50%, -50%) 等平移操作。

  • 特点:在 GPU 上加速,性能极佳,常用于动画。元素在占位上依然保留在原处,只是视觉上发生了偏移和叠加。

5. 伪元素 (Pseudo-elements)

使用 ::before::after 创建装饰性叠加层。

  • 特点:减少 HTML 结构的冗余,非常适合做遮罩(Overlay)、阴影或小装饰。

6. Flexbox + 负边距/定位

虽然 Flexbox 是一维布局,但配合 margin-left: -50px 或子元素定位也能实现叠加。

7. SVG 与 Canvas

在图形内部实现叠加,SVG 依靠元素的书写顺序(后写的盖在先写的上面),Canvas 依靠绘图指令的执行顺序。


深度实战代码演示

下面的代码展示了上述所有技术的具体实现,包含了大量的注释和逻辑说明。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>前端元素叠加技术深度总结</title>
    <style>
        :root {
            --primary: #6366f1;
            --secondary: #ec4899;
            --overlay: rgba(0, 0, 0, 0.5);
            --card-bg: #ffffff;
        }

        body {
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            background-color: #f1f5f9;
            color: #1e293b;
            padding: 40px;
            line-height: 1.6;
        }

        .section {
            background: white;
            padding: 30px;
            border-radius: 12px;
            margin-bottom: 50px;
            box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
        }

        h2 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 25px; color: var(--primary); }

        /* 基础容器演示样式 */
        .container {
            border: 2px dashed #cbd5e1;
            padding: 20px;
            border-radius: 8px;
            min-height: 200px;
            background: #f8fafc;
        }

        .box {
            width: 100px;
            height: 100px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
            border-radius: 8px;
            transition: all 0.3s ease;
        }

        .box-1 { background: var(--primary); }
        .box-2 { background: var(--secondary); opacity: 0.9; }

        /* --- 1. 定位方式 (Absolute Positioning) --- */
        .pos-container {
            position: relative; /* 必须建立参考系 */
        }
        .pos-box-top {
            position: absolute;
            top: 40px;
            left: 40px;
            z-index: 10; /* 控制层叠顺序 */
            box-shadow: 0 10px 15px -3px rgba(0,0,0,0.2);
        }

        /* --- 2. 负边距方式 (Negative Margin) --- */
        .margin-box-2 {
            margin-top: -50px; /* 向上移动并覆盖 box-1 */
            margin-left: 50px;
        }

        /* --- 3. Grid 网格叠加 (推荐方案) --- */
        .grid-container {
            display: grid;
            grid-template-columns: 1fr;
            grid-template-rows: 1fr;
            width: 250px;
        }
        .grid-item {
            /* 关键点:所有子元素分配到同一个网格区域 */
            grid-area: 1 / 1 / 2 / 2;
        }
        .grid-base {
            padding: 20px;
            background: #e2e8f0;
            color: #475569;
            height: 150px;
        }
        .grid-overlay {
            align-self: center; /* 在单元格内居中 */
            justify-self: center;
            width: 80%;
            height: 60%;
            background: var(--primary);
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        }

        /* --- 4. Transform 偏移叠加 --- */
        .transform-box {
            transform: translate(30px, -40px);
        }

        /* --- 5. 伪元素遮罩叠加 --- */
        .pseudo-card {
            position: relative;
            width: 300px;
            height: 180px;
            background: url('https://picsum.photos/300/180') center/cover;
            border-radius: 12px;
            overflow: hidden;
            display: flex;
            align-items: flex-end;
            padding: 20px;
            color: white;
        }
        /* 伪元素创建渐变遮罩层 */
        .pseudo-card::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
            z-index: 1;
        }
        .pseudo-card span {
            position: relative;
            z-index: 2; /* 确保文字在遮罩之上 */
        }

        /* --- 6. 混合模式 (Blend Modes) - 视觉叠加增强 --- */
        .blend-container {
            display: flex;
            gap: 20px;
        }
        .blend-box {
            mix-blend-mode: multiply; /* 颜色混合叠加效果 */
        }

    </style>
</head>
<body>

    <div class="section">
        <h2>1. 经典定位叠加 (Position: Absolute)</h2>
        <p><strong>逻辑:</strong>父元素 relative,子元素 absolute。这是最灵活的方式,但要注意父元素高度塌陷问题。</p>
        <div class="container pos-container">
            <div class="box box-1">底层 (Base)</div>
            <div class="box box-2 pos-box-top">顶层 (Top)</div>
        </div>
    </div>

    <div class="section">
        <h2>2. 负外边距叠加 (Negative Margin)</h2>
        <p><strong>逻辑:</strong>利用 margin-top: -50px 将元素强行拉回。元素依然在文档流中,会影响下方元素的排列。</p>
        <div class="container">
            <div class="box box-1">元素 A</div>
            <div class="box box-2 margin-box-2">元素 B (覆盖 A)</div>
            <div style="margin-top: 20px; color: #94a3b8;">注:由于元素 B 移动了,这里的文字位置也会受影响。</div>
        </div>
    </div>

    <div class="section">
        <h2>3. CSS Grid 叠加 (最现代化方案)</h2>
        <p><strong>逻辑:</strong>将多个子元素显式指定到同一个 grid-area。优点是容器能感知内容高度,且不需要复杂的定位计算。</p>
        <div class="container">
            <div class="grid-container">
                <div class="grid-item grid-base">
                    这是底层内容,它可以很长很长,撑开整个容器的高度。
                </div>
                <div class="grid-item grid-overlay box">
                    叠加层
                </div>
            </div>
        </div>
    </div>

    <div class="section">
        <h2>4. Transform 偏移叠加</h2>
        <p><strong>逻辑:</strong>视觉偏移。元素原来的占位保持不变,类似于 ghost 效果,适合高性能动画交互。</p>
        <div class="container">
            <div class="box box-1">原始占位</div>
            <div class="box box-2 transform-box">偏移覆盖</div>
        </div>
    </div>

    <div class="section">
        <h2>5. 伪元素装饰叠加 (::before/::after)</h2>
        <p><strong>逻辑:</strong>在不增加 DOM 节点的情况下实现叠加。常用于图片遮罩、装饰边框、阴影增强。</p>
        <div class="container">
            <div class="pseudo-card">
                <span>图片上的标题 (伪元素遮罩)</span>
            </div>
        </div>
    </div>

    <div class="section">
        <h2>6. 颜色混合叠加 (Mix-blend-mode)</h2>
        <p><strong>逻辑:</strong>不仅是物理重叠,还涉及到颜色的数学运算。常用于艺术化排版。</p>
        <div class="container blend-container">
            <div class="box box-1" style="background: cyan;"></div>
            <div class="box box-2 blend-box" style="background: yellow; margin-left: -50px;">Multiply</div>
        </div>
    </div>

</body>
</html>

深度对比与总结

实现方式 物理重叠 脱离文档流 容器高度自适应 性能 最佳场景
Position Absolute 弹出层、气泡、固定位置的 UI
Negative Margin 稍微偏离原位的装饰效果
CSS Grid 极佳 复杂的卡片内部叠加、响应式重叠布局
Transform 是 (按原位) 最优 悬浮动画、位移特效
Pseudo-elements 遮罩层、按钮修饰、视觉背景

关键知识点:层叠上下文 (Stacking Context)

在讨论叠加时,必须提到 z-index。但 z-index 并不是绝对的,它受到“层叠上下文”的限制:

  1. 根元素:HTML 文档本身就是一个层叠上下文。
  2. 定位元素position 值为 absolute/relativez-index 不为 auto 的元素。
  3. 现代属性opacity 小于 1、transform 不为 nonefilter 不为 noneflex/grid 子元素设置了 z-index 都会创建新的层叠上下文。

避坑指南:如果你发现设置了 z-index: 9999 依然被另一个 z-index: 1 的元素盖住,通常是因为两者的父元素处于不同的层叠上下文中,而父层级的顺序已经决定了胜负。

前端-请求接口中断处理指南

请求中断处理指南

📋 今日处理总结

处理的问题

  1. 实现 eligibleForPrizeExchange 请求中断:当打开 auto select 时,中断正在进行的 eligibleForPrizeExchange 请求
  2. 实现 target-prizes 请求中断:当关闭 auto select 时,中断正在进行的 target-prizes 请求
  3. 解决竞态条件问题:防止旧请求的结果覆盖新请求的结果
  4. 修复 loading 状态管理:确保 loading 状态能正确显示和更新

核心实现逻辑

1. 使用 AbortController + RequestId 双重保障
// 1. 定义状态管理
const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);

// 2. 在请求函数中
const fetchData = async () => {
    // 中断之前的请求
    if (abortController.value) {
        abortController.value.abort();
    }
    
    // 创建新的 AbortController 和 requestId
    const requestId = ++currentRequestId.value;
    const newAbortController = new AbortController();
    abortController.value = newAbortController;
    
    try {
        // 检查 requestId 是否仍然是最新的
        if (requestId !== currentRequestId.value) {
            return; // 已被新请求取代,直接返回
        }
        
        const signal = newAbortController.signal;
        
        // 发送请求,传递 signal
        const result = await apiCall(params, signal);
        
        // 请求完成后,再次检查 requestId
        if (requestId !== currentRequestId.value) {
            return; // 已被新请求取代,忽略结果
        }
        
        // 检查当前状态是否仍然匹配
        if (!shouldProcessResult()) {
            return; // 状态已改变,不处理结果
        }
        
        // 处理结果
        updateData(result);
        
    } catch (error) {
        // 如果是 AbortError,不需要处理错误
        if (error instanceof Error && error.name !== 'AbortError') {
            handleError(error);
        }
    } finally {
        // 只有当前请求完成时,才清理 AbortController
        if (requestId === currentRequestId.value) {
            abortController.value = null;
        }
    }
};
2. 在 Store 中传递 signal
// stores/example.ts
async function fetchData(params: any, signal?: AbortSignal) {
    const uniqueKey = `fetchData-${Date.now()}-${Math.random()}`;
    const { data, execute } = useApiFetch(url, {
        method: "POST",
        body: JSON.stringify(params),
        signal: signal,
        key: uniqueKey, // 确保每次创建新实例
    });
    
    await execute();
    
    // 处理结果...
}

// 在 catch 中处理 AbortError
catch (e: any) {
    if (e?.name === 'AbortError' || e?.message?.includes('aborted')) {
        return { success: false, data: null, message: 'Request aborted' };
    }
    // 处理其他错误...
}
3. 在 useApiFetch 中动态设置 signal
// composables/useApiFetch.ts
onRequest({ options }) {
    if (signal !== undefined && signal !== null) {
        options.signal = signal;
        // 检查 signal 是否已经被中断
        if (signal.aborted) {
            throw new DOMException('The operation was aborted.', 'AbortError');
        }
    }
}

🎯 通用处理模式

模式一:单一请求中断

场景:用户快速操作,需要中断之前的请求

const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);

const fetchData = async () => {
    // 1. 中断之前的请求
    if (abortController.value) {
        abortController.value.abort();
    }
    
    // 2. 创建新的请求标识
    const requestId = ++currentRequestId.value;
    const controller = new AbortController();
    abortController.value = controller;
    
    try {
        // 3. 检查是否仍然是最新请求
        if (requestId !== currentRequestId.value) return;
        
        // 4. 发送请求
        const result = await apiCall(controller.signal);
        
        // 5. 再次检查
        if (requestId !== currentRequestId.value) return;
        
        // 6. 处理结果
        processResult(result);
        
    } catch (error) {
        if (error.name !== 'AbortError') {
            handleError(error);
        }
    } finally {
        if (requestId === currentRequestId.value) {
            abortController.value = null;
        }
    }
};

模式二:多请求互斥中断

场景:两个不同的请求,一个触发时中断另一个

const requestAController = ref<AbortController | null>(null);
const requestBController = ref<AbortController | null>(null);

const fetchA = async () => {
    // 中断 B
    if (requestBController.value) {
        requestBController.value.abort();
        requestBController.value = null;
    }
    
    // 开始 A
    const controller = new AbortController();
    requestAController.value = controller;
    // ... 发送请求
};

const fetchB = async () => {
    // 中断 A
    if (requestAController.value) {
        requestAController.value.abort();
        requestAController.value = null;
    }
    
    // 开始 B
    const controller = new AbortController();
    requestBController.value = controller;
    // ... 发送请求
};

模式三:状态检查 + RequestId 双重验证

场景:请求结果需要匹配当前状态(如 autoSelect 状态)

const fetchData = async () => {
    const requestId = ++currentRequestId.value;
    const controller = new AbortController();
    
    try {
        const result = await apiCall(controller.signal);
        
        // 1. 检查 requestId(防止竞态条件)
        if (requestId !== currentRequestId.value) return;
        
        // 2. 检查状态(确保结果匹配当前状态)
        if (!isValidState()) return;
        
        // 3. 处理结果
        updateData(result);
        
    } catch (error) {
        // 处理错误...
    }
};

🔍 常见场景及处理方法

场景 1:搜索框输入

问题:用户快速输入,需要中断之前的搜索请求

解决方法

const searchController = ref<AbortController | null>(null);
const searchRequestId = ref<number>(0);

const handleSearch = debounce(async (keyword: string) => {
    // 中断之前的搜索
    if (searchController.value) {
        searchController.value.abort();
    }
    
    const requestId = ++searchRequestId.value;
    const controller = new AbortController();
    searchController.value = controller;
    
    try {
        if (requestId !== searchRequestId.value) return;
        
        const results = await searchApi(keyword, controller.signal);
        
        if (requestId !== searchRequestId.value) return;
        
        updateSearchResults(results);
    } catch (error) {
        if (error.name !== 'AbortError') {
            handleError(error);
        }
    }
}, 300);

场景 2:标签页切换

问题:切换标签页时,需要中断当前标签页的请求

解决方法

const tabControllers = ref<Map<string, AbortController>>(new Map());

const fetchTabData = async (tabId: string) => {
    // 中断当前标签页的请求
    const currentController = tabControllers.value.get(tabId);
    if (currentController) {
        currentController.abort();
    }
    
    // 创建新的请求
    const controller = new AbortController();
    tabControllers.value.set(tabId, controller);
    
    try {
        const data = await fetchTabDataApi(tabId, controller.signal);
        updateTabData(tabId, data);
    } catch (error) {
        if (error.name !== 'AbortError') {
            handleError(error);
        }
    }
};

场景 3:表单提交

问题:用户快速点击提交按钮,需要防止重复提交

解决方法

const submitController = ref<AbortController | null>(null);
const isSubmitting = ref<boolean>(false);

const handleSubmit = async () => {
    if (isSubmitting.value) {
        // 中断之前的提交
        if (submitController.value) {
            submitController.value.abort();
        }
    }
    
    isSubmitting.value = true;
    const controller = new AbortController();
    submitController.value = controller;
    
    try {
        await submitForm(formData, controller.signal);
        showSuccess();
    } catch (error) {
        if (error.name !== 'AbortError') {
            showError(error);
        }
    } finally {
        isSubmitting.value = false;
        submitController.value = null;
    }
};

场景 4:下拉刷新

问题:用户快速下拉刷新,需要中断之前的刷新请求

解决方法

const refreshController = ref<AbortController | null>(null);

const handleRefresh = async () => {
    // 中断之前的刷新
    if (refreshController.value) {
        refreshController.value.abort();
    }
    
    const controller = new AbortController();
    refreshController.value = controller;
    
    try {
        const data = await refreshData(controller.signal);
        updateData(data);
    } catch (error) {
        if (error.name !== 'AbortError') {
            handleError(error);
        }
    } finally {
        refreshController.value = null;
    }
};

✅ 最佳实践

1. 必须使用 RequestId

原因:防止竞态条件,确保只有最新请求的结果被使用

// ✅ 正确
const requestId = ++currentRequestId.value;
// ... 请求完成后检查
if (requestId !== currentRequestId.value) return;

// ❌ 错误:没有 requestId,无法防止竞态条件
const controller = new AbortController();

2. 多次检查 RequestId

原因:异步操作中,状态可能随时改变

// ✅ 正确:在关键步骤前都检查
const result = await apiCall();
if (requestId !== currentRequestId.value) return;

processData(result);
if (requestId !== currentRequestId.value) return;

updateUI(result);

3. 状态检查 + RequestId 双重验证

原因:确保结果不仅是最新的,还要匹配当前状态

// ✅ 正确
if (requestId !== currentRequestId.value) return;
if (!isValidState()) return; // 检查状态是否匹配

// ❌ 错误:只检查 requestId,不检查状态
if (requestId !== currentRequestId.value) return;
// 直接使用结果,可能状态已改变

4. 正确处理 AbortError

原因:AbortError 是正常的取消操作,不应该显示错误

// ✅ 正确
catch (error) {
    if (error.name !== 'AbortError') {
        handleError(error); // 只处理真正的错误
    }
}

// ❌ 错误:所有错误都处理,包括 AbortError
catch (error) {
    handleError(error); // 会显示"请求被中断"的错误提示
}

5. 在 finally 中清理 AbortController

原因:确保只有当前请求完成时才清理,避免影响新请求

// ✅ 正确
finally {
    if (requestId === currentRequestId.value) {
        abortController.value = null; // 只有当前请求完成时才清理
    }
}

// ❌ 错误:总是清理,可能影响新请求
finally {
    abortController.value = null; // 可能清理了新请求的 controller
}

6. 使用唯一的 key 创建新的 useApiFetch 实例

原因:避免 useFetch 缓存导致的问题

// ✅ 正确
const uniqueKey = `apiCall-${Date.now()}-${Math.random()}`;
const { data, execute } = useApiFetch(url, {
    signal: signal,
    key: uniqueKey, // 确保每次创建新实例
});

// ❌ 错误:使用相同的 key,可能导致缓存问题
const { data, execute } = useApiFetch(url, {
    signal: signal,
    // 没有 key,可能使用缓存的实例
});

📝 代码模板

完整模板

// 1. 定义状态
const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);
const isLoading = ref<boolean>(false);

// 2. 请求函数
const fetchData = async (params: any) => {
    // 中断之前的请求
    if (abortController.value) {
        abortController.value.abort();
    }
    
    // 创建新的请求标识
    const requestId = ++currentRequestId.value;
    const controller = new AbortController();
    abortController.value = controller;
    
    isLoading.value = true;
    
    try {
        // 检查 1:请求开始前
        if (requestId !== currentRequestId.value) {
            isLoading.value = false;
            return;
        }
        
        const signal = controller.signal;
        if (!signal) {
            isLoading.value = false;
            return;
        }
        
        // 发送请求
        const result = await store.fetchData(params, signal);
        
        // 检查 2:请求完成后
        if (requestId !== currentRequestId.value) {
            return;
        }
        
        // 检查 3:状态验证(如果需要)
        if (!isValidState()) {
            return;
        }
        
        // 检查 4:更新数据前
        if (requestId !== currentRequestId.value) {
            return;
        }
        
        // 处理结果
        updateData(result);
        
    } catch (error: any) {
        // 处理错误(忽略 AbortError)
        if (error instanceof Error && error.name !== 'AbortError') {
            handleError(error);
        }
    } finally {
        // 清理(只有当前请求完成时)
        if (requestId === currentRequestId.value) {
            abortController.value = null;
        }
        isLoading.value = false;
    }
};

// 3. 中断函数(在状态改变时调用)
const interruptRequest = () => {
    if (abortController.value) {
        abortController.value.abort();
        abortController.value = null;
        currentRequestId.value++; // 更新 requestId,确保旧请求结果被忽略
    }
};

🚨 注意事项

  1. 不要忘记传递 signal:确保 signal 正确传递到 API 调用
  2. 不要忘记检查 requestId:防止竞态条件
  3. 不要忘记检查状态:确保结果匹配当前状态
  4. 不要忘记处理 AbortError:避免显示"请求被中断"的错误
  5. 不要忘记清理 AbortController:避免内存泄漏
  6. 使用唯一的 key:避免 useFetch 缓存问题

📚 相关文件

  • pages/wallet/components/redemption/WalletSelectionSection.vue - target-prizes 中断实现
  • pages/wallet/components/RedeemWithPrizesModal.vue - eligibleForPrizeExchange 中断实现
  • stores/wallet.ts - API 调用中的 signal 传递
  • composables/useApiFetch.ts - signal 的动态设置

还在用‘Z’字图案密码?这个网站能帮你找到更复杂个性化的手机图案密码

手机图案密码全库检索Web应用

2026.01 | ming


AI Generated Art

图案密码,是我们日常生活中非常熟悉的一种验证方式。无论是手机锁屏、应用加密,还是隐私相册、银行类软件的二次验证,它都以直观、易操作的特点被广泛使用。虽然从安全性角度来说,它可能不如复杂字符密码,但图案密码在记忆门槛和操作趣味性上,有着非常独特的优势。

最近在给新手机设置图案密码的时候,我想了很久要用什么图案当密码,怎样的图案才能既美观别致,又具备足够的迷惑性,让他人难以轻易看透?于是就有了研究图案密码的想法。

通常所说的图案密码,指的是在一个 3×3 的点阵上,依次连接 4 到 9 个不重复的点所形成的路径。现在大部分软件的图案密码至少需要使用到4个点,并且每个点不能重复连接。那么我们就可以把这9个点看成数字小键盘,如下图所示:

[123456789]\begin{bmatrix} 1&2 &3 \\ 4&5 &6 \\ 7&8 &9 \end{bmatrix}

这样一来,每一个图案密码都能对应一串由 1~9 中不重复数字所组成的序列。比如经典的“Z”字形图案,对应的密码序列就是 [1, 2, 3, 5, 7, 8, 9]。因此,图案密码问题就转化为了数字序列的生成与筛选问题。

既然每个点只能使用一次,且密码长度范围为 4~9,那么总的图案数量就是从 9 个点中选取 nn 个点(nn 从 4 到 9),并对它们进行全排列的总和。用数学公式表达就是:

n=49A9n=985824\sum_{n=4}^{9} A_{9}^{n} = 985824

也就是说,一共有 985,824 种可能的图案。这个数字听起来很大,但对于现代计算机而言,完全在可遍历处理的范围内——我们完全可以通过编写筛选算法,快速找出符合特定样式或规则的图案。

如果你觉得 98 万仍然是个令人望而却步的数字,甚至担心遍历会耗时过长,那或许低估了现代计算机的运算能力。当然,我们还可以通过“去冗余”进一步简化问题。仔细观察下面这组图案:

c3.jpg

你会发现,这 8 个图案其实源自同一个“基础图形”,只不过分别经过了 90°、180°、270° 旋转,以及水平翻转、垂直翻转、沿主对角线翻转、沿副对角线翻转等对称变换。在视觉和结构上,它们属于同一类密码模式。因此,如果我们只保留每组对称图案中的“基础版本”,就可以将图案总数量减少至原来的 1/8:

9858248=123228\frac{985824}{8} = 123228

这样一来,我们只需在 12.3 万 个有效图案中进行筛选和检索,效率大大提升,也更便于用户聚焦于真正独特的密码设计。

有了想法,就开始着手设计筛选程序了,我一开始打算使用React框架开发一个Web网页的,但是由于有几年没碰React了,我的React还是5年前学的,当初学习的版本与如今的最新语法差异巨大,一时间感到无从下手。并且我也不愿再花时间重新学习它,于是就干脆决定转向 Vue3,好在有过去的前后端开发基础,学习起来并不算吃力。

更重要的是,如今有了 AI 的加持。回想五年前刚入门前后端时,无论是学习新技术还是动手开发,整个过程都相当漫长,每个新知识都要依赖文档、教程或是反复试错,过程漫长且充满不确定性。而现在,AI 极大地加速了这一进程——遇到问题随时提问,AI 给出的解答往往比许多教学视频更直接、更有针对性。在开发过程中,它的辅助更是让效率大幅提升:想要什么组件,什么布局,什么样式,直接向AI描述清楚即可。就拿这个图案密码筛选网站来说,从动手编码到基本完成,前后只用了两天时间。我的大部分精力都放在了系统架构的设计、用户交互的逻辑、筛选规则的制定、图案的绘制方式,以及如何将各个模块有机整合……这些真正需要思考的问题上。

在 AI 时代,我认为程序员的核心价值,越来越体现在对系统架构的把握与全局设计的视野上。很多人为了省事,倾向于让 AI 智能体直接生成整个应用或大型模块;在我看来,这并不是 AI 的最佳使用方式。没有扎实的技术基础,仅凭 AI 生成的项目无异于空中楼阁,难以稳固、更难以迭代。但当你对一个项目的整体流程有了清晰概念和想法,AI 就能成为你得力的助手,是你在主导AI,而不是AI在领导你。这正是“懂技术”和“不懂技术”的人在使用 AI 时的关键差异,你的技术基础,决定着你的AI生成能力的上界。工具越是强大,人的思维和视野就越显珍贵。

回到正题,你可以直接访问这个网址:narrastory.rth1.xyz/ 来在线体验这个工具。(推荐使用125%页面缩放)

c1.jpg

接下来,我将详细介绍这个工具中提供的几个筛选条件,帮助你更精准地找到心仪的图案密码。

1. 节点数量

节点数量指的是图案密码中连接的点数,范围在 4 到 9 个 之间。点数越多,图案越复杂,可能的组合也越多。

c4.jpg

2. 模式选择

在手机图案密码的设置中,通常有两种连接模式:

  • 限制模式:也是大部分手机的默认模式。当你想连接的两个点之间存在未被使用过的中间点时,系统会自动将该中间点纳入路径。 例如:连接 1→3 会自动变成 1→2→3;连接 1→9 会自动经过 5,变成 1→5→9。 这种模式虽然方便,但也限制了一些特殊“跳点”图案的生成。
  • 非限制模式:关闭限制后,你可以自由连接任意两个点,中间即使有未使用的点也不会被自动加入。 例如:可以直接连接 1→3,系统不会添加点 2

c5.jpg

3. 跳点设置

“跳点”是指在连接路径中,跳过某个中间点直接连接更远的点。例如在序列 [2,1,3] 中,从 1 连接到 3 就跳过了 2;在 [3,2,1,9] 中,从 19 跳过了 5

跳点数量反映了一个图案的“非常规”程度。一般来说,跳点越多的图案看起来越不规则,迷惑性也更强,有助于提高密码的防窥视能力。

c7.jpg

4. 线长排序

“线长”是指构成整个图案的线段总长度。我们可以依次计算相邻两点间的欧几里得距离并累加得到总长。例如下图所示:

c6.jpg

上面这个图案的线长就可以这么计算

线长=5+2+1+26.65线长 = \sqrt{5} + \sqrt{2} + 1 + 2 \approx 6.65

你可以选择“按照总线长降序排列”,让最长的图案排在前列。

5. 线条样式设置

图案密码中的线段可分为三种基本类型:

  1. 直线:水平或垂直方向(如 1→24→7
  2. 45° 斜线:斜率为 ±1(如 1→53→7
  3. 1:2斜线:斜率为 ±2 或 ±1/2(如 1→62→7

c2.jpg

你可以分别设置筛选每种线型的数量,从而控制图案的“几何风格”。比如,只包含 45° 斜线的图案会呈现出整齐的对角线美感;而混合多种线型则会让图案更富有变化。

这个工具不仅是一个密码生成器,也是一次关于排列组合、几何连接与交互设计的轻量实践。如果你对实现细节感兴趣,或希望在本地方便地修改和尝试,项目的完整代码已经开源在:

GitHub:github.com/narrastory/…

需要提醒的是,图案密码在安全性上存在固有弱点。图案密码虽然有趣易记,但如果让他人看到你的图案,即使别人未直接看清连接顺序,但仅凭图案形状,也可能在短时间内推测出可能的连接路径。因此,它更适合用于对安全性要求不高的场景,或作为辅助记忆的趣味选择。

感谢阅读,祝你探索愉快! 🔐✨

前端开发中如何优雅地避免空指针异常?

通常发生 空指针异常(NullPointException) 的场景是,当我们尝试访问一个 nullundefined 值的属性或方法。(服了我,上班还没一会儿就遇上几次没处理好空指针的情况,这是什么新手入门第一坑吗

前端开发中典型空指针错误场景

1. 访问空对象

对象声明但未赋值,或赋值为 null / undefined,然后尝试访问其属性,会出现空指针异常。通常在开发时,应该使用可选链访问对象,并为空值提供默认值。

let userProfile;
console.log(userProfile.name); // TypeError: Cannot read property 'name' of undefined
console.log(userProfile?.name ?? '访客'; // 可选链 + 空值合并

2. 函数参数未传递

函数期望接收一个对象,但调用时传递了 nullundefined,会导致空指针异常。

function processUser(user) {
  return user.name;
}
processUser(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

如果函数参数期望接收一个对象,建议为它提供一个默认值(通过是空对象或一个特定结构)。也可以通过可选链、if语句判空等方式来避免空指针异常。

// 提供一个空对象的默认值(适合内部逻辑依赖特定结构)
function processUser1(user = {}) {
  return user.name ?? 访客
}

// 可选链(适合简单的属性访问)
function processUser2(user) {
  return user?.name ?? 访客
}

// if语句判空(适合需要复杂逻辑处理的场景)
function processUser3(user) {
  if (!user) {
    return '访客';
  }
  return user.name;
}

3. 访问空数组

数组是 null,但尝试通过索引或属性访问数组元素,会出现空指针异常。

const users = null;
console.log(users[0]); // TypeError: Cannot read properties of null

为了安全访问数组,要保证该变量是数组类型 x、数组不为空、索引有效等,例如:

const getUserName = (users, index) => {
  // 1. 验证数组存在且是数组类型
  if (!Array.isArray(users)) return '未知用户';
  
  // 2. 验证索引有效
  if (index < 0 || index >= users.length) return '用户不存在';
  
  // 3. 安全访问嵌套属性
  return users[index]?.name ?? '匿名用户';
}

4. 异步数据

从API获取数据,在数据返回前尝试访问数据属性。

let apiData;
fetchData().then(data => { apiData = data; });
console.log(apiData.items); // Uncaught TypeError: Cannot read properties of undefined (reading 'items')

应该正确使用 async/await、promise 等异步处理来确保数据已获取。

// 使用async/await
async function fetchDataAndUse() {
  const data = await fetchData();
  console.log(data?.property);
}

// 使用promise链
fetchData().then(data => {
  console.log(data?.property);
});

5. DOM元素

试图操作尚未渲染或已移除的DOM元素。例如,当试图访问 ctx 时,可能会因为访问到空值导致异常,此时应该事先判空来防止可能的报错。

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); // 由于浏览器环境不同,ctx可能为空

// 进行条件判断,防止ctx不存在导致的空指针异常
if (!ctx) {
  console.error('Failed to get canvas 2d context');
  return;
}

ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

使用监听器时也常常需要注意:

// 元素可能不存在
const button = document.querySelector('#non-existent');
// 错误:button可能为null
button.addEventListener('click', handler);

// 正确:使用可选链
button?.addEventListener('click', handler);

// 正确:if判空验证元素存在
if (button) {
  button.addEventListener('click', handler);
}

总结解决方案

  • 可选链操作符(?.) :在访问属性或调用方法时使用,如果前面的值为null或undefined,则返回undefined。
  • 空值合并运算符(??) :为可能为null或undefined的变量提供默认值。
  • 默认参数:在函数定义中为参数提供默认值。
  • 条件判断:在访问变量前检查其是否为null或undefined。
  • 初始化变量:确保变量在使用前已被正确初始化。
  • 使用类型检查:如Array.isArray()检查数组,typeof检查类型等。

在日常开发过程中,要防止空指针异常影响代码健壮性。对于可能为空的变量,始终假设它可能为空,并采取防御性编程。

❌