前端面试套题系列(第四篇)
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 个部分
-
接口层(api)
react
包, 平时在开发过程中使用的绝大部分api
均来自此包(不是所有). 在react
启动之后, 正常可以改变渲染的基本操作有 3 个.- class 组件中使用
setState()
- function 组件里面使用 hook,并发起
dispatchAction
去改变 hook 对象 - 改变 context(其实也需要
setState
或dispatchAction
的辅助才能改变)
以上
setState
和dispatchAction
都由react
包直接暴露. 所以要想 react 工作, 基本上是调用react
包的 api 去与其他包进行交互. - class 组件中使用
-
内核层(core) 整个内核部分, 由 3 部分构成:
- 调度器
scheduler
包, 核心职责只有 1 个, 就是执行回调.- 把
react-reconciler
提供的回调函数, 包装到一个任务对象中. - 在内部维护一个任务队列, 优先级高的排在最前面.
- 循环消费任务队列, 直到队列清空.
- 把
- 构造器
react-reconciler
包, 有 3 个核心职责:- 装载渲染器, 渲染器必须实现
HostConfig
协议(如:react-dom
), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如:dom
节点). - 接收
react-dom
包(初次render
)和react
包(后续更新setState
)发起的更新请求. - 将
fiber
树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler
包等待调度.
- 装载渲染器, 渲染器必须实现
- 渲染器
react-dom
包, 有 2 个核心职责:- 引导
react
应用的启动(通过ReactDOM.render
). - 实现
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执行
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中取出回调函数
- 返回带有合成事件参数的回调函数
优势
- 提供了一致的事件处理接口,使得编写代码时可以更加轻松;
- 能够跨多个浏览器保持一致的行为,这样可以大大减少兼容性问题;
- 可以在没有 DOM 的环境下使用,比如 React Native;
- 提供了对事件的统一的管理,可以及时的取消事件处理器等等。