2023.08.20 更新前端面试问题总结(12道题)

388 阅读29分钟

2023.07.28 - 2023.08.20 更新前端面试问题总结(12道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…

目录:

  • 初级开发者相关问题【共计 1 道题】

    • 513.link 标签有 那些属性,作用都是啥?【热度: 839】【浏览器】
  • 中级开发者相关问题【共计 9 道题】

    • 505.[React] 如何给 children 添加额外的属性【热度: 527】【web框架】【出题公司: PDD】
    • 506.never 是什么类型,详细讲一下【热度: 667】【TypeScript】
    • 507.unknown 是什么类型,详细讲一下【热度: 801】【TypeScript】
    • 508.联合类型是什么?【热度: 1,180】【TypeScript】
    • 509.extends 条件类型定义【热度: 297】【TypeScript】
    • 510.infer 关键字是什么【热度: 100】【TypeScript】
    • 511.is 作用是什么【热度: 458】【TypeScript】
    • 512.in 运算符作用是什么【热度: 844】【TypeScript】
    • 514.[Vue] 组件之间的通信方式有哪些?【热度: 1,109】【web框架】【出题公司: PDD】
  • 高级开发者相关问题【共计 1 道题】

    • 515.[性能] 衡量页面性能的指标有哪些?【热度: 1,045】【工程化】【出题公司: 美团】
  • 资深开发者相关问题【共计 1 道题】

    • 504.[React] hooks 和 memorizedState 是什么关系?【热度: 836】【web框架】【出题公司: PDD】

初级开发者相关问题【共计 1 道题】

513.link 标签有 那些属性,作用都是啥?【热度: 839】【浏览器】

关键词:link 标签属性

link标签有以下几个常用的属性:

  1. href:指定所链接文档的URL地址,可以是一个外部CSS文件的URL或者其他文档的URL。
  2. rel:用于定义当前文档与所链接文档之间的关系。常用的取值有stylesheet(指定所链接文档是一个外部CSS文件)、icon(指定所链接文档是一个图标文件)、preconnect(预连接到指定的URL,加快页面加载速度)等等。
  3. type:指定所链接文档的MIME类型。常用的取值有text/css(链接一个外部CSS文件)、image/x-icon(链接一个图标文件)等等。
  4. media:指定链接的文档在哪些媒体设备上生效。常用的取值有print(应用于打印样式)和screen(应用于屏幕样式)。
  5. crossorigin:用于指定跨域资源的处理方式。常用的取值有anonymous(允许跨域请求,但不发送凭据)和use-credentials(允许跨域请求,并发送凭据)。
  6. integrity:用于指定链接的文档的完整性校验值,以确保外部资源不被篡改。通常结合subresource integrity(SRI)一起使用。
  7. as:用于指定所链接资源的预期用途,以优化资源的加载方式。常用的取值有image(图片资源)、font(字体资源)、script(脚本资源)等等。

link标签的作用是在HTML文档中引入外部资源,例如外部CSS文件、图标文件等。通过link标签,可以将外部资源与HTML文档关联起来,使得浏览器能够正确加载和渲染页面所需的样式和其他资源。

中级开发者相关问题【共计 9 道题】

505.[React] 如何给 children 添加额外的属性【热度: 527】【web框架】【出题公司: PDD】

关键词:cloneElement、children 添加额外属性

在 React 中,可以使用 React.cloneElement() 方法来给 children 添加额外的属性。

React.cloneElement(element, props, ...children)

其中,element 是需要克隆的 React 元素,props 是要添加的属性,children 是要传递给克隆元素的子元素。

以下是一个示例:

import React from "react";

function ParentComponent() {
  return (
    <div>
      {React.Children.map(children, (child) =>
        React.cloneElement(child, { additionalProp: "value" })
      )}
    </div>
  );
}

function ChildComponent(props) {
  return <div>{props.additionalProp}</div>;
}

function App() {
  return (
    <ParentComponent>
      <ChildComponent />
      <ChildComponent />
    </ParentComponent>
  );
}

export default App;

在上面的示例中,ParentComponent 是一个父组件,它接收了一些 children,并使用 React.Children.map() 方法遍历每个 child,然后使用 React.cloneElement() 方法给每个 child 添加了一个名为 additionalProp 的属性。

ChildComponent 是一个子组件,它通过 props.additionalProp 获取到了父组件传递的 additionalProp 属性值。

这样,通过 React.cloneElement() 方法,我们可以给 children 添加额外的属性。

506.never 是什么类型,详细讲一下【热度: 667】【TypeScript】

关键词:never类型、never类型应用

never 是其他任意类型的子类型的类型被称为底部类型(bottom type)。

在 TypeScript 中,never 类型便为空类型和底部类型。never 类型的变量无法被赋值,与其他类型求交集为自身,求并集不参与运算。

应用一: 联合类型中的过滤

never在联合类型中会被过滤掉:

type Exclude<T, U> = T extends U ? never : T;

// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>

T | never // 结果为T
T & never // 结果为never

取一个映射类型中所有value为指定类型的key。例如,已知某个React组件的props类型,我需要“知道”(编程意义上)哪些参数是function类型。

interface SomeProps {
  a: string
  b: number
  c: (e: MouseEvent) => void
  d: (e: TouchEvent) => void
}

// 如何得到 'c' | 'd' ? 

type GetKeyByValueType<T, Condition> = {
  [K in keyof T]: T[K] extends Condition ? K : never
} [keyof T];

type FunctionPropNames = GetKeyByValueType<SomeProps, Function>;    // 'c' | 'd'

运算过程如下:

// 开始
{
  a: string
  b: number
  c: (e: MouseEvent) => void
    d
:
  (e: TouchEvent) => void
}
// 第一步,条件映射
{
  a: never
  b: never
  c: 'c'
  d: 'd'
}
// 第二步,索引取值
never | never | 'c' | 'd'
// never的性质
'c' | 'd'

应用二:防御性编程

举个具体点的例子,当你有一个 union type:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

507.unknown 是什么类型,详细讲一下【热度: 801】【TypeScript】

关键词:unknown类型、unknown类型应用

关键词:unknown类型、unknown类型应用

关键词:unknown类型、unknown类型应用

unknown指的是不可预先定义的类型,在很多场景下,它可以替代any的功能同时保留静态检查的能力。

const num: number = 10;
(num as unknown as string).split('');          // 注意,这里和any一样完全可以通过静态检查

这个时候unknown的作用就跟any高度类似了,你可以把它转化成任何类型,不同的地方是,在静态编译的时候,unknown不能调用任何方法,而any可以。

const foo: unknown = 'string';
foo.substr(1);           // Error: 静态检查不通过报错
const bar: any = 10;
bar.substr(1); 

unknown的一个使用场景是,避免使用any作为函数的参数类型而导致的静态类型检查bug:

function test(input: unknown): number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: 这个代码块中,类型守卫已经将input识别为array类型
  }
  return input.length;      // Error: 这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险
}

我们在一些无法确定函数参数(返回值)类型中 unknown 使用的场景非常多

// 在不确定函数参数的类型时
// 将函数的参数声明为unknown类型而非any
// TS同样会对于unknown进行类型检测,而any就不会
function resultValueBySome(val: unknown) {
  if (typeof val === 'string') {
    // 此时 val 是string类型   
    // do someThing 
  } else if (typeof val === 'number') {
    // 此时 val 是number类型   
    // do someThing  
  }
  // ...
}

508.联合类型是什么?【热度: 1,180】【TypeScript】

关键词:联合类型、联合类型应用

在 TypeScript 中,联合类型是指将多个类型组合到一起形成的新类型。联合类型使用 | 符号来表示,表示允许变量具有其中任意一个类型的值。

例如,可以声明一个变量为 string | number 类型,表示该变量可以是字符串类型或者数值类型。这样可以增加变量的灵活性,可以在不确定变量具体类型的情况下使用它。

以下是一个使用联合类型的示例:

function displayData(data: string | number) {
  console.log(data);
}

displayData("Hello"); // 输出: Hello
displayData(123); // 输出: 123

在上面的例子中,displayData 函数可以接受一个参数,该参数可以是字符串类型或者数值类型。函数内部使用 console.log 打印参数的值。

需要注意的是,在使用联合类型的情况下,只能访问所有类型共有的属性和方法,无法访问特定类型独有的属性和方法。如果需要针对不同类型执行不同的操作,可以使用类型断言或类型保护等技术来处理。

509.extends 条件类型定义【热度: 297】【TypeScript】

关键词:extends 类型继承、extends 条件类型定义

在 TypeScript 中,extends 关键字不仅仅用于类之间的继承关系,还可以用于条件类型的定义。

条件类型是一种在类型系统中根据条件进行推断的方式。通过使用 extends 关键字,可以根据给定的条件选择不同的类型。

以下是一个使用 extends 条件语句定义条件类型的示例:

type TypeName<T> =
  T extends string ? "string" :
    T extends number ? "number" :
      T extends boolean ? "boolean" :
        "unknown";

let type1: TypeName<string>;  // 类型为 "string"
let type2: TypeName<number>;  // 类型为 "number"
let type3: TypeName<boolean>; // 类型为 "boolean"
let type4: TypeName<object>;  // 类型为 "unknown"

在上面的例子中,我们定义了一个条件类型 TypeName,它根据给定的泛型类型 T 来选择不同的类型。如果 T 是 string 类型,那么返回值类型为 "string";如果 T 是 number 类型,那么返回值类型为 "number";如果 T 是 boolean 类型,那么返回值类型为 "boolean";否则返回值类型为 "unknown"

通过上述定义,我们可以根据不同的类型获取它们的类型名称。例如,type1 的类型为 "string"type2 的类型为 "number",依此类推。

注意,条件类型的定义中可以使用嵌套的 extends 关键字,以支持更复杂的条件判断。

510.infer 关键字是什么【热度: 100】【TypeScript】

关键词:infer 关键字、infer 关键字作用

在 TypeScript 中,infer 是一个用于条件类型中的关键字。它的作用是从待推断的类型中提取特定的类型,并将其赋值给一个类型变量。这个类型变量可以在条件类型的 true 分支中使用。

通过使用 infer 关键字,我们可以实现一些高级的类型操作,比如从函数类型中提取参数类型、从数组类型中提取元素类型等。

以下是一个示例,展示了如何使用 infer 关键字提取函数参数的类型:

type ParamType<T> = T extends (param: infer P) => any ? P : never;

function foo(arg: number): void {
  // ...
}

type FooParam = ParamType<typeof foo>; // FooParam 的类型是 number

在上述示例中,我们定义了一个条件类型 ParamType<T>,它接受一个泛型参数 T。在 extends 条件语句中,我们检查泛型参数 T 是否可以赋值给一个函数类型,并使用 infer 关键字提取函数参数的类型并赋值给类型变量 P。如果不是函数类型,则返回 never 类型。

然后,我们定义了一个函数 foo,它接受一个 number 类型的参数。通过使用 typeof foo,我们获取函数 foo 的类型,并使用 ParamType<typeof foo> 提取函数参数的类型,赋值给类型变量 FooParam。在本例中,FooParam 的类型为 number

因此,infer 是 TypeScript 中用于条件类型中的关键字,用于类型推断和提取特定类型的操作。

511.is 作用是什么【热度: 458】【TypeScript】

关键词:is 谓词语法、is 语法作用

在 TypeScript 中,is 是一种类型谓词(type predicate)语法。它用于在运行时对一个值的类型进行检查,并返回一个布尔值。

is 通常与条件类型和类型保护(type guards)一起使用。条件类型可以基于类型谓词 is 的结果来进行类型细化,从而在编译时获取更准确的类型推断。

以下是一个示例,展示了如何使用 is 进行类型谓词检查:

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase());
  } else {
    console.log('Value is not a string.');
  }
}

processValue('hello'); // 输出: HELLO
processValue(42); // 输出: Value is not a string.

在上述示例中,我们定义了一个 isString 函数,它接受一个 unknown 类型的值,并使用 typeof 运算符检查该值是否为字符串类型。函数返回一个布尔值,指示值是否为字符串类型。

然后,我们定义了一个 processValue 函数,它接受一个 unknown 类型的值,并通过调用 isString 函数进行类型谓词检查。如果值是字符串类型,就将其转换为大写并打印出来;否则,打印出值不是字符串类型的消息。

最后,我们调用 processValue 函数两次,一次传入字符串 'hello',一次传入数值 42。第一次调用输出 HELLO ,表示字符串类型的值通过了类型谓词检查;第二次调用输出 Value is not a string.,表示数值类型的值未通过类型谓词检查。

因此,is 是 TypeScript 中用于类型谓词检查的关键字,用于在运行时对一个值的类型进行判断,并返回一个布尔值。

512.in 运算符作用是什么【热度: 844】【TypeScript】

关键词:in 运算符、in 运算符作用、in 运算符应用

在 TypeScript 中,in 是一个运算符,用于检查对象是否具有指定的属性或者类实例是否实现了指定的接口。

对于对象类型,in 运算符可以用来检查对象是否具有某个属性。语法为 property in object,其中 property 是一个字符串,object 是一个对象。

示例:

interface Person {
  name: string;
  age: number;
}

function printPersonInfo(person: Person) {
  if ('name' in person) {
    console.log('Name:', person.name);
  }
  if ('age' in person) {
    console.log('Age:', person.age);
  }
}

let person = { name: 'Alice', age: 25 };
printPersonInfo(person); // 输出: Name: Alice, Age: 25

在上述示例中,我们定义了一个接口 Person,具有 name 和 age 两个属性。然后定义了一个函数 printPersonInfo,它接收一个参数 person,类型为 Person 。在函数内部,我们使用 in 运算符检查 person 对象是否具有 name 和 age 属性,如果有则打印对应的值。

对于类类型,in 运算符可以用来检查类的实例是否实现了指定的接口。语法为 interfaceName in object,其中 interfaceName 是一个接口名字,object 是一个对象或类的实例。

示例:

interface Printable {
  print(): void;
}

class MyClass implements Printable {
  print() {
    console.log('Printing...');
  }
}

function printObjectInfo(obj: any) {
  if ('print' in obj) {
    obj.print();
  }
}

let myObj = new MyClass();
printObjectInfo(myObj); // 输出: Printing...

在上述示例中,我们定义了一个接口 Printable,具有一个方法 print。然后定义了一个类 MyClass,它实现了 Printable 接口,并且实现了 print 方法。接着定义了一个函数 printObjectInfo,它接收一个参数 obj,类型为 any。在函数内部,我们使用 in 运算符检查 obj 对象是否实现了 Printable 接口,如果是则调用 print 方法。

总的来说,in 关键字在 TypeScript 中用于检查对象是否具有指定的属性或类实例是否实现了指定的接口。它可以帮助我们在运行时根据对象的属性或接口的实现情况来进行相应的处理。

514.[Vue] 组件之间的通信方式有哪些?【热度: 1,109】【web框架】【出题公司: PDD】

关键词:vue组件通信、vue通信、Vuex组件通信、$refs组件通信

在Vue中 组件之间的通信总结

在Vue中,组件之间的通信可以通过以下几种方式实现:

  1. Props/Attributes:父组件通过向子组件传递属性(props),子组件通过props接收父组件传递的数据。这是一种单向数据流的方式。
  2. Events/Custom Events:子组件可以通过触发自定义事件($emit),向父组件发送消息。父组件可以监听子组件的自定义事件,在事件回调中处理接收到的消息。
  3. $refs:父组件可以通过在子组件上使用ref属性,获取子组件的实例,并直接调用子组件的方法或访问子组件的属性。
  4. Event Bus:通过创建一个全局事件总线实例,可以在任何组件中触发和监听事件。组件之间可以通过事件总线进行通信。
  5. Vuex:Vuex是Vue官方提供的状态管理库,用于在组件之间共享状态。组件可以通过Vuex的store来进行状态的读取和修改。
  6. Provide/Inject:父组件通过provide选项提供数据,子组件通过inject选项注入数据。这样可以在跨层级的组件中进行数据传递。

Props/Attributes

在Vue中,可以通过props和attributes来实现组件之间的通信。

  1. 使用props: 父组件可以通过props向子组件传递数据。子组件通过在props选项中声明属性来接收父组件传递的数据。

例如,父组件传递一个名为message的属性给子组件:

<template>
  <div>
    <child-component :message="parentMessage"></child-component>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';

  export default {
    components: {
      ChildComponent
    },
    data() {
      return {
        parentMessage: 'Hello from parent'
      };
    }
  };
</script>

子组件接收并使用父组件传递的属性:

<template>
  <div>
    {{ message }}
  </div>
</template>

<script>
  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    }
  };
</script>
  1. 使用attributes: 父组件可以通过attributes向子组件传递数据。子组件通过$attrs属性来访问父组件传递的所有属性。

例如,父组件传递一个名为message的属性给子组件:

<template>
  <div>
    <child-component message="Hello from parent"></child-component>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';

  export default {
    components: {
      ChildComponent
    }
  };
</script>

子组件访问父组件传递的属性:

<template>
  <div>
    {{ $attrs.message }}
  </div>
</template>

<script>
  export default {
    inheritAttrs: false
  };
</script>

这些是使用props和attributes在Vue中实现组件之间通信的示例。通过props可以实现父子组件之间的单向数据流,而通过attributes可以实现更灵活的通信方式。

Events/Custom Events

在Vue中,可以使用Events/Custom Events(事件/自定义事件)来实现组件之间的通信。以下是一个示例:

  1. 在父组件中触发事件:
<template>
  <div>
    <button @click="sendMessage">发送消息给子组件</button>
    <child-component @message-received="handleMessage"></child-component>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';

  export default {
    components: {
      ChildComponent
    },
    methods: {
      sendMessage() {
        this.$emit('message-received', 'Hello from parent');
      },
      handleMessage(message) {
        console.log(message);
      }
    }
  };
</script>
  1. 在子组件中监听事件:
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        message: ''
      };
    },
    mounted() {
      this.$on('message-received', this.handleMessage);
    },
    methods: {
      handleMessage(message) {
        this.message = message;
      }
    }
  };
</script>

在这个示例中,父组件中有一个按钮,当点击按钮时会触发sendMessage方法,该方法通过$emit触发名为message-received的自定义事件,并传递了一个消息作为参数。

子组件中通过$on方法监听message-received事件,并在事件触发时调用handleMessage方法,该方法用于接收并处理接收到的消息。

通过这种方式,父组件可以通过自定义事件向子组件传递数据,子组件则可以通过监听相应的自定义事件来接收并处理父组件传递的数据。

这是使用Events/Custom Events在Vue中实现组件之间通信的示例。通过自定义事件,可以实现父子组件之间的双向通信。

$refs

在Vue中,可以使用$refs来访问子组件的实例,从而进行组件之间的通信。以下是一个示例:

  1. 在父组件中访问子组件的实例:
<template>
  <div>
    <child-component ref="child"></child-component>
    <button @click="sendMessage">发送消息给子组件</button>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';

  export default {
    components: {
      ChildComponent
    },
    methods: {
      sendMessage() {
        this.$refs.child.handleMessage('Hello from parent');
      }
    }
  };
</script>
  1. 子组件中的方法处理接收到的消息:
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        message: ''
      };
    },
    methods: {
      handleMessage(message) {
        this.message = message;
      }
    }
  };
</script>

在这个示例中,父组件通过在子组件上使用ref属性来获取子组件的实例。在父组件的sendMessage方法中,通过this.$refs.child访问子组件的实例,并调用子组件的handleMessage 方法,将消息作为参数传递给子组件。

子组件的handleMessage方法接收到父组件传递的消息,并更新message的值。这样,父组件就可以通过$refs来访问子组件的实例,并调用子组件中的方法,从而实现组件之间的通信。

需要注意的是,$refs只能用于访问子组件的实例,在父组件中直接修改子组件的数据是不推荐的。更好的做法是在子组件中提供相应的方法,父组件通过$refs调用这些方法来进行通信。

Event Bus

在Vue中,可以使用Event Bus(事件总线)来实现组件之间的通信。Event Bus是一个空的Vue实例,可以用于作为中央事件总线,用于组件之间的通信。以下是一个示例:

  1. 创建一个Event Bus实例:
// EventBus.js
import Vue from 'vue';

export const EventBus = new Vue();
  1. 在需要通信的组件中,使用Event Bus来发送和接收事件:
<template>
  <div>
    <button @click="sendMessage">发送消息给另一个组件</button>
  </div>
</template>

<script>
  import { EventBus } from './EventBus.js';

  export default {
    methods: {
      sendMessage() {
        EventBus.$emit('messageReceived', 'Hello from Component A');
      }
    }
  };
</script>
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
  import { EventBus } from './EventBus.js';

  export default {
    data() {
      return {
        message: ''
      };
    },
    mounted() {
      EventBus.$on('messageReceived', (message) => {
        this.message = message;
      });
    }
  };
</script>

在这个示例中,我们首先创建了一个Event Bus实例EventBus,并将其导出。然后在发送消息的组件中,通过EventBus.$emit方法发送一个名为messageReceived的事件,并传递消息作为参数。

在接收消息的组件中,通过在mounted钩子中使用EventBus.$on方法来监听messageReceived事件,并定义一个回调函数来处理接收到的消息。

当发送消息的组件点击按钮时,会触发sendMessage方法,该方法通过EventBus.$emit发送一个事件,并将消息作为参数传递给该事件。

在接收消息的组件中,mounted钩子函数会在组件挂载后执行,此时会调用EventBus.$on方法来监听事件。当messageReceived事件被触发时,回调函数中的逻辑会执行,将接收到的消息更新到message的值上。

这样,通过Event Bus实例,可以实现不同组件之间的通信,组件A通过发送事件,组件B通过监听事件来接收消息。

需要注意的是,使用Event Bus时需要确保事件名称唯一,并在适当的生命周期钩子中进行事件监听和解绑操作,以避免内存泄漏和不必要的事件监听。

Vuex

在Vue中,可以使用Vuex来进行组件之间的通信。Vuex是一个专为Vue.js应用程序开发的状态管理模式。以下是一个使用Vuex进行组件之间通信的示例:

  1. 安装并配置Vuex: 安装Vuex:npm install vuex --save 创建store.js文件:
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    message: ''
  },
  mutations: {
    setMessage(state, payload) {
      state.message = payload;
    }
  }
});

在main.js中引入store.js并注册:

import Vue from 'vue'
import App from './App.vue'
import store from './store.js'

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
  1. 在需要通信的组件中,使用Vuex来发送和接收数据:
<template>
  <div>
    <button @click="sendMessage">发送消息给另一个组件</button>
  </div>
</template>

<script>
  export default {
    methods: {
      sendMessage() {
        this.$store.commit('setMessage', 'Hello from Component A');
      }
    }
  };
</script>
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
  export default {
    computed: {
      message() {
        return this.$store.state.message;
      }
    }
  };
</script>

在这个示例中,我们首先安装并配置了Vuex。

然后,在store.js文件中,我们创建了一个store实例,并定义了一个名为message的状态和一个名为setMessage的mutation,用于更新message的值。

在发送消息的组件中,我们通过this.$store.commit('mutationName', payload)的形式来调用mutation,从而更新Vuex的状态。

在接收消息的组件中,我们通过计算属性来获取Vuex中的message状态,并在模板中使用该计算属性来展示消息。

这样,通过Vuex的状态管理,可以实现组件之间的通信。组件A通过调用mutation来更新状态,组件B通过计算属性来获取状态并进行展示。

需要注意的是,在实际应用中,可以根据需求来定义更多的状态和mutations,以满足组件之间的通信需求。

Provide/Inject

在Vue中,可以使用provide/inject来实现组件之间的通信。provide和inject是Vue的高级特性,可以在祖先组件中提供数据,并在后代组件中注入数据。以下是一个使用provide/inject实现组件之间通信的示例:

父组件:

<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';

  export default {
    components: {
      ChildComponent
    },
    provide() {
      return {
        message: 'Hello from Parent Component'
      };
    }
  };
</script>

子组件:

<template>
  <div>
    <p>{{ injectedMessage }}</p>
  </div>
</template>

<script>
  export default {
    inject: ['message'],
    computed: {
      injectedMessage() {
        return this.message;
      }
    }
  };
</script>

在这个示例中,父组件通过provide属性提供了一个名为message的数据,值为'Hello from Parent Component'。

子组件通过inject属性注入了父组件提供的message数据,并将其存储在一个名为injectedMessage的计算属性中。

最后,子组件通过模板中的{{ injectedMessage }}来展示通过provide/inject传递的数据。

这样,通过provide/inject,父组件可以将数据提供给后代组件,并且后代组件可以通过注入的方式来获取这些数据,实现了组件之间的通信。

需要注意的是,provide/inject是一种上下文注入的方式,因此数据的变化会影响到所有注入了该数据的组件。在实际应用中,要谨慎使用provide/inject,确保数据的使用和变化符合预期。

通过provide/inject,可以在组件之间实现数据的传递和共享,从而实现组件之间的通信。

高级开发者相关问题【共计 1 道题】

515.[性能] 衡量页面性能的指标有哪些?【热度: 1,045】【工程化】【出题公司: 美团】

关键词:web性能指标

性能的核心问题

  • 什么样的性能指标最能度量人的感觉?
  • 怎样才能从我们的真实用户中获取这些指标?
  • 如何用我们所获取的指标来确定一个页面表现得是否「快」?
  • 当我们得知用户所感知的真实性能表现后,我们应该如何做才能避免重蹈覆辙,并在未来提高性能表现?

以用户为中心的性能指标

开始了吗?  页面开始加载了吗?得到了服务端的回应吗?

有用吗?  有足够用户期望看到的内容被渲染出来了吗?

能用吗?  用户能够与我们的页面交互了吗?还是依然在加载?

好用吗?  交互是否流畅、自然、没有延迟与其他的干扰?

首次绘制(First Paint)和首次内容绘制(First Contentful Paint)

首次绘制(FP)和首次内容绘制(FCP)。在浏览器导航并渲染出像素点后,这些指标点立即被标记。 这些点对于用户而言十分重要,因为它回答了我们的第一个问题问题:开始了吗

FP与FCP的主要区别在于,FP标记浏览器所渲染的任何与导航前内容不同的点,而FCP所标记的是来自于DOM中的内容,可能是文本、图片、SVG,甚至是canvas元素。

首次有效绘制(First Meaningful Pain)和主要元素时间点(Hero Element Timing)

首次有效绘制(FMP)回答了我们的问题:有用吗?对于现存的所有网页而言,我们不能去清晰地界定哪些元素的加载是「有用」的(因此目前尚无规范), 但是对于开发者他们自己而言,他们很容易知道页面的哪些部分对于用户而言是最为有用的。

image

这些页面中「最重要的部分」通常被称为主要元素。举一些例子,在YouTube的播放页面,播放器就是主要元素。在Twitter中可能是通知的图标,或者是第一条推文。在 天气应用中,主要元素应是指定位置的预测信息。在一个新闻站点中,它可能是摘要,或者是第一张插图。

网页中总有一部分内容的重要性大于其余的部分。如果这部分的内容能够很快地被加载出来,用户甚至都不会在意其余部分的加载情况。

可交互时间(TTI)

可交互时间(TTI)标记了你的页面已经呈现了画面,并且能够响应用户输入的时间点。页面不能响应用户输入有以下常见的原因:

  • 将被JavaScript所操作的元素还未被加载出来;
  • 一些慢会话阻塞了浏览器的主线程(如我们在上一部分所描述的)

TTI所记录实际上是页面的JavaScript完成了初始化,主线程处于空闲的时间点。

long tasks

浏览器像是单线程的。 某些情况下,一些任务将可能会花费很长的时间来执行,如果这种情况发生了,主线程阻塞,剩下的任务只能在队列中等待。

用户所感知到的可能是输入的延迟,或者是哐当一下全部出现。这些是当今网页糟糕体验的主要来源。

Long Tasks API认为任何超过50毫秒的任务都可能存在潜在的问题,并将这些任务揭露给开发者。既然能够满足50毫秒内完成任务,也就能够符合RAIL在100毫秒内相应用户输入的要求。

指标所反映的用户体验

下表概述了我们的性能指标如何对应到我们的问题之上:

开始了吗

  • 首次绘制、首次内容绘制 First Paint (FP) / First Contentful Paint (FCP)

有用吗

  • 首次有效绘制、主要元素时间点 First Meaningful Paint (FMP) / Hero Element Timing

能用吗

  • 可交互时间点 Time to Interactive (TTI)

好用吗

  • 慢会话 Long Tasks (从技术上来讲应该是:没有慢会话)

获取指标

主要依赖浏览器提供的 api

PerformanceObserver 使用示范

要使用 PerformanceObserver,首先需要创建一个 PerformanceObserver 实例,并指定一个回调函数作为参数。回调函数将在性能事件触发时被调用。然后,通过 PerformanceObserver 的 observe() 方法去监听所关注的性能事件类型。

以下是一个使用 PerformanceObserver 的示例代码:

// 创建回调函数
function performanceCallback(list, observer) {
  list.getEntries().forEach(entry => {
    console.log(entry.name + ":" + entry.startTime);
  });
}

// 创建 PerformanceObserver 实例
const observer = new PerformanceObserver(performanceCallback);

// 监听性能事件类型
observer.observe({ entryTypes: ["measure", "paint"] });

在上面的代码中,定义了一个名为 performanceCallback 的回调函数,它接收两个参数:list 和 observerlist 参数是一个 PerformanceEntryList 对象,包含了所有触发的性能事件,可以通过 getEntries() 方法获取详细信息。observer 参数表示 PerformanceObserver 实例本身。

然后,通过 new PerformanceObserver(performanceCallback) 创建了一个 PerformanceObserver 实例,并将 performanceCallback 作为回调函数传递进去。

最后,通过 observer.observe({ entryTypes: ["measure", "paint"] }) 方法,指定了要监听的性能事件类型,这里监听了 "measure" 和 "paint" 两种类型的事件。

当指定的性能事件类型发生时,回调函数将被调用,并传递触发事件的 PerformanceEntry 对象作为参数。开发者可以在回调函数中进一步处理和分析这些对象,以获取性能数据并进行优化。

需要注意的是,观察者模式是异步的,因此回调函数可能不会立即执行。另外,一旦创建了 PerformanceObserver 实例,需要调用其 disconnect() 方法来停止监听性能事件,避免内存泄漏。

以上是 PerformanceObserver 的基本用法,开发者可以根据实际需求和业务场景来灵活运用。

PerformanceObserver 如何统计FP、FCP

要使用 PerformanceObserver 统计 FP(First Paint)和 FCP(First Contentful Paint),可以按照以下步骤进行:

  1. 创建 PerformanceObserver 实例,并指定一个回调函数作为参数。
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach((entry) => {
    if (entry.name === 'first-paint') {
      console.log('FP:', entry.startTime);
    } else if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  });
});
  1. 使用 PerformanceObserver 的 observe() 方法监听 'paint' 类型的性能事件。
observer.observe({ entryTypes: ['paint'] });
  1. 在回调函数中,通过遍历 PerformanceEntryList 对象的 getEntries() 方法获取所有触发的性能事件,然后根据 entry.name 来判断是否是 FP 或 FCP。
  2. 如果是 FP,可以通过 entry.startTime 获取其开始的时间戳,进行相应的处理。同样,如果是 FCP,也可以通过 entry.startTime 获取其开始的时间戳。

完整的示例代码如下:

const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach((entry) => {
    if (entry.name === 'first-paint') {
      console.log('FP:', entry.startTime);
    } else if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  });
});

observer.observe({ entryTypes: ['paint'] });

在上述代码中,创建了一个 PerformanceObserver 实例,并指定一个回调函数。在回调函数中,根据 entry.name 的值判断是否是 FP 或 FCP,并打印出对应的开始时间戳。然后通过调用 observer.observe() 方法监听 'paint' 类型的性能事件。

通过以上步骤,就可以使用 PerformanceObserver 统计 FP 和 FCP,并对这些性能指标进行进一步的处理和分析。

PerformanceObserver 如何统计 long tasks

要使用 PerformanceObserver 统计长任务(Long Tasks),可以按照以下步骤进行:

  1. 创建 PerformanceObserver 实例,并指定一个回调函数作为参数。
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach((entry) => {
    console.log('Long Task:', entry);
  });
});
  1. 使用 PerformanceObserver 的 observe() 方法监听 'longtask' 类型的性能事件。
observer.observe({ entryTypes: ['longtask'] });
  1. 在回调函数中,通过遍历 PerformanceEntryList 对象的 getEntries() 方法获取所有触发的长任务事件。
  2. 可以通过遍历获得的长任务事件数据,并进行进一步的处理和分析。

完整的示例代码如下:

const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach((entry) => {
    console.log('Long Task:', entry);
  });
});

observer.observe({ entryTypes: ['longtask'] });

在上述代码中,创建了一个 PerformanceObserver 实例,并指定一个回调函数。在回调函数中,遍历 PerformanceEntryList 对象的 getEntries() 方法获取所有长任务事件,并打印出相关的长任务数据。

通过以上步骤,就可以使用 PerformanceObserver 统计长任务,并对这些长任务进行进一步的处理和分析。

补充: 页面性能指标有哪些?

以下是常见的页面性能指标,按照阶段顺序进行表述:

阶段指标名称描述
导航阶段DNS 解析时间浏览器解析域名并获取目标服务器IP地址所花费的时间
导航阶段TCP 连接时间浏览器与服务器建立 TCP 连接所花费的时间
导航阶段SSL/TLS 握手时间如果网站启用了 HTTPS,浏览器与服务器进行 SSL/TLS 握手所花费的时间
导航阶段请求时间浏览器发送 HTTP 请求并等待服务器响应的时间
导航阶段首字节时间(TTFB)浏览器收到服务器响应的第一个字节所花费的时间
渲染阶段DOM 解析时间浏览器将 HTML 文档解析为 DOM 树的时间
渲染阶段CSS 解析时间浏览器将 CSS 样式表解析为 CSSOM 树的时间
渲染阶段首次渲染时间(FP)浏览器将 DOM 树和 CSSOM 树合并,开始绘制页面的时间
渲染阶段首次内容绘制时间(FCP)页面首次有可见内容被绘制的时间
渲染阶段首次有意义绘制时间(FMP)页面首次有有意义的内容被绘制的时间
交互阶段首次输入延迟时间(FID)用户首次与页面进行交互(点击按钮、输入框等)后,页面响应交互的时间
交互阶段首次可交互时间(TTI)页面变得完全可交互(用户可以进行大部分常规操作)所花费的时间
交互阶段页面完全加载时间(Page Load)页面所有资源(包括图片、CSS、JavaScript等)加载完成、渲染完毕并且可交互的时间
用户体验阶段页面响应时间用户发起请求后,页面完成响应所花费的时间
用户体验阶段页面加载时间用户打开页面到页面加载完成所花费的时间
用户体验阶段页面交互性能页面响应用户交互(点击、滚动等)的流畅程度

请注意,以上仅为常见的页面性能指标,并非所有指标都适用于每个网站。具体的指标选择取决于你的应用的特点和需求。

资深开发者相关问题【共计 1 道题】

504.[React] hooks 和 memorizedState 是什么关系?【热度: 836】【web框架】【出题公司: PDD】

关键词:memorizedState、添加和管理状态

hooks 和 memorizedState 之间的关系

在React中,Hooks是一种特殊的函数,用于在函数组件中添加和管理状态以及其他React特性。而memorizedState是React内部用于存储和管理Hooks状态的数据结构。

当你在函数组件中使用Hooks(如useStateuseEffect等)时,React会在组件首次渲染时创建一个memorizedState链表。这个链表中的节点包含了组件的各个状态值。

每个节点都包含了两个重要的属性:memoizedStatenextmemoizedState是该节点对应的状态值,而next是指向下一个节点的指针。这样就形成了一个链表,其中的节点对应于组件中的不同状态。

当组件重新渲染时,React会通过memorizedState链表找到与组件对应的节点,并将其中的状态值返回给组件。当调用状态更新的函数时,React会在memorizedState 链表中找到与组件对应的节点,并将其中的状态值更新为新的值。

因此,Hooks和memorizedState是紧密相关的,Hooks通过memorizedState实现了状态的管理和更新。这种关系使得在函数组件中使用Hooks能够实现声明式的、可持久的状态管理,并且方便React进行性能优化。

hooks 和 memorizedState 是怎么关联起来的?

在React中,Hooks和memorizedState通过一种特殊的数据结构关联起来,这个数据结构被称为Fiber节点。

每个函数组件都对应一个Fiber节点,Fiber节点中包含了组件的各种信息,包括组件的状态(memorizedState)、props、子节点等。

当一个函数组件被调用时,React会创建一个新的Fiber节点,并将其与函数组件关联起来。在这个Fiber节点中,React会通过memoizedState属性存储组件的状态值。

当函数组件重新渲染时,React会更新对应的Fiber节点。在更新过程中,React会根据函数组件中的Hooks调用顺序,遍历memorizedState链表中的节点。

React会根据Hooks调用的顺序,将当前的memorizedState链表中的节点与新的Hooks调用结果进行比较,并更新memoizedState中的值。

这个过程中,React会使用一些算法来比较和更新memorizedState链表中的节点,以确保状态的正确性和一致性。例如,React可能会使用链表的插入、删除、移动等操作来更新memorizedState链表。

通过这样的机制,Hooks和memorizedState实现了状态的管理和更新。Hooks提供了一种声明式的方式,让我们能够在函数组件中使用和更新状态,而memorizedState 则是React内部用于存储和管理这些状态的数据结构。

useState 和 memorizedState 状态举例

当组件首次渲染时(mount阶段),React会创建一个新的Fiber节点,并在其中创建一个memorizedState来存储useState hook的初始值。这个memorizedState 会被添加到Fiber节点的memoizedState属性中。

在更新阶段(update阶段),当组件重新渲染时,React会通过比较前后两次渲染中的memorizedState来判断状态是否发生了变化。

React会根据useState hook的调用顺序来确定memorizedState的位置。例如,在一个组件中多次调用了useState hook,React会按照调用的顺序在memoizedState 属性中创建对应的memorizedState

举一个例子,假设我们有一个表单组件,其中使用了两个useState hook来存储用户名和密码的值:

import React, { useState } from 'react';

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  return (
    <form>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

在这个例子中,我们在组件函数中分别调用了两次useState hook,创建了usernamepassword这两个状态。

在首次渲染时(mount阶段),React会为每一个useState hook创建一个memorizedState对象,并将它们存储在组件的Fiber节点的memoizedState属性中。

当我们输入用户名或密码并触发onChange事件时,React会进入更新阶段(update阶段)。在这个阶段,React会比较前后两次渲染中的memorizedState,并根据变化的状态来更新UI。

React会比较usernamepassword的旧值和新值,如果有变化,会更新对应的Fiber节点中的memoizedState,然后重新渲染组件,并将最新的usernamepassword 值传递给相应的input元素。

通过比较memorizedState,React能够检测到状态的变化,并只更新发生变化的部分,以提高性能和优化渲染过程。