Fork me on GitHub

前端面试套题系列(第四篇)

1.websocket如何实现

websocket 严格来说和 http 没什么关系,是另外一种协议格式。但是需要一次从 http 到 websocekt 的切换过程。

 websocket 是二进制协议,一个字节可以用来存储很多信息

简单理解:切换协议的过程,然后二进制的 weboscket 协议的收发。

切换过程除了要带 upgrade 的 header 外,还要带 sec-websocket-key,服务端根据这个 key 算出结果,通过 sec-websocket-accept 返回。响应是 101 Switching Protocols 的状态码。

这个计算过程比较固定,就是 key + 固定的字符串 通过 sha1 加密后再 base64 的结果。

加这个机制是为了确保对方一定是 websocket 服务器,而不是随意返回了个 101 状态码。

之后就是 websocket 协议了,这是个二进制协议,我们根据格式完成了 websocket 帧的解析和生成。

js

//ws.js
const { EventEmitter } = require('events');
const http = require('http');
const crypto = require('crypto');

function hashKey(key) {
  const sha1 = crypto.createHash('sha1');
  sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
  return sha1.digest('base64');
}

function handleMask(maskBytes, data) {
  const payload = Buffer.alloc(data.length);
  for (let i = 0; i < data.length; i++) {
    payload[i] = maskBytes[i % 4] ^ data[i];
  }
  return payload;
}

const OPCODES = {
  CONTINUE: 0,
  TEXT: 1,
  BINARY: 2,
  CLOSE: 8,
  PING: 9,
  PONG: 10,
};

function encodeMessage(opcode, payload) {
  //payload.length < 126
  let bufferData = Buffer.alloc(payload.length + 2 + 0);;
  
  let byte1 = parseInt('10000000', 2) | opcode; // 设置 FIN 为 1
  let byte2 = payload.length;

  bufferData.writeUInt8(byte1, 0);
  bufferData.writeUInt8(byte2, 1);

  payload.copy(bufferData, 2);
  
  return bufferData;
}

class MyWebsocket extends EventEmitter {
  constructor(options) {
    super(options);

    const server = http.createServer();
    server.listen(options.port || 8080);

    server.on('upgrade', (req, socket) => {
      this.socket = socket;
      socket.setKeepAlive(true);

      const resHeaders = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),
        '',
        ''
      ].join('\r\n');
      socket.write(resHeaders);

      socket.on('data', (data) => {
        this.processData(data);
        // console.log(data);
      });
      socket.on('close', (error) => {
          this.emit('close');
      });
    });
  }

  handleRealData(opcode, realDataBuffer) {
    switch (opcode) {
      case OPCODES.TEXT:
        this.emit('data', realDataBuffer.toString('utf8'));
        break;
      case OPCODES.BINARY:
        this.emit('data', realDataBuffer);
        break;
      default:
        this.emit('close');
        break;
    }
  }

  processData(bufferData) {
    const byte1 = bufferData.readUInt8(0);
    let opcode = byte1 & 0x0f; 
    
    const byte2 = bufferData.readUInt8(1);
    const str2 = byte2.toString(2);
    const MASK = str2[0];

    let curByteIndex = 2;
    
    let payloadLength = parseInt(str2.substring(1), 2);
    if (payloadLength === 126) {
      payloadLength = bufferData.readUInt16BE(2);
      curByteIndex += 2;
    } else if (payloadLength === 127) {
      payloadLength = bufferData.readBigUInt64BE(2);
      curByteIndex += 8;
    }

    let realData = null;
    
    if (MASK) {
      const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4);  
      curByteIndex += 4;
      const payloadData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);
      realData = handleMask(maskKey, payloadData);
    } 
    
    this.handleRealData(opcode, realData);
  }

  send(data) {
    let opcode;
    let buffer;
    if (Buffer.isBuffer(data)) {
      opcode = OPCODES.BINARY;
      buffer = data;
    } else if (typeof data === 'string') {
      opcode = OPCODES.TEXT;
      buffer = Buffer.from(data, 'utf8');
    } else {
      console.error('暂不支持发送的数据类型')
    }
    this.doSend(opcode, buffer);
  }

  doSend(opcode, bufferDatafer) {
    this.socket.write(encodeMessage(opcode, bufferDatafer));
  }
}

module.exports = MyWebsocket;

Index:

const MyWebSocket = require('./ws');
const ws = new MyWebSocket({ port: 8080 });

ws.on('data', (data) => {
  console.log('receive data:' + data);
  setInterval(() => {
    ws.send(data + ' ' + Date.now());
  }, 2000)
});

ws.on('close', (code, reason) => {
  console.log('close:', code, reason);
});

html:

<!DOCTYPE HTML>
<html>
<body>
    <script>
        const ws = new WebSocket("ws://localhost:8080");

        ws.onopen = function () {
            ws.send("发送数据");
            setTimeout(() => {
                ws.send("发送数据2");
            }, 3000)
        };

        ws.onmessage = function (evt) {
            console.log(evt)
        };

        ws.onclose = function () {
        };
    </script>
</body>

</html>

2、手写lodash的_.chunk

例子:

_.chunk(['a', 'b', 'c', 'd', 'e'], 2);
// => [['a', 'b'], ['c', 'd'], ['e']]

源码:

_.chunk

import slice from './slice.js'
import toInteger from './toInteger.js'


function chunk(array, size = 1) {     //默认size为1
  size = Math.max(toInteger(size), 0)     //size若小于0,则取值0,否则取值为size
  const length = array == null ? 0 : array.length     //array为null,则取值为0,否则取值为array.length
  if (!length || size < 1) {     //length为0或者size小于1,则返回空数组
    return []
  }
  let index = 0
  let resIndex = 0
// 用数组的长度除以size并向上取整以得到分块的个数,新建一个长度为分块个数的数组result
  const result = new Array(Math.ceil(length / size))    


// while循环用来遍历array数组,每次截取array中的size个元素并将截取结果添加到result数组中
// slice函数用的是lodash自己封装的方法
  while (index < length) {
    result[resIndex++] = slice(array, index, (index += size))
// 这两个是一样的意思
// result[resIndex] = slice(array, index, (index += size))
//
resIndex++
  }
  return result
}

 _.slice

function slice(array, start, end) {
  let length = array == null ? 0 : array.length
  if (!length) {
    return []
  }
  start = start == null ? 0 : start
  end = end === undefined ? length : end

  if (start < 0) {
    start = -start > length ? 0 : (length + start)
  }
  end = end > length ? length : end
  if (end < 0) {
    end += length
  }
  length = start > end ? 0 : ((end - start) >>> 0)
  start >>>= 0

  let index = -1
  const result = new Array(length)
  while (++index < length) {
    result[index] = array[index + start]
  }
  return result
}


3、怎么实现一个 webpack loader ,对markdown文件转换成html文件

const loaderUtils = require("loader-utils");
const md = require('markdown-ast'); //md通过正则匹配的方法把buffer转抽象语法树
const hljs = require('highlight.js'); //代码高亮插件
// 利用 AST 作源码转换
class MdParser {
  constructor(content) {
    this.data = md(content);
    this.parse()
  }
  parse() {
    this.data = this.traverse(this.data);
  }
  traverse(ast) {
    console.log(ast)
    let body = '';
    ast.map(item => {
      switch (item.type) {
        case "bold":
          body += `'<strong>${this.traverse(item.block)}</strong>'`
          break;
        case "break":
          body += '<br/> '
          break;
        case "codeBlock":
          const highlightedCode = hljs.highlight(item.syntax, item.code).value
          body += highlightedCode
          break;
        case "codeSpan":
          body += `<code>${item.code}</code>`
          break;
        case "image":
          body += `<img src=${item.type} alt=${item.alt} rel=${item.rel||''}>`
          break;
        case "italic":
          body += `<em> ${this.traverse(item.block)}</em>`;
          break;
        case "link":
          let linkString = this.traverse(item.block)
          body += `<a href=${item.url}> ${linkString}<a/>`
          break;
        case "list":
          item.type = (item.bullet === '-') ? 'ul' : 'ol'
          if (item.type !== '-') {
            item.startatt = (` start=${item.indent.length}`)
          } else {
            item.startatt = ''
          }
          body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
          break;
        case "quote":
          let quoteString = this.traverse(item.block)
          body += '<blockquote>\n' + quoteString + '</blockquote>\n';
          break;
        case "strike":
          body += `<del>${this.traverse(item.block)}</del>`
          break;
        case "text":
          body += item.text
          break;
        case "title":
          body += `<h${item.rank}>${this.traverse(item.block)}</h${item.rank}>`
          break;
        default:
          throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
      }
    })
    return body
  }
}

module.exports = function(content) {
  this.cacheable && this.cacheable();
  const options = loaderUtils.getOptions(this);
  try {
    const parser = new MdParser(content);
    return parser.data
  } catch (err) {
    throw err;
  }
};

4、react原理,整个过程

 

 

 

 

架构分层

为了便于理解, 可将 react 应用整体结构分为接口层(api)和内核层(core)2 个部分

  1. 接口层(api)react包, 平时在开发过程中使用的绝大部分api均来自此包(不是所有). 在react启动之后, 正常可以改变渲染的基本操作有 3 个.

    • class 组件中使用setState()
    • function 组件里面使用 hook,并发起dispatchAction去改变 hook 对象
    • 改变 context(其实也需要setStatedispatchAction的辅助才能改变)

    以上setStatedispatchAction都由react包直接暴露. 所以要想 react 工作, 基本上是调用react包的 api 去与其他包进行交互.

  2. 内核层(core) 整个内核部分, 由 3 部分构成:

    1. 调度器scheduler包, 核心职责只有 1 个, 就是执行回调.
      • react-reconciler提供的回调函数, 包装到一个任务对象中.
      • 在内部维护一个任务队列, 优先级高的排在最前面.
      • 循环消费任务队列, 直到队列清空.
    2. 构造器react-reconciler包, 有 3 个核心职责:
      1. 装载渲染器, 渲染器必须实现HostConfig协议(如: react-dom), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如: dom节点).
      2. 接收react-dom包(初次render)和react包(后续更新setState)发起的更新请求.
      3. fiber树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler包等待调度.
    3. 渲染器react-dom包, 有 2 个核心职责:
      1. 引导react应用的启动(通过ReactDOM.render).
      2. 实现HostConfig协议(源码在 ReactDOMHostConfig.js 中), 能够将react-reconciler包构造出来的fiber树表现出来, 生成 dom 节点(浏览器中), 生成字符串(ssr).

5、qiankun源码实现

  • 注册微应用时通过fetch请求HTML entry,然后正则匹配得到内部样式表、外部样式表、内部脚本、外部脚本
  • 通过fetch获取外部样式表、外部脚本然后与内部样式表、内部脚本按照原来的顺序组合组合之前为样式添加属性选择器(data-微应用名称);将组合好的样式通过style标签添加到head中
  • 创建js沙盒:不支持Proxy的用SnapshotSandbox(通过遍历window对象进行diff操作来激活和还原全局环境),支持Proxy且只需要单例的用LegcySandbox(通过代理来明确哪些对象被修改和新增以便于卸载时还原环境),支持Proxy且需要同时存在多个微应用的用ProxySandbox(创建了一个window的拷贝对象,对这个拷贝对象进行代理,所有的修改都不会在rawWindow上进行而是在这个拷贝对象上),最后将这个proxy对象挂到window上面
  • 执行脚本:将上下文环境绑定到proxy对象上,然后eval执行
registerMicroApps: 注册微应用
loadMicroApp: 手动加载微应用的 

importEntry: 加载html入口的方法

createSandboxContainer:创建js沙盒

execScripts:执行js脚本

 

6、虚拟列表的实现,横向的怎么做

虚拟列表: 根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据 

 

具体步骤如下:

  • 计算当前可见区域起始数据的 startIndex
  • 计算当前可见区域结束数据的 endIndex
  • 计算当前可见区域的数据,并渲染到页面中
  • 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
  • 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上

纵向使用scrollTop 去判断

横向使用 scrollLeft 去判断

getBoundingClientRect可以准确的获取dom的width、height

7、React 事件绑定原理 官方为什么这么做?

1、事件注册

  • 组件装载 / 更新。
  • 通过lastProps、nextProps判断是否新增、删除事件分别调用事件注册、卸载方法。
  • 调用EventPluginHub的enqueuePutListener进行事件存储
  • 获取document对象。
  • 根据事件名称(如onClick、onCaptureClick)判断是进行冒泡还是捕获。
  • 判断是否存在addEventListener方法,否则使用attachEvent(兼容IE)。
  • 给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。

2、事件存储

  • EventPluginHub负责管理React合成事件的callback,它将callback存储在listenerBank中,另外还存储了负责合成事件的Plugin。
  • EventPluginHub的putListener方法是向存储容器中增加一个listener。
  • 获取绑定事件的元素的唯一标识key。
  • 将callback根据事件类型,元素的唯一标识key存储在listenerBank中。
  • listenerBank的结构是:listenerBank[registrationName][key]。

3、事件触发执行

  • 触发document注册原生事件的回调dispatchEvent
  • 获取到触发这个事件最深一级的元素

          这里的事件执行利用了React的批处理机制

<div onClick={this.parentClick} ref={ref => this.parent = ref}>
      <div onClick={this.childClick} ref={ref => this.child = ref}>
          test
     </div>
</div>
  • 首先会获取到this.child
  • 遍历这个元素的所有父元素,依次对每一级元素进行处理。
  • 构造合成事件。
  • 将每一级的合成事件存储在eventQueue事件队列中。
  • 遍历eventQueue。
  • 通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法。
  • 如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件。
  • 释放处理完成的事件。

4、合成事件

  • 调用EventPluginHub的extractEvents方法。
  • 循环所有类型的EventPlugin(用来处理不同事件的工具方法)。
  • 在每个EventPlugin中根据不同的事件类型,返回不同的事件池。
  • 在事件池中取出合成事件,如果事件池是空的,那么创建一个新的。
  • 根据元素nodeid(唯一标识key)和事件类型从listenerBink中取出回调函数
  • 返回带有合成事件参数的回调函数
 

优势

  1. 提供了一致的事件处理接口,使得编写代码时可以更加轻松;
  2. 能够跨多个浏览器保持一致的行为,这样可以大大减少兼容性问题;
  3. 可以在没有 DOM 的环境下使用,比如 React Native;
  4. 提供了对事件的统一的管理,可以及时的取消事件处理器等等。
posted @ 2023-02-27 10:25  广东靓仔-啊锋  阅读(64)  评论(0编辑  收藏  举报