Vue-Mixin-RenderTrack

背景

  • 开始使用 vue 3.x 开发项目, 并使用 pinia 等相关新的生态 并阅读相关文档
  • 希望基于 vue 3 写点代码, 用了 新的 dev_tools, 功能丰富了很多.
  • 最近几个项目里都遇到了不同情况下的性能问题

RenderTracker 的作用

vue 3 生命周期 onRenderTriggered 文档

Registers a debug hook to be called when a reactive dependency triggers the component’s render effect to be re-run. 是一个 debug 模式下的 hook, 会因为响应式的依赖触发组件的重新渲染

https://vuejs.org/api/composition-api-lifecycle.html#onrendertriggered

综合文档看, 这个 hook 比较适合查看是否某些组件过多的频繁渲染, 并给出一些简单的信息协助定位问题

小问题

因为这个工具的注入是针对所有的组件为目标的

但是: vue 3.x 本身似乎没有提供, 也不提倡这种全局的无差别注入, 特别是咱们还想注入到 hook 中让它自动执行

In Vue 2, mixins were the primary mechanism for creating reusable chunks of component logic. While mixins continue to be supported in Vue 3, Composition API is now the preferred approach for code reuse between components.

从某些方面看, vue 3.x 并没有完全覆盖 vue 2.x 的所有能力. 比如 https://vuejs.org/guide/components/attrs.html#disabling-attribute-inheritance

所以这个小工具使用 options 的写法来注入还是可以理解的

工具说明

输出信息包含 3 种组件分类:

  • 路由组件

    可以输出当时的路由信息

  • 具名组件 (可以获取名字的,一般来说 组件库的都是有名字的)

    图中未包含,只是显示名字,只需要看哪些是否触发非常频繁就足够了

  • 无名组件 (一般都是业务自己写的)

    会输出 操作类型 / key / value , 并可以展开 trace 信息协助定位到具体代码位置

工具实现

import {
defineComponent,
type ComponentPublicInstance,
type DebuggerEvent,
type Ref,
} from "vue";
import type { RouteLocationNormalizedLoaded } from "vue-router";

// 配置参数
export interface RenderTrackOptions {
exclusive: RenderTrackKind[];
}

// 组件类型
export enum RenderTrackKind {
Router, // 路由组件
NamedComponent, // 具名组件
UnnamedComponent, // 无名组件
}

const commonStyle = ["color: gray", "padding: 3px 5px", "border-radius:10px"];
const style = [...commonStyle, "background: greenyellow"].join(";");

const pageStyle = ["background: gold", ...commonStyle].join(";");

const routerStyle = ["background: #ef94ff", ...commonStyle].join(";");
const renderStyle = [...commonStyle, "background:#de4242", "color:black"].join(
";"
);

export default (
options: RenderTrackOptions = {
exclusive: [],
}
) =>
defineComponent({
renderTriggered(e: DebuggerEvent) {
if (
options.exclusive.length ==
[
RenderTrackKind.NamedComponent,
RenderTrackKind.Router,
RenderTrackKind.UnnamedComponent,
].length
) {
// 如果所有类型都被排除, 则无需执行
return;
}

[runUnnamedComponent, runRoute, runNamedComponent].forEach((fn) => {
if (fn(options, this, e)) {
// 有一个匹配的就中断
return;
}
});
},
beforeUpdate() {
const name = extractFileName(this.$options.__file);
if (name) {
console.log(
`%c ${String.fromCodePoint(0x26a1)} ${name} Render`,
renderStyle
);
}
},
});

/**
* 根据 trace 路径获取到文件名
* @param {string} [filePath]
* @returns {string} 文件名字
*/
function extractFileName(filePath?: string): string {
const p = filePath || "";
const arr = p.split("/");
return arr[arr.length - 1];
}

function runRoute(
options: RenderTrackOptions,
currentComponent: ComponentPublicInstance,
e: DebuggerEvent
): boolean | undefined {
// 获取组件名字
let name =
currentComponent.$options.name || currentComponent.$options._componentTag;
// 判断是否是路由, 或者用 (e.target as Ref<RouteLocationNormalizedLoaded>)?.value?.fullPath 来判断
const isRouter = name == "RouterView";
const hasPermission = !options.exclusive.includes(RenderTrackKind.Router);
if (isRouter && hasPermission) {
// 输出路由组件信息
const route = (e.target as Ref<RouteLocationNormalizedLoaded>).value;
name = `Router: ${route.fullPath || route.path}`;
console.groupCollapsed(
`%c ${String.fromCodePoint(0x1f6b4)} ${name} `,
routerStyle
);
console.log(`${String.fromCodePoint(0x1f308)} Route`, route);
console.groupEnd();
return true;
}
}

function runNamedComponent(
options: RenderTrackOptions,
currentComponent: ComponentPublicInstance,
_: DebuggerEvent
): boolean | undefined {
// 获取组件名字
const name =
currentComponent.$options.name || currentComponent.$options._componentTag;

const hasPermission = !options.exclusive.includes(
RenderTrackKind.NamedComponent
);

if (name && hasPermission) {
// 输出具名组件信息
console.log(`%c ${String.fromCodePoint(0x1f6b4)} ${name} `, style);
return true;
}
}

function runUnnamedComponent(
options: RenderTrackOptions,
currentComponent: ComponentPublicInstance,
e: DebuggerEvent
): boolean | undefined {
// 获取组件名字
const name =
currentComponent.$options.name || currentComponent.$options._componentTag;
const hasPermission = !options.exclusive.includes(
RenderTrackKind.UnnamedComponent
);

if (!name && hasPermission) {
// 输出无名组件信息
console.groupCollapsed(
`%c ${String.fromCodePoint(0x1f6b4)} ${extractFileName(
currentComponent.$options.__file
)}`,
pageStyle
);

console.log(
`${String.fromCodePoint(0x1f91c)} ${e.type.toUpperCase()} [${e.key}] `,
e.newValue || (e.target as Ref<unknown>).value
);

console.groupCollapsed(`${String.fromCodePoint(0x1f308)} Trace`);
console.trace("");
console.groupEnd();
console.groupEnd();
return true;
}
}

注入使用

// 排除具名(大部分是组件库)组件的信息, 默认是都不排除
if (import.meta.env.DEV) {
app.mixin(
renderTrack({
exclusive: [RenderTrackKind.NamedComponent],
})
);
}

// 全部输出
if (import.meta.env.DEV) {
app.mixin(
renderTrack()
);
}