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 去与其他包进行交互.
- 内核层(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执行
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中取出回调函数
- 返回带有合成事件参数的回调函数
优势
- 提供了一致的事件处理接口,使得编写代码时可以更加轻松;
- 能够跨多个浏览器保持一致的行为,这样可以大大减少兼容性问题;
- 可以在没有 DOM 的环境下使用,比如 React Native;
- 提供了对事件的统一的管理,可以及时的取消事件处理器等等。