robbiemie/keep-learning

🦅面试(day3)

Opened this issue · 8 comments

  1. 最近做过哪些项目,有哪些优化点(从技术深度,广度展开)
  2. webpack commonChunk 底层实现原理 (查看答案)
  3. webpack 如何拆分依赖模块,拆分规则是什么
  4. webpack 构建产物的底层结构,模块加载原理(查看答案)
  5. nuxt 架构,底层实现原理,ssr 使用哪些模块实现 (实现原理)
  6. nodejs 如何实现多进程,如何监听端口号(操作系统层面讲实现原理)
  7. v8 引擎原理,编译阶段如何进行优化字节码、机器码,什么场景下会执行反优化(查看答案)
  8. vuex 如何实现响应式的,组件层面如何实现响应式,哪些阶段进行依赖收集,都包含哪些场景,展开讲讲(参考)
  9. https 执行过程(查看答案)
  10. http 2.0 做了哪些优化?多路复用的原理?分帧传输如果数据传输异常,会如何处理?(参考答案)
  11. 设计一个 Form 表单,给出整体架构设计(如何拆分模块,功能点,应用设计模式)多个子模块间如何进行发布订阅,如何统一维护关联子模块与父模块的数据联系(无)
  12. redux 底层实现原理,说出实现过程(参考答案)
  13. react fiber,讲讲双缓冲模式(参考答案)
  14. useState 执行到渲染阶段都经历了哪些过程
  15. 讲讲 react 生命周期,如何利用生命周期 Hook 进行调优
  16. reactNative 实现逻辑,底层架构实现方案是什么,如何进行多端磨平适配

算法题

  1. 找出根节点到叶子节点的最大数字和的路径(使用广度优先算法)
/*
 ** TreeNode {
 **         value: number;
 **   children: TreeNode[] | null
 ** }
 ** SingleNode {
 **         value: number;
 **         next: SingleNode | null
 ** }
 */
function findPath(tree: TreeNode): SingleNode {}

查看答案

  1. 二维数组中,“1”相连的最大数量
const grid = [
  [0,0,1,0,0,0,0,1,0,0,0,0,0],
  [0,0,0,0,0,0,0,1,1,1,0,0,0],
  [0,1,1,0,1,0,0,0,0,0,0,0,0],
  [0,1,0,0,1,1,0,0,1,0,1,0,0],
  [0,1,0,0,1,1,0,0,1,1,1,0,0],
  [0,0,0,0,0,0,0,0,0,0,1,0,0],
  [0,0,0,0,0,0,0,1,1,1,0,0,0],
  [0,0,0,0,0,0,0,1,1,0,0,0,0]
]
function maxAreaCount(grid) {

};

查看答案

Q: webpack commonChunk 底层实现原理

A: webpack 在 make 阶段进行模块的依赖收集,生成一个 dependency graph ,并确定模块之间的依赖关系。然后,在遍历图,根据依赖引用的次数,如果达到阈值就生成一个新的 chunk,然后将原来的依赖图的 chunk 进行删除,并创建一个引用指向新的 chunk 。同时,webpack 会生成一个 manifest.json 文件,用来管理和映射资源文件之间的依赖关系。

// Webpack manifest
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["manifest"],{
    // 模块标识符和对应的模块函数
    "moduleA": function(module, exports, require) {
        // 模块A的代码...
    },
    "moduleB": function(module, exports, require) {
        // 模块B的代码...
    },
    // 更多模块...

    // Chunk 映射和加载逻辑
    "0": [
        "moduleA", "moduleB" // 表示 chunk 0 包含模块A和模块B
    ],
    // 更多 chunk 映射...
  }
]);

Q: webpack 如何拆分依赖模块,拆分规则是什么?

A: 首先根据entry 配置生成依赖图,并确定模块之间的依赖关系。遍历依赖图,根据配置规则来决定哪些共享模块是需要拆分进行分组。然后确定模块之间的加载顺序,并删除重复模块。最后根据优化后的依赖图输出打包静态资源。

Q: nodejs 如何实现多进程,如何监听端口号

A: Node.js 是基于单线程的事件循环模型设计的,这意味着它在默认情况下不会利用多核 CPU 的全部能力。然而,Node.js 提供了一些模块和机制来创建和管理多个进程,从而实现并行执行和负载均衡。主要的方法包括使用 child_process 模块和 cluster 模块。

使用 child_process 模块

child_process 模块允许 Node.js 运行一个子进程,并与之通信。可以使用它来执行一个系统命令、调用另一个 Node.js 脚本或任何其他程序。

这个模块提供了几种创建子进程的方法,包括:

  1. exec():用于执行命令,缓冲每个子进程的输出,并将完整的输出作为回调函数的参数返回。
  2. spawn():用于启动一个新进程来运行命令。它返回一个带有 stdout 和 stderr 流的对象,你可以使用这些流来读取子进程的输出。
  3. fork():是 spawn() 的特殊形式,专门用于创建新的 Node.js 进程。与 spawn() 不同的是,它允许父进程和子进程之间通过 IPC(进程间通信)通信。

使用 cluster 模块

cluster 模块允许你轻松地创建共享服务器端口的子进程。这对于在多核 CPU 上运行的 Node.js 应用特别有用,因为它可以帮助你利用所有的 CPU 核心。基本上,cluster 模块允许你启动一个主进程和若干个工作进程(worker processes),主进程可以监控和控制工作进程。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length; // 获取 CPU 的核心数

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,它是一个 HTTP 服务器
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

Node.js 中使用 child_process 模块创建的子进程是独立的进程。当你在 Node.js 应用中创建一个子进程时(无论是通过 exec(), spawn(), fork() 或 execFile() 方法),Node.js 会在操作系统层面启动一个新的进程。这个新启动的进程与启动它的父进程(即你的 Node.js 应用的进程)是独立的,它们在操作系统中拥有不同的进程 ID(PID)。

Q: 设计一个 Form 表单,给出整体架构设计(如何拆分模块,功能点,应用设计模式)多个子模块间如何进行发布订阅,如何统一维护关联子模块与父模块的数据联系

A:

1. 模块化设计

将表单拆分为多个模块,每个模块负责一个具体的功能,例如:

  • Form Manager:负责管理整个表单的状态,包括数据收集、验证、提交等。
  • Field Component:每种类型的输入字段(如文本框、选择器、复选框等)都应该有自己的组件,以便重用。
  • Validation Module:负责字段验证逻辑。
  • Data Binding Module:负责管理表单数据和UI之间的双向绑定。
  • Event Bus:用于模块间的通信。

2. 功能点

  • 动态字段:表单应该能够根据需求动态添加或移除字段。
  • 数据验证:支持各种验证规则,并能给出用户友好的提示。
  • 数据收集和提交:收集表单数据并支持异步提交。
  • 状态管理:跟踪表单的状态(如是否被修改、是否正在提交等)。

3. 设计模式

  • 观察者模式(发布订阅模式):用于模块间的通信,如字段值变化、表单提交等事件。
  • 工厂模式:用于创建不同类型的字段组件。
  • 策略模式:用于实现不同的验证规则。

4. 发布订阅模式的应用

Event Bus 或 EventEmitter 可用于实现发布订阅模式,使得子模块(字段组件、验证模块等)能够订阅表单管理器发布的事件,并在适当的时候发布自己的事件。例如,一个字段值的变化可以发布一个事件,表单管理器订阅这个事件来更新表单的总体状态。

5. 数据联系的维护

  • 单一真实数据源:Form Manager作为单一真实数据源,存储所有字段的当前值。
  • 双向数据绑定:字段组件通过Data Binding Module与Form Manager双向绑定,确保UI和数据的一致性。
  • 状态提升:将子模块(字段组件)的状态提升到父模块(Form Manager),由父模块统一管理。
Form System
│
├── Form Manager
│    ├── 状态管理(字段值、验证状态、表单状态)
│    ├── 数据收集和提交
│    └── 事件发布(字段变化、表单提交等)
│
├── Field Components
│    ├── 文本框、选择器、复选框等
│    └── 数据绑定和事件订阅
│
├── Validation Module
│    ├── 规则定义
│    └── 验证执行
│
└── Event Bus
     └── 模块间通信

Q: 补充完善Person类

const p = new Person();
p.eat().wait(1000).eat().eat().wait(1000).eat()

A:

class Person {
  promise: Promise<void>
  constructor() {
    this.promise = Promise.resolve()
  }
  eat() {
    this.promise.then(() => {
      console.log('eat')
      return Promise.resolve()
    })
    return this;
  }
  wait(delay) {
    this.promise = this.promise.then(() => {
      return new Promise(resolve => setTimeout(resolve, delay));
    })
    return this;
  }
}
const p = new Person();
p.eat().wait(1000).eat().eat().wait(1000).eat()

// 输出: eat -> 延迟1s -> eat
/*
 ** 找出根节点到叶子节点的最大数字和的路径(使用广度优先算法)
 ** TreeNode {
 **         value: number;
 **   children: TreeNode[] | null
 ** }
 ** SingleNode {
 **         value: number;
 **         next: SingleNode | null
 ** }
 */
/**
 * (1)
 * (2),(3),(5)
 * (4,5)(6,7),(4,6,7)
 */

class TreeNode {
  value: number
  children: TreeNode[] | null
  constructor(value: number, children: TreeNode[]) {
    this.value = value
    this.children = children
  }
}

class SingleNode {
  value: number
  next: SingleNode | null
  constructor(value: number) {
    this.value = value
    this.next = null
  }
}
type returnType = {
  node: TreeNode,
  total: number,
  path: number[]
}

function findPath(tree: TreeNode): SingleNode | null {
  if(!tree) return null;
  // 构造记录路径
  let record:returnType = {
    node: tree,
    total: tree.value,
    path: [tree.value]
  }
  let maxValue = tree.value

  function travelTree(node: TreeNode, record):returnType {
    if(node.children?.length === 0
      && record.total > maxValue) {
      maxValue = record.total
      return record
    }
    for(let item of (node.children as TreeNode[])) {
      record = travelTree(item, {
        node: item,
        total: record.total + item.value,
        path: record.path.push(item.value)
      })
    }
    return record
  }
  record = travelTree(tree, record)

  let head
  let point= head
  const path = record.path
  while(path.length) {
    const val = <number>(path.shift());
    if(!head) {
      head = new SingleNode(val)
      point = head
    } else {
      point.next = new SingleNode(val)
      point = point.next
    }
  }
  return head;
}
const grid = [
  [0,0,1,0,0,0,0,1,0,0,0,0,0],
  [0,0,0,0,0,0,0,1,1,1,0,0,0],
  [0,1,1,0,1,0,0,0,0,0,0,0,0],
  [0,1,0,0,1,1,0,0,1,0,1,0,0],
  [0,1,0,0,1,1,0,0,1,1,1,0,0],
  [0,0,0,0,0,0,0,0,0,0,1,0,0],
  [0,0,0,0,0,0,0,1,1,1,0,0,0],
  [0,0,0,0,0,0,0,1,1,0,0,0,0]
]

function maxAreaCount(grid:number[][]):number {
  const rows = grid.length;
  const cols = grid[0].length
  let maxCount = 0
  function dfs(a, i, j, count) {
    if(i < 0 || j < 0 || i >= rows || j >= cols || a[i][j] === 0) {
      return count
    }
    const arr = a.slice();
    arr[i][j] = 0
    // 上
    if(a[i-1][j] === 1) count += dfs(arr,i - 1, j, count + 1);
    // 右
    if(a[i][j + 1] === 1) count += dfs(arr, i, j + 1, count + 1);
    // 下
    if(a[i+1][j] === 1) count += dfs(arr,i + 1, j, count + 1);
    // 左
    if(a[i][j - 1] === 1) count += dfs(arr, i, j - 1, count + 1);
  }
  for(let i = 0; i < rows; i++) {
    for(let j = 0; j < cols; j++) {
      if(grid[i][j] === 1) {
        dfs(grid, i, j, 0)
      }
    }
  }

  return maxCount
}

maxAreaCount(grid)
/*
 ** 找出根节点到叶子节点的最大数字和的路径(使用广度优先算法)
 ** TreeNode {
 **         value: number;
 **   children: TreeNode[] | null
 ** }
 ** SingleNode {
 **         value: number;
 **         next: SingleNode | null
 ** }
 */
/**
 * 
 * (1)
 * (2,3,4)
 * (5,6,8)(2,3,4)(1,2,3)
 */
 class TreeNode {
  value: number;
  children: TreeNode[] | null;

  constructor(value: number, children: TreeNode[] | null = null) {
      this.value = value;
      this.children = children;
  }
}

class SingleNode {
  value: number;
  next: SingleNode | null;

  constructor(value: number, next: SingleNode | null = null) {
      this.value = value;
      this.next = next;
  }
}

interface valueType {
  parent: TreeNode | null
  value: number
}

function findPath(tree: TreeNode): SingleNode | null {
  
  if(!tree) return null;
  let queue = [tree];
  // 构造map
  const map = new Map<TreeNode, valueType>();
  let maxValue = tree.value
  let maxNode = tree
  map.set(tree, { parent: null, value: tree.value })
  // bfs
  while(queue.length) {
    const node = <TreeNode>(queue.shift());
    const children = node.children || [];
    const parentValue:number = map.get(node)?.value || 0
    // 叶子节点
    if(children.length === 0) {
      if(parentValue > maxValue) {
        // 记录最大叶子
        maxValue = parentValue
        maxNode = node
      }
    }
    for(let item of children) {
      map.set(item, { parent: node, value: item.value + parentValue })
      queue.push(item);
    }
  }
  let point:TreeNode|null = maxNode;
  let path:number[] = []
  let head:SingleNode | null = null
  while(point) {
    // 反向查找map
    path.push(point.value)
    const current = map.get(point) || {};
    point = current.parent
  }
  for(let value of path) {
    // 构造链表
    head = new SingleNode(value, head)
  }
  return head
}

const child1 = new TreeNode(1)
const child2 = new TreeNode(2)
const child3 = new TreeNode(3)

const root = new TreeNode(1, [child1,child2,child3])

findPath(root)