2025面试大全(9)

259 阅读55分钟

1. vue 中 Scoped Styles 是如何实现样式隔离的?说说具体的原理

在Vue中,Scoped Styles是通过在<style>标签上添加scoped属性来实现的。这允许开发者定义只作用于当前组件的样式,而不会影响到其他组件。具体的实现原理如下:

  1. 唯一属性选择器: 当Vue编译带有scoped属性的<style>标签时,它会为组件内的所有元素添加一个唯一的属性,例如data-v-f3f3eg9。这个属性是随机生成的,确保每个组件的属性是唯一的。
  2. 修改CSS规则: Vue会修改组件内的所有CSS规则,使其选择器包含这个唯一的属性。例如,原始的CSS规则:
    .example {
      color: red;
    }
    
    会被修改为:
    .example[data-v-f3f3eg9] {
      color: red;
    }
    
    这样,只有拥有相应属性的元素才会被这个CSS规则选中。
  3. 深度选择器: 如果需要穿透scoped样式影响到子组件,可以使用>>>/deep/::v-deep选择器。例如:
    ::v-deep .child-component {
      color: blue;
    }
    
    这允许开发者有选择性地覆盖子组件的样式。
  4. 动态样式: 对于动态添加的样式,Vue会通过JavaScript在运行时将唯一的属性添加到相应的DOM元素上,以确保样式仍然scoped。 通过这些机制,Vue实现了Scoped Styles的样式隔离,使得组件的样式不会泄露到外部,也不会被外部样式所影响,从而保持了组件的封装性和可重用性。 需要注意的是,虽然scoped样式提供了隔离,但它并不是完全隔离的。例如,它不能阻止通过内联样式或!important声明的样式。因此,在使用scoped样式时,仍然需要谨慎以避免潜在的样式冲突。

2. React 中的 forwardsRef,作用是什么, 有哪些使用场景?

在React中,forwardRef 是一个高阶组件(HOC),用于在函数组件中转发引用(ref)到子组件。由于函数组件没有实例,因此它们无法直接接收ref属性。forwardRef 允许我们在函数组件中获取对子组件的引用,从而可以访问子组件的DOM节点或实例。

作用:

  1. 获取子组件的引用:允许父组件通过ref访问子组件的DOM节点或实例。
  2. 传递引用:可以将父组件的ref传递给子组件,使得子组件可以将其内部的DOM节点或实例暴露给父组件。

使用场景:

  1. 访问子组件的DOM节点: 当需要直接操作子组件的DOM节点时,例如聚焦输入框、滚动到特定位置等。
    const InputComponent = forwardRef((props, ref) => {
      return <input ref={ref} {...props} />;
    });
    // 父组件中使用
    const inputRef = useRef();
    useEffect(() => {
      inputRef.current.focus();
    }, []);
    return <InputComponent ref={inputRef} />;
    
  2. 与第三方库集成: 当使用需要引用的第三方库(如D3、jQuery插件等)时,可以通过forwardRef将引用传递给这些库。
  3. 封装高阶组件: 在创建高阶组件时,可能需要将ref传递给被包装的组件。
    const withLogging = WrappedComponent => {
      return forwardRef((props, ref) => {
        useEffect(() => {
          console.log('Component mounted');
          return () => console.log('Component unmounted');
        }, []);
        return <WrappedComponent ref={ref} {...props} />;
      });
    };
    
  4. 在函数组件中使用createRefuseRef: 当需要在函数组件内部创建引用并传递给子组件时。
    const ParentComponent = () => {
      const childRef = useRef();
      // 可以通过childRef访问子组件
      return <ChildComponent ref={childRef} />;
    };
    
  5. 实现受控组件: 在受控组件中,可能需要通过ref来直接访问子组件的值或状态。
  6. 自定义DOM组件: 当创建自定义DOM组件(如封装原生的<input><select>等)时,需要使用forwardRef来允许父组件通过ref访问这些DOM元素。

注意事项:

  • forwardRef 需要与 useRefcreateRef 结合使用。
  • forwardRef 只能在函数组件中使用,类组件本身就可以接收ref属性。
  • 在使用forwardRef时,确保子组件能够正确地处理传递给它的ref。 通过forwardRef,React函数组件可以获得与类组件相似的引用处理能力,使得函数组件在需要引用的场景下更加灵活和强大。

3. 实现一个 cookies 解析函数,输出结果要求是一个对象

要实现一个简单的cookies解析函数,我们需要将浏览器中的cookie字符串转换为一个JavaScript对象。cookie字符串通常由多个键值对组成,每个键值对之间由分号和空格分隔(; ),每个键值对内部由等号连接(=)。 以下是一个基本的cookies解析函数的实现:

function parseCookies(cookieStr) {
  const cookies = {};
  if (cookieStr) {
    const cookiePairs = cookieStr.split('; ');
    cookiePairs.forEach(pair => {
      const [key, value] = pair.split('=');
      cookies[decodeURIComponent(key)] = decodeURIComponent(value);
    });
  }
  return cookies;
}
// 示例使用
const cookieStr = 'username=John%20Doe; sessionToken=abc123; path=/';
const cookies = parseCookies(cookieStr);
console.log(cookies);
// 输出:{ username: 'John Doe', sessionToken: 'abc123', path: '/' }

这个函数首先检查输入的cookie字符串是否为空。如果不为空,它会按照分号和空格分割字符串,得到每个键值对。然后,对于每个键值对,它会按照等号分割,得到键和值。最后,它使用decodeURIComponent函数对键和值进行解码(因为cookie值可能会被编码),并将它们添加到结果对象中。 请注意,这个简单的解析函数没有处理所有可能的cookie属性(如expiresdomainsecure等),也没有处理cookie的编码或解码中的特殊情况。在实际应用中,可能需要更复杂的解析器来处理这些情况。

4. V8 里面的 JIT 是什么?

V8 是 Google 开发的一个开源的 JavaScript 引擎,用于 Google Chrome 和 Node.js 等。V8 使用即时编译(Just-In-Time compilation,简称 JIT)技术来提高 JavaScript 的执行效率。 JIT 是一种编译器策略,它将源代码或字节码在运行时动态地编译成本地机器码,然后执行。这种策略结合了解释执行和传统编译执行的优点,既提供了快速启动,又通过编译优化提高了执行速度。 V8 中的 JIT 编译过程大致如下:

  1. 解析(Parsing):V8 首先将 JavaScript 源代码解析成抽象语法树(AST)。
  2. 生成字节码(Bytecode Generation):从 AST 生成字节码。字节码是一种介于源代码和机器码之间的中间表示,它比源代码更接近机器码,但仍然与平台无关。
  3. 解释执行(Interpretation):V8 使用一个解释器来执行字节码。这是为了快速启动和执行代码。
  4. 热点优化(HotSpot Optimization):V8 会监控代码的执行,识别出频繁执行的热点代码(HotSpot)。
  5. 即时编译(JIT Compilation):一旦识别出热点代码,V8 会将这部分字节码编译成本地机器码,以便更快地执行。
  6. 优化编译(Optimized Compilation):V8 还会进一步对热点代码进行优化编译,以生成更高效的机器码。
  7. 反优化(Deoptimization):如果优化假设不成立(例如,类型变化导致优化不再有效),V8 会回退到解释执行或非优化编译的代码。 V8 的 JIT 编译器包括几个关键组件:
  • Ignition:一个高效的字节码解释器,用于快速启动和执行代码。
  • TurboFan:一个优化编译器,用于生成高效的本机机器码。
  • Orinoco:一个垃圾回收器,负责管理内存。 通过这种 JIT 编译策略,V8 能够在保持快速启动的同时,提供高效的代码执行性能,这是现代 JavaScript 引擎的一个重要特性。

5. React 19 有哪些新特性?

React 19 引入了许多新特性和改进,以下是一些主要的新特性:

1. Actions

Actions 是 React 19 中引入的一项革命性新功能,旨在简化异步数据处理的复杂逻辑。通过支持异步函数,Actions 可以自动管理数据变更、加载状态、错误处理和乐观更新(optimistic updates)。

  • 自动管理 Pending 状态:使用 useActionStateuseFormStatus 等新钩子轻松处理表单的加载状态。
  • 内置乐观更新支持:通过 useOptimistic 钩子实现实时数据更新。
  • 更智能的错误处理:集成错误边界,简化错误回退逻辑。

2. React 编译器

React 19 引入了一个新的编译器,目前已在 Instagram 上使用。这个编译器将在未来版本的 React 中全面发布,预计会显著提升性能和开发体验。

3. 服务器组件

React 19 正式引入了服务器组件的概念,现在可以在 Next.js 中使用。这允许开发者将部分组件渲染逻辑放在服务器端,从而提高应用性能和用户体验。

4. Web 组件

React 19 增加了对 Web 组件的集成支持,使得开发者能够更方便地使用和集成 Web 组件,从而开启更多的开发可能性。

5. 文档元数据

React 19 引入了文档元数据的功能,使开发人员能够用更少的代码实现更多功能,提高开发效率和可维护性。

6. 资源加载

React 19 改进了资源加载机制,支持在后台加载资源,从而改善应用程序的加载时间和用户体验。

7. 新的 React Hooks

React 19 引入了一些新的 Hooks,如 use()useFormStatus()useFormState()useOptimistic(),这些 Hooks 进一步简化了状态管理和数据处理逻辑。

8. React DOM 静态 API

React 19 添加了新的 React DOM 静态 API,提供了更多的灵活性和控制能力。

9. 改进 Suspense

React 19 对 Suspense 进行了改进,包括预热 suspend 树,以进一步优化异步组件的加载和渲染。 这些新特性和改进使得 React 19 在性能、开发效率和用户体验方面都有了显著的提升。如果你有兴趣,可以参考官方文档和升级指南来了解更多详细信息并进行升级。

6. less 文件中怎么使用函数?

在 Less 中,你可以使用函数来操作颜色、数学计算、类型转换等。Less 提供了一系列内置函数,同时你也可以自定义函数。以下是使用 Less 函数的一些示例:

内置函数

1. 颜色函数
@base-color: #f04615;
// 使用 lighten 函数将颜色变亮
@lighter-color: lighten(@base-color, 10%);
// 使用 darken 函数将颜色变暗
@darker-color: darken(@base-color, 10%);
2. 数学函数
@width: 100px;
@height: 200px;
// 使用 add 函数进行加法运算
@total-width: add(@width, 20px);
// 使用 subtract 函数进行减法运算
@total-height: subtract(@height, 20px);
3. 类型转换函数
@string-value: "100px";
@number-value: 200px;
// 使用 to-number 函数将字符串转换为数字
@converted-number: to-number(@string-value);
// 使用 to-string 函数将数字转换为字符串
@converted-string: to-string(@number-value);

自定义函数

你也可以使用 .mixin 来创建自定义函数:

// 定义一个简单的 mixin 函数
.rounded-corners(@radius: 5px) {
  border-radius: @radius;
  -webkit-border-radius: @radius;
  -moz-border-radius: @radius;
}
// 使用自定义函数
#box {
  .rounded-corners(10px);
}

在上面的例子中,.rounded-corners 是一个自定义的 mixin 函数,它接受一个参数 @radius,并应用圆角样式。

使用 JavaScript 函数

Less 还允许你使用 JavaScript 代码:

@base-color: #f04615;
// 使用 JavaScript 的 rgb 函数
@color-from-rgb: rgb(255, 70, 21);
// 使用 JavaScript 的 Math 函数
@random-color: rgb(Math.round(Math.random() * 255), Math.round(Math.random() * 255), Math.round(Math.random() * 255));

在 Less 中使用函数可以极大地提高样式的复用性和维护性。你可以根据需要选择内置函数或创建自定义函数来满足你的需求。更多关于 Less 函数的信息,可以查看 Less 官方文档

7. 怎么实现 commit lint?

实现 commit lint(提交信息 linting)的目的是为了确保每个 Git 提交信息都符合特定的格式和风格。这可以通过几种方法实现,其中最常见的是使用 commitlint 工具配合 Husky 的 Git 钩子。 以下是实现 commit lint 的步骤:

1. 安装 commitlint

首先,你需要安装 commitlint 及其相关配置:

npm install --save-dev @commitlint/config-conventional @commitlint/cli

2. 创建配置文件

在项目的根目录下创建一个 commitlint.config.js 文件,并配置 commitlint

module.exports = {
  extends: ['@commitlint/config-conventional']
};

@commitlint/config-conventional 提供了一套基于 Angular 提交指南的默认规则。

3. 安装 Husky

Husky 是一个用于管理 Git 钩子的工具,可以帮助你在提交前运行 commitlint

npm install --save-dev husky

4. 配置 Husky

package.json 中配置 Husky,以使用 commitlint

{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

这样,每次提交时,Husky 都会调用 commitlint 来检查提交信息。

5. 自定义规则(可选)

如果你需要自定义提交信息的规则,可以在 commitlint.config.js 中进行配置:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
    'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]
  }
};

6. 使用

现在,每次你尝试提交时,commitlint 都会自动检查你的提交信息。如果信息不符合配置的规则,提交将会被阻止,并提示你修正。

示例

假设你的提交信息如下:

git commit -m "Fix bug in user profile"

如果这个信息不符合你在 commitlint.config.js 中设置的规则,你将会看到类似以下的错误信息:

input: Fix bug in user profile
✖   subject does not match subject pattern [expected:.fix, actual:Fix]
✖   type must be one of [feat, fix, docs, style, refactor, test, chore] [type:subject]

这时,你需要修改提交信息以符合规则,例如:

git commit -m "fix(user-profile): fix bug in user profile"

这样,提交信息就符合了 commitlint 的规则,提交将会成功。 通过实施 commit lint,你可以确保团队遵循一致的提交信息格式,这有助于维护清晰的项目历史和自动化流程。更多关于 commitlint 的配置和规则,可以查看 commitlint 官方文档

8. React 中,如何避免使用 context 时, 引起整个挂载节点树的重新渲染

在 React 中,使用 Context 提供了一个强大的方式来跨组件传递数据,但如果不正确使用,可能会导致不必要的渲染性能问题。以下是一些避免使用 Context 时引起整个挂载节点树重新渲染的策略:

1. 使用 React.memoPureComponent

对于不需要重新渲染的组件,可以使用 React.memo(对于函数组件)或 PureComponent(对于类组件)来避免不必要的渲染。

const MyComponent = React.memo(function MyComponent(props) {
  // 组件内容
});

2. 分解 Context

将 Context 分解为更细粒度的 Context,这样只有相关的组件会在 Context 值变化时重新渲染。

const ThemeContext = React.createContext();
const UserContext = React.createContext();
// 使用时,只订阅需要的 Context
<ThemeContext.Consumer>
  {theme => (
    <UserContext.Consumer>
      {user => (
        <MyComponent theme={theme} user={user} />
      )}
    </UserContext.Consumer>
  )}
</ThemeContext.Consumer>

3. 使用 contextSelector

如果你使用的是 React 18 及以上版本,可以利用 contextSelector 来只订阅 Context 中需要的一部分数据。

const ThemeContext = React.createContext();
// 使用时,只选择需要的部分
const theme = useContext(ThemeContext, (contextValue) => contextValue.theme);

4. 避免在 Context 中传递大对象

尽量避免在 Context 中传递大对象或复杂的数据结构,因为这会导致任何订阅了该 Context 的组件在数据变化时重新渲染。尽量传递简单的值或使用多个 Context。

5. 使用 useContextSelector

如果你的项目中使用了 react useContextSelector 这个库,可以利用它来选择 Context 中的特定字段,从而减少不必要的渲染。

import { useContextSelector } from 'use-context-selector';
const theme = useContextSelector(ThemeContext, (context) => context.theme);

6. 优化 Context 的更新

确保只在必要时更新 Context 的值。可以使用 useMemouseCallback 来避免不必要的重新渲染。

const value = useMemo(() => ({ theme, user }), [theme, user]);

7. 使用 unstatedmobx-react

考虑使用状态管理库如 unstatedmobx-react,这些库提供了更细粒度的控制,可以减少不必要的渲染。

8. 分析渲染性能

使用 React Developer Tools 的 Profiler 来分析组件的渲染性能,找出不必要的渲染并优化。

示例

以下是一个简单的示例,展示如何使用 React.memouseContextSelector 来避免不必要的渲染:

import React, { createContext, useContext, useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
const ThemeButton = React.memo(function ThemeButton() {
  const toggleTheme = useContextSelector(ThemeContext, (context) => context.toggleTheme);
  return <button onClick={toggleTheme}>Toggle Theme</button>;
});
const ThemeDisplay = React.memo(function ThemeDisplay() {
  const theme = useContextSelector(ThemeContext, (context) => context.theme);
  return <div>The current theme is: {theme}</div>;
});
function App() {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({
    theme,
    toggleTheme: () => setTheme(theme === 'light' ? 'dark' : 'light'),
  }), [theme]);
  return (
    <ThemeContext.Provider value={value}>
      <ThemeButton />
      <ThemeDisplay />
    </ThemeContext.Provider>
  );
}

在上述示例中,ThemeButtonThemeDisplay 组件都使用了 React.memouseContextSelector 来避免不必要的渲染。只有当它们依赖的特定部分 of the context 发生变化时,它们才会重新渲染。 通过这些策略,你可以有效地减少使用 Context 时引起的整个挂载节点树的重新渲染,从而提高 React 应用的性能。

9. React 的循环渲染中,为什么不推荐使用 index 作为元素的 key?

在 React 中,使用数组索引作为元素的 key 是不推荐的,主要原因如下:

1. 性能问题

  • 重复渲染:当数组项的顺序发生变化时,即使内容没有变化,React 也会认为每个元素都发生了变化,从而导致不必要的重新渲染。
  • 重新排序成本高:如果列表的顺序经常变化,React 需要花费更多的时间来重新排序和重新渲染元素。

2. 不稳定性

  • 索引变化:数组中的项可能会被添加、删除或重新排序,这会导致索引变化。如果使用索引作为 key,React 可能无法正确识别哪些项实际上没有变化。
  • 状态错位:在组件内部状态或动画中,使用索引作为 key 可能会导致状态错位,因为 React 可能会错误地复用组件实例。

3. 错误的元素复用

  • 错误的复用:React 使用 key 来识别哪些元素被更改、添加或删除。如果使用索引作为 key,React 可能会错误地复用组件,导致显示错误的数据。

4. 列表操作问题

  • 插入和删除:在列表中间插入或删除项时,后续元素的索引都会改变,这会导致整个列表的重新渲染,而不是只渲染受影响的元素。

示例

假设有一个简单的列表,使用索引作为 key

function List({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

如果列表的顺序发生变化,例如通过排序,React 会认为每个元素都发生了变化,从而导致整个列表的重新渲染。

正确的做法

  • 使用唯一标识符:尽量使用每个列表项的唯一标识符(如数据库中的 ID)作为 key
  • 稳定的生成 key:如果确实没有唯一标识符,可以考虑使用稳定的生成 key 的方法,例如结合内容和其他属性。
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.text}</li> // 使用 item.id 作为 key
      ))}
    </ul>
  );
}

总结

使用索引作为 key 在某些情况下可能会导致性能问题、状态错位和错误的元素复用。为了确保 React 能够正确地识别和处理列表中的元素,应尽量使用唯一且稳定的标识符作为 key。这样可以提高性能,避免潜在的问题,并使组件的行为更加可预测。

10. 微前端架构中,一般是如何做到 JavaScript 隔离的?

微前端架构中,JavaScript 隔离是确保不同微前端应用之间不会相互干扰的关键。以下是一些常见的实现 JavaScript 隔离的方法:

1. 使用 Web Components

  • Shadow DOM:通过 Shadow DOM,每个微前端都可以有自己的封闭的 DOM 环境,从而避免样式和脚本的冲突。
  • Custom Elements:定义自定义元素,封装微前端的逻辑,减少全局作用域的污染。

2. 命名空间隔离

  • 全局变量封装:将微前端的代码封装在一个立即执行函数表达式(IIFE)中,避免污染全局命名空间。
  • 模块化:使用 ES Modules 或 CommonJS 等模块系统,确保每个微前端的代码在模块范围内执行。

3. 构建时隔离

  • 独立打包:每个微前端独立打包,通过构建工具(如 Webpack)的配置,确保打包后的文件不会相互影响。
  • Scope Hoisting:利用构建工具的 Scope Hoisting 功能,将模块的作用域提升,减少全局作用域的污染。

4. 运行时隔离

  • iframe:虽然不常用,但 iframe 可以提供最严格的隔离,每个微前端运行在独立的 iframe 中。
  • 微前端框架:使用微前端框架(如 SingleSPA、Qiankun)提供的运行时隔离机制,这些框架通常提供了沙箱环境来隔离 JavaScript。

5. 样式隔离

  • CSS Modules:使用 CSS Modules,确保样式只作用于特定的组件,避免全局样式冲突。
  • 命名约定:采用独特的命名约定,如 BEM(Block Element Modifier),减少样式冲突。

6. 事件隔离

  • 自定义事件:使用自定义事件而非直接操作 DOM 事件,减少事件冒泡和捕获过程中的冲突。
  • 事件监听器管理:在微前端卸载时,确保移除所有相关的事件监听器,避免内存泄漏。

7. 使用微前端框架的沙箱机制

  • SingleSPA:提供了一种名为“沙箱”的机制,可以隔离每个微前端的 JavaScript 执行环境。
  • Qiankun:基于 SingleSPA,提供了更强大的沙箱能力,包括样式隔离和 JavaScript 隔离。

8. 窗口对象隔离

  • Proxy:使用 Proxy 对象代理窗口对象,拦截对全局变量的访问和修改。
  • 快照恢复:在微前端挂载前保存全局状态快照,卸载时恢复,确保全局状态不被污染。

示例

// 使用 IIFE 进行命名空间隔离
(function() {
  // 微前端 A 的代码
  console.log('This is Micro Frontend A');
})();
(function() {
  // 微前端 B 的代码
  console.log('This is Micro Frontend B');
})();

总结

在微前端架构中,JavaScript 隔离可以通过多种技术手段实现,包括使用 Web Components、命名空间隔离、构建时隔离、运行时隔离、样式隔离、事件隔离以及利用微前端框架提供的沙箱机制等。选择合适的隔离策略取决于具体的项目需求和架构设计。正确的隔离可以确保微前端之间的独立性和稳定性,避免潜在的冲突和问题。

11. qiankun 是如何实现 JS 隔离的?

Qiankun 是一个基于 SingleSPA 的微前端框架,它提供了强大的 JavaScript 隔离能力。Qiankun 实现 JavaScript 隔离的主要方式是通过创建一个沙箱环境来隔离每个微前端的执行。以下是 Qiankun 实现 JavaScript 隔离的详细机制:

1. 沙箱机制

Qiankun 为每个微前端应用创建了一个独立的沙箱环境,这个沙箱环境可以隔离微前端的 JavaScript 执行,防止它影响主应用或其他微前端应用。

快照沙箱(Snapshot Sandbox)
  • 原理:在微前端挂载前,对全局状态(如 window 对象)进行快照;在微前端卸载时,恢复到之前的快照状态。
  • 优点:实现简单,适用于不支持 Proxy 的浏览器。
  • 缺点:状态隔离不彻底,可能存在状态污染的问题。
代理沙箱(Proxy Sandbox)
  • 原理:使用 Proxy 对象代理全局状态(如 window 对象),拦截对全局变量的访问和修改,从而实现隔离。
  • 优点:隔离更彻底,可以动态地拦截和修改全局状态。
  • 缺点:性能开销相对较大,需要浏览器支持 Proxy。
组合沙箱
  • 原理:结合快照沙箱和代理沙箱的优点,根据实际情况选择合适的沙箱类型。
  • 优点:兼顾性能和隔离效果,提供更灵活的隔离策略。

2. 样式隔离

虽然主要讨论 JavaScript 隔离,但 Qiankun 也提供了样式隔离机制,以避免样式冲突:

  • Shadow DOM:利用 Shadow DOM 的封装特性,为每个微前端创建独立的样式作用域。
  • CSS Modules:支持 CSS Modules,确保样式只作用于特定的组件。
  • 样式隔离插件:可以通过插件实现更复杂的样式隔离策略。

3. 事件隔离

Qiankun 还考虑了事件隔离,以避免微前端之间的事件冲突:

  • 事件监听器管理:在微前端卸载时,自动移除所有相关的事件监听器,避免内存泄漏。
  • 自定义事件:鼓励使用自定义事件,减少对全局事件的依赖。

4. 动态加载和卸载

Qiankun 支持动态加载和卸载微前端应用,这有助于实现更细粒度的隔离:

  • 动态加载:按需加载微前端应用,减少初始加载时间。
  • 动态卸载:在微前端应用卸载时,清理相关资源,包括 JavaScript、样式和事件监听器。

示例

以下是一个简单的示例,展示如何在 Qiankun 中注册一个微前端应用并实现 JavaScript 隔离:

import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'micro-app',
    entry: '//localhost:3000',
    container: '#micro-app-container',
    activeRule: '/micro-app',
  },
]);
start();

在这个示例中,micro-app 是一个微前端应用,它将在满足 activeRule 时加载到 #micro-app-container 中。Qiankun 会为这个微前端应用创建一个沙箱环境,实现 JavaScript 隔离。

总结

Qiankun 通过沙箱机制(包括快照沙箱、代理沙箱和组合沙箱)实现了强大的 JavaScript 隔离能力。此外,Qiankun 还提供了样式隔离和事件隔离机制,确保微前端应用之间的独立性和稳定性。这些隔离机制使得 Qiankun 成为了一个强大且灵活的微前端框架。

12. 业务需要实现前端项目的灰度发布,你会怎么设计?

前端项目灰度发布设计 一、概述 灰度发布是一种逐步扩大新版本覆盖范围,以降低新版本上线风险的技术手段。在前端项目中,灰度发布可以确保新功能或修复的错误能够逐步推向用户,同时监控效果,确保稳定后再全面推广。 二、设计目标

  1. 平滑过渡:确保用户在使用过程中不会感受到明显的切换或中断。
  2. 可控性:能够精确控制哪些用户或流量被导向新版本。
  3. 可监控:实时监控新版本的表现,如性能、错误率等。
  4. 可回滚:一旦新版本出现问题,能够快速回滚到稳定版本。 三、设计策略
  5. 版本控制
    • 使用版本号明确区分不同版本的前端资源。
    • 通过构建系统自动生成带版本号的资源文件。
  6. 流量分配
    • 根据用户标识(如用户ID、设备ID等)或流量特征(如IP地址、浏览器类型等)进行流量分配。
    • 使用权重分配策略,如50%的用户访问新版本,50%的用户访问旧版本。
  7. 动态配置
    • 利用配置中心或feature flag系统动态控制灰度开关和规则。
    • 配置信息可实时更新,无需重新部署前端资源。
  8. 前端路由控制
    • 在前端路由层面加入灰度判断逻辑,根据配置决定是否导向新版本。
    • 使用前端路由的拦截功能,实现无感的版本切换。
  9. 后端支持
    • 后端提供灰度标识接口,前端根据接口返回结果决定版本切换。
    • 后端记录灰度数据,用于监控和分析。
  10. 监控与日志
    • 集成前端监控工具,实时监控新版本的性能和错误。
    • 记录灰度用户的操作日志,用于后续分析。
  11. 回滚机制
    • 设计快速回滚流程,一旦发现新版本问题,能够迅速切换回旧版本。
    • 确保回滚操作对用户透明,不影响用户体验。 四、实现步骤
  12. 准备阶段
    • 确定灰度范围和目标用户群体。
    • 准备新版本的前端资源,并确保兼容性。
  13. 部署阶段
    • 部署新版本资源到生产环境,但不直接对外暴露。
    • 更新配置中心或feature flag系统的灰度规则。
  14. 灰度启动
    • 启用灰度功能,开始将部分用户导向新版本。
    • 监控新版本的表现,收集用户反馈。
  15. 调整与优化
    • 根据监控数据和用户反馈调整灰度规则。
    • 优化新版本功能,解决发现的问题。
  16. 全面推广
    • 当新版本表现稳定且用户反馈良好时,逐步扩大灰度范围。
    • 最终将所有用户切换到新版本。
  17. 回滚与总结
    • 如新版本出现问题,执行回滚操作。
    • 总结灰度发布过程,优化后续流程。 五、技术选型
  • 前端框架:React、Vue等现代前端框架。
  • 构建工具:Webpack、Parcel等支持版本管理的构建工具。
  • 配置中心:Apollo、Nacos等分布式配置中心。
  • 监控工具:Sentry、Datadog等前端监控工具。
  • 日志系统:ELK Stack、Logtail等日志收集和分析系统。 六、注意事项
  • 用户体验:确保灰度发布过程中用户体验不受影响。
  • 数据安全:在灰度发布过程中确保用户数据安全。
  • 沟通协调:与产品、测试、运维等部门密切沟通,确保灰度发布的顺利进行。 通过以上设计,可以实现前端项目的平滑、可控、可监控的灰度发布,降低新版本上线的风险,提升用户体验。

13. 一般怎么做代码重构?

代码重构的一般做法 代码重构是一种改进现有代码结构而不改变外部行为的技术,旨在提高代码的可读性、可维护性和可扩展性。以下是进行代码重构的一般步骤和最佳实践: 一、准备阶段

  1. 明确重构目标
    • 确定重构的目的,如提高性能、减少复杂性、改善代码结构等。
  2. 评估重构风险
    • 评估重构可能带来的风险,包括功能回归、性能下降等。
  3. 制定重构计划
    • 制定详细的重构计划,包括重构的范围、时间表和资源分配。
  4. 备份现有代码
    • 在重构前备份现有代码,以便在需要时回滚。 二、分析阶段
  5. 理解现有代码
    • 阅读和理解现有代码的结构、逻辑和功能。
  6. 识别重构点
    • 标记出需要重构的代码段,如重复代码、过长函数、复杂条件判断等。
  7. 设计新结构
    • 设计新的代码结构,包括模块划分、类设计、函数定义等。 三、实施阶段
  8. 小步快走
    • 采用小步快走的方式,每次只重构一小部分代码。
  9. 编写单元测试
    • 在重构前为现有代码编写单元测试,确保重构后功能不变。
  10. 重构代码
    • 根据设计的新结构逐步重构代码,注意保持功能不变。
  11. 持续集成
    • 将重构后的代码集成到主分支,确保与其他代码的兼容性。 四、验证阶段
  12. 运行单元测试
    • 运行单元测试,验证重构后的代码是否满足功能要求。
  13. 代码审查
    • 进行代码审查,确保重构后的代码符合编码规范。
  14. 性能测试
    • 进行性能测试,确保重构后的代码性能不低于原有代码。
  15. 用户验收测试
    • 如果可能,进行用户验收测试,确保重构没有影响用户体验。 五、后续阶段
  16. 文档更新
    • 更新相关文档,包括设计文档、用户手册等。
  17. 团队分享
    • 与团队分享重构的经验和教训,提高团队整体技能。
  18. 持续优化
    • 代码重构是一个持续的过程,需要不断优化和改进。 最佳实践
  • 保持功能不变:重构过程中确保不改变外部行为。
  • 逐步推进:避免一次性大规模重构,采用逐步推进的方式。
  • 自动化测试:依靠自动化测试确保重构后的代码质量。
  • 代码审查:通过代码审查发现潜在问题,提高代码质量。
  • 持续集成:利用持续集成工具自动化构建、测试和部署过程。
  • 文档同步:确保文档与代码同步更新,减少后续维护成本。 工具支持
  • IDE插件:如IntelliJ IDEA、Visual Studio等IDE提供的重构插件。
  • 版本控制:如Git等版本控制工具,用于管理代码变更。
  • 持续集成工具:如Jenkins、Travis CI等,用于自动化构建和测试。
  • 代码质量工具:如SonarQube、ESLint等,用于代码质量分析和检查。 通过遵循上述步骤和最佳实践,可以有效地进行代码重构,提高代码质量,降低维护成本,并为未来的开发奠定良好基础。

14. 请求失败会弹出一个 toast,如何保证批量请求失败时, 只弹出一个 toast?

要保证批量请求失败时只弹出一个toast,可以采用以下几种策略: 1. 使用全局状态管理

  • 思路:维护一个全局状态,用于标记是否已经显示过toast。
  • 实现:在请求失败时,先检查这个全局状态,如果已经显示过toast,则不再显示;如果没有,则显示toast并更新这个状态。 代码示例(使用Redux为例):
// Redux action
const showFailureToast = () => ({
  type: 'SHOW_FAILURE_TOAST'
});
// Redux reducer
const toastReducer = (state = { hasShownFailureToast: false }, action) => {
  switch (action.type) {
    case 'SHOW_FAILURE_TOAST':
      return { ...state, hasShownFailureToast: true };
    // 其他action处理
    default:
      return state;
  }
};
// 请求失败处理函数
const handleRequestFailure = (error) => {
  const { hasShownFailureToast } = store.getState().toast;
  if (!hasShownFailureToast) {
    store.dispatch(showFailureToast());
    Toast.show('请求失败'); // 显示toast
  }
};

2. 使用定时器

  • 思路:设置一个定时器,在一段时间内只显示一次toast。
  • 实现:在请求失败时,启动定时器,如果在定时器有效期内再次失败,则不显示toast;定时器到期后重置。 代码示例:
let toastTimer = null;
const handleRequestFailure = (error) => {
  if (!toastTimer) {
    Toast.show('请求失败');
    toastTimer = setTimeout(() => {
      toastTimer = null;
    }, 3000); // 3秒内不再显示toast
  }
};
**3. 使用请求队列管理**
- **思路**:将所有请求放入一个队列中,队列管理器负责处理请求和错误。
- **实现**:队列管理器在处理请求时,只会在第一个失败请求时显示toast,后续失败请求不再显示。
**代码示例:**
```javascript
class RequestQueue {
  constructor() {
    this.queue = [];
    this.hasShownFailureToast = false;
  }
  add(request) {
    this.queue.push(request);
  }
  process() {
    this.queue.forEach(request => {
      request()
        .catch(error => {
          if (!this.hasShownFailureToast) {
            Toast.show('请求失败');
            this.hasShownFailureToast = true;
          }
        });
    });
  }
}
// 使用队列
const requestQueue = new RequestQueue();
requests.forEach(request => requestQueue.add(request));
requestQueue.process();

4. 使用事件总线或发布-订阅模式

  • 思路:通过事件总线或发布-订阅模式,在第一个请求失败时发布一个事件,后续请求失败时订阅这个事件,如果事件已经发布,则不再显示toast。
  • 实现:使用事件总线库(如EventBus)或自定义发布-订阅实现。 代码示例(使用EventBus为例):
// 请求失败处理函数
const handleRequestFailure = (error) => {
  if (!EventBus.has('REQUEST_FAILURE')) {
    Toast.show('请求失败');
    EventBus.emit('REQUEST_FAILURE');
  }
};
// 在请求发起前订阅事件
EventBus.on('REQUEST_FAILURE', () => {
  // 不再显示toast
});
// 发起请求
requests.forEach(request => {
  request().catch(handleRequestFailure);
});

以上策略可以根据具体的项目需求和技术栈进行选择和实现。重要的是确保在批量请求失败时,能够有效地控制toast的显示,避免用户被多次打扰。

15. Vue 有了数据响应式,为何还要 diff ?

Vue 的数据响应式和虚拟DOM的diff算法是两个不同的概念,它们在Vue的工作流程中扮演着不同的角色。以下是为什么即使有了数据响应式,Vue仍然需要diff算法的原因:

数据响应式的角色:

  1. 追踪依赖:Vue使用数据响应式系统(如Object.defineProperty或Proxy)来追踪数据的变化。当数据被修改时,Vue能够知道哪些组件依赖于这些数据。
  2. 通知更新:一旦数据发生变化,Vue会通知依赖于这些数据的组件进行更新。

虚拟DOM和diff算法的角色:

  1. 高效更新:虽然响应式系统知道数据变了,但它并不知道如何高效地更新DOM。虚拟DOM是一个轻量的JavaScript对象,代表了真实的DOM结构。当数据变化时,Vue会先在虚拟DOM上进行变化,然后通过diff算法计算出最小化的变更集。
  2. 最小化重绘和重排:直接操作DOM是昂贵的,因为它可能导致浏览器的重绘(repaint)和重排(reflow)。diff算法能够找出变化的最小集合,只对必要的DOM节点进行更新,从而减少重绘和重排的次数,提高性能。
  3. 组件级别的更新:在Vue中,组件的渲染输出可能依赖于多个数据源。即使其中一个数据源变化了,我们也不希望重新渲染整个组件,而是只更新受影响的部分。diff算法帮助实现这一目标。
  4. 跨平台渲染:Vue的虚拟DOM和diff算法不仅用于浏览器环境,还可以用于其他平台,如服务器端渲染(SSR)或原生移动应用(如Weex)。这种抽象层使得Vue能够跨平台工作。
  5. 功能复用:虚拟DOM和diff算法是现代前端框架的常见模式,它们提供了一种标准的方式来处理UI更新,使得开发者可以复用这些成熟的技术。

为什么两者都需要:

  • 响应式系统负责检测数据变化并通知组件。
  • 虚拟DOM和diff算法负责高效地更新DOM,确保只有必要的部分被重新渲染。 两者相辅相成,共同构成了Vue高效、可预测的更新机制。没有diff算法,Vue将无法高效地更新DOM,导致性能问题;而没有数据响应式,Vue将无法知道何时需要更新DOM。因此,两者都是Vue框架中不可或缺的部分。

16. vue3 为什么不需要时间分片?

Vue 3在设计上对性能进行了优化,特别是在更新机制上,使得它在大多数情况下不需要依赖时间分片(time slicing)来保证流畅的用户体验。以下是Vue 3不需要时间分片的主要原因:

  1. 更快的虚拟DOM算法
    • Vue 3采用了更高效的虚拟DOM算法,包括静态树提升(Static Tree Hoisting)和静态属性提升(Static Props Hoisting),这些优化减少了不必要的重渲染和DOM操作,从而提高了更新速度。
  2. 编译时优化
    • Vue 3在编译阶段进行了更多的优化,比如通过编译时静态分析来减少运行时的开销,这样可以在执行更新时减少计算量。
  3. Proxy替代Object.defineProperty
    • Vue 3使用Proxy代替了Vue 2中的Object.defineProperty来实现响应式系统。Proxy提供了更原生、更高效的数据变化追踪,减少了响应式系统的性能开销。
  4. 组件的异步更新
    • Vue 3继续采用了异步更新队列,所有的数据变化都会被缓存起来,然后在下一个事件循环中统一进行更新。这种机制可以避免在同一事件循环中多次更新DOM,从而提高性能。
  5. 更细粒度的更新
    • Vue 3的响应式系统可以更精确地追踪依赖,只有真正依赖变化的数据才会触发组件的更新,减少了不必要的渲染。
  6. 更好的调度策略
    • Vue 3内部使用了一种更智能的调度策略,可以根据任务的优先级和类型来调度更新,确保用户交互的流畅性。
  7. 自定义渲染器
    • Vue 3引入了自定义渲染器的概念,允许开发者针对特定平台或场景优化渲染过程,从而实现更高效的更新。 虽然Vue 3在大多数情况下不需要时间分片,但在某些极端情况下,如果遇到非常复杂的组件或大量的数据更新,仍然可能会出现性能问题。在这种情况下,开发者可以手动控制更新过程,或者使用Web Workers等技术在后台线程处理复杂计算,以避免阻塞主线程。 总的来说,Vue 3通过一系列的性能优化,使得它在大多数场景下能够提供流畅的用户体验,而不需要依赖时间分片。然而,前端性能优化是一个持续的过程,随着应用复杂性的增加,未来可能还需要更多的策略来保证性能。

17. vue3 为什么要引入 Composition API ?

Vue 3引入Composition API的主要目的是为了解决Vue 2中Options API在复杂组件开发时遇到的一些问题。以下是引入Composition API的几个主要原因:

  1. 更好的逻辑组织和复用
    • 在Vue 2的Options API中,组件的逻辑被分散在data、methods、computed、watch等不同的选项中,当组件变得复杂时,这些选项会变得庞大且难以管理。Composition API允许将相关的逻辑组合在一起,形成可复用的函数,提高了代码的组织性和可读性。
  2. 更灵活的代码组合
    • Composition API提供了更灵活的代码组合方式,允许开发者根据功能而不是选项来组织代码,这样可以更容易地实现复杂的功能和组件。
  3. 更好的类型推断
    • 使用Composition API时,由于逻辑更加集中,类型推断变得更加容易,这为使用TypeScript的开发者提供了更好的开发体验。
  4. 解决命名冲突
    • 在Options API中,所有的方法和计算属性都共享同一个命名空间,容易导致命名冲突。Composition API通过将逻辑封装在函数中,避免了这种问题。
  5. 更好的性能优化
    • Composition API允许更细粒度地控制依赖和更新,可以更容易地实现性能优化,比如通过reactiverefcomputed等API精确控制响应式状态。
  6. 适应更复杂的场景
    • 随着应用复杂度的增加,组件的逻辑也越来越复杂,Composition API提供了更强大的工具来处理这些复杂场景,比如多源数据联动、复杂状态管理等。
  7. 促进函数式编程
    • Composition API鼓励使用函数式编程的思维来编写组件,这使得代码更加声明式和可预测。
  8. 更好的代码分割和懒加载
    • Composition API可以与Vue 3的异步组件和动态导入结合使用,实现更好的代码分割和懒加载,提高应用性能。
  9. 为未来升级做准备
    • Composition API为Vue的未来升级提供了更多的可能性,比如更容易地引入新的特性和改进。 总的来说,Composition API的引入是为了提供一种更灵活、更强大、更适应复杂应用开发的方法,同时保持与Vue 2的兼容性。它不是用来替代Options API的,而是作为其补充,让开发者可以根据项目的需求和个人的偏好选择最适合的API风格。

18. 谈谈 Vue 事件机制,并手写onon、off、emitemit、once

Vue的事件机制是一种组件间通信的方式,它允许组件通过自定义事件进行交互。Vue实例实现了事件触发和监听的方法,包括$on$off$emit$once。这些方法可以在Vue实例上使用,也可以在组件实例上使用。

Vue事件机制概述

  • **on(event,callback):监听当前实例上的自定义事件。事件可以由on(event, callback)**:监听当前实例上的自定义事件。事件可以由`emit`触发。回调函数会接收所有传入事件触发函数的额外参数。
  • $off([event, callback]):移除事件监听器。如果没有提供参数,则移除所有事件监听器;如果只提供了事件,则移除该事件的所有监听器;如果同时提供了事件和回调,则只移除这个回调的监听器。
  • $emit(event, [...args]):触发当前实例上的事件。附加参数会传给监听器回调。
  • $once(event, callback):监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

手写onon、off、emitemit、once

以下是一个简单的事件总线(Event Bus)的实现,它包含了$on$off$emit$once的方法:

class EventBus {
  constructor() {
    this.events = {};
  }
  $on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  $off(event, callback) {
    if (!this.events[event]) {
      return;
    }
    if (!callback) {
      this.events[event] = null;
    } else {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
  $emit(event, ...args) {
    if (!this.events[event]) {
      return;
    }
    this.events[event].forEach(callback => {
      callback(...args);
    });
  }
  $once(event, callback) {
    const onceCallback = (...args) => {
      callback(...args);
      this.$off(event, onceCallback);
    };
    this.$on(event, onceCallback);
  }
}
// 使用示例
const eventBus = new EventBus();
// 监听事件
eventBus.$on('custom-event', (data) => {
  console.log('Custom event triggered with data:', data);
});
// 触发事件
eventBus.$emit('custom-event', { message: 'Hello, Vue!' });
// 监听一次事件
eventBus.$once('once-event', (data) => {
  console.log('Once event triggered with data:', data);
});
// 触发一次事件
eventBus.$emit('once-event', { message: 'This will be logged once.' });
// 移除事件监听
eventBus.$off('custom-event');

在这个实现中,EventBus类维护了一个events对象,用于存储事件和对应的回调函数数组。$on方法用于添加事件监听,$off方法用于移除事件监听,$emit方法用于触发事件,$once方法用于添加只触发一次的事件监听。 请注意,这个实现是一个简单的事件总线,它没有处理事件命名空间、异步事件触发等更复杂的情况。在实际的Vue实例中,事件机制会更加复杂,并且与Vue的响应式系统紧密集成。

19. computed 计算值为什么还可以依赖另外一个 computed 计算值?

在Vue中,computed计算属性可以依赖另一个computed计算属性,这是因为computed计算属性本质上是Vue响应式系统的一部分,它们都是基于依赖跟踪和缓存的机制来工作的。以下是为什么computed计算值可以依赖另一个computed计算值的原因:

依赖跟踪

  • Vue的响应式系统会跟踪computed计算属性所依赖的响应式数据(包括其他computed计算属性)。
  • 当依赖的响应式数据发生变化时,Vue会知道需要重新计算依赖于这些数据的computed计算属性。

缓存机制

  • computed计算属性是缓存的,只有当它的依赖发生变化时,它才会重新计算。
  • 这意味着如果computed计算属性A依赖于computed计算属性B,而B的依赖没有变化,那么B不会重新计算,同样A也不会重新计算。
  • 这种缓存机制可以提高性能,避免不必要的计算。

重新计算

  • 当computed计算属性B的依赖发生变化,导致B重新计算时,Vue会检测到依赖于B的computed计算属性A也需要重新计算。
  • 这种连锁反应确保了所有依赖于变化数据的computed计算属性都能得到更新。

示例

new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName;
    },
    reversedFullName: function () {
      // 依赖另一个computed计算属性fullName
      return this.fullName.split(' ').reverse().join(' ');
    }
  }
});

在上述示例中,reversedFullName依赖于fullName,而fullName又依赖于firstNamelastName。当firstNamelastName发生变化时,fullName会重新计算,进而触发reversedFullName的重新计算。

总结

computed计算属性可以依赖另一个computed计算属性,因为它们都是响应式系统的一部分,通过依赖跟踪和缓存机制,Vue能够高效地管理这些依赖关系,并在必要时重新计算值。这种设计使得computed计算属性非常灵活和强大,能够用于构建复杂的响应式数据流。

20. 说一下 vm.$set 原理

vm.$set 是 Vue.js 中用于更新数据的方法之一。以下是它的原理:

原理

1. 响应式更新
  • 当你使用 vm.$set 更新数据时,Vue 会通过响应式系统跟踪依赖关系,并更新所有依赖于该数据的计算属性(computed properties)。
2. 批量更新
  • vm.$set 允许你同时更新多个数据,Vue 会将这些更新批量处理,以提高性能。
3. 深度响应式
  • Vue 的响应式系统会深度跟踪数据依赖,不仅跟踪第一层依赖,还跟踪深层依赖。
4. 异步更新
  • Vue 支持异步更新,这意味着更新操作不会阻塞视图的重新渲染。
示例
vm.$set('firstName', 'Foo');
vm.$set('lastName', 'Bar');

在上述示例中,vm.$set 同时更新了 firstNamelastName,Vue 会处理这些更新并更新所有依赖于这些数据的计算属性。

总结

vm.$set 是 Vue 响应式系统的一部分,允许你高效地更新数据,并通过深度响应式和批量更新来优化性能。它使得 Vue 能够处理复杂的响应式数据流,并在必要时进行异步更新,而不会阻塞用户界面的响应。

21. 怎么在 Vue 中定义全局方法?

在Vue中定义全局方法,你可以使用以下几种方式:

1. 在 Vue 实例上定义

你可以在 Vue 实例上直接定义全局方法。例如:

const app = Vue({
  el: '#app',
  data: {
    // ... 
  },
  methods: {
    globalMethod() {
      // 全局方法的实现
    }
  }
});

2. 使用 Vue.prototype

你可以在 Vue 的原型上定义全局方法,这样所有 Vue 实例都可以访问该方法。例如:

Vue.prototype.globalMethod = function() {
  // 全局方法的实现
}

3. 使用外部函数

你还可以将全局方法定义在外部函数中,然后在 Vue 实例中引用该方法。例如:

function globalMethod() {
  // 全局方法的实现
}
const app = Vue({
  el: '#app',
  data: {
    // ... 
  },
  methods: {
    globalMethod: globalMethod
  }
});

总结

在 Vue 中,你可以通过多种方式定义全局方法,包括在 Vue 实例上定义、使用 Vue.prototype 定义,或者使用外部函数定义。选择哪种方式取决于你的具体需求和个人偏好。无论哪种方式,全局方法都可以在所有 Vue 实例中访问。

注意

  • 定义全局方法时,确保方法名不会与 Vue 内置方法冲突。
  • 考虑方法的可维护性和可读性。

22. Vue 中父组件怎么监听到子组件的生命周期?

在Vue中,父组件监听到子组件的生命周期可以通过以下几种方式实现:

1. 使用生命周期钩子

子组件可以在其生命周期钩子中触发事件,父组件可以监听这些事件。例如,子组件可以在mounted钩子中触发一个事件,父组件可以监听这个事件:

<!-- 子组件 -->
<template>
  <div>子组件</div>
</template>
<script>
export default {
  mounted() {
    this.$emit('child-mounted');
  }
}
</script>
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent @child-mounted="handleChildMounted" />
  </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  methods: {
    handleChildMounted() {
      console.log('子组件已挂载');
    }
  }
}
</script>

2. 使用 ref 属性和 $refs

父组件可以使用ref属性为子组件设置一个引用名,然后通过this.$refs访问子组件的实例,从而调用子组件的方法或访问其数据:

<!-- 父组件 -->
<template>
  <div>
    <ChildComponent ref="childComponent" />
  </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  mounted() {
    this.$refs.childComponent.childMethod(); // 调用子组件的方法
  }
}
</script>

3. 使用 Vue 3 的 onMounted Composition API

如果你使用的是Vue 3,可以利用Composition API中的onMounted钩子来监听子组件的挂载状态:

<!-- 子组件 -->
<template>
  <div>子组件</div>
</template>
<script>
import { onMounted } from 'vue';
export default {
  setup(props, { emit }) {
    onMounted(() => {
      emit('child-mounted');
    });
  }
}
</script>
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent @child-mounted="handleChildMounted" />
  </div>
</template>
<script>
import { onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  setup() {
    const handleChildMounted = () => {
      console.log('子组件已挂载');
    };
    return {
      handleChildMounted
    };
  }
}
</script>

4. 使用事件总线(Event Bus)

虽然不推荐作为主要的通信方式,但事件总线可以用于在组件间进行通信,包括监听子组件的生命周期事件:

// event-bus.js
import Vue from 'vue';
export const EventBus = new Vue();
<!-- 子组件 -->
<template>
  <div>子组件</div>
</template>
<script>
import { EventBus } from './event-bus.js';
export default {
  mounted() {
    EventBus.$emit('child-mounted');
  }
}
</script>
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent />
  </div>
</template>
<script>
import { EventBus } from './event-bus.js';
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  created() {
    EventBus.$on('child-mounted', this.handleChildMounted);
  },
  beforeDestroy() {
    EventBus.$off('child-mounted', this.handleChildMounted);
  },
  methods: {
    handleChildMounted() {
      console.log('子组件已挂载');
    }
  }
}
</script>

总结

父组件监听子组件的生命周期可以通过触发事件、使用ref$refs、利用Vue 3的Composition API或者使用事件总线来实现。选择哪种方式取决于你的具体需求和项目结构。在实际开发中,通常推荐使用事件触发或ref的方式,因为它们更符合Vue的设计理念。

23. vue组件里写的原生addEventListeners监听事件,要手动去销毁吗?为什么?

在Vue组件中,如果你使用了原生的addEventListener来监听事件,那么通常情况下是需要手动去销毁(移除)这些事件的。原因如下:

1. 防止内存泄漏

  • 原生的事件监听器会在其绑定的元素上持续存在,即使Vue组件已经被销毁。如果不在组件销毁时移除这些监听器,它们会一直留在内存中,导致内存泄漏。

2. 避免不必要的副作用

  • 如果组件被销毁后事件监听器仍然存在,那么这些监听器可能会触发一些不必要的行为,因为它们所依赖的组件状态可能已经不存在了。

3. Vue的生命周期管理

  • Vue提供了生命周期钩子,如beforeDestroy(Vue 2)或beforeUnmount(Vue 3),以便在组件销毁之前进行清理工作。在这些钩子中移除事件监听器是Vue组件开发的最佳实践。

如何手动销毁

在Vue 2中,你可以在beforeDestroy钩子中移除事件监听器:

export default {
  mounted() {
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize);
  },
  methods: {
    handleResize() {
      // 处理resize事件
    }
  }
}

在Vue 3中,你可以在beforeUnmount钩子中移除事件监听器:

export default {
  setup() {
    const handleResize = () => {
      // 处理resize事件
    };
    onMounted(() => {
      window.addEventListener('resize', handleResize);
    });
    onBeforeUnmount(() => {
      window.removeEventListener('resize', handleResize);
    });
  }
}

自动销毁的替代方案

如果你不想手动管理事件监听器的销毁,可以考虑以下替代方案:

  • 使用Vue的自定义指令来封装事件监听器,并在指令中处理销毁逻辑。
  • 使用第三方库,如lodashdebouncethrottle函数,它们通常提供了自动清理的功能。

总结

在Vue组件中,使用原生的addEventListener监听事件时,为了防止内存泄漏和避免不必要的副作用,通常需要手动在组件销毁时移除这些事件监听器。这是Vue组件开发的最佳实践,也是保证应用性能和稳定性的重要步骤。

24. vue中,推荐在哪个生命周期发起请求?

在Vue中,推荐在mounted生命周期钩子中发起请求。原因如下:

1. 组件已挂载

  • mounted钩子在组件的模板和DOM已经被渲染并挂载完成后调用。这时,组件已经完全渲染到页面上,可以确保请求的数据是用来更新已经存在的DOM。

2. 数据更新

  • mounted钩子中发起请求,获取数据后可以调用this.$nextTick或其他方法来确保DOM更新完成。这样可以避免在数据还未渲染到DOM时就进行操作。

3. 避免不必要的请求

  • 如果在created钩子中发起请求,那么在组件实例创建时就会发送请求,无论组件是否最终会被渲染到页面上。这可能导致不必要的请求。

4. 与异步组件配合

  • 当使用异步组件时,mounted钩子确保在组件实际渲染到页面上后才发起请求,这样可以更好地控制加载状态和用户体验。

5. 适用于SSR(服务器端渲染)

  • 如果你使用Vue进行服务器端渲染(SSR),在mounted钩子中发起请求可以避免在服务器端执行不必要的异步操作。

示例

export default {
  data() {
    return {
      // 初始化数据
    };
  },
  mounted() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 发起请求,获取数据
      axios.get('/api/data').then(response => {
        this.data = response.data;
      });
    }
  }
}

注意事项

  • 如果你的组件在服务器端渲染(SSR)环境中使用,并且需要在服务器端获取数据,那么你可能需要在createdbeforeMount钩子中发起请求。
  • 对于需要立即显示数据的场景,如路由导航后立即加载数据,可以考虑在Vue路由的beforeRouteEnter守卫中发起请求。

总结

在大多数情况下,mounted是发起请求的最佳生命周期钩子,因为它确保了组件已经挂载到DOM上,可以更好地控制数据加载和DOM更新的流程。然而,根据具体的应用场景和需求,有时也可能需要在其他生命周期钩子中发起请求。

25. Vue中,created和mounted两个钩子之间调用时间差值受什么影响?

在Vue中,createdmounted两个生命周期钩子之间的调用时间差值受多种因素影响,主要包括:

1. 组件复杂度

  • 模板解析:如果组件的模板非常复杂,包含很多嵌套的子组件,Vue需要更多时间来解析和编译模板。
  • 数据依赖:在created钩子中定义的数据和计算属性,如果它们依赖于其他复杂的数据处理或异步操作,可能会延长从createdmounted的时间。

2. 子组件数量

  • 子组件初始化:如果组件包含多个子组件,每个子组件也会经历自己的createdmounted生命周期,这会增加总体时间。

3. 异步操作

  • 异步数据获取:在created钩子中进行的异步数据获取(如API请求)不会直接影响mounted的调用时间,但如果在created钩子中有等待异步操作完成的行为,可能会间接影响。

4. DOM渲染性能

  • DOM操作:在mounted钩子调用之前,Vue需要将虚拟DOM渲染到实际的DOM中,如果DOM结构复杂或包含大量DOM操作,这个过程可能会比较慢。

5. JavaScript执行时间

  • 脚本执行createdmounted钩子中的JavaScript代码执行时间也会影响两者之间的时间差,如果这些钩子中包含复杂的逻辑或大量的计算,可能会延长时间差。

6. CSS和样式

  • 样式计算:如果组件的样式复杂,浏览器需要更多时间来计算和应用样式,这可能会影响mounted的调用时间。

7. 浏览器性能

  • 浏览器差异:不同的浏览器在解析HTML、执行JavaScript和渲染DOM方面的性能可能会有所不同,这也会影响createdmounted之间的时间差。

8. 网络延迟

  • 外部资源加载:如果组件依赖于外部资源(如脚本、样式表或图片),网络延迟可能会影响这些资源的加载时间,从而间接影响mounted的调用时间。

示例

export default {
  data() {
    return {
      // 初始化数据
    };
  },
  created() {
    // 这里执行一些操作,如数据初始化、异步请求等
    console.log('created');
  },
  mounted() {
    // 组件挂载完成,可以操作DOM
    console.log('mounted');
  }
}

总结

createdmounted之间的时间差主要受组件的复杂度、子组件数量、异步操作、DOM渲染性能、JavaScript执行时间、CSS和样式、浏览器性能以及网络延迟等因素的影响。在实际开发中,这些因素可能会相互交织,导致时间差值变化。理解这些影响因素有助于更好地优化组件的性能和用户体验。

26. 为什么Node在使用es module时必须加上文件扩展名?

Node.js在使用ES模块(ESM,ECMAScript Modules)时要求必须加上文件扩展名,主要有以下几个原因:

1. 明确性

  • 避免歧义:在CommonJS模块系统中,Node.js允许省略文件扩展名,因为它会尝试添加.js.json.node等扩展名来查找文件。但在ESM中,为了明确指出模块的类型和内容,要求必须指定扩展名,这样可以避免解析时的歧义。

2. 与浏览器兼容

  • 一致性:浏览器在导入ES模块时始终要求指定文件扩展名,Node.js为了与浏览器中的ES模块行为保持一致,也采用了相同的要求。这种一致性有助于开发者在不同环境间迁移代码时减少环境差异带来的困扰。

3. 兼容性

  • 模块类型识别:通过要求扩展名,Node.js可以更容易地识别模块的类型,从而正确地应用不同的加载策略。例如,.js 文件通常作为JavaScript模块,.json 文件作为JSON数据,.node 文件作为二进制模块。明确扩展名有助于Node.js在加载模块时采取正确的处理方式。

4. 性能考虑

  • 减少查找时间:在CommonJS模块系统中,省略扩展名会导致Node.js尝试多种扩展名进行文件查找,这会增加文件系统调用的开销。而在ESM中要求扩展名,可以减少文件系统调用的次数,从而在一定程度上提升模块加载的性能。

5. 模块解析

  • 解析优化:要求扩展名可以使得Node.js在解析模块时进行更有效的优化,因为解析器可以更直接地知道如何处理特定类型的文件。

6. 未来发展

  • 标准化:随着JavaScript生态的发展,标准化和模块化的要求越来越高,要求扩展名是向标准化迈进的一步,有助于未来JavaScript模块化的发展。

总结

Node.js在使用ES模块时要求加上文件扩展名,主要是为了明确性、与浏览器兼容、兼容性、性能考虑、模块解析优化以及未来标准化发展的需要。这种设计选择在一定程度上提升了模块处理的效率和准确性,同时也为开发者提供了更一致的模块化开发体验。

27. package.json 文件中的 devDependencies 和 dependencies 对象有什么区别?

package.json 文件中,devDependenciesdependencies 是两个重要的对象,它们用于定义项目的依赖关系,但它们的用途和含义有所不同:

dependencies

  • 生产环境依赖dependencies 对象中列出的依赖是项目在生产环境中运行时所必需的。这些依赖会随着项目的发布而一起被部署到生产环境。
  • 安装方式:通常使用 npm install <package-name> 或在 package.json 中直接添加依赖后运行 npm install 来安装这些依赖。
  • 示例:如果您的项目是一个Web应用程序,那么像 expressreactlodash 等库可能会被列为 dependencies,因为它们是运行您的应用程序所必需的。

devDependencies

  • 开发环境依赖devDependencies 对象中列出的依赖是项目在开发过程中使用的,但不是运行生产环境所必需的。这些依赖通常用于测试、打包、编译、linting等开发任务。
  • 安装方式:通常使用 npm install <package-name> --save-dev 或在 package.json 中直接添加依赖后运行 npm install 来安装这些依赖。
  • 示例:像 webpackbabel-clieslintmocha 等工具或库可能会被列为 devDependencies,因为它们仅在开发过程中使用,而不是运行最终产品所必需的。

区别总结

  • 环境不同dependencies 是生产环境依赖,devDependencies 是开发环境依赖。
  • 用途不同dependencies 中的包是运行项目所必需的,而 devDependencies 中的包是用于帮助开发项目的。
  • 安装命令不同:安装到 dependencies 使用 npm install <package-name>,安装到 devDependencies 使用 npm install <package-name> --save-dev

注意事项

  • 优化依赖:在发布项目到生产环境之前,应确保 package.json 中只包含必要的 dependencies,以减少最终产品的体积和提高安全性。
  • 环境配置:在一些持续集成/持续部署(CI/CD)流程中,可能会根据 dependenciesdevDependencies 来决定安装哪些包,因此正确区分它们是很重要的。 通过合理使用 dependenciesdevDependencies,可以更好地管理项目的依赖关系,确保开发效率和生产环境的稳定性。

28. 不会冒泡的事件有哪些?

在JavaScript中,"冒泡事件"(Event Bubbling)是指在DOM树中,事件从目标元素向上传播到父元素,直到到达根元素的过程。冒泡事件是浏览器处理事件传播的一种方式,以下是冒泡事件的一些常见类型:

  1. 鼠标事件
    • click
    • 当用户点击元素时触发。
    • dblclick
    • 当用户双击元素时触发。
    • mouseover
    • 当鼠标悬停在元素上时触发。
    • mouseout
    • 当鼠标离开元素时触发。
    • mousedown
    • 当鼠标按钮按下时触发。
    • mouseup
    • 当鼠标按钮释放时触发。
  2. 键盘事件
    • keydown
    • 当用户按下键盘键时触发。
    • keyup
    • 当用户释放键盘键时触发。
    • keypress
    • 当用户按下并释放键盘键时触发。
  3. 表单事件
    • submit
    • 当用户提交表单时触发。
  4. 触摸事件
    • touchstart
    • 当用户开始触摸元素时触发。
    • touchmove
    • 当用户移动触摸时触发。
    • touchend
    • 当用户结束触摸时触发。
  5. 其他事件
    • focus
    • 当元素获得焦点时触发。
    • blur
    • 当元素失去焦点时触发。
  6. 自定义事件
    • 开发者可以自定义事件,如 scrollresize 等。 冒泡事件允许事件从特定元素向上传播,允许父元素捕获和处理事件,这在事件处理和事件委托中非常有用。通过冒泡事件,可以更容易地管理和组织事件处理逻辑,确保事件在正确的上下文中被处理。

29. 子组件是一个 Portal,发生点击事件能冒泡到父组件吗?

是的,子组件是一个 Portal,发生点击事件能冒泡到父组件。 Portal 是 React 的一个功能,它允许将子组件渲染到父组件以外的 DOM 节点中。尽管 Portal 的子组件在 DOM 结构上与父组件分离,但它们在 React 组件树中仍然保持父子关系。因此,当 Portal 的子组件上发生事件时,这些事件会按照正常的冒泡机制在 React 组件树中传播。 这意味着,如果 Portal 的子组件上发生了点击事件,并且没有在子组件内部被阻止(例如,使用 event.stopPropagation()),那么这个事件会冒泡到 Portal 的父组件。父组件可以通过在自身或更高层级的组件上添加事件监听器来捕获这些事件。 需要注意的是,事件冒泡是在 React 组件树中发生的,而不是在 DOM 树中。因此,即使 Portal 的子组件在 DOM 结构上与父组件分离,它们在 React 组件树中的父子关系仍然保持不变,事件冒泡也遵循这个关系。 以上信息仅供参考,如有需要,建议咨询专业技术人员。