├── README.MD ├── article ├── LSIF-TypeScript-Extension.md ├── LSIF-problem.md ├── README.png ├── Redux源码解读------applyMiddleware.MD ├── Redux源码解读------combineReducers.MD ├── Redux源码解读------createStore.MD ├── VSCode-Workbench.md ├── VSCode插件机制.md ├── language-server-protocol.md ├── language-services.md ├── mid-year-summary.md ├── ot.md ├── react个人总结.MD ├── vscode-debug-adapter.md ├── 二叉树.MD ├── 利用PureComponent+ImmutableJS实现Pure-render.md ├── 用于创建高阶组件的React辅助库---recompose.MD ├── 链表.MD └── 面试题整理.md └── src ├── BinarySearchTree ├── .cache │ ├── 0752c7392981e2f65281b9cdb792dcf8.json │ ├── 1138c81668eebadec234d5b337ea23cc.json │ ├── 71df19c220b62a8761ad8e68b602611f.json │ ├── 7d1ce5460eb03f9742f8b5b0436ce881.json │ ├── acbebe7ba772c1f2ab6e7f4649a117fc.json │ ├── d19b650e0f36f4fb8a50a5d0bbaa4a87.json │ ├── eb3b882b0eea59dfd1b2c991a709ffa9.json │ └── f605b6c2309f88802bc2f2e70f35957a.json ├── Node.ts ├── dist │ ├── a9e8758779a7f02c0273e555b3d60a05.js │ └── index.html ├── inOrderTraverseNode.ts ├── index.html ├── index.ts ├── insertNode.ts ├── postOrderTraverseNode.ts └── preOrderTraverseNode.ts ├── DoublyLinkedList ├── .cache │ ├── 080dc902d6fa4ae70f724f2417e1d094.json │ ├── 0bc1c7ed26503281c778a01b0cef91e5.json │ └── 158a03e5a2e4585ff7fdc0a23bb30a11.json ├── DoublyNode.ts ├── dist │ ├── 42e86c00ec971c65acc323f3f2982061.js │ └── index.html ├── index.html └── index.ts └── LinkedList ├── .cache ├── 394ef263b0cffd3f1cd0e5de1214c20d.json ├── 68e76d4a9b84942d8e8546b5e904aaec.json └── e09955fecb90623ab1878c09b54cf4a2.json ├── Node.ts ├── dist ├── 339c3ca8229ad11106d1f8fd0b86f537.js └── index.html ├── index.html └── index.ts /README.MD: -------------------------------------------------------------------------------- 1 | ### React 相关 2 | 2017-09-13: [Redux源码解读------createStore.js](https://github.com/SakuraAsh/blog/issues/1) 3 | 4 | 2017-09-14: [Redux源码解读------combineReducers.js](https://github.com/SakuraAsh/blog/issues/2) 5 | 6 | 2017-09-15: [Redux源码解读------applyMiddleware.js](https://github.com/SakuraAsh/blog/issues/3) 7 | 8 | 2017-09-19:[Recompose,用于创建高阶组件的React辅助库](https://github.com/SakuraAsh/blog/issues/4) 9 | 10 | 2017-09-26:[React技术栈不完全总结](https://github.com/SakuraAsh/blog/issues/5) 11 | 12 | 2017-12-09: [利用PureComponent+ImmutableJS实现Pure-render](https://github.com/SakuraAsh/blog/issues/6) 13 | 14 | ### 基础 15 | 2018-03-10: [数据结构之链表](https://github.com/SakuraAsh/blog/issues/8) 16 | 17 | 2018-03-10: [数据结构之二叉树](https://github.com/SakuraAsh/blog/issues/9) 18 | 19 | 2018-03-24: [面试题整理](https://github.com/SakuraAsh/blog/issues/7) 20 | 21 | ### WebIDE 22 | 2018-07-01: [多人协同编辑的实现](https://github.com/Aaaaash/blog/issues/10) 23 | 24 | 2018-07-03: [LanguageServerProtocol](https://github.com/Aaaaash/blog/issues/11) 25 | 26 | 2018-07-29: [面向 Web 端的通用 LanguageServer 实现](https://github.com/Aaaaash/blog/issues/12) 27 | 28 | 2018-08-12:[VSCODE 调试器实现原理及实现在线编辑器的调试功能](https://github.com/Aaaaash/blog/issues/13) 29 | 30 | ### LSIF 31 | 2019-07: [LSIF TypeScript Extenion](https://github.com/Aaaaash/blog/issues/16) 32 | 33 | 2019-07: [LSIF 插件的一些问题](https://github.com/Aaaaash/blog/issues/17) 34 | ### VS Code 35 | 36 | 2019-01: [VSCode 插件运行机制](https://github.com/Aaaaash/blog/issues/14) 37 | 38 | 39 | 2019-03: [VSCode Workbench 源码解读](https://github.com/Aaaaash/blog/issues/15) 40 | -------------------------------------------------------------------------------- /article/LSIF-TypeScript-Extension.md: -------------------------------------------------------------------------------- 1 | > 上个月 GitHub 一个[新功能(Navigating code)](https://help.github.com/en/articles/navigating-code-on-github)开启 beta 测试, 目前只对部分用户开放. 作为一个非常依赖 GitHub 看源码/学(chao)技(dai)术(ma)的程序员, 虽然我日常一直使用 sourcegraph 插件作为源码辅助阅读工具, 看到 GitHub 官方终于开始着力提升代码阅读体验, 还是期待了很久. 2 | 3 | 简单来说它主要的作用是在 GitHub 仓库代码里点击相应符号显示一些信息(譬如函数签名, 变量类型)并且可以跳转到定义的位置, 也就是我们在 IDE 里常用到的 **hover** 和 **gotoDefinition.** 可以方便的在线阅读代码, 对于一些中大型的项目可以省去 clone 到本地用 IDE 阅读的成本. 最近在某些仓库代码区域顶部已经可以看到 *You're using jump to definition to discover and navigate code.* 字样, 表示 jump to definition 功能可以在这个仓库使用. 初步体验了一下除了第一次索引较慢, 之后跳转和显示信息速度都还能接受. 4 | 5 | [](https://www.notion.so/c24ce770be564371aac48e49731316b1#8139c3e82ad34c6881378a8de3e87249) 6 | 7 | 在 GitHub 官方博客中可以看到这个功能是基于前段时间开源的 [semantic](https://github.com/github/semantic) 实现的, 关于这个项目纸糊上也有[相关的讨论](https://www.zhihu.com/question/327367431), 有兴趣可以移步围观, 这里我就不献丑了. 8 | 9 | 关于在线**代码阅读辅助工具**, 我个人比较常用的就是 [sourcegraph](https://github.com/sourcegraph/sourcegraph) , 包含了独立的网站, 命令行工具和 Chrome 插件, 这个项目使用支持 LSP 的语言服务在后台对项目代码进行分析, 关于一些技术细节可以看一下他们的[官方博客](https://about.sourcegraph.com/blog), [其中一些文章](https://about.sourcegraph.com/blog/part-1-how-sourcegraph-scales-with-the-language-server-protocol)详细介绍了他们基于 LSP 的整体架构和对 LSP 的一些扩展. sourcegraph 也支持类似 VS Code API 的[插件系统](https://sourcegraph.com/extensions), 开发者可以通过插件的形式增强 sourcegraph 对语言的支持, 目前 sourcegraph 已经支持数十种主流编程语言, 并且完全免费开源. 10 | 11 | 今年 2 月份, VS Code 官方博客更新了一篇名为**[The Language Server Index Format (LSIF)](https://code.visualstudio.com/blogs/2019/02/19/lsif)**的文章, 介绍了主要用于增强代码阅读体验的语言服务索引格式(LSIF)规范, 定义了一种基于图的索引数据结构, 将 IDE 中 hover , 跳转, 引用等 feature 的结果预先缓存下来, 可以为 GitHub 这种代码托管平台提供丰富的阅读体验, 只要平台提供相应的 Client 请求并显示这些内容. 目前 LSIF 规范还在草案阶段, 但已经有了 [TypeScript](https://github.com/microsoft/lsif-node) , [Java](https://github.com/microsoft/lsif-java) 等语言的实现. sourcegraph 也已经着手准备开发下一代代码阅读辅助工具. 但到目前为止, 还没有看到基于 LSIF 的代码阅读工具, 官方只有一个 VS Code 插件作为演示 demo, 但我的需求并不是在 VS Code 里看代码, 而是 GitHub. 所以我开发了基于 LSIF 的 Chrome 插件(目前只支持 TypeScript), 一方面作为一个尝试, 另一方面可以弥补在网络状况不佳(你懂)的情况下 sourcegraph 速度太慢的不足. 12 | 13 | 先来看一下插件的功能 14 | 15 | 鼠标划过显示类型或注释信息 16 | 17 | ![](https://raw.githubusercontent.com/Aaaaash/lsif-typescript-chrome-extension/master/snapshot/hover.png) 18 | 19 | 和 VS Code outline 同款的代码导航 20 | 21 | ![](https://raw.githubusercontent.com/Aaaaash/lsif-typescript-chrome-extension/master/snapshot/navigate.png) 22 | 23 | 是的, 目前只有这两个功能. 这个插件大概花了不到一星期的时间开发, 还有很多坑, 目前也只是能实现基本的功能. 24 | 25 | ## 实现原理 26 | 27 | 首先插件需要通过类似 LSP 的方式和一个 LSIF 后端通信, 这里借鉴了 LSP 的一部分方法, 初次打开 GitHub 项目会发送一个 initialize 请求告诉 LSIF 后端开始初始化, LSIF 后端会 clone 项目代码并使用 lsif-tsc 工具分析一遍项目代码, 然后将结果缓存在一个特定文件中, 索引结果大概长这样 28 | ```json 29 | {"id":1,"type":"vertex","label":"metaData","version":"0.4.2","projectRoot":"file:///path/to/project"} 30 | {"id":2,"type":"vertex","label":"project","kind":"typescript"} 31 | {"id":3,"type":"vertex","label":"$event","kind":"begin","scope":"project","data":2} 32 | {"id":4,"type":"vertex","label":"document","uri":"file:///path/to/project/file.ts","languageId":"typescript","contents":"xxxx"} 33 | {"id":5,"type":"vertex","label":"$event","kind":"begin","scope":"document","data":4} 34 | {"id":6,"type":"vertex","label":"resultSet"} 35 | {"id":7,"type":"vertex","label":"moniker","kind":"export","scheme":"tsc","identifier":"out/common/file:"} 36 | {"id":8,"type":"edge","label":"moniker","outV":6,"inV":7} 37 | {"id":9,"type":"vertex","label":"range","start":{"line":0,"character":0},"end":{"line":0,"character":0},"tag":{"type":"definition","text":"","kind":7,"fullRange":{"start":{"line":0,"character":0},"end":{"line":39,"character":1}}}} 38 | {"id":10,"type":"edge","label":"next","outV":9,"inV":6} 39 | {"id":11,"type":"vertex","label":"document","uri":"file:///path/to/project/file.ts","languageId":"typescript","contents":"yyyyy"} 40 | {"id":12,"type":"vertex","label":"$event","kind":"begin","scope":"document","data":11} 41 | {"id":13,"type":"vertex","label":"resultSet"} 42 | {"id":14,"type":"vertex","label":"moniker","kind":"export","scheme":"tsc","identifier":"out/common/diffHunk:"} 43 | {"id":15,"type":"edge","label":"moniker","outV":13,"inV":14} 44 | {"id":16,"type":"vertex","label":"range","start":{"line":0,"character":0},"end":{"line":0,"character":0},"tag":{"type":"definition","text":"","kind":7,"fullRange":{"start":{"line":0,"character":0},"end":{"line":291,"character":0}}}} 45 | {"id":17,"type":"edge","label":"next","outV":16,"inV":13} 46 | {"id":18,"type":"vertex","label":"resultSet"} 47 | {"id":19,"type":"vertex","label":"moniker","kind":"export","scheme":"tsc","identifier":"out/common/diffHunk:DiffHunk"} 48 | {"id":20,"type":"edge","label":"moniker","outV":18,"inV":19} 49 | {"id":21,"type":"vertex","label":"range","start":{"line":49,"character":13},"end":{"line":49,"character":21},"tag":{"type":"definition","text":"DiffHunk","kind":5,"fullRange":{"start":{"line":49,"character":0},"end":{"line":59,"character":1}}}} 50 | {"id":22,"type":"edge","label":"next","outV":21,"inV":18} 51 | {"id":23,"type":"vertex","label":"hoverResult","result":{"contents":[{"language":"typescript","value":"class DiffHunk"}]}} 52 | {"id":24,"type":"edge","label":"textDocument/hover","outV":18,"inV":23} 53 | {"id":25,"type":"vertex","label":"resultSet"} 54 | {"id":26,"type":"edge","label":"next","outV":25,"inV":18} 55 | {"id":27,"type":"vertex","label":"moniker","scheme":"$local","identifier":"vYHm3Ot2dv3ly39PHoEc0w=="} 56 | {"id":28,"type":"edge","label":"moniker","outV":25,"inV":27} 57 | {"id":29,"type":"vertex","label":"range","start":{"line":5,"character":9},"end":{"line":5,"character":17},"tag":{"type":"definition","text":"DiffHunk","kind":7,"fullRange":{"start":{"line":5,"character":9},"end":{"line":5,"character":17}}}} 58 | {"id":30,"type":"edge","label":"next","outV":29,"inV":25} 59 | {"id":31,"type":"vertex","label":"hoverResult","result":{"contents":[{"language":"typescript","value":"(alias) class DiffHunk\nimport DiffHunk"}]}} 60 | {"id":32,"type":"edge","label":"textDocument/hover","outV":25,"inV":31} 61 | {"id":33,"type":"vertex","label":"range","start":{"line":5,"character":25},"end":{"line":5,"character":37},"tag":{"type":"reference","text":"'./diffHunk'"}} 62 | {"id":34,"type":"edge","label":"next","outV":33,"inV":13} 63 | {"id":35,"type":"vertex","label":"resultSet"} 64 | {"id":36,"type":"vertex","label":"moniker","kind":"export","scheme":"tsc","identifier":"out/common/file:GitChangeType"} 65 | ``` 66 | 67 | 这个过程一般不会很久(除非是超大项目), 例如 [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) 这个项目大概需要 20s 以内的时间, 最终会生成 24m 的索引文件, 然后将这个文件逐行读取并构造出一个图(来不及解释了, 这段代码是我抄的), 可以以很快的速度查询 hover/references 等数据. 之后会返回 initialized 表示初始化完毕, 这时候就可以发起像 LSP 一样的请求了. 68 | 69 | 我的第一个需求是显示一个类似 VS Code 大纲视图的列表, 方便我在读超长的代码时快速跳转到文件内相应的位置, 只需要发送 documentSymbol 请求, 在后端会去之前构造的图里找到对应文件的 documentSymbol 结果并返回给客户端(这里的客户端就是我们的 Chrome 插件). documentSymbol 的结构长这样 70 | ```json 71 | { 72 | "result": [ 73 | { 74 | "name": "uriToFilePath", 75 | "detail": "", 76 | "kind": 12, 77 | "range": { 78 | "start": { "line": 15, "character": 0 }, 79 | "end": { "line": 35, "character": 1 } 80 | }, 81 | "selectionRange": { 82 | "start": { "line": 15, "character": 16 }, 83 | "end": { "line": 15, "character": 29 } 84 | } 85 | }, 86 | { 87 | "name": "isWindows", 88 | "detail": "", 89 | "kind": 12, 90 | "range": { 91 | "start": { "line": 37, "character": 0 }, 92 | "end": { "line": 39, "character": 1 } 93 | }, 94 | "selectionRange": { 95 | "start": { "line": 37, "character": 9 }, 96 | "end": { "line": 37, "character": 18 } 97 | } 98 | }, 99 | ], 100 | "id": 2, 101 | "method": "documentSymbol" 102 | } 103 | ``` 104 | 可以看到相应是一个数组, 包含了文件中所有 definition 的名称, kind(表示他是啥)以及位置信息(zero base). 105 | 106 | 拿到这些就可以在 GitHub 代码页面展示出来了, 大概就是每个 item 放一个 a 标签, 指向对应的行 107 | 108 | [https://github.com/microsoft/vscode-languageserver-node/blob/[commit]/server/src/files.ts#L162](https://github.com/microsoft/vscode-languageserver-node/blob/8801c20b667945f455d7e023c71d2f741caeda25/server/src/files.ts#L162) , 点击这个 a 标签会跳转到相应的行并高亮显示(GitHub 自带). 109 | 110 | [](https://www.notion.so/c24ce770be564371aac48e49731316b1#f0035bd23d1d467bb3f74d4723435223) 111 | 112 | 另一个功能是 hover 效果, 这也是 LSP 本身就支持的方法, 需要先找到触发事件的 token 所处的位置, 可以通过遍历页面 DOM 节点计算得出, 具体不再赘述. 然后发起 hover 请求, 并带上表示 token 位置和路径的参数, 在 LSIF 服务端同样去图里找到预先缓存的 hoverResult 返回即可. 界面上可以用 marked + highlight.js 这套组合将返回的信息以 markdown 的形式渲染出来, 因为标准的 jsdoc 等注释内容在 LSP/LSIF 的实现里可以被解析为 markdown 格式的字符串. 剩下的事情对我这个切图仔来说就很简单了😀. 113 | 114 | 以上就是整个插件的实现过程, 因为大部分是抄了 LSP 的实现, 所以一些代码是从其他开源项目中直接 copy 过来的, 当然也有一些坑点需要解决. 115 | 116 | 1. 索引需要和 Git 版本对应, 查看 master 分支的代码不能返回 dev 分支的索引信息, 这里我目前的做法是 initialize 时携带 commit 号或分支名, 索引文件以 .lsif 命名. 117 | 2. 索引前需要 clone 代码到服务端, 后续推送了代码需要及时 fetch 下来, 这部分还没想好怎么优雅的处理. 118 | 3. 索引和代码文件会比较大, 暂时没有找到合适的数据库方案存储, 目前只是存放在特定目录😂. 119 | 4. lsif-node 本身支持 npm 依赖分析, 如果要做的话还需要 npm install 一次, 有点不能接受. 120 | 5. lsif-tsc 基于 TypeScript 编译器进行代码分析, 部分 tsconfig 不全的项目分析会有异常抛出(可能要等官方后续更新) 121 | 122 | 插件和 LSIF 服务端代码都在我的 GitHub, 有兴趣的可以 pr/issue 甩过来. 123 | 124 | [lsif-typescript-chrome-extension](https://github.com/Aaaaash/lsif-typescript-chrome-extension) 125 | 126 | [lsif-typescript-server](https://github.com/Aaaaash/lsif-typescript-server) 127 | 128 | 后续会继续维护这两个项目, 尽量实现 TypeScript 代码阅读的体验能超过 sourcegraph. 129 | 130 | ## 参考链接 131 | 132 | 1. [Navigating code on GitHub](https://help.github.com/en/articles/navigating-code-on-github) 133 | 2. [如何评价 GitHub 开源的程序分析库 semantic ?](https://www.zhihu.com/question/327367431) 134 | 3. [Part 1: How Sourcegraph scales with the Language Server Protocol](https://about.sourcegraph.com/blog/part-1-how-sourcegraph-scales-with-the-language-server-protocol) 135 | 4. [Part 2: How Sourcegraph scales with the Language Server Protocol](https://about.sourcegraph.com/blog/part-2-how-sourcegraph-scales-with-the-language-server-protocol) 136 | 5. [The Language Server Index Format (LSIF)](https://code.visualstudio.com/blogs/2019/02/19/lsif) 137 | 6. [lsif-node](https://github.com/microsoft/lsif-node) -------------------------------------------------------------------------------- /article/LSIF-problem.md: -------------------------------------------------------------------------------- 1 | 之前[一篇文章](https://zhuanlan.zhihu.com/p/73837942)大致介绍了 [lsif-typescript-chrome-extension](https://github.com/Aaaaash/lsif-typescript-chrome-extension) 的基本功能和实现原理, 经过这段时间的开发, 已经实现了令我比较满意的使用体验 2 | 3 | ![](https://raw.githubusercontent.com/Aaaaash/lsif-typescript-chrome-extension/master/snapshot/hover-navigate-jump.gif) 4 | 5 | 主要做了几点优化 6 | 7 | - documentSymbol 的样式优化了一下, 和 VS Code 大致体验相同 8 | - Hover 的样式也变好看了一点, 同样基本照抄了 VS Code 9 | - 添加了 gotoDefinition 功能, 鼠标放到相应 token 上面点击一下, 不过第三方依赖暂时无法跳转 10 | 11 | 其中插件几个 script 之间以及和 lsif-server 的通信机制也做了两次大的优化. 一开始没有考虑到复用 WebSocket 连接, 每个页面都注入了一个 content script, 并且每次打开一个 GitHub 的代码页面都会和 lsif-server 之间建立一个 WebSocket 连接, 考虑到多数情况下打开代码页面不一定会停留太久, 同时太多连接势必会拖慢服务甚至浏览器性能, 所以第一步是把 WebSocket 连接挪到 background page. 12 | 13 | 简单解释一下这里 background script 是指 Chrome 插件的一个背景页, 每个插件都可以有一个独立的后台脚本, 会随浏览器启动运行, 而 content script 是指可以访问当前页面的一段脚本, 准确来说 content script 可以和当前的页面共享 DOM, 但并不能访问页面上的 window 对象. 我的思路是, background 负责维护一个和 lsif-server 的 WebSocket 连接, content script 只负责当前页面的事件监听及 DOM 操作, 另外还有一个 popup script (也就是右上角插件点击后弹出的小框)负责显示 WebSocket 连接状态. 14 | 15 | content script 不直接和 lsif-server 通信, 所有消息都经过 background 转发, Chrome 插件支持在 content 和 background 之间维持一个长连接 16 | ```ts 17 | // content script 18 | const messagePort = chrome.runtime.connect({ name: 'lsif-typescript-message-channel' }); 19 | messagePort.postMessage({ 20 | //... 21 | }); 22 | 23 | // background script 24 | chrome.runtime.onConnect.addListener((messagePort) => { 25 | if(messagePort.name === 'lsif-typescript-message-channel') { 26 | messagePort.onMessage.addListener((message) => { 27 | // ... 28 | } 29 | } 30 | }); 31 | ``` 32 | 这种模式下, content 只需要维持和 background 之间的通信即可, 同时 background 还需要及时向 content 发送连接状态, 保证 content < - > background < - > lsif-server 消息同步. 33 | 34 | 第二个优化源于一个想法, 先来回顾一下插件流程, 当打开一个 GitHub 代码页面, content 会检查 background 和 lsif-server 的连接状态, 然后依次发送 initialize, documentSymbol 等请求, 一旦切换到另一个页面(这里我用 [insight.io](http://insigh.io/) 插件的文件树功能切换代码页面), 会刷新页面并跳转到新的文件, 然后依然是上述流程, 这个过程没有太大问题. 但当我从 GitHub 项目主页点文件链接时发现页面并没有刷新, 而是直接请求了代码页面的数据并且渲染出来, 这时插件是没有工作的, 因为一开始进入页面 content 脚本只会检查一次 window.location, 非代码页面实际什么也不会发生, 而通过这种方式不刷新直接打开代码页时插件没有监听任何事件, 所以此时插件依然不会运行. 35 | 36 | 解决方案自然是监听 url change 事件, 进入代码页面开始运行插件, 很遗憾虽然有相应的 API 直接修改 url(不是 hash), 但并没有监听这个操作的事件, 好在社区依然有很 hack 的方案, 也就是魔改 window.history.pushState 37 | ```ts 38 | function nativeHistoryWrapper(eventType: string): () => ReturnType { 39 | const origin = window.top.history[eventType]; 40 | return function () { 41 | const rev = origin.apply(this, arguments); 42 | const event = new Event(eventType); 43 | // @ts-ignore 44 | event.args = arguments; 45 | window.dispatchEvent(event); 46 | return rev; 47 | } 48 | } 49 | 50 | const wrappedPushState = nativeHistoryWrapper('pushState'); 51 | window.history.pushState = wrappedPushState; 52 | 53 | window.addEventListener('pushState', () => { 54 | //... 55 | }); 56 | ``` 57 | 当调用 pushState 时会自动 dispatchEvent, 然后直接监听即可. 58 | 59 | 看上去很完美, 直到我在 content 脚本里加入了这段代码, 从项目主页开始点击链接, 没有任何反应. 还记得之前说的吗, content 脚本和当前页面共享 DOM, 但并不能访问当前页面的 window 对象, 也就是这段代码修改了的 window.history 并不会在当前页面生效, 因为 content 脚本本身运行就不在当前页面上下文. 60 | 61 | 当然解决办法也是有的, 常见的方式是 content 页面不做具体逻辑处理, 只负责在 document.body 里动态插入一个 script 标签, src 即是我们真正的 content 代码. 62 | ```ts 63 | const script = document.createElement('script'); 64 | script.src = chrome.runtime.getURL('out/content.js'); 65 | script.type = 'text/javascript'; 66 | document.body.appendChild(script); 67 | ``` 68 | 但这样显然还不够, 因为之前 content 和 background 之间的长连接在content 被直接注入到页面后无法通信了, 而且因为这种行为本身就比较 hack , 所以并没有官方的通信方案. 不过我们还是可以借助强大的 postMessage. 69 | 70 | 为了区分我们把注入的 content 脚本叫做 inject script, 被注入到页面真正的 content 叫做 injected script, 这两个脚本之间可以通过 postMessage 通信, 我们需要把之前 content 和 background的通信方式改为 inject < - > injected < - > background < - > lsif-server, 而 injected 可以看做一个代理 agent, 它和 inject 通过 postMessage 通信, 和 background 通过长连接通信, inject 通过 window.postMessage 发送消息到 injected, injected 不需要做任何处理直接发送给 background, background 再发送到 lsif-server , 请求响应流程则是反过来. 71 | 72 | 这样我们先前魔改 window.history 的代码就可以直接运行在当前页面, 当从项目主页进入时, 插件不会发送任何请求, 一旦通过页面链接点开代码页面, 插件会按照上述的流程向 lsif-server 发送请求获取相关的索引信息. 73 | 74 | ## 参考资料 75 | 76 | 1. [Message Passing](https://developers.chrome.com/extensions/messaging) 77 | 2. [Chrome extensions: Handling messaging from injected scripts](https://thomasboyt.github.io/2014/10/06/chrome-message-workaround.html) -------------------------------------------------------------------------------- /article/README.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aaaaash/blog/da829d2c3e4db7deb13928deba3a80ad09171583/article/README.png -------------------------------------------------------------------------------- /article/Redux源码解读------applyMiddleware.MD: -------------------------------------------------------------------------------- 1 | ## redux源码解读第三篇---applyMiddleware 2 | `applyMiddleware`用于应用自定义中间件,也是整个redux架构中的难点所在,代码非常简短,但是运用了一些较难理解的函数式编程范式,而middleware的多种使用方式也使得这短短的几十行代码非常难以理解 3 | 4 | [查看applyMiddleware源码](https://github.com/reactjs/redux/blob/ab5cafdd50ee740261032cef94935c1f99354173/src/applyMiddleware.js) 5 | 6 | > 这篇文章推荐深度使用redux及middleware之后再阅读,本文会结合之前createStore中遗漏的第三个参数enhancer一同解读 7 | 8 | ### 首先redux的middleware有两种使用方式 9 | 10 | 第一种通过调用`createStore`函数传入第三个参数`enhancer`,官方文档的解释是 11 | > `enhancer` (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。 12 | 13 | 实际在使用时这个参数就是事先调用`applyMiddleware`函数之后的返回结果 14 | 15 | 16 | ```javascript 17 | /*...省略部分代码*/ 18 | const middlewares = [ 19 | middlewarex, 20 | middlewarey, 21 | ]; 22 | 23 | const enhaner = applyMiddleware(...middlewares); 24 | const store = createStore(reducers, {}, enhaner); 25 | ``` 26 | 27 | applyMiddleware源码中有这个`enhaner`函数的定义 28 | 29 | ```javascript 30 | export default function applyMiddleware(...middlewares) { 31 | return (createStore) => (reducer, preloadedState, enhancer) => { 32 | /*省略*/ 33 | } 34 | } 35 | ``` 36 | 37 | 使用es6箭头函数后这段代码看起来有点难懂,实际这里做了柯里化处理,翻译一下就是这样的 38 | ```javascript 39 | function applyMiddleware(...middlewares) { 40 | // 第一次调用applyMiddleware(...middlewares)后返回的是一个接受createStore函数为参数的函数 41 | return function (createStore) { 42 | // 最终返回类似于createStore的增强型函数 43 | return function (reducer, preloadState, enhancer) { 44 | /* 省略*/ 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | 在调用createStore时,内部对第三个参数`enhancer`做了这样的处理 51 | ```javascript 52 | if (typeof enhancer !== 'undefined') { 53 | if (typeof enhancer !== 'function') { 54 | throw new Error('Expected the enhancer to be a function.') 55 | } 56 | // 直接返回并把自身传给enhancer,并不会执行之后剩余的代码 57 | // 因为在applyMiddleware第一次调用后就返回了一个接受createStore做为参数的函数 58 | // 而传入createStore之后最终又返回了一个增强后的createStore 59 | // applyMiddleware中增强的这个createStore同样接受reducer、state、enhancer三个参数 60 | return enhancer(createStore)(reducer, preloadedState) 61 | } 62 | ``` 63 | 64 | 而在applyMiddleware中这个所谓增强型的createStore是这样定义的 65 | 66 | ```javascript 67 | // 再回头调用createStore传入参数创建一个store 68 | const store = createStore(reducer, preloadedState, enhancer) 69 | // 获取dispatch 70 | let dispatch = store.dispatch 71 | // 用户缓存middleware运行结果 72 | let chain = [] 73 | 74 | // 获取middleware所需的api,分别为getState和dispatch 75 | const middlewareAPI = { 76 | getState: store.getState, 77 | dispatch: (...args) => dispatch(...args) 78 | } 79 | 80 | // 依次给每个middleware传入api并直接运行 81 | // 将运行结果储存在chain数组中 82 | chain = middlewares.map(middleware => middleware(middlewareAPI)) 83 | 84 | /* 85 | 最重要的一步 86 | 用compose组合所有middleware的运行结果并创建一个新的dispatch函数 87 | */ 88 | dispatch = compose(...chain)(store.dispatch) 89 | 90 | return { 91 | ...store, 92 | dispatch 93 | } 94 | ``` 95 | 96 | `compose`是函数式编程中将多个函数一起使用的过程,叫做组合函数,想象有两个功能不同的函数,第一个函数的返回值(输出)可以使是第二个函数的参数(输入),那么这两个函数可以被组合使用,类似于这样 97 | ```javascript 98 | const funca = (a, b) => a + b; 99 | 100 | const funcs = (num) => num * 2; 101 | 102 | funcs(funca(1,2)); // 6 103 | 104 | // 使用组合 105 | const compose = (fn2, fn1) => (...args) => fn2(fn1(...args)); 106 | 107 | // 注意调用时最后调用的函数要最先传进去 108 | // 调用顺序和传参顺序是相反的 109 | const result = compose(funcs, funca); 110 | result(1, 2); // 6 111 | ``` 112 | 而如果现在我们有多个函数,恰好前一个函数的输出是后一个函数的输出,那么可以使用通用的`compose`函数,最简单的compose函数可以是这样 113 | ```javascript 114 | const compose = (...fns) => result => { 115 | const list = fns.slice(); 116 | while(list.length > 0) { 117 | result = list.pop()(result); 118 | } 119 | return result; 120 | } 121 | 122 | const result = compose(funcs, funca, ...func); 123 | result(1, 2); 124 | ``` 125 | 126 | 理解了这一点再来看redux中的[compose函数源码](https://github.com/reactjs/redux/blob/feb85574115ec2c7174527b126435abd6154481a/src/compose.js) 127 | 128 | ```javascript 129 | export default function compose(...funcs) { 130 | if (funcs.length === 0) { 131 | return arg => arg 132 | } 133 | 134 | if (funcs.length === 1) { 135 | return funcs[0] 136 | } 137 | 138 | /* 139 | 比之前实现方式更简单的是,redux直接用数组的reduce方法 140 | 遍历funcs数组并按照顺序最后传进的函数被最先调用依次执行最后返回结果 141 | */ 142 | return funcs.reduce((a, b) => (...args) => a(b(...args))) 143 | } 144 | ``` 145 | 146 | 经过这些步骤最终applyMiddleware返回了一个新的store,包含了增强后的dispatch函数,每次调用dispatch触发一个action都会经过每个middleware然后才到达reducer 147 | 148 | 而第二种调用方式是这样的 149 | ```javascript 150 | const store = applyMiddleware(...middlewares)(createStore)(reducers, initialState); 151 | /* 152 | 这里拆开来看 153 | applyMiddleware(...middlewares) === enhancer 154 | enhancer(createStore) ==== createStore 155 | createStore(reducers, initialState) 156 | */ 157 | ``` 158 | 经过上面的分析其实可以看得出来这种调用方式省去了先调用createStore传入enhancer又把createStore传给enhancer的过程,相当于直接执行createStore里的`enhancer(createStore)(reducer, preloadedState)`,之后运行过程就和第一种方式一样了 159 | 160 | applyMiddleware作为redux中最重要的api之一,其运行过程中增强了createStore函数,并改造了store的dispatch方法,使action => reducer这个过程中middleware可以进行副作用操作 161 | -------------------------------------------------------------------------------- /article/Redux源码解读------combineReducers.MD: -------------------------------------------------------------------------------- 1 | ## redux源码解读第二篇---combineReducers 2 | `combineReducer`是redux中相当重要的一个函数,它接受一个对象作为参数,包含一组子reducer,当应用较为庞大时需要按照某种规则切分reducer,combineReducer就是用来组合这些reducer给`createStore`生成store树的 3 | 4 | > createStore函数的第一个参数是可以返回一个state对象的reducer 5 | 6 | 以下对部分类型判断以及错误信息做了省略,可结合[官方源码combineReducers](https://github.com/reactjs/redux/blob/ab5cafdd50ee740261032cef94935c1f99354173/src/combineReducers.js)一同食用 7 | 8 | ```javascript 9 | export default function combineReducers(reducers) { 10 | // 使用Object.keys方法将reducers中可枚举的属性生成一个keys数组 11 | const reducerKeys = Object.keys(reducers) 12 | const finalReducers = {} 13 | for (let i = 0; i < reducerKeys.length; i++) { 14 | const key = reducerKeys[i] 15 | /*省略部分类型判断*/ 16 | if (typeof reducers[key] === 'function') { 17 | // 遍历keys数组,并把相应的subreducer赋值给finalReducers 18 | finalReducers[key] = reducers[key] 19 | } 20 | } 21 | 22 | //第一步其实只是简单的将reducers对象中type为’function’的subreducer筛选出来赋值给finalReducers对象 23 | 24 | 25 | // 将finalReducers中可枚举的属性生成一个keys数组 26 | const finalReducerKeys = Object.keys(finalReducers) 27 | 28 | // 这里返回最终合成的reducer,注意这个reducer将会被传递给createStore函数 29 | 30 | /* !!!!!!!!!! 划重点 31 | 这个函数不同于之前例子中的reducer 32 | 33 | 将它称为reducer函数是因为他们会有相同的作用 34 | 35 | 既传入默认state和action将会生成一个计算后的state对象 36 | 37 | 不同之处在于: 这个函数并不需要根据action.type使用相应的策略更新state 38 | 39 | 只是简单依次执行所有reducer并将所有reducer生成的state合并起来返回给store 40 | 41 | 并且它所接受的state参数也是所有reducer生成的state组合后产生的总的state 42 | 43 | !!!!!!!!!! 44 | (东厂厂公) 45 | */ 46 | 47 | return function combination(state = {}, action) { 48 | 49 | let hasChanged = false 50 | // 最终产生的state树 51 | const nextState = {} 52 | for (let i = 0; i < finalReducerKeys.length; i++) { 53 | const key = finalReducerKeys[i] 54 | // 根据key获取相应的subReducer 55 | const reducer = finalReducers[key] 56 | // 根据key获取相应的subState 57 | const previousStateForKey = state[key] 58 | 59 | /* 60 | 这里直接执行reducer函数, 传入subState和action 61 | 这个循环内会依次执行subReducer函数 62 | 第一个参数state会根据每个reducer的key不同来进行筛选 63 | 第二个参数action不出意外会每个reducer都传一遍,有相应策略则会更新 64 | 所以不同action的type不能相同,否则会出现状态混乱的情况 65 | */ 66 | const nextStateForKey = reducer(previousStateForKey, action) 67 | 68 | // 把新的state赋值给nextState对象 69 | nextState[key] = nextStateForKey 70 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey 71 | } 72 | // 简单对比后返回最终state 73 | return hasChanged ? nextState : state 74 | } 75 | } 76 | 77 | ``` 78 | 79 | 最后返回的`combination`函数通过引用`finalReducers`对象形成了一个闭包,当这个函数被返回给createStore调用后,内部依然可以访问finalReducers,而createStore内部dispatch调用reducer时也会把state对象传递进去,再通过reducer的策略更新并返回,形成一个闭环 80 | -------------------------------------------------------------------------------- /article/Redux源码解读------createStore.MD: -------------------------------------------------------------------------------- 1 | ## redux源码解读第一篇---createStore 2 | 3 | [这里是源码createStore.js](https://github.com/reactjs/redux/blob/ab5cafdd50ee740261032cef94935c1f99354173/src/createStore.js) 4 | 5 | 6 | `createStore`函数是redux状态管理流程的入口,通过调用`createStore(reducer, preloadState)`之后可以创建一个store对象,store包含四个方法(不包含调用后产生的ubsubscribe) 7 | 8 | * `getState` 9 | * `subscribe` 10 | * `dispatch` 11 | * `replaceReducer` 12 | 13 | ### 首先初始化内部变量 14 | ```javascript 15 | 16 | let currentReducer = reducer // 传入的reducer函数 17 | 18 | let currentState = preloadedState // 初始state 19 | 20 | let currentListeners = [] // 事件监听列表 21 | 22 | let nextListeners = currentListeners // 当前监听函数 23 | 24 | let isDispatching = false // 是否正在dispatch 25 | ``` 26 | 27 | ### getState 28 | 很简单,直接返回当前state对象 29 | ```javascript 30 | function getState() { 31 | return currentState 32 | } 33 | ``` 34 | 35 | ### subscribe 36 | redux内部实现了一个简单的观察者模式, 通过subscribe可以传入事件监听函数 37 | ```javascript 38 | function subscribe(listener) { 39 | // 接受一个事件处理函数 40 | 41 | //在react-redux库的connect函数中的用法大致如下 42 | /* 43 | trySubscribe() { 44 | if (shouldSubscribe && !this.unsubscribe) { 45 | this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) 46 | } 47 | } 48 | 调用store.subscribe时传入了this.handleChange函数 49 | 50 | this.handleChange函数的作用是在收到新的store后手动更新传给react组件的store对象: 51 | handleChange = () => { 52 | const prevStore = this.state.currentStore; 53 | const nextStore = this.store.getState(); 54 | // 这里通常会做一次深度对比,确定store是否更新 55 | this.setState({ currentStore: nextStore }); 56 | } 57 | */ 58 | 59 | 60 | /* 省略部分代码*/ 61 | 62 | 63 | nextListeners.push(listener) // 将事件监听函数添加到监听列表 64 | 65 | // store调用subscribe方法后会得到一个ubsubscribe方法用于取消事件监听 66 | return function unsubscribe() { 67 | // 找到当前监听的函数并使用splice方法移除 68 | const index = nextListeners.indexOf(listener) 69 | nextListeners.splice(index, 1) 70 | } 71 | } 72 | ``` 73 | 74 | ### dispatch 75 | `dispatch`方法接受`action`作为参数,用于触发一个action,可以理解为手动触发事件 76 | ```javascript 77 | function dispatch(action) { 78 | try { 79 | // 设置当前dispatch状态为true 80 | isDispatching = true 81 | 82 | // 将当前state和action作为参数传给当前reducer函数 83 | /* 84 | reducer函数应当类似于这种形式: 85 | function someReducer(state = {}, action) { 86 | switch (action.type) { 87 | case xxx: 88 | return Object.assign({}, state, { 89 | xx: xx, 90 | }); 91 | default: 92 | return state; 93 | } 94 | } 95 | */ 96 | 97 | // 调用后reducer函数会根据传递的action.type来对当前state做出相应修改最后返回一个新的state 98 | currentState = currentReducer(currentState, action) 99 | } finally { 100 | isDispatching = false 101 | } 102 | 103 | // 将所有订阅监听的函数赋值给listers 104 | const listeners = currentListeners = nextListeners 105 | for (let i = 0; i < listeners.length; i++) { 106 | const listener = listeners[i] 107 | // 分别执行lister 108 | listener() 109 | } 110 | /* 111 | 到这一步基本实现: 112 | 创建store=>subscribe 113 | 订阅更新 =>reducer 114 | 根据action.type完成更新 => 更新完执行事件监听函数 115 | */ 116 | 117 | // 返回action 118 | return action 119 | } 120 | ``` 121 | 122 | ### replaceReducer 123 | 用于在redux热更新等场景中替换reducer 124 | ```javascript 125 | function replaceReducer(nextReducer) { 126 | // 传入新的reducer函数并替换当前reducer 127 | currentReducer = nextReducer 128 | // 手动触发初始action 129 | dispatch({ type: ActionTypes.INIT }) 130 | } 131 | ``` 132 | 133 | 实际上createStore做的事情很简单,创建store,添加订阅接口,手动触发事件,自动执行订阅函数,createStore最多接受三个参数,前两个分别是`reducer`和`preloadState`,第三个参数为`enhancer`,由于逻辑较为复杂,将在`applyMiddleware`部分单独解读. 134 | 135 | 下一篇分析`combineReducers.js` 136 | -------------------------------------------------------------------------------- /article/VSCode-Workbench.md: -------------------------------------------------------------------------------- 1 | VSCode 作为时下最为流行的代码编辑器,自2015年推出以来逐渐蚕食了 Sublime Text、Atom 等编辑器的市场份额,占领了编辑器领域的半壁江山,截至目前其 GitHub 仓库的 star 数已经达到了 7w+,GitHub 2018年度报告 显示 VSCode 占据开源项目热度第一,Contributors 接近 2w。 2 | 3 | 上一篇文章 只对插件系统及其运行机制做了粗略的剖析,本文将开始尝试从源码入手解读 VSCode 的整体架构。 4 | ## Workbench 5 | Workbench 即「工作区」,也就是 VSCode 主界面,众所周知 VSCode 是基于 Electron 构建的桌面应用程序,Electron 是基于 Chromium 和 Node.js 的跨平台桌面应用框架,VSCode 的工作区即是一个 Electron 的 BrowserWindow,与浏览器不同的是它还包含一个 Node.js 运行时,其渲染进程可以和 Node.js 进程通过 IPC 通信,所以在 BrowserWindow 中可以运行任何 Nodejs.js 模块。 6 | ```js 7 | src/main.js 负责初始化 Electron 应用 8 | // src/main.js 9 | const app = require('electron').app; 10 | 11 | app.once('ready', function () { 12 | onReady(); 13 | } 14 | ``` 15 | onReady 中读取了用户语言设置并劫持了默认的 require 为一个修改过的 loader,用它来加载 src/vs/code/electron-main/main 模块,这是 VSCode 真正的入口,负责解析环境变量和初始化主界面以及创建其他模块所依赖的「Services」。 16 | Services(服务) 是 VSCode 中一系列可以被注入的公共模块,这些 Services 分别负责不同的功能,在这里创建了几个基本服务 17 | ```ts 18 | // src/vs/code/electron-main/main.ts 19 | function createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService { 20 | const services = new ServiceCollection(); 21 | 22 | const environmentService = new EnvironmentService(args, process.execPath); 23 | 24 | const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]); 25 | process.once('exit', () => logService.dispose()); 26 | 27 | // environmentService 一些基本配置,包括运行目录、用户数据目录、工作区缓存目录等 28 | services.set(IEnvironmentService, environmentService); 29 | // logService 日志服务 30 | services.set(ILogService, logService); 31 | // LifecycleService 生命周期相关的一些方法 32 | services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); 33 | // StateService 持久化数据 34 | services.set(IStateService, new SyncDescriptor(StateService)); 35 | // ConfigurationService 配置项 36 | services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); 37 | // RequestService 请求服务 38 | services.set(IRequestService, new SyncDescriptor(RequestService)); 39 | // DiagnosticsService 诊断服务,包括程序运行性能分析及系统状态 40 | services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); 41 | 42 | return new InstantiationService(services, true); 43 | } 44 | ``` 45 | 除了这些基本服务,VSCode 内还包含了大量的服务,如 IModeService、ICodeEditorService、IPanelService 等,通过 VSCode 实现的「依赖注入」模式,可以在需要用到这些服务的地方以 Decorator 的方式做为构造函数参数声明依赖,会被自动注入到类中。 46 | 例如 47 | ```ts 48 | // src/vs/workbench/electron-browser/workbench.ts 49 | class Workbench extends Disposable implements IPartService { 50 | constructor( 51 | private container: HTMLElement, 52 | private configuration: IWindowConfiguration, 53 | serviceCollection: ServiceCollection, 54 | private mainProcessClient: IPCClient, 55 | @IInstantiationService private readonly instantiationService: IInstantiationService, 56 | @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, 57 | @IStorageService private readonly storageService: IStorageService, 58 | @IConfigurationService private readonly configurationService: WorkspaceService, 59 | @IEnvironmentService private readonly environmentService: IEnvironmentService, 60 | @ILogService private readonly logService: ILogService, 61 | @IWindowsService private readonly windowsService: IWindowsService 62 | ){} 63 | } 64 | ``` 65 | 这些服务会在不同的阶段被创建,关于依赖注入的细节之后会单独写一篇文章解读原理,这里不再赘述。 66 | 基础服务初始化完成后会加载 IPC 信道并创建 CodeApplication 实例,调用 startup 方法启动 code 67 | ```ts 68 | // src/vs/code/electron-main/app.ts 69 | function startup(args: ParsedArgs): void { 70 | 71 | // We need to buffer the spdlog logs until we are sure 72 | // we are the only instance running, otherwise we'll have concurrent 73 | // log file access on Windows (https://github.com/Microsoft/vscode/issues/41218) 74 | const bufferLogService = new BufferLogService(); 75 | // 使用之前创建的 services 创建「实例服务」 76 | const instantiationService = createServices(args, bufferLogService); 77 | instantiationService.invokeFunction(accessor => { 78 | const environmentService = accessor.get(IEnvironmentService); 79 | const stateService = accessor.get(IStateService); 80 | 81 | // 根据 environmentService 的配置将必要的环境变量添加到 process.env 中 82 | const instanceEnvironment = patchEnvironment(environmentService); 83 | 84 | // Startup 85 | return initServices(environmentService, stateService as StateService) 86 | .then(() => instantiationService.invokeFunction(setupIPC), error => { // setupIPC 负责加载 IPC 信道用于进程间通信 87 | 88 | // Show a dialog for errors that can be resolved by the user 89 | handleStartupDataDirError(environmentService, error); 90 | 91 | return Promise.reject(error); 92 | }) 93 | .then(mainIpcServer => { 94 | bufferLogService.logger = createSpdLogService('main', bufferLogService.getLevel(), environmentService.logsPath); 95 | // 实例服务创建 CodeApplication 实例并调用 startup 96 | return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); 97 | }); 98 | }).then(null, err => instantiationService.invokeFunction(quit, err)); 99 | } 100 | ``` 101 | CodeApplication.startup 中首先会启动 SharedProcess 共享进程,同时也创建了一些窗口相关的服务,包括 WindowsManager、WindowsService、MenubarService 等,负责窗口、多窗口管理及菜单等功能。 102 | app.ts 中的 openFirstWindow 负责处理首次开启窗口,这里会先创建一系列 Electron 的 IPC 频道,用于主进程和渲染进程间通信 103 | ```ts 104 | const appInstantiationService = accessor.get(IInstantiationService); 105 | 106 | // Register more Main IPC services 107 | const launchService = accessor.get(ILaunchService); 108 | const launchChannel = new LaunchChannel(launchService); 109 | this.mainIpcServer.registerChannel('launch', launchChannel); 110 | 111 | // Register more Electron IPC services 112 | const updateService = accessor.get(IUpdateService); 113 | const updateChannel = new UpdateChannel(updateService); 114 | this.electronIpcServer.registerChannel('update', updateChannel); 115 | 116 | const issueService = accessor.get(IIssueService); 117 | const issueChannel = new IssueChannel(issueService); 118 | this.electronIpcServer.registerChannel('issue', issueChannel); 119 | 120 | const workspacesService = accessor.get(IWorkspacesMainService); 121 | const workspacesChannel = appInstantiationService.createInstance(WorkspacesChannel, workspacesService); 122 | this.electronIpcServer.registerChannel('workspaces', workspacesChannel); 123 | 124 | const windowsService = accessor.get(IWindowsService); 125 | const windowsChannel = new WindowsChannel(windowsService); 126 | this.electronIpcServer.registerChannel('windows', windowsChannel); 127 | this.sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); 128 | 129 | const menubarService = accessor.get(IMenubarService); 130 | const menubarChannel = new MenubarChannel(menubarService); 131 | this.electronIpcServer.registerChannel('menubar', menubarChannel); 132 | 133 | const urlService = accessor.get(IURLService); 134 | const urlChannel = new URLServiceChannel(urlService); 135 | this.electronIpcServer.registerChannel('url', urlChannel); 136 | 137 | const storageMainService = accessor.get(IStorageMainService); 138 | const storageChannel = this._register(new GlobalStorageDatabaseChannel(storageMainService as StorageMainService)); 139 | this.electronIpcServer.registerChannel('storage', storageChannel); 140 | 141 | // Log level management 142 | const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService)); 143 | this.electronIpcServer.registerChannel('loglevel', logLevelChannel); 144 | this.sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); 145 | ``` 146 | 其中 window 和 logLevel 频道还会被注册到 sharedProcessClient ,sharedProcessClient 是主进程与共享进程(SharedProcess)进行通信的 client,我们之后再解释 SharedProcess 的 具体功能。 147 | 之后根据 environmentService 提供的相关参数(file_uri、folder_uri)准备打开窗口,最终调用了 windowsMainService.open 方法 (windowsMainService 即前文创建的 WindowsManager),open 方法解析了参数判断打开的目录路径并调用了 doOpen 方法,这里会根据传入的参数判断将要打开的窗口及相应的工作空间(或目录),创建 CodeWindow 实例,CodeWindow 封装了一个 Electron.BrowserWindow 对象,windowsMainService 中创建CodeWindow 实例后会调用其 load 方法正式加载窗口,实际是调用 browserWindow.loadURL 加载一个 HTML 文件,在这里是加载了 vs/code/electron-browser/workbench/workbench.html ,这是整个 Workbench 的入口,内容也很简单 148 | ```html 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | ``` 163 | 加载了一个 workbench.js 文件,这个文件负责加载真正的 Workbench 模块并调用其 main 方法初始化主界面 164 | ```js 165 | // src/vs/code/electron-browser/workbench/workbench.js 166 | bootstrapWindow.load([ 167 | 'vs/workbench/workbench.main', 168 | 'vs/nls!vs/workbench/workbench.main', 169 | 'vs/css!vs/workbench/workbench.main' 170 | ], 171 | function (workbench, configuration) { 172 | perf.mark('didLoadWorkbenchMain'); 173 | 174 | return process['lazyEnv'].then(function () { 175 | perf.mark('main/startup'); 176 | 177 | // @ts-ignore 178 | // 加载 Workbench 并初始化主界面 179 | return require('vs/workbench/electron-browser/main').main(configuration); 180 | }); 181 | }, { 182 | removeDeveloperKeybindingsAfterLoad: true, 183 | canModifyDOM: function (windowConfig) { 184 | showPartsSplash(windowConfig); 185 | }, 186 | beforeLoaderConfig: function (windowConfig, loaderConfig) { 187 | loaderConfig.recordStats = !!windowConfig['prof-modules']; 188 | if (loaderConfig.nodeCachedData) { 189 | const onNodeCachedData = window['MonacoEnvironment'].onNodeCachedData = []; 190 | loaderConfig.nodeCachedData.onData = function () { 191 | onNodeCachedData.push(arguments); 192 | }; 193 | } 194 | }, 195 | beforeRequire: function () { 196 | perf.mark('willLoadWorkbenchMain'); 197 | } 198 | }); 199 | ``` 200 | 前文中的大量代码只是为这里最终创建主界面做铺垫,Workbench 模块主要代码都在 vs/workbench 目录下,主要负责界面元素的创建和具体业务功能的实现。 201 | src/vs/workbench/electron-browser/main.ts 的 main 函数代码很简单 202 | ```ts 203 | // src/vs/workbench/electron-browser/main.ts 204 | export function main(configuration: IWindowConfiguration): Promise { 205 | const window = new CodeWindow(configuration); 206 | 207 | return window.open(); 208 | } 209 | ``` 210 | 要注意这里的 CodeWindow 和前面那个封装了 BrowserWindow 的 CodeWindow 并不是一个东西,这个 CodeWindow 只负责主界面渲染相关的功能,而之前的 CodeWindow 是负责整个窗口创建及生命周期管理(命名令人困惑)。PS: 最新代码中 CodeWindow 改名为 CodeRendererMain 211 | window.open 里同样创建了依赖的一些服务,监听了 DOMContentLoaded 事件,浏览器 DOM 结构加载完成后创建 Workbench 实例并调用 workbench.startup 开始构建主界面布局、创建全局事件监听、加载设置项以及同样实例化一些依赖的服务,全部完成后会还原之前打开的编辑器,整个 Workbench 加载完成。 212 | ```ts 213 | // src/vs/workbench/electron-browser/workbench.ts 214 | private doStartup(): Promise { 215 | this.workbenchStarted = true; 216 | 217 | // Logging 218 | this.logService.trace('workbench configuration', JSON.stringify(this.configuration)); 219 | 220 | // ARIA 221 | setARIAContainer(document.body); 222 | 223 | // Warm up font cache information before building up too many dom elements 224 | restoreFontInfo(this.storageService); 225 | readFontInfo(BareFontInfo.createFromRawSettings(this.configurationService.getValue('editor'), getZoomLevel())); 226 | this._register(this.storageService.onWillSaveState(() => { 227 | saveFontInfo(this.storageService); // Keep font info for next startup around 228 | })); 229 | 230 | // Create Workbench Container 231 | this.createWorkbench(); 232 | 233 | // Install some global actions 234 | this.createGlobalActions(); 235 | 236 | // Services 237 | this.initServices(); 238 | 239 | // Context Keys 240 | this.handleContextKeys(); 241 | 242 | // Register Listeners 243 | this.registerListeners(); 244 | 245 | // Settings 246 | this.initSettings(); 247 | 248 | // Create Workbench and Parts 249 | this.renderWorkbench(); 250 | 251 | // Workbench Layout 252 | this.createWorkbenchLayout(); 253 | 254 | // Layout 255 | this.layout(); 256 | 257 | // Driver 258 | if (this.environmentService.driverHandle) { 259 | registerWindowDriver(this.mainProcessClient, this.configuration.windowId, this.instantiationService).then(disposable => this._register(disposable)); 260 | } 261 | 262 | // Handle case where workbench is not starting up properly 263 | const timeoutHandle = setTimeout(() => { 264 | this.logService.warn('Workbench did not finish loading in 10 seconds, that might be a problem that should be reported.'); 265 | }, 10000); 266 | 267 | this.lifecycleService.when(LifecyclePhase.Restored).then(() => { 268 | clearTimeout(timeoutHandle); 269 | }); 270 | 271 | // Restore Parts 272 | return this.restoreParts(); 273 | } 274 | ``` 275 | 276 | 后记 277 | VSCode 整体架构非常复杂,但同时源码非常清晰明了,也极少有第三方依赖,核心模块大都是由自身实现,包括依赖注入系统、模块加载(拦截加载器)、插件系统、语言服务、调试器前端及调试器协议等。同时界面包括文件树以及编辑器(Monaco)等长列表都实现了无限滚动(或者叫虚拟列表),整体性能表现非常卓越,虽然在安装大量插件后依然会出现卡顿甚至卡死等情况,但相比同样基于 Electron 架构的 Atom 编辑器来说表现已经非常令人满意了。 278 | 本文仅从 Workbench 创建的流程做粗略的解读,中间省去了部分代码及底层实现细节,之后逐步会从不同角度逐步深入,解读 VSCode 架构中一些值得学习的地方。 -------------------------------------------------------------------------------- /article/VSCode插件机制.md: -------------------------------------------------------------------------------- 1 | > 写这篇文章是因为最近一段时间的工作涉及到 Cloud Studio 插件这一块的内容,旧的插件系统在面向用户开放后暴露了安全性、扩展性等诸多问题。调研了几个不同架构下 IDE 的插件系统实现( Theia, VSCode 等),也大致阅读了一遍 VSCode 插件系统相关的源码,在这里做一个简单的分享,个人水平有限,如有错误之处还请观众老爷们指点一下。 2 | 3 | ## 从加载一个插件开始 4 | 以我们熟悉的 vscode-eslint 为例,查看源码会发现入口是 extension.ts 文件里的 activate 函数,它的函数签名像这样: 5 | ```typescript 6 | activate(context: ExtensionContext): void 7 | ``` 8 | 需要了解的一点是, package.json 里的 activationEvents 字段定义了插件的激活事件,考虑到性能问题,我们并不需要一启动 VSCode 就立即激活所有的插件。activation-events 定义了一组事件,当 activationEvents 字段指定的事件被触发时才会激活相应的插件。包含了特定语言的文件被打开,或者特定的【命令】被触发,以及某些视图被切换甚至是一些自定义命令被触发等等事件。 9 | 例如在 vscode-java 中,activationEvents 字段的值为 10 | ```json 11 | "activationEvents": [ 12 | "onLanguage:java", 13 | "onCommand:java.show.references", 14 | "onCommand:java.show.implementations", 15 | "onCommand:java.open.output", 16 | "onCommand:java.open.serverLog", 17 | "onCommand:java.execute.workspaceCommand", 18 | "onCommand:java.projectConfiguration.update", 19 | "workspaceContains:pom.xml", 20 | "workspaceContains:build.gradle" 21 | ] 22 | ``` 23 | 其中包含 languageId 为 java 的文件被打开,以及由该插件自定义的几个 JDT 语言服务命令被触发,和【工作空间】包含 pom.xml/buld.gradle 这些事件。在以上事件被触发时插件将会被激活。 24 | 这段逻辑被定义在 `src/vs/workbench/api/node/extHostExtensionService.ts` 中 25 | 26 | ```typescript 27 | // 由 ExtensionHostProcessManager 调用并传入相应事件作为参数 28 | public $activateByEvent(activationEvent: string): Thenable { 29 | return ( 30 | this._barrier.wait() 31 | .then(_ => this._activateByEvent(activationEvent, false)) 32 | ); 33 | } 34 | 35 | /* 省略部分代码 */ 36 | 37 | // 实例化 activator 38 | this._activator = new ExtensionsActivator(this._registry, { 39 | 40 | /* 省略部分代码 */ 41 | 42 | actualActivateExtension: (extensionDescription: IExtensionDescription, reason: ExtensionActivationReason): Promise => { 43 | return this._activateExtension(extensionDescription, reason); 44 | } 45 | }); 46 | 47 | // 调用 ExtensionsActivator 的实例 activator 的方法激活插件 48 | private _activateByEvent(activationEvent: string, startup: boolean): Thenable { 49 | const reason = new ExtensionActivatedByEvent(startup, activationEvent); 50 | return this._activator.activateByEvent(activationEvent, reason); 51 | } 52 | ``` 53 | 其中 ExtensionsActivator 定义在 src/vs/workbench/api/node/extHostExtensionActivator.ts 中 54 | 55 | ```typescript 56 | export class ExtensionsActivator { 57 | constructor( 58 | registry: ExtensionDescriptionRegistry, 59 | // 既上文中实例化 activator 传的第二个参数 60 | host: IExtensionsActivatorHost, 61 | ) { 62 | this._registry = registry; 63 | this._host = host; 64 | } 65 | } 66 | ``` 67 | 当调用 activator.activateByEvent 方法时(既某个事件被触发),activator 会获取所有符合该事件的插件并逐一执行 extHostExtensionService._activateExtension 方法(也就是 activator.actualActivateExtension) ,中间省去获取上下文,记录日志等一通操作后调用了 extHostExtensionService._callActivateOptional 静态方法 68 | 69 | ```typescript 70 | /* 省略部分代码 */ 71 | // extension.ts 里的 activate 函数 72 | if (typeof extensionModule.activate === 'function') { 73 | try { 74 | activationTimesBuilder.activateCallStart(); 75 | logService.trace(`ExtensionService#_callActivateOptional ${extensionId}`); 76 | // 调用并传入相关参数 77 | const activateResult: Thenable = extensionModule.activate.apply(global, [context]); 78 | activationTimesBuilder.activateCallStop(); 79 | 80 | activationTimesBuilder.activateResolveStart(); 81 | return Promise.resolve(activateResult).then((value) => { 82 | activationTimesBuilder.activateResolveStop(); 83 | return value; 84 | }); 85 | } catch (err) { 86 | return Promise.reject(err); 87 | } 88 | } 89 | ``` 90 | 至此,插件被成功激活。 91 | 92 | ## 插件如何运行 93 | 再来看插件的代码,插件中需要引入一个叫 vscode 的模块 94 | import * as vscode from 'vscode'; 95 | 熟悉 TypeScript 的朋友都知道这实际上只是引入了一个 vscode.d.ts 类型声明文件而已,这个文件包含了所有插件可用的 API 及类型定义。 96 | 这些 API 在插件 import 时就被注入到了插件的运行环境中,它们定义在源码 `src/vs/workbench/api/node/extHost.api.impl.ts` 文件 `createApiFactory` 函数中,通过 defineAPI 函数统一被注入到插件运行环境。 97 | 98 | ```typescript 99 | function defineAPI(factory: IExtensionApiFactory, extensionPaths: TernarySearchTree, extensionRegistry: ExtensionDescriptionRegistry): void { 100 | 101 | // each extension is meant to get its own api implementation 102 | const extApiImpl = new Map(); 103 | let defaultApiImpl: typeof vscode; 104 | 105 | // 已被全局劫持过的 require 106 | const node_module = require.__$__nodeRequire('module'); 107 | const original = node_module._load; 108 | // 重写 Module.prototype._load 方法 109 | node_module._load = function load(request: string, parent: any, isMain: any) { 110 | // 模块名不是 vscode 调用原方法返回模块 111 | if (request !== 'vscode') { 112 | return original.apply(this, arguments); 113 | } 114 | 115 | // 这里会为每一个插件生成一份独立的 API (为了安全考虑?) 116 | const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath); 117 | if (ext) { 118 | let apiImpl = extApiImpl.get(ext.id); 119 | if (!apiImpl) { 120 | // factory 函数会返回所有 API 121 | apiImpl = factory(ext, extensionRegistry); 122 | extApiImpl.set(ext.id, apiImpl); 123 | } 124 | return apiImpl; 125 | } 126 | /* 省略部分代码 */ 127 | } 128 | } 129 | ``` 130 | 实际上也很简单,这里的 `require` 已经被 Microsoft/vscode-loader 劫持了,所以在插件代码中所有通过 import (运行时会被编译为 require) 引入的模块都会经过这里,通过这种方式将 API 注入到了插件执行环境中。 131 | 一般我们查看资源管理器或者进程会发现 VSCode 创建了很多个子进程,且所有插件都在一个独立的 Extension Host 进程在运行,这是考虑到插件需要在一个与主线程完全隔离的环境下运行,保证安全性。那么问题来了,我们调用 vscode.window.setStatusBarMessage('Hello World') 时是怎么在编辑器状态栏插入消息的?前文我们提到所有的 API 被定义在 extHost.api.impl.ts 文件的 createApiFactory 里,例如 vscode.window.setStatusBarMessage 的实现 132 | ```typescript 133 | const window: typeof vscode.window = { 134 | /* 省略部分代码 */ 135 | setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { 136 | return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); 137 | }, 138 | /* 省略部分代码 */ 139 | } 140 | ``` 141 | 实际调用的是 `extHostStatusBar.setStatusBarMessage` 函数,而 extHostStatusBar 则是 ExtHostStatusBar 的实例 142 | ```typescript 143 | const extHostStatusBar = new ExtHostStatusBar(rpcProtocol); 144 | ``` 145 | ExtHostStatusBar 包含了两个方法 createStatusBarEntry 和 setStatusBarMessage,createStatusBarEntry 返回了一个 ExtHostStatusBarEntry ,它被包装了一层代理,在 ExtHostStatusBar 被实例化化的同时也会产生一个 ExtHostStatusBarEntry 实例 146 | ```typescript 147 | export class ExtHostStatusBar { 148 | 149 | private _proxy: MainThreadStatusBarShape; 150 | private _statusMessage: StatusBarMessage; 151 | 152 | constructor(mainContext: IMainContext) { 153 | // 获取代理 154 | this._proxy = mainContext.getProxy(MainContext.MainThreadStatusBar); 155 | // 传入 this, StatusBarMessage 中也随即实例化了一个 ExtHostStatusBarEntry 156 | this._statusMessage = new StatusBarMessage(this); 157 | } 158 | /* 省略部分代码 */ 159 | } 160 | 161 | class StatusBarMessage { 162 | 163 | private _item: StatusBarItem; 164 | private _messages: { message: string }[] = []; 165 | 166 | constructor(statusBar: ExtHostStatusBar) { 167 | // 调用 createStatusBarEntry 168 | this._item = statusBar.createStatusBarEntry(void 0, ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); 169 | } 170 | /* 省略部分代码 */ 171 | } 172 | ``` 173 | 所以当我们调用 setStatusBarMessage 时,先是调用了 this._statusMessage.setMessage 方法 174 | ```typescript 175 | // setStatusBarMessage 方法 176 | let d = this._statusMessage.setMessage(text); 177 | ``` 178 | 179 | 而 this._statusMessage.setMessage 方法经过层层调用,最终调用了 ExtHostStatusBarEntry 实例的 update 方法,也就是前面的 StatusBarMessage 构造函数中的 this._item.update,而这里就到了重头戏,update 方法中包含了一个 延时为 0 的 setTimeout : 180 | ```typescript 181 | this._timeoutHandle = setTimeout(() => { 182 | this._timeoutHandle = undefined; 183 | 184 | // Set to status bar 185 | // 还记得一开始实例化 ExtHostStatusBar 中的 this._proxy = mainContext.getProxy(MainContext.MainThreadStatusBar); 吗 186 | this._proxy.$setEntry(this.id, this._extensionId, this.text, this.tooltip, this.command, this.color, 187 | this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, 188 | this._priority); 189 | }, 0); 190 | ``` 191 | 这里的 this.proxy 就是 ExtHostStatusBar 构造函数中的 this.proxy 192 | ```typescript 193 | constructor(mainContext: IMainContext) { 194 | this._proxy = mainContext.getProxy(MainContext.MainThreadStatusBar); 195 | this._statusMessage = new StatusBarMessage(this); 196 | } 197 | ``` 198 | 这里的 IMainContext 其实就是继承了 IRPCProtocol 的一个别名而已,new ExtHostStatusBar 的参数是一个 rpcProtocol 实例,它被定义在 src/vs/workbench/services/extensions/node/rpcProtocol.ts 中,我们重点看一下 getProxy 的实现 199 | ```typescript 200 | // 我错了,这里才是重头戏,VSCode 源码太绕了 /(ㄒoㄒ)/~~ 201 | public getProxy(identifier: ProxyIdentifier): T { 202 | // 这里只是根据对应的 identifier 生成对应的 scope 而已,插件调用和 API 的调用一模一样比较方便一些 203 | const rpcId = identifier.nid; 204 | // 例如 StatusBar 的 identifier.nid 就是 'MainThreadStatusBar' 205 | if (!this._proxies[rpcId]) { 206 | // 缓存中没有代理则生成新的代理 207 | this._proxies[rpcId] = this._createProxy(rpcId); 208 | } 209 | // 返回代理后的对象 210 | return this._proxies[rpcId]; 211 | } 212 | 213 | 214 | // 创建代理 215 | private _createProxy(rpcId: number): T { 216 | let handler = { 217 | get: (target: any, name: string) => { 218 | // target 即表示 scope,name 即为被调用方法名 219 | if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) { 220 | target[name] = (...myArgs: any[]) => { 221 | // 插件中的 API 实际被代理到 remoteCall,因为这是一个 RPC 协议 222 | return this._remoteCall(rpcId, name, myArgs); 223 | }; 224 | } 225 | return target[name]; 226 | } 227 | }; 228 | // 返回 API 代理 229 | return new Proxy(Object.create(null), handler); 230 | } 231 | ``` 232 | 233 | _createProxy 返回的是一个代理对象,即它代理了主线程中真正实现这些 API 的对象,例如 'MainThreadStatusBar' 返回的是一个 `MainThreadStatusBarShape` 类型的代理。 234 | ```typescript 235 | export interface MainThreadStatusBarShape extends IDisposable { 236 | $setEntry(id: number, extensionId: string, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: MainThreadStatusBarAlignment, priority: number): void; 237 | $dispose(id: number): void; 238 | } 239 | ``` 240 | 插件 API 定义中并没有实现这个接口,它只需要被主线程中对应的模块实现即可,前面我们说到 setStatusMessage 最终调用了 this._proxy.$setEntry。 241 | _remoteCall 里会调用 RPCProcotol 的静态方法 serializeRequest 将 rpcId 方法名以及参数序列化成一个 Buffer 并发送给主线程。 242 | ```typescript 243 | const msg = MessageIO.serializeRequest(req, rpcId, methodName, args, !!cancellationToken, this._uriReplacer); 244 | 245 | // 省略部分代码 246 | this._protocol.send(msg); 247 | ``` 248 | 关于主线程中接收到消息如何处理其实已经不用多说了,根据 rpcId 找到对应的 Services 以及方法,传入参数即可。 249 | -------------------------------------------------------------------------------- /article/language-server-protocol.md: -------------------------------------------------------------------------------- 1 | > 本系列文章为Monaco-Editor编辑器折腾、踩坑记录,涉及到协同编辑、代码提示、智能感知等功能的实现,不定期更新 2 | 3 | ## LanguageServerProtocol 4 | [LanguageServerProtocol](https://microsoft.github.io/language-server-protocol/)(以下简称LSP)是由微软提出,并与 Redhat、Codenvy、Sourcegraph 等公司联合推出的开源协议。用于语言服务程序向编辑器、IDE 等工具提供一系列代码提示、定义跳转等功能的通用协议。它将高级语言相关的一些功能特性从传统 IDE 中抽象出一个单独的程序来运行,LSP 定义了一套通用的API,遵循LSP协议实现某个语言的特性功能后,编辑器只需要调用该语言的 LanguageServer ,即可实现代码提示、定义跳转、代码诊断等功能。 5 | 6 | 传统的IDE或编辑器要实现诸如智能提示、自动补全等功能,需要根据不同的IDE来开发相应语言的特性功能程序,多个 IDE 要想支持多种高级语言,且每个 IDE 的具体实现及 API 可能都大不相同,开发成本非常高。LSP的出现则很好的解决了这个问题,N 个 IDE 和 M 个语言,只需要开发一次相应语言的语言服务器程序即可在每个IDE中使用。 7 | 8 | [LanguageServerProtocol起源](https://github.com/Microsoft/language-server-protocol/wiki/Protocol-History) 9 | 10 | ## 概览 11 | LSP使用[JSON-RPC](http://www.jsonrpc.org/)协议作为 Server/Client 通信的消息格式,且支持 TCP、Stdin/Stdout 进行消息传输,所以它即可以运行在本地客户端,也可以运行在远程服务器上。 12 | 截至目前 LSP 版本为3.8,实现了数十个方法(具体没数😆),部分主流 IDE/编辑器也已经支持了 LSP ,包括 Eclipse、VScode、Sublime Text & Sublime Text 3、Atom 等。 13 | 14 | LSP协议基本消息格式由 `header` 与 `content` 组成,中间使用`\r\n`作为分隔符。 15 | ``` 16 | Content-length: ... \r\n 17 | \r\n 18 | { 19 | "jsonrpc": "2.0", 20 | "id": 1, 21 | "method": "textDocument/didOpen", 22 | "params": { 23 | ... 24 | } 25 | } 26 | ``` 27 | LSP消息大体来说分为三种类型 28 | 29 | - 通知 (Notifiction) 30 | - 请求 (Request) 31 | - 日志及错误信 (LogMessage/ShowMessage) 32 | 33 | 通信是双向的,Client 可以向 Server 发送请求/通知,比如打开文件、修改文档内容等。 Server 也可以向 Client 发送请求/通知,比如动态注册客户端功能。每个请求需要使用 `id` 为唯一标识符,对这个请求的返回值也应当包含这个 id,一般来说 id 为递增的数字。 34 | LSP的工作流程如下: 35 | 36 | - Client 发送 `initialize` 请求,包含一些初始化参数。Server 收到请求后开始准备启动语言服务,之后 Server 会发送 `initialized` 通知到客户端,语言服务开始工作。 37 | 38 | - 初始化成功后 Server 可能会向 Client 发送一些动态注册功能的请求 `client/registerCapability`。 39 | 40 | - 每次打开一个文件, Client 需要向 Server 发送一个 `textDocument/didOpen` 请求,携带文件 [URI](https://tools.ietf.org/html/rfc3986) 参数。同理关闭文件后要发送一个 `textDocument/didClose` 请求。 41 | 42 | - 编辑文档时,当输入`.`或按下语法提示快捷键时, Client 发送 `textDocument/completion` 请求来获取智能提示列表。 43 | 44 | - 当用户查询某个类/变量/方法的声明时(点击跳转),Client 发送 `textDocument/definition` ,Server 将返回对应的文件 URI 及位置信息。Client 需要实现打开这个新文件的方法。 45 | 46 | - 当用户关闭编辑器时,Client 先发送 `shutdown` 请求,Server 收到请求后会立即关闭但并不会退出进程,而是等待 Client 发送 `exit` 通知。 47 | 48 | ## 如何使 LSP 为 monaco 编辑器提供服务 49 | 50 | 虽然 monaco 编辑器脱胎于 VScode ,但其只是一个编辑器实现,没有文件树,多标签页支持。同时 VScode 是基于 Electron 的桌面端应用,自带 Nodejs 环境,可以利用 TCP 或 Stdin/Stdout 来开启语言服务,虽然 VScode 团队开源了一些 LSP 相关的库,但由于运行环境的巨大差异,在 Web 端并不能直接应用。 51 | 52 | ### LanguageClient 53 | 54 | 要在 VScode 中体验 LSP, 需要先下载安装 [vscode-java](https://github.com/redhat-developer/vscode-java) 插件。这个插件由 redhat-developer 团队开源,使用 TypeScript 及 JavaScript 编写,主要作用是下载和构建 [eclipse.jdt.ls](https://github.com/eclipse/eclipse.jdt.ls) 程序, 以及创建 LanguageClient 使 VScode 能够启动 LSP。 eclipse.jdt.ls 就是 eclipse 开发的 Java 语言服务器 LSP 实现。 55 | 56 | LanguageClient 类由 VScode 团队开源的 [vscode-languageclient](https://www.npmjs.com/package/vscode-languageclient) 库提供,它的主要作用是根据传入的配置连接到指定语言的 LSP,并对 LSP 支持的各种方法做一层封装,还包含了本地运行 LSP 程序时对 TCP 消息进行粘包处理的功能。 57 | 58 | ```typescript 59 | import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-language-client'; 60 | 61 | const clientOptions: LanguageClientOptions = { 62 | documentSelector: [ 63 | { scheme: 'file', language: 'java' }, 64 | { scheme: 'jdt', language: 'java' }, 65 | { scheme: 'untitled', language: 'java' } 66 | ], 67 | synchronize: { 68 | configurationSection: 'java', 69 | fileEvents: [ 70 | workspace.createFileSystemWatcher('**/*.java'), 71 | workspace.createFileSystemWatcher('**/pom.xml'), 72 | workspace.createFileSystemWatcher('**/*.gradle'), 73 | workspace.createFileSystemWatcher('**/.project'), 74 | workspace.createFileSystemWatcher('**/.classpath'), 75 | workspace.createFileSystemWatcher('**/settings/*.prefs'), 76 | workspace.createFileSystemWatcher('**/src/**') 77 | ], 78 | }, 79 | }; 80 | 81 | const serverOptions: ServerOptions = { 82 | command: 'java', 83 | args: [ 84 | // jdt.ls 启动参数 85 | ], 86 | options: { 87 | // 相关配置 88 | } 89 | } 90 | const client = new LanguageClient({ 91 | 'java', 92 | 'Language Support for Java', 93 | serverOptions, 94 | clientOptions, 95 | }); 96 | 97 | client.start(); 98 | ``` 99 | 100 | > 这个库源代码实际包含在 [vscode-languageserver-node](https://github.com/Microsoft/vscode-languageserver-node) 中,猜测可能是 VScode 团队实现 Nodejs 的 LSP 客户端/服务端后觉得它可以作为一个通用的客户端实现,所以单独发布到了 npm 上。 101 | 102 | ### 通信方式 103 | 104 | 之前说过,LSP 支持 TCP 和 Stdin/out 来和客户端通信。 105 | 106 | - 如果是以 TCP 的方式, jdt.ls 启动时需要指定一个 `CLIENT_PORT` 参数表明 TCP 服务的端口,需要注意的是以 TCP 模式启动 jdt.js,LSP 是作为 TCP 客户端,所以需要再开启一个 TCP 服务器,之后 jdt.ls 才会连接到指定端口的服务上进行通信。 107 | 108 | - 如果是以 Stdin/Stdout 启动,则只需要使用 Nodejs 的 Childprocess 开启一个子进程,然后利用标准输入输出与 Client 通信。 109 | 110 | ## Web 端如何实现 111 | 112 | 浏览器是一个封闭的环境,它只能操作 DOM ,所以要想在浏览器中为 monaco 编辑器提供 LSP 服务,必须要把 LSP 运行在服务器上。 113 | 114 | 由于 LSP 和 monaco 本身就是同一个团队开发的,所以 jdt.ls 的实现也可以完美兼容 monaco。我们使用 webSocket 与服务端通信,由于浏览器端的限制,我们无法直接使用 vscode-languageclient ,幸好 typefox 团队基于 vscode-languageclient 开发了使用于浏览器端的适配器 [monaco-languageclient](https://github.com/TypeFox/monaco-languageclient)。借助这个库,我们可以使用 webSocket 轻松的连接远端 LSP 服务。 115 | 116 | ```typescript 117 | import { createMonacoServices } from 'monaco-languageclient'; 118 | import { listen, MessageConnection } from 'vscode-ws-jsonrpc'; 119 | import * as monaco from 'monaco-editor'; 120 | 121 | const editor = monaco.editor.create(root, { 122 | model: monaco.editor.createModel(value, 'java', monaco.Uri.parse(`file://javademo/Hello.java`)), 123 | theme: 'vs-dark', 124 | }); 125 | 126 | const url = 'ws://127.0.0.1/java-lsp'; 127 | // 创建 services,向编辑器注册一系列命令 128 | const services = createMonacoServices(editor, { rootUri: `file://javademo` }); 129 | const webSocket = new WebSocket(url); 130 | 131 | // 监听 webSocket 连接,连接成功后创建客户端并启动 132 | listen({ 133 | webSocket, 134 | onConnection: (connection: MessageConnection) => { 135 | const languageClient = createLanguageClient(connection); 136 | const disposable = languageClient.start(); 137 | connection.onClose(() => disposable.dispose()); 138 | } 139 | }); 140 | 141 | function createLanguageClient(connection: MessageConnection): BaseLanguageClient { 142 | return new BaseLanguageClient({ 143 | name: "Java LSP client", 144 | clientOptions: { 145 | documentSelector: ['java'], 146 | errorHandler: { 147 | error: () => ErrorAction.Continue, 148 | closed: () => CloseAction.DoNotRestart 149 | } 150 | }, 151 | services, 152 | connectionProvider: { 153 | get: (errorHandler, closeHandler) => { 154 | return Promise.resolve(createConnection(connection, errorHandler, closeHandler)) 155 | } 156 | } 157 | }) 158 | } 159 | 160 | ``` 161 | 162 | 这里我们还使用了一个库 `vscode-ws-jsonrpc`,这也是 typefox 团队根据原 VScode 的 `vscode-jsonrpc` 修改而来。原本的 `vscode-jsonrpc` 并不支持 WebSocket,所以对它进行了扩展以支持浏览器端。 163 | 164 | 在服务端我们需要用 Nodejs 的 Childprocess 启动 jdt.ls,同时还要再开启一个 webSocket 服务器。监听 websocket 的 onmessage 事件,将 data 通过 stdin 发送给 LSP, 再监听 stdout 的 ondata 事件,将返回结果通过 webSocket 发送到浏览器端。 165 | 166 | ```typescript 167 | import * as cp from 'child-process'; 168 | import * as express from 'express'; 169 | import * as glob from 'glob'; 170 | import WebSocket from 'ws'; 171 | 172 | const CONFIG_DIR = process.platform === 'darwin' ? 'config_mac' : process.platform === 'linux' ? 'config_linux' : 'config_win'; 173 | const BASE_URI = '/data/eclipse.jdt.ls/server'; 174 | type IJavaExecutable = { 175 | options: any; 176 | command: string; 177 | args: Array; 178 | } 179 | 180 | const PORT = 9988; 181 | const SERVER_HOME = 'lsp-java-server'; 182 | const launchersFound: Array = glob.sync('**/plugins/org.eclipse.equinox.launcher_*.jar', { cwd: `./${SERVER_HOME}` }); 183 | 184 | if (launchersFound.length === 0 || !launchersFound) { 185 | throw new Error('**/plugins/org.eclipse.equinox.launcher_*.jar Not Found!'); 186 | } 187 | 188 | const params: Array = [ 189 | '-Xmx256m', 190 | '-Xms256m', 191 | '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=,quiet=y', 192 | '-Declipse.application=org.eclipse.jdt.ls.core.id1', 193 | '-Dosgi.bundles.defaultStartLevel=4', 194 | '-noverify', 195 | '-Declipse.product=org.eclipse.jdt.ls.core.product', 196 | '-jar', 197 | `${BASE_URI}/${launchersFound[0]}`, 198 | '-configuration', 199 | `${BASE_URI}/${CONFIG_DIR}` 200 | ]; 201 | 202 | export function prepareExecutable(): IJavaExecutable { 203 | let executable = Object.create(null); 204 | let options = Object.create(null); 205 | options.env = process.env; 206 | options.stdio = 'pipe'; 207 | executable.options = options; 208 | executable.command = 'java'; 209 | executable.args = params; 210 | return executable; 211 | } 212 | 213 | 214 | const executable = prepareExecutable(); 215 | const app = express(); 216 | const server = app.listen(3000); 217 | 218 | const ws = new WebSocket.Server({ 219 | noServer: true, 220 | perMessageDeflate: false 221 | }); 222 | 223 | server.on('upgrade', (request: http.IncomingMessage, socket: net.Socket, head: Buffer) => { 224 | const pathname = request.url ? url.parse(request.url).pathname : undefined; 225 | if (pathname === '/java-lsp') { 226 | wss.handleUpgrade(request, socket, head, webSocket => { 227 | const socket: rpc.IWebSocket = { 228 | send: content => webSocket.send(content, error => { 229 | if (error) { 230 | throw error; 231 | } 232 | }), 233 | onMessage: cb => webSocket.on('message', cb), 234 | onError: cb => webSocket.on('error', cb), 235 | onClose: cb => webSocket.on('close', cb), 236 | dispose: () => webSocket.close() 237 | }; 238 | if (webSocket.readyState === webSocket.OPEN) { 239 | launch(socket); 240 | } else { 241 | webSocket.on('open', () => launch(socket)); 242 | } 243 | }); 244 | } 245 | }); 246 | 247 | function launch(socket) { 248 | const process = cp.spawn(executable.command, executable.args); 249 | 250 | sockt.onMessage((data) => { 251 | process.stdin.write(data) 252 | }); 253 | 254 | process.stdout.on('data', (respose) => { 255 | socket.send(respose) 256 | }); 257 | } 258 | ``` 259 | 260 | webSocket 服务器实际作为一个中转层,将浏览器与 LSP 连接起来,这样就实现了最基本的语言服务连接。 261 | 除此之外,jdt.ls 还支持 maven 项目的原生支持以及 gradle 项目的有限支持(不支持 Android )项目,客户端还需要实现文件监控功能,当 `pom.xml`、`budile.gradle` 等构建工具相关配置文件发生改变时语言服务会自动下载依赖修改项目配置。 262 | 263 | ## 总结 264 | 265 | 本文介绍了 LanguageServerProtocol 的基本概念及编辑器与 LSP 的简单交互流程,了解了 VScode 如何利用 LSP 实现代码提示、智能感知、自动完成等功能,最后在 Web 端实现了编辑器与 LSP 服务的简单连接。LSP 打破了传统 IDE 重复实现多次语言特性功能的尴尬局面,并在 VScode 上做了非常好的实践,文中使用的 `eclipse.jdt.ls` 语言服务器已经在 [Cloud Studio 2.0](https://studio.coding.net/ws/default) 版本正式上线,感兴趣的读者可以点击创建一个 Java 项目试用。 266 | 267 | ## 参考资料 268 | 269 | - [language-server-protocol](https://microsoft.github.io/language-server-protocol/) 270 | - [teaching-the-language-server-protocol-to-microsofts-monaco-editor](https://typefox.io/teaching-the-language-server-protocol-to-microsofts-monaco-editor) 271 | - [eclipse.jdt.ls](https://github.com/eclipse/eclipse.jdt.ls) 272 | -------------------------------------------------------------------------------- /article/language-services.md: -------------------------------------------------------------------------------- 1 | [上一篇文章](https://github.com/Aaaaash/blog/issues/11)简单介绍了 LSP 协议和如何利用 LSP 为 Monaco 编辑器提供语言特性功能,以及如何向 Web 端的在线编辑器适配 LSP 服务。本文将继续深入这一话题,了解面向在线编辑器环境下,利用 LSP 实现这些功能有哪些需要注意的点以及填坑指南。由于笔者水平有限,如有疏漏之处还请指出。 2 | 3 | ## 从搭建一个简单的 WebSocket 服务器开始 4 | 5 | 上篇说到,要实现这样一个服务,需要有一层 WebSocket 与客户端相连接做中转层,由于 LSP 服务不涉及其他功能,所以这个服务器只需要有一个简单的 HTTP 服务,能够与客户端连接相互通信即可。 6 | 7 | 我们使用 [socket.io](https://github.com/socketio/socket.io) 来搭建 WebSocket 服务,代码非常简单 8 | 9 | ```typescript 10 | import * as http from "http"; 11 | import * as io from "socket.io"; 12 | 13 | const server = http.createServer(); 14 | 15 | const socket = io(server); 16 | 17 | server.listen(PORT, () => { 18 | logger.info("Language Server start in 9988 port!"); 19 | }); 20 | ``` 21 | 22 | 在客户端同样使用 [socket.io-client](https://github.com/socketio/socket.io-client) 模块来连接这个服务器 23 | 24 | ```javascript 25 | import io from 'socket.io-client'; 26 | 27 | const socketOptions = { 28 | reconnection: true, 29 | reconnectionAttempts: 5, 30 | reconnectionDelay: 10000, 31 | path: '', 32 | transports: ['websocket'], 33 | }; 34 | 35 | const ws = io.connect('localhost', socketOptions); 36 | ``` 37 | 38 | 需要注意的一点是,我们使用的 `vscode-ws-jsonrpc` 是扩展了原本的`vscode-jsonrpc`,为其添加了 websocket 功能的支持。但它只接受原生 WebSocket 对象作为 listen 方法的参数,两者实现的接口略有不同,我们需要对 socket.io 进行一层包装 39 | 40 | ```javascript 41 | import { listen } from 'vscode-ws-jsonrpc'; 42 | import { createMonacoServices } from 'monaco-languageclient'; 43 | const socket = createWebSocket(); 44 | 45 | // send 方法包装为 socket.emit 46 | const ioToWebSocket = { 47 | send: (message) => { 48 | socket.emit('message', { message }) 49 | }, 50 | onerror: err => socket.on('error', err), 51 | onclose: socket.onclose, 52 | close: socket.close, 53 | }; 54 | 55 | /** 56 | * 原生 websocket 在连接成功后会触发一个 onopen 方法 57 | * 用于连接成功后的回调函数 58 | * 所以在这里我们手动调用 onopen 59 | */ 60 | socket.on('connect', () => { 61 | ioToWebSocket.onopen() 62 | }); 63 | 64 | socket.on('message', ({ data }) => { 65 | ioToWebSocket.onmessage({ data }) 66 | }); 67 | 68 | // 然后将这个 ioToWebSocket 对象传递给 listen 方法作为参数 69 | 70 | const services = createMonacoServices(null, { rootUri: `file://xxx` }); 71 | 72 | listen({ 73 | webSocket: this.ioToWebSocket, 74 | onConnection: (connection) => { 75 | // connection 连接成功后返回的一个连接对象,languageServer-client 借助这个 connection 来收发消息 76 | const client = new BaseLanguageClient({ 77 | name: 'lsp', 78 | clientOptions: { 79 | commands: undefined, 80 | // 表示相应语言的选择器 81 | documentSelector: ['python'], 82 | synchronize: { 83 | configurationSection: 'pyls', 84 | }, 85 | // 连接成功后的初始化参数,每个语言的 lsp 实现略有不同,可在相应项目的 package.json 中找到。 86 | // vscode文档中也有介绍 https://code.visualstudio.com/docs/extensions/example-language-server 87 | initializationOptions: { 88 | ...initializationOption, 89 | // 提供 lsp 服务的项目 uri,绝对地址 90 | workspaceFolders: [`file:///xxx`] 91 | }, 92 | // 默认错误处理函数 93 | initializationFailedHandler: (err) => { 94 | const detail = err instanceof Error ? err.message : '' 95 | }, 96 | diagnosticCollectionName: language, 97 | }, 98 | // 服务对象,与客户端的区别在于,这个 services 主要用于绑定一些编辑器的操作命令及消息的转换 99 | // 而客户端里,这个 services 被叫做 serverOptions ,用于在本地启动 LSP 服务,会根据不同类型的参数以指定的模式启动 LSP 100 | services, 101 | connectionProvider: { 102 | get: (errorHandler, closeHandler) => 103 | Promise.resolve(createConnection(connection, errorHandler, closeHandler)), 104 | }, 105 | }); 106 | } 107 | }); 108 | ``` 109 | 其中`createMonacoServices`函数所接受的`rootUri`以及`BaseLanguageClient`的`workSpaceFolders`均为一个标准的 [URI](https://tools.ietf.org/html/rfc3986),表示需要提供 LSP 服务的项目绝对路径,也可以传输一个相对路径然后在 Server 端做转换处理。 110 | ``` 111 | foo://example.com:8042/over/there?name=ferret#nose 112 | \_/ \______________/\_________/ \_________/ \__/ 113 | | | | | | 114 | scheme authority path query fragment 115 | | _____________________|__ 116 | / \ / \ 117 | urn:example:animal:ferret:nose 118 | ``` 119 | 120 | 到这一步,客户端已经可以成功的通过 WebSocket 连接到服务器,不出意外的话,客户端会发出第一条 `initialize` 请求。此时我们的服务还没有对请求做处理,所以客户端也不会收到任何回复。 121 | 122 | ## 在服务器上启动 LSP 服务 123 | 124 | 前文说到,传统客户端的实现中,`new LanguageClient` 在实例化时需要传入一个 `serverOptions` 的参数用于启动本地的 LSP 程序,以 [vscode-java](https://github.com/redhat-developer/vscode-java) 为例,这个 repo 是一个 vscode 的插件,用于在 vscode 中为 Java 语言提供 LSP 相关功能。 125 | 126 | ![vscode-java](https://raw.githubusercontent.com/redhat-developer/vscode-java/master/images/vscode-java.0.0.1.gif) 127 | 128 | 查看其[源码](https://github.com/redhat-developer/vscode-java/blob/69d1b0a78441edc8117c23b9d5d962ed65c19678/src/extension.ts#L79)可以得出,当找到环境变量 `SERVER_PORT` 时,会开启一个 TCP 服务器,等待 vscode-java 底层的 [jdt.ls](https://github.com/eclipse/eclipse.jdt.ls) 作为客户端通过这个端口来建立连接。反之则将 jdt.ls 的启动参数及 JAVA_HOME 作为 serverOptions ,然后由客户端自行启动。 129 | 130 | 在我们的服务端同样可以用这两种方式来启动 LSP 程序,我们创建一个名为`JavaLanguageServer`的类来管理这个 LSP 连接。这个类需要监听 WebSocket 的消息,在初始化时启动 jdt.ls ,以及在客户端断开连接时杀死进程以确保资源及时回收。还有一点是建议在客户端连接 WebSocket 时携带两个参数`language`和`workspace`,方便服务端区分不同的语言和相应的项目目录,同时类似 jdt.ls 这种服务,在运行时会产生一些元数据,可以通过 workspace 名来指定元数据存放在哪个目录,否则这些数据会直接被保存在当前服务运行的目录下,启动多个项目时会产生错误消息。 131 | 132 | ```typescript 133 | // 使用 stdio 模式启动 LSP 134 | import * as cp from 'child_process'; 135 | import * as io from 'socket.io'; 136 | 137 | class JavaLanguageServer { 138 | constructor( 139 | private socket: io.Socket, 140 | ) {} 141 | start() { 142 | const javahome = 'xxx/bin/java'; 143 | const params = this.prepareParams(); 144 | 145 | this.process = cp.spawn(javahome, params); 146 | } 147 | 148 | // 准备 jdt.ls 启动参数 149 | prepareParams() { 150 | const params: string[] = [ 151 | '-Xmx256m', 152 | '-Xms256m', 153 | '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=,quiet=y', 154 | '-Declipse.application=org.eclipse.jdt.ls.core.id1', 155 | '-Dosgi.bundles.defaultStartLevel=4', 156 | '-noverify', 157 | '-Declipse.product=org.eclipse.jdt.ls.core.product', 158 | '-jar', 159 | // serverUri 表示 jdt.ls 构建后的目录 160 | // 启动服务所需的 jar 包 161 | `${serverUri}/${launchersFound[0]}`, 162 | '-configuration', 163 | // 不同平台的配置文件,可以使用 process.platform 来获取系统信息,指定不同的配置 164 | `${serverUri}/${JAVA_CONFIG_DIR}`, 165 | `-data`, 166 | // 客户端传入的 workspace 参数,用于存放元数据 167 | workspace, 168 | ]; 169 | 170 | return params; 171 | } 172 | } 173 | 174 | ``` 175 | 176 | 服务端 WebSocket 收到客户端的 'connection' 事件后,实例化这个 JavaLanguageServer,将 WebSocket 对象作为参数,之后调用 start 方法就会启动一个 jdt.ls 的子进程。 177 | 178 | ## 消息处理 179 | 180 | 在实例对象内部,我们需要监听 WebSocket 的消息,并通过 `childProcess.stdin.write` 传送给 jdt.ls 进程,然后监听 `childProcess.stdout`的 `ondata` 事件接收返回的消息。 181 | 182 | 但是这里有一个坑,我们知道 TCP 协议传输的是字节流,直接连接 TCP 服务进行通信,在数据量较大时会产生所谓的`粘包`问题,也就是多个消息包粘在一起。如果不经过处理直接把消息发送给客户端的话,编辑器无法识别并处理这些消息。 183 | 184 | 实际上 TCP 协议中并没有`包`这个概念,所有数据都是以流的形式来传输,而 TCP 协议为了保证可靠传输,减少每次发送数据都要验证的额外开销,使用流的形势传输,并且使用了优化算法(Nagle算法),会将多次间隔较小/量小的数据合并成一个大的数据块,这样一来减少了发送包的数量,提高了传输效率。而接受方也会引起这个问题,由于接收数据不及时,导致下一段数据被放在系统缓冲区,等待接收进程取出消息,若下一段数据还未被取出就收到了新的消息,那么这两段消息会被`粘`在一起,从而产生粘包现象。在这里我们使用标准输入输出的方式也会有同样的情况,也正是因为 Stdio 基于字节流,数据量较大时没有及时处理数据,缓冲区数据滞留从而引发粘包问题。 185 | 186 | 并且从理论上来讲,TCP 协议只是传输层协议,也并不存在`粘包`这个概念。我们需要再建立一层应用层协议来自行处理这些问题,这也就是网络编程中常见的所谓`分包`等问题的来源。 187 | 188 | 传统的`粘包`处理方式有几种, 189 | 190 | * 发送方引起粘包现象,用户可以通过编程来避归,TCP提供了强制数据立即传送的指令`push`,接收到该指令后,会将消息立即发送出去,不必等待缓冲区满。 191 | * 接收方引起的粘包,可通过优化程序设计、提高接受优先级等方法,使其及时接受数据。 192 | * 定义应用层协议,发送方将消息尺寸与消息一起发送,接收方负责按照指定长度来接收数据。 193 | 194 | 对于我们的 LSP 程序来说,第一种方式需要修改 LSP 源码,显然行不通。第二种方式只能减少粘包出现的频率,并不能完全解决问题。第三种方式则最完美,因为 LSP 协议本身就包含了 `Content-Length`,所以我们可以根据这个消息长度来获取消息内容。 195 | 196 | 服务端我们使用`vscode-jsonrpc`这个包已经解决了这一问题,[查看MessageReader源码](https://github.com/Microsoft/vscode-languageserver-node/blob/5f9c993ff38f5c369949aeb359b3e9b178172dbc/jsonrpc/src/messageReader.ts#L210)可以得知在接收到消息后,将消息写入一个 Buffer 中,然后在这个 Buffer 里寻找消息的 Header,也就是 `Content-Length` 字段。读取到消息长度后,继续在接受到的消息包里截取这个长度的内容,将其组合起来再发送给 callback 函数。 197 | 198 | ```typescript 199 | private onData(data: Buffer | String): void { 200 | // 写入 buffer 201 | this.buffer.append(data); 202 | while (true) { 203 | if (this.nextMessageLength === -1) { 204 | // 读取消息头 205 | let headers = this.buffer.tryReadHeaders(); 206 | if (!headers) { 207 | return; 208 | } 209 | let contentLength = headers['Content-Length']; 210 | if (!contentLength) { 211 | throw new Error('Header must provide a Content-Length property.'); 212 | } 213 | let length = parseInt(contentLength); 214 | if (isNaN(length)) { 215 | throw new Error('Content-Length value must be a number.'); 216 | } 217 | // 将取到的消息长度赋值给 nextMessageLength 218 | this.nextMessageLength = length; 219 | // Take the encoding form the header. For compatibility 220 | // treat both utf-8 and utf8 as node utf8 221 | } 222 | // 根据 nextMessageLength 长度读取消息内容 223 | var msg = this.buffer.tryReadContent(this.nextMessageLength); 224 | if (msg === null) { 225 | /** We haven't recevied the full message yet. */ 226 | this.setPartialMessageTimer(); 227 | return; 228 | } 229 | this.clearPartialMessageTimer(); 230 | this.nextMessageLength = -1; 231 | this.messageToken++; 232 | var json = JSON.parse(msg); 233 | this.callback(json); 234 | } 235 | } 236 | ``` 237 | 238 | 这里`tryReadHeader`和`tryReadContent`函数的实现方法不再赘述,有兴趣的可以阅读源码。 239 | 240 | `vscode-jsonrpc`包中不但解决了粘包问题,还以不同的连接方式抽象出了几个 Reader 类以供我们使用。 241 | 242 | * StreamMessageReader 流的形式,接收 `NodeJS.ReadableStream` 对象为参数 243 | * IPCMessageReader IPC 模式,接收 `Process | ChildProcess` 对象为参数 244 | * SocketMessageReader Socket 模式,接收 `net.Socket` 对象为参数 245 | 246 | 在这里我们使用`StreamMessageReader`,传入 childProcess.stdout 来读取子进程的可读流消息。 247 | 248 | ```typescript 249 | // JavaLanguageServer.ts 250 | const messageReader = new StreamMessageReader(this.process.stdout); 251 | this.socket.on('message', (data) => { 252 | this.process.stdin.write(data.message); 253 | }); 254 | 255 | messageReader.listen((data) => { 256 | const jsonrpcData = JSON.stringify(data); 257 | const length = Buffer.byteLength(jsonrpcData, 'utf-8'); 258 | const headers: string[] = [ 259 | contentLength, 260 | length.toString(), 261 | CRLF, 262 | CRLF, 263 | ]; 264 | this.socket.send({ data: `${headers.join('')}${jsonrpcData}` }); 265 | }); 266 | ``` 267 | 268 | 这段代码中我们创建了一个 StreamMessageReader 实例,调用 `listen` 方法传入回调函数。在收到完整的消息包后将消息序列化并调用 `Buffer.byteLength` 方法获取序列化后消息的字节数。这里需要非常注意的是,虽然 JSON.stringify 将对象序列化成了字符串,但是不能直接用 `jsonrpcData.length` 作为 Content-Length 消息长度,因为 [LSP 协议规定](https://microsoft.github.io/language-server-protocol/specification)合法的 Content-Length 值应当为内容部分的字节长度,而不是内容部分的字符串数,这两者有[些许差别](http://www.wquanzhan.com/documentation/nodejs/buffer/byte-length)。 269 | 270 | > 在纯ASCII码下,字节数=字符串长度=字符个数,因为每个字符就一个字节。 271 | 在Unicode下,字节数/2=字符串长度=字符个数,因为每个字符都是2个字节。 272 | 在ASCII码与其它双字节字符系统混用时,字节数=ASCII码字符个数+双字节字符个数*2,而此时字符串长度到底怎么统计就不好说了,有的语言如C语言,此时字符串长度=字节数,有的语言如JS,此时字符产长度=字符个数。 273 | 274 | 使用 `string.length` 把字符数当做字节长度会导致客户端接收消息时产生读取消息出错的问题。 275 | 276 | 到这里我们的客户端与服务端成功的建立了连接,并在 LSP 的作用下在线编辑器有了基本的代码提示、诊断等功能。 277 | 278 | 在客户端断开连接后要调用 `process.kill` 方法及时杀死进程,某些情况下可能存在进程没有杀死的情况,建议使用[node-tree-kill](https://github.com/pkrumins/node-tree-kill)来确保进程退出。 279 | 280 | ## 存在的问题 281 | 282 | 可以看出向 Web 端在线编辑器提供 LSP 服务是完全可行的,但每次打开一个项目或目录就在服务器启动一个 LSP 实例进程,且单个进程内存占用较大,例如 jdt.ls 启动后平均内存占用在 400m 左右,用户量较多时资源消耗太大,这对相对紧张的服务资源来说是一个非常奢侈的。LSP 协议也不支持多个用户共享同一进程,所以在功能实现和资源占用之间需要权衡一下。但其他语言如 `TypeScirpt`内存消耗只有100m左右,这对服务端来说是完全可以承受的(TypeScript大法好)。 283 | 284 | ## 容器化的可能性 285 | 286 | 在这个服务中,我们使用 NodeJs 的 `childProcess` 来启动 LSP 程序,如果单纯的把服务运行在 Docker 中显然不能接受,因为这样的话我们的 Docker 镜像需要包含 NodeJs、Java、Python 等许多语言的运行环境,这将导致生成的镜像非常大,也违背了容器单服务单进程的约定。所以最好的办法是将每个 LSP 程序拆成一个容器,通用服务也作为一个容器运行,使用 docker-compose 来管理多个容器。 287 | 288 | ## 最后 289 | 290 | 本文代码托管在 [GitHub](https://github.com/Aaaaash/LanguageServices-WebIDE),CloudStudio 已经实现 Java、Python 的 LSP 服务,有兴趣可以[戳这里体验](https://studio.coding.net/)。 291 | 292 | 容器化完成后再来续下一篇... 293 | 294 | ### 参考链接 295 | 296 | * [vscode-language-node](https://github.com/TypeFox/vscode-languageserver-node) 297 | 298 | * [typefox.io](https://typefox.io/?s=language-server) 299 | 300 | -------------------------------------------------------------------------------- /article/mid-year-summary.md: -------------------------------------------------------------------------------- 1 | 思来想去觉得应该写一篇总结记录这段时间的一些事情,顺便谈谈未知的将来。 2 | 3 | ## 废话 4 | 5 | 这篇文章写下第一行字时,距离我正式入行做`前端`已经整整1年零11个月了。不知从何时开始变得越来越焦虑,初入社会时的迷茫到满脑子工作生活家庭的身心疲惫。习惯了大城市快节奏的生活,每天看着地铁呜呜泱泱的人海,脚下的步伐越来越快。一年多以前还在地铁站嘲笑`这群人`这么急。如今我也成了`这群人`之一。 6 | 7 | 人的改变是潜移默化的,很多年前我的梦想是成为一名自由职业者,有一个大大的办公桌,一台大大的屏幕,敲下魔法一般的代码多开心。的确,写代码是一件令人兴奋的事。但现如今我并不是想象中的那个程序员,整日过着循规蹈矩的生活,越来越黑的眼圈、越来越焦虑的心态、越来越嫌自己技术差(以及工资低)。毕业后第一份简历自我描述是一句话`写自己喜欢的代码`,但现实是每天都在面对别人祖传的代码,面对不感冒的业务需求,面对修不完的bug。下班回到家没有多余的时间追剧、看电影、玩游戏,甚至这些娱乐活动已经变得有些奢侈了。 8 | 9 | 相比那些一周一篇博客,从服务端到客户端,机器学习到图形学,不是分析源码就是造轮子的人来说,我不是一个合格的程序员,我可能连他们的脚趾头都够不到。 10 | -------------------------------------------------------------------------------- /article/ot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Monaco-Editor折腾记--多人协同编辑的实现 3 | date: 2018-05-01 16:03:04 4 | tags: monaco, 协作, ot 5 | --- 6 | > 本系列文章为Monaco-Editor编辑器折腾、踩坑记录,涉及到协同编辑、代码提示、智能感知等功能的实现,不定期更新 7 | ## Monaco-Editor简介 8 | 9 | [monaco-editor](https://github.com/Microsoft/monaco-editor)是微软开源的一款web端文本编辑器,也就是vscode内置的编辑器,扩展性很强,原生暴露了很多用于代码提示、高亮显示等API 10 | > 仅为核心编辑器部分,不包含vscode的插件系统、文件数及terminal 11 | 12 | ## 基本用法 13 | 14 | monaco的基本用法非常简单,导入核心依赖及相应语言依赖包,调用`monaco.editor.create`方法即可创建一个简单的编辑器 15 | 16 | 17 | 18 | ```javascript 19 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 20 | import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands'; 21 | import 'monaco-editor/esm/vs/editor/contrib/find/findController'; 22 | 23 | // php依赖包,提供代码语法解析及代码高亮等功能 24 | import 'monaco-editor/esm/vs/basic-languages/php/php'; 25 | import 'monaco-editor/esm/vs/basic-languages/php/php.contribution'; 26 | 27 | const container = document.querySelector('#container'); 28 | 29 | 30 | const editor = monaco.editor.create(container, { 31 | language: 'php', 32 | glyphMargin: true, 33 | lightbulb: { 34 | enabled: true, 35 | }, 36 | theme: 'vs-dark', 37 | }); 38 | 39 | ``` 40 | 41 | monaco的文档是基于typescript的类型声明及注释生成的,所以要开发高级功能大多数情况下需要翻阅monaco.d.ts文件来查找api定义及用法(参考如何画马)😆 42 | 43 | 44 | 45 | ## 多人协同编辑 46 | 47 | 多人协同编辑,顾名思义就是像Google Doc以及石墨文档、腾讯文档等在线文档产品一样可以两人或两人以上同时编辑同一个文件,双方编辑操作互不干扰且能够自动解决冲突,这里不讨论代码编辑器实时协作功能的必要性,只谈实现。 48 | 49 | 协同编辑基本实现思路有两种 50 | 51 | * OT(Operational-Transformation) 52 | * peer-to-peer 53 | 54 | ### OT 55 | `Operational-Transformation`是指对文档编辑以及同时编辑冲突解决的一类技术,不仅仅是一个算法,ot将文档变更表示为三类操作(Operational) 56 | * Insert 插入 57 | * Retain 移动光标 58 | * Delete 删除 59 | 60 | 举例来说,对文档的插入操作可以看做一个`insert operational`: 61 | ```javascript 62 | // 如在第10个字符的位置插入‘hello’ 63 | retain(10); 64 | insert('hello') 65 | ``` 66 | -------------------------------------------------------------------------------- /article/react个人总结.MD: -------------------------------------------------------------------------------- 1 | ## 基础 2 | 3 | ### 组件 4 | React组件大致可分为三种写法 5 | 一种es6的class语法,继承React.Component类实现带有完整生命周期的组件 6 | ```javascript 7 | import React, { Component } from 'react'; 8 | 9 | export default class SingleComponent extends Component { 10 | /* 11 | 包含一些生命周期函数、内部函数以及变量等 12 | */ 13 | render() { 14 | return (
{/**/}
) 15 | } 16 | } 17 | ``` 18 | 第二种是`无状态组件`,也叫函数式组件 19 | ```javascript 20 | const SingleComponent = (props) => ( 21 |
{props.value}
22 | ); 23 | export default SingleComponent; 24 | ``` 25 | 还有一种较为特殊,叫[`高阶组件`](https://facebook.github.io/react/docs/higher-order-components.html),严格来说高阶组件只是用来包装以上两种组件的一个`高阶函数` 26 | ```javascript 27 | const HighOrderComponent = (WrappedComponent) => { 28 | class Hoc extends Component { 29 | /*包含一些生命周期函数*/ 30 | render() { 31 | return (); 32 | } 33 | } 34 | return Hoc; 35 | } 36 | ``` 37 | 高阶组件的原理是接受一个组件并返回一个包装后的组件,可以在返回的组件里插入一些生命周期函数做相应的操作,高阶组件可以使被包装的组件逻辑不受干扰从外部进行一些扩展 38 | 39 | ### props和state 40 | react中组件自身的状态叫做state,在es6+的类组件中可以使用很简单的语法进行初始化 41 | ```javascript 42 | export default class Xxx extends Component { 43 | state = { 44 | name: 'sakura', 45 | } 46 | render() { 47 | const { name } = this.state; 48 | return (
{name}
); 49 | } 50 | } 51 | ``` 52 | 53 | state可以赋值给某个标签,如果需要更新state可以调用`this.setState()`传入一个对象,通过这个方法修改state之后绑定了相应值的元素也会触发渲染,这就是简单的数据绑定 54 | 55 | 不能通过`this.state.name = 'xxx'`的方式修改state,这样就会失去更新state同时相应元素改变的效果 56 | 57 | `setState`函数是react中较为重要也是使用频率较高的一个api,它接受最多两个参数,第一个参数是要修改的state对象,第二个参数为一个回调函数,会在state更新操作完成后自动调用,所以setState函数是`异步`的。 58 | 调用this.setState之后react并没有立刻更新state,而是将几次连续调用setState返回的对象合并到一起,以提高性能,以下代码可能不会产生期望的效果 59 | ```javascript 60 | class SomeButton extends Component { 61 | state = { 62 | value: 1, 63 | } 64 | handleClick = () => { 65 | const { value } = this.state; 66 | this.setState({ value: value + 1 }); 67 | this.setState({ value: value + 1 }); 68 | this.setState({ value: value + 1 }); 69 | this.setState({ value: value + 1 }); 70 | } 71 | render() { 72 | const { value } = this.state; 73 | return (
74 | {vlaue} 75 | 76 |
); 77 | } 78 | } 79 | ``` 80 | 实际上这里并没有对value进行4次+1的操作,react会对这四次更新做一次合并,最终只保留一个结果,类似于 81 | ```javascript 82 | Object.assign({}, 83 | { value: value + 1 }, 84 | { value: value + 1 }, 85 | { value: value + 1 }, 86 | ); 87 | ``` 88 | 并且因为setState是异步的,所以不能在调用之后立马获取新的state,如果要用只能给setState传入第二个参数回调函数来获取 89 | ```javascript 90 | /*省略部分代码*/ 91 | this.setState({ 92 | value: 11, 93 | }, () => { 94 | const { value } = this.state; 95 | console.log(value); 96 | }) 97 | ``` 98 | 99 | `props`是由父元素所传递给给子元素的一个属性对象,用法通常像这样 100 | ```javascript 101 | class Parent extends Component { 102 | /*父组件的state中保存了一个value*/ 103 | state = { 104 | value: 0, 105 | }; 106 | 107 | handleIncrease = () => { 108 | const { value } = this.state; 109 | this.setState({ value: value + 1 }); 110 | } 111 | 112 | render() { 113 | const { value } = this.state; 114 | // 通过props传递给子组件Child,并传递了一个函数,用于子组件点击后修改value 115 | return (
116 | 117 |
) 118 | } 119 | } 120 | 121 | // 子组件通过props获取value和increase函数 122 | const Child = (props) => ( 123 |
124 |

{props.value}

125 | 126 |
127 | ); 128 | ``` 129 | props像一个管道,父组件的状态通过props这个管道流向子组件,这个过程叫做`单向数据流` 130 | 131 | > react中修改state和props都会引起组件的重新渲染 132 | 133 | ### 组件的生命周期 134 | 生命周期是一组用来表示组件从渲染到卸载以及接收新的props以及state声明的特殊函数 135 | 136 | ![react生命周期函数执行过程](https://www.codevoila.com/uploads/images/201607/reactjs_component_lifecycle_functions.png) 137 | 这张图展示了react几个生命周期函数执行的过程,可以简单把组件的生命周期分为三个阶段,共包含9个生命周期函数,在不同阶段组件会自动调用 138 | 139 | * 挂载 140 | * componentWillMount 141 | * render 142 | * componentDidMount 143 | * 更新 144 | * componentWillReceiveProps 145 | * shouldComponentUpdate 146 | * componentWillUpdate 147 | * render 148 | * componentDidUpdate 149 | * 卸载 150 | * componentWillUnmount 151 | 152 | #### 挂载--componentWillMount 153 | 这个阶段组件准备开始渲染DOM节点,可以在这个方法里做一些请求之类的操作,但是因为组件还没有首次渲染完成,所以并不能拿到任何dom节点 154 | #### 挂载--render 155 | 正式渲染,这个方法返回需要渲染的dom节点,并且做数据绑定,这个方法里不能调用`this.setState`方法修改state,因为setState会触发重新渲染,导致再次调用render函数触发死循环 156 | #### 挂载--componentDidMount 157 | 这个阶段组件首次渲染已经完成,可以拿到真实的DOM节点,也可以在这个方法里做一些请求操作,或者绑定事件等等 158 | 159 | #### 更新--componentWillReceiveProps 160 | 当组件收到新的props和state且还没有执行render时会自动触发这个方法,这个阶段可以拿到新的props和state,某些情况下可能需要根据旧的props和新的props对比结果做一些相关操作,可以写在这个方法里,比如一个弹窗组件的弹出状态保存在父组件的state里通过props传给自身,判断这个弹窗弹出可以这样写 161 | ```javascript 162 | class Dialog extends Component { 163 | componentWillReveiceProps(nextProps) { 164 | const { dialogOpen } = this.props; 165 | if (nextProps.dialogOpen && nextProps.dialogOpen !== dialogOpen) { 166 | /*弹窗弹出*/ 167 | } 168 | } 169 | } 170 | ``` 171 | #### 更新--shouldComponentUpdate 172 | `shouldComponentUpdate`是一个非常重要的api。react的组件更新过程经过以上几个阶段,到达这个阶段需要确认一次组件是否真的需要根据新的状态再次渲染,确认的依据就是对比新旧状态是否有所改变,如果没有改变则返回false,后面的生命周期函数不会执行,如果发生改变则返回true,继续执行后续生命周期,而react默认就返回true 173 | 174 | 所以可以得出shouldComponentUpdate可以用来优化性能,可以手动实现shouldComponentUpdate函数来对比前后状态的差异,从而阻止组件不必要的重复渲染 175 | ```javascript 176 | class Demo extends Component { 177 | shouldComponentUpdate(nextProps, nextState) { 178 | return this.props.value !== nextProps.value; 179 | } 180 | } 181 | ``` 182 | 这段代码是一个最简单的实现,通过判断`this.props.value`和`nextProps.value`是否相同来决定组件要不要重新渲染,但是实际项目中数据复杂多样,并不仅仅是简单的基本类型,可能有对象、数组甚至是更深嵌套的对象,而数据嵌套越深就意味着这个方法里需要做更深层次的对比,这对react性能开销是极大的,所以官方更推荐使用[Immutable.js](https://github.com/facebook/immutable-js)来代替原生的JavaScript对象和数组 183 | 184 | 由于immutablejs本身是不可变的,如果需要修改状态则返回新的对象,也正因为修改后返回了新对象,所以在shouldComponentUpdate方法里只需要对比对象的引用就很容易得出结果,并不需要做深层次的对比。但是使用immutablejs则意味着增加学习成本,所以还需要做一些取舍 185 | 186 | #### 更新--componentWillUpdate 187 | 这个阶段是在收到新的状态并且shouldComponentUpdate确定组件需要重新渲染而还未渲染之前自动调用的,在这个阶段依然能获取到新的props和state,是组件重新渲染前最后一次更新状态的机会 188 | 189 | #### 更新--render 190 | 根据新的状态重新渲染 191 | 192 | #### 更新--componentDidMount 193 | 重新渲染完毕 194 | 195 | #### 卸载--componentWillmount 196 | 组件被卸载之前,在这里可以清除定时器以及解除某些事件 197 | 198 | ### 组件通信 199 | 很多业务场景中经常会涉及到父=>子组件或者是子=>父组件甚至同级组件间的通信,父=>子组件通信非常简单,通过props传给子组件就可以。而子=>父组件通信则是大多数初学者经常碰到的问题 200 | 假设有个需求,子组件是一个下拉选择菜单,父组件是一个表单,在菜单选择一项之后需要将值传给父级表单组件,这是典型的子=>父组件传值的需求 201 | ```javascript 202 | const list = [ 203 | { name: 'sakura', id: 'x0001' }, 204 | { name: 'misaka', id: 'x0003' }, 205 | { name: 'mikoto', id: 'x0005' }, 206 | { name: 'react', id: 'x0002' }, 207 | ]; 208 | 209 | class DropMenu extends Component { 210 | handleClick = (id) => { 211 | this.props.handleSelect(id); 212 | } 213 | 214 | render() { 215 | 216 | {list.map((v) => ( 217 | this.handleClick(v.id)}>{v.name} 218 | ))} 219 | 220 | } 221 | } 222 | 223 | class FormLayout extends Component { 224 | state = { 225 | selected: '', 226 | } 227 | handleMenuSelected = (id) => { 228 | this.setState({ selected: id }); 229 | } 230 | render() { 231 |
232 | 233 |
234 | } 235 | } 236 | ``` 237 | 这个例子中,父组件`FormLayout`将一个函数传给子组件,子组件的`Menu`点击后调用这个函数并把值传进去,而父组件则收到了这个值,这就是简单的子=>父组件通信 238 | 239 | 而对于更为复杂的同级甚至类似于叔侄关系的组件可以通过状态提升的方式互相通信,简单来说就是如果两个组件互不嵌套,没有父子关系,这种情况下,可以找到他们上层公用的父组件,将state存在这个父组件中,再通过props给两个组件传入相应的state以及对应的回调函数即可 240 | 241 | ## 路由 242 | React中最常用的路由解决方案就是[React-router](https://reacttraining.com/react-router/),react-router迄今为止已经经历了四个大版本的迭代,每一版api变化较大,本文将按照最新版react-router-v4进行讲解 243 | 244 | ### 基本用法 245 | 使用路由,要先用`Router`组件将App包起来,并把history对象通过props传递进去,最新版本中history被单独分出一个包,使用的时候需要先引入。对于同级组件路由的切换,需要使用Switch组件将多个Route包起来,每当路由变更,只会渲染匹配到的一个组件 246 | 247 | ```javascript 248 | import ReactDOM from 'react-dom'; 249 | import createHistory from 'history/createBrowserHistory'; 250 | import { Router } from 'react-router'; 251 | 252 | import App from './App'; 253 | 254 | const history = createHistory(); 255 | 256 | ReactDOM.render( 257 | 258 | 259 | , 260 | element, 261 | ); 262 | 263 | // App.js 264 | //... 省略部分代码 265 | 266 | import { 267 | Switch, Route, 268 | } from 'react-router'; 269 | 270 | class App extends Component { 271 | render() { 272 | return ( 273 |
274 | 275 | 276 | 277 | 278 |
279 | ); 280 | } 281 | } 282 | ``` 283 | [CodesanBox在线示例](https://codesandbox.io/s/mj9kp308xj) 284 | 285 | ## 状态管理 286 | 287 | > 关于单页面应用状态管理可以先阅读民工叔这篇文章[单页应用的数据流方案探索](https://github.com/xufei/blog/issues/47) 288 | 289 | React生态圈的状态管理方案由facebook提出的[flux](https://github.com/facebook/flux)架构为基础,并有多种不同实现,而最为流行的两种是 290 | * [Mobx](https://github.com/mobxjs/mobx) 291 | * [Redux](https://github.com/reactjs/redux) 292 | 293 | ![flux架构](https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png) 294 | 295 | ### Flux 296 | > Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow. It's more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code. 297 | 298 | >Flux是facebook用于构建web应用的一种架构,它通过使用单向数据流补充来补充React的组件,它只是一种模式,而不是一个正式的框架 299 | 300 | 首先,Flux将一个应用分为三个部分: 301 | * dispatcher 302 | * stores 303 | * views 304 | 305 | #### dispatcher 306 | `dispatcher`是管理Flux应用中所有数据流的中心枢纽,它的作用仅仅是将actions分发到stores,每一个store都监听自己并且提供一个回调函数,当用户触发某个操作时,应用中的所有store都将通过监听的回调函数来接收这个操作 307 | 308 | [facebook官方实现的Dispatcher.js](https://github.com/facebook/flux/blob/520a60c18aa3e9af59710d45cd37b9a6894a7bce/src/Dispatcher.js) 309 | 310 | #### stores 311 | stores包含应用程序的状态和逻辑,类似于传统MVC中的model,stores用于存储应用程序中特定区域范围的状态 312 | 313 | 一个store向dispatcher注册一个事件并提供一个回调函数,这个回调函数可以接受action作为参数,并且基于actionType来区分并解释操作。在store中提供相应的数据更新函数,在确认更新完毕后广播一个事件用于应用程序根据新的状态来更新视图 314 | ```javascript 315 | // Facebook官方实现FluxReduceStore的用法 316 | import { ReduceStore, Dispatcher } from 'flux'; 317 | import Immutable from 'immutable'; 318 | const dispatch = new Dispatcher(); 319 | 320 | class TodoStore extends ReduceStore { 321 | constructor() { 322 | super(dispatch); 323 | } 324 | getInitialState() { 325 | return Immutable.OrderedMap(); 326 | } 327 | reduce(state, action) { 328 | switch(action.type) { 329 | case 'ADD_TODO': 330 | return state.set({ 331 | id: 1000, 332 | text: action.text, 333 | complete: false, 334 | }); 335 | default: 336 | return state; 337 | } 338 | } 339 | } 340 | 341 | export default new TodoStore(); 342 | 343 | ``` 344 | #### views 345 | React提供了views所需的可组合以及可以自由的重新渲染的视图,在React最顶层组件里,通过某种粘合代码从stores中获取所需数据,并将数据通过props传递到它的子组件中,我们就可以通过控制这个顶层组件的状态来管理页面任何部分的状态 346 | 347 | Facebook官方实现中有一个[FluxContainer.js](https://github.com/facebook/flux/blob/520a60c18aa3e9af59710d45cd37b9a6894a7bce/src/container/FluxContainer.js)用于连接store与react组件,并在store更新数据后刷新组件状态更新视图。基本原理是用一个高阶组件传入Stores和组件需要的state与方法以及组件本身,返回注入了state和action方法的组件,基本用法像这样 348 | ```javascript 349 | import TodoStore from './TodoStore'; 350 | import Container from 'flux'; 351 | import TodoActions from './TodoActions'; 352 | 353 | // 可以有多个store 354 | const getStores = () => [TodoStore]; 355 | 356 | const getState = () => ({ 357 | // 状态 358 | todos: TodoStore.getState(), 359 | 360 | // action 361 | onAdd: TodoActions.addTodo, 362 | }); 363 | 364 | export default Container.createFunctional(App, getStore, getState); 365 | ``` 366 | [CodeSanbox在线示例](https://codesandbox.io/s/lpm0zyp3y7) 367 | 后续会补充flux官方实现的源码解析 368 | ### Redux 369 | [Redux](https://github.com/reactjs/redux)是由[Dan Abramov](https://github.com/gaearon)对Flux架构的另一种实现,它延续了flux架构中`views`、`store`、`dispatch`的思想,并在这个基础上对其进行完善,将原本store中的reduce函数拆分为`reducer`,并将多个stores合并为一个store,使其更利于测试 370 | ![redux](https://camo.githubusercontent.com/c24115bb93ccefa396aee0be23f06daa3776105a/687474703a2f2f7777772e6265626574746572646576656c6f7065722e636f6d2f696d672f706f73745f696d672f7272652d322e706e67) 371 | [The Evolution of Flux Frameworks这篇文章](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31),是他对原Flux架构的看法以及他的改进 372 | >The first change is to have the action creators return the dispatched action.What looked like this: 373 | ```javascript 374 | 375 | export function addTodo(text) { 376 | AppDispatcher.dispatch({ 377 | type: ActionTypes.ADD_TODO, 378 | text: text 379 | }); 380 | } 381 | ``` 382 | > can look like this instead: 383 | ```javascript 384 | export function addTodo(text) { 385 | return { 386 | type: ActionTypes.ADD_TODO, 387 | text: text 388 | }; 389 | } 390 | ``` 391 | stores拆分为单一store和多个reducer 392 | ```javascript 393 | const initialState = { todos: [] }; 394 | export default function TodoStore(state = initialState, action) { 395 | switch (action.type) { 396 | case ActionTypes.ADD_TODO: 397 | return { todos: state.todos.concat([action.text]) }; 398 | default: 399 | return state; 400 | } 401 | ``` 402 | 403 | Redux把应用分为四个部分 404 | * views 405 | * action 406 | * reducer 407 | * store 408 | 409 | views可以触发一个action,reducer函数内部根据action.type的不同来对数据做相应的操作,最后返回一个新的state,store会将所有reducer返回的state组成一个state树,再通过订阅的事件函数更新给views 410 | 411 | #### views 412 | react组件作为应用中的视图层 413 | #### action 414 | action是一个简单的JavaScript对象,包含一个type属性以及action操作需要用到的参数,推荐使用`actionCreator`函数来返回一个action,actionCreator函数可以作为state传递给组件 415 | ```javascript 416 | function singleActionCreator(payload) { 417 | return { 418 | type: 'SINGLE_ACTION', 419 | paylaod, 420 | }; 421 | } 422 | ``` 423 | 424 | #### reducer 425 | reducer是一个纯函数,简单的根据指定输入返回相应的输出,reducer函数不应该有副作用,并且最终需要返回一个state对象,对于多个reducer,可以使用combineReducer函数组合起来 426 | ```javascript 427 | function singleReducer(state = initialState, action) { 428 | switch(action.type) { 429 | case 'SINGLE_ACTION': 430 | return { ...state, value: action.paylaod }; 431 | default: 432 | return state; 433 | } 434 | } 435 | 436 | function otherReducer(state = initialState, action) { 437 | switch(action.type) { 438 | case 'OTHER_ACTION': 439 | return { ...state, data: action.data }; 440 | default: 441 | return state; 442 | } 443 | } 444 | 445 | const rootReducer = combineReducer([ 446 | singleReducer, 447 | otherReducer, 448 | ]); 449 | 450 | ``` 451 | #### store 452 | redux中store只有一个,通过调用createStore传入reducer就可以创建一个store,并且这个store包含几个方法,分别是subscribe, dispatch,getState,以及replaceReducer,subscribe用于给state的更新注册一个回调函数,而dispatch用于手动触发一个action,getState可以获取当前的state树,replaceReducer用于替换reducer,要在react项目中使用redux,必须再结合react-redux 453 | 454 | ```javascript 455 | import { connect } from 'react-redux'; 456 | const store = createStore(rootReducer); 457 | 458 | // App.js 459 | class App extends Component { 460 | render() { 461 | return ( 462 |
463 | test 464 |
465 | ); 466 | } 467 | } 468 | 469 | const mapStateToProps = (state) => ({ 470 | vlaue: state.value, 471 | data: state.data, 472 | }); 473 | 474 | const mapDispatchToProps = (dispatch) => ({ 475 | singleAction: () => dispatch(singleActionCreator()); 476 | }); 477 | 478 | export default connect(mapStateToProps, mapDispatchToProps)(App); 479 | 480 | // index.js 481 | import { Provider } from 'react-redux'; 482 | 483 | ReactDOM.render( 484 | 485 | 486 | , 487 | element, 488 | ); 489 | ``` 490 | [CodeSanbox在线示例](https://codesandbox.io/s/oj7px08qy9) 491 | ### Redux异步 492 | Redux本身从action==>reducer==>store==>views这个过程是完全同步的,如果要进行异步操作比如请求接口,那么异步请求写在哪里是个问题,actioncreator是一个只返回action的函数,而reducer又是一个纯函数,原则上最好不要有其他操作 493 | 494 | 基于这个问题,redux很巧妙的实现了`中间件`机制,中间件的用法可以看我的这篇文章了解[applymiddleware](https://github.com/SakuraAsh/blog/blob/master/Redux%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB------applyMiddleware.MD) 495 | 496 | 目前较为常用的redux中间件是[redux-saga](https://github.com/redux-saga/redux-saga)、[redux-observable](https://github.com/redux-saga/redux-saga) 497 | 498 | 用法可以参考社区的这篇文章[Redux异步方案选型](https://zhuanlan.zhihu.com/p/24337401) 499 | -------------------------------------------------------------------------------- /article/vscode-debug-adapter.md: -------------------------------------------------------------------------------- 1 | [Visual Studio Code](https://github.com/Microsoft/vscode) 是微软开源的一款轻量级代码编辑器,支持数十种主流语言的语法高亮、智能补全提示及 Git、Docker 集成等特性。因其自身使用 TypeScript 语言及 Electron 平台开发,对 ES/JavaScript/NodeJS 支持度较高,已经逐渐成为前端领域的主流开发工具。 2 | 3 | 前几篇文章介绍了 LSP 协议及在 Web 端在线编辑器中的集成,可以看到基于 LSP 协议,我们只需要找到对应语言的实现,就可以以非常低的成本在多个编辑器中使用语言服务器,甚至是在 Web 端。 4 | 5 | ## VSCODE 调试器协议 6 | 7 | 同样在 VSCODE 中还存在一个 [vscode-debug-protocol](https://github.com/Microsoft/vscode-debugadapter-node/blob/768e505c7d362f733a29c89fa973c6285ce8fb27/protocol/README.md),这是一个通用的调试协议,允许在 VSCODE 的通用调试器 UI 下集成特定语言的调试器。 8 | 9 | ![vscode nodejs debugger](https://code.visualstudio.com/assets/docs/nodejs/nodejs-debugging/auto-attach.gif) 10 | 11 | 和 LSP 一样,vscode-debug-protocol 使用 JSONRPC 来描述请求、响应及事件,协议的具体规范可以在 [debugProtocol.ts](https://github.com/Microsoft/vscode-debugadapter-node/blob/768e505c7d362f733a29c89fa973c6285ce8fb27/protocol/src/debugProtocol.ts) 中找到。 12 | 13 | ## 调试器协议详解 14 | 15 | 仍然以 Java 语言为例,在 VSCODE 中搜索并安装扩展 `Debugger for Java`, 重载编辑器后即可使用 Java 调试器。 16 | 17 | 这里再简单介绍一下 Java 调试器的实现原理。 18 | 19 | ### Java-Debug-Interface 20 | 21 | JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI)。这三个模块把调试过程分解成几个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。 22 | 23 | JDI(Java Debug Interface)是 JPDA 三层模块中最高层的接口,定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。 24 | 25 | #### JDI 工作方式 26 | 27 | * 调试器通过 Bootstrap 获取唯一的虚拟机管理器 28 | ```java 29 | VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager(); 30 | ``` 31 | * 虚拟机管理器将在第一次被调用时初始化可用的链接池。默认会采用启动型链接器进行链接。 32 | ```java 33 | LaunchingConnector defaultConnector = virtualMachineManager.defaultConnector(); 34 | ``` 35 | * 调用链接器的 launch 来启动目标程序,同时完成调试器与目标虚拟机的链接。 36 | ```java 37 | VirtualMachine targetVM = defaultConnector.launch(arguments); 38 | ``` 39 | 40 | JDI 中 Mirror 接口是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。例如,在目标虚拟机上,已装载的类被映射成 ReferenceType 镜像,对象实例被映射成 ObjectReference 镜像,基本类型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中,在调试过程中所触发的事件被映射成 Event 镜像(如 StepEvent 等),调试器发出的事件请求被映射成 EventRequest 镜像(如 StepRequest 等),被调试的目标虚拟机则被映射成 VirtualMachine 镜像。 41 | 42 | 上面提到虚拟机管理器默认使用启动型链接器进行链接,在 JDI 中共有三种链接器接口,分别是依附型链接器(AttachingConnector)、监听型链接器(ListeningConnector)和启动型链接器(LaunchingConnector)。而根据调试器在链接过程中扮演的角色,又分为主动链接和被动链接,例如由调试器启动目标虚拟机或当目标虚拟机已运行时调试器链接成为主动型,由于篇幅有限这里不再深入展开。 43 | 44 | JDI 还包含了一个事件请求和处理模块,共包含了18种事件类型,分别作用于调试过程中的断点、异常、线程改变以及目标虚拟机生命周期等功能。 45 | 46 | ### 调试器流程 47 | 48 | 事实上这里的 Java 调试器是作为前几篇文章中提到的 JDT.LS 语言服务的插件。在语言服务器初始化参数中指定调试器的 jar 包绝对路径,LSP 会把调试器注册为一个插件,并且将调试器插件所支持的命令以及请求注册到语言服务的 `workspace/executeCommand` 请求中作为子命令。Java 调试器共支持以下几个子命令用于调试器相关的初始化配置及启动等功能,这些命令由调试器实现,通过 LSP 注册并提供给客户端调用。 49 | 50 | ```json 51 | // 调试器子命令调用方式 52 | { 53 | "jsonrpc":"2.0", 54 | "id":10, 55 | "method":"workspace/executeCommand", 56 | "params":{ 57 | "command":"vscode.java.updateDebugSettings", 58 | "arguments":[ 59 | "{\"showHex\":true,\"showStaticVariables\":true,\"showQualifiedNames\":true,\"maxStringLength\":0,\"enableHotCodeReplace\":true,\"logLevel\":\"FINER\"}" 60 | ] 61 | } 62 | } 63 | ``` 64 | 65 | * vscode.java.fetchUsageData 获取调试器默认配置。 66 | 67 | * vscode.java.startDebugSession 启动调试器的 TCP 服务,返回端口号。 68 | 69 | * vscode.java.resolveClasspath 获取被调试 Java 程序的类路径。 70 | 71 | * vscode.java.resolveMainClass 获取被调试 Java 程序的 main 方法所在类,与类路径一同用于初始化调试器配置,最终会在指定的链接器中调用 `.launch` 时作为参数。这个参数在 VSCODE 中也可以由用户指定。 72 | 73 | * vscode.java.buildWorkspace 在启动调试之前构建被调试程序。 74 | 75 | * vscode.java.updateDebugSettings 更新调试器设置。 76 | 77 | 调试器启动之前,会先向 LSP 服务发送 `vscode.java.resolveClasspath`、`vscode.java.resolveMainClass`、`vscode.java.buildWorkspace` 等请求来构建被调试程序并获取 `mainClass`、`classPaths` 等必要的参数。 78 | 79 | 之后客户端发送 `vscode.java.startDebugSession` 命令后会启动 TCPServer 等待客户端连接。 80 | 81 | ```java 82 | // java-debug JavaDebugServer.java 83 | private JavaDebugServer() { 84 | try { 85 | this.serverSocket = new ServerSocket(0, 1); 86 | } catch (IOException e) { 87 | logger.log(Level.SEVERE, String.format("Failed to create Java Debug Server: %s", e.toString()), e); 88 | } 89 | } 90 | ``` 91 | 92 | 在客户端也就是 Java 调试器扩展中, 93 | [查看扩展源码](https://github.com/Microsoft/vscode-java-debug/blob/90ea267a547f525e5ffe169efce9a6fa534acaf3/src/configurationProvider.ts#L34)可以看到这段逻辑包含在 `JavaDebugConfigurationProvider` 中,这个类负责给 VSCODE 的 `debugServices` 提供上面提到的参数。当扩展被激活时,会调用 `registerDebugConfigurationProvider` 函数来注册这个类。 94 | 95 | ```typescript 96 | // vscode-java-debug extension.ts 97 | vscode.debug.registerDebugConfigurationProvider("java", new JavaDebugConfigurationProvider()); 98 | ``` 99 | 100 | VSCODE 则会调用其中的 `resolveDebugConfiguration` 方法借助 LSP 获取调试器初始配置。 101 | 102 | ```typescript 103 | // vscode debugConfigurationManager.ts 104 | public resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: IConfig): TPromise { 105 | // pipe the config through the promises sequentially. append at the end the '*' types 106 | const providers = this.providers.filter(p => p.type === type && p.resolveDebugConfiguration) 107 | .concat(this.providers.filter(p => p.type === '*' && p.resolveDebugConfiguration)); 108 | 109 | return providers.reduce((promise, provider) => { 110 | return promise.then(config => { 111 | if (config) { 112 | return provider.resolveDebugConfiguration(folderUri, config); 113 | } else { 114 | return Promise.resolve(config); 115 | } 116 | }); 117 | }, TPromise.as(debugConfiguration)); 118 | } 119 | ``` 120 | 121 | 此时点击 VSCODE 界面上点击启动调试,便会尝试连接调试器的 TCPServer。 122 | 123 | ```typescript 124 | // vscode rawDebugSession.ts 125 | startSession(): TPromise { 126 | return new TPromise((c, e) => { 127 | this.socket = net.createConnection(this.port, this.host, () => { 128 | this.connect(this.socket, this.socket); 129 | c(null); 130 | }); 131 | this.socket.on('error', (err: any) => { 132 | e(err); 133 | }); 134 | this.socket.on('close', () => this._onExit.fire(0)); 135 | }); 136 | } 137 | ``` 138 | 139 | 由于调试器并不知道客户端什么时候准备启动调试,所以需要等待连接成功后客户单发送 `initialize` 请求来表示自己已经准备开始调试。 140 | 141 | ```javascript 142 | // initialize 请求 143 | { 144 | "command":"initialize", 145 | "seq":1, 146 | "arguments":{ 147 | "clientID":"coding", 148 | "clientName":"Cloud Studio", 149 | "adapterID":"java", 150 | "locale":"zh-cn", 151 | "linesStartAt1":true, 152 | "columnsStartAt1":true, 153 | "pathFormat":"path", 154 | "supportsVariableType":true, 155 | "supportsVariablePaging":true, 156 | "supportsRunInTerminalRequest":true 157 | }, 158 | "type":"request" 159 | } 160 | ``` 161 | 162 | 请求成功后,客户端再发送 `launch` 请求,包含了以上获取到的 `classaPaths` 以及 `mainClass` 等参数,这时调试器真正开始启动被调试程序。这里 `launch` 对应了 JDI 链接器中的启动型号链接器,表示由调试器来启动目标虚拟机(vm)。 163 | 164 | ```javascript 165 | // lanunch 请求 166 | { 167 | command: "launch", 168 | seq: 2, 169 | type: "response", 170 | arguments: { 171 | args: "", 172 | classPaths: [], 173 | mainClass: "net.coding.demo.Application", 174 | modulePaths: [], 175 | request: "launch", 176 | type: "java", 177 | } 178 | } 179 | ``` 180 | 181 | ```java 182 | // java-debug AdvancedLaunchingConnector.java 183 | 184 | // constructLaunchCommand 构建被调试程序启动参数 185 | String[] cmds = constructLaunchCommand(connectionArgs, address); 186 | Process process = Runtime.getRuntime().exec(cmds, envVars, workingDir); 187 | 188 | VirtualMachineImpl vm; 189 | 190 | try { 191 | vm = (VirtualMachineImpl) listenConnector.accept(args); 192 | } catch (IOException | IllegalConnectorArgumentsException e) { 193 | process.destroy(); 194 | throw new VMStartException(String.format("VM did not connect within given time: %d ms", ACCEPT_TIMEOUT), process); 195 | } 196 | 197 | // 调用 setLaunchedProcess 将被调试程序的进程赋值给目标虚拟机,目标虚拟机监听此进程的运行信息 198 | vm.setLaunchedProcess(process); 199 | ``` 200 | 201 | 此时被调试程序已经正式启动,客户端可以根据协议规范来进行调试相关操作。 202 | 203 | ## Web 端实现 204 | 205 | 同样的,由于平台差异,在 Web 端无法直接监听调试器端口来进行通信,我们还需要一层 WebSocket 来转发调试器与客户端的消息。 206 | 207 | 服务端需要启动一个 WebSocket 服务,当调试器启动 TCPServer 时,客户端携带调试端口连接到服务器,服务器再作为 TCPClient 连接到调试器,然后将客户端(网页端)的请求转发到给调试器服务。 208 | 209 | 服务端实现非常简单,只需要在接收到客户端请求后按照协议规范拼接好带有 `Content-Length` 字段的协议字符串发送给提调试器。同样收到调试器回复或事件消息时再发送给客户端即可。 210 | 211 | 这里重点介绍一下客户端如何监听 WebSocket 消息并转化为事件机制。因为前几篇文章中提到的 LSP 相关操作本身就封装在 Monaco 编辑器中,所以实现起来相对比较简单,只要调用 `monaco-languageClient` 中的相关方法,编辑器就会自动发送 LSP 请求及识别回复,除了一些超出编辑器本身的操作,都由编辑器自行完成。 212 | 213 | 而调试器界面是在编辑器之外的,Monaco 编辑器也并没有自带调试器UI,所以这部分工作需要我们自己完成。 214 | 215 | 具体来说我们需要一个简单的通用调试器UI,可以照 VSCODE 界面来抄(反正都是现成的。。 216 | 217 | 之后还需要一个 WebSocket 客户端来与服务器通信,使用与服务端配套的 `socket.io-client` 来实现这个客户端,上面提到,客户端需要将请求以及接收到的回复/事件转化为事件订阅机制,因为这样更方便与 UI 同步。 218 | 219 | 我们使用 React + Redux 实现客户端界面,同时使用 Redux-Saga 作为异步方案来实现 WebSocket 的事件转化机制。这里不详细介绍 Redux-Saga 的用法,有兴趣的可以自行查看[官方文档](https://redux-saga.js.org/); 220 | 221 | 首先将 WebSocket 封装为一个单例模式,这样方便给 Saga 作为 API 来调用且避免被多次实例化。 222 | 223 | ```javascript 224 | class WebSocketApi { 225 | constructor() { 226 | this._instance = null; 227 | // 请求时携带的唯一自增 ID 228 | this.sequence = 1; 229 | // 缓存请求的回调函数 230 | this.pendingRequests = new Map(); 231 | // 缓存事件的处理函数(由 Saga 在注册时提供,这里实现应为一个 generator 函数) 232 | this.eventCallback = new Map(); 233 | } 234 | 235 | static getInstance() { 236 | if (!this._instance) { 237 | this._instance = new WebSocketApi(); 238 | } 239 | return this._instance; 240 | } 241 | } 242 | ``` 243 | 244 | 然后需要有一个供 Saga 调用发送请求的方法 sendRequest,在调试协议中每个请求都会有相应的回复,所以我们还需要把这个请求 ID 缓存起来,并提供一个接收到回复的处理函数。(这个回复的处理函数由 WebSocketApi 自行实现,给 Saga 调用再封装为 Promise 的形式) 245 | 246 | ```javascript 247 | // 供 Saga 调用 这一层实现代码比较简单,就不再多说了。 248 | sendRequest = (command, args) => { 249 | return new Promise((resolve, reject) => { 250 | this.internalSend(command, args, (response) => { 251 | if (response.success) { 252 | resolve(response); 253 | } else { 254 | reject(response); 255 | } 256 | }); 257 | }); 258 | } 259 | 260 | internalSend = (command, args, cb) => { 261 | const request = { 262 | command, 263 | seq: this.sequence++, 264 | }; 265 | if (args && Object.keys(args).length > 0) { 266 | request.arguments = args; 267 | } 268 | 269 | this._internalSend('request', request); 270 | 271 | if (cb) { 272 | // store callback for this request 273 | this.pendingRequests.set(request.seq, cb); 274 | } 275 | } 276 | 277 | _internalSend = (type, message) => { 278 | message.type = type; 279 | if (this.ws) { 280 | this.ws.send(JSON.stringify(message)); 281 | } 282 | } 283 | ``` 284 | 285 | 接下来是接收到调试器事件的机制,这里的事件是指前文中提到的 JDI 中的事件模块,调试器会把这些事件发送给客户端。 286 | 287 | ```javascript 288 | connect = (port) => { 289 | this.ws = createWebSocket(port); 290 | 291 | this.ws.on('message', this.handleMessage); 292 | return new Promise((resolve, reject) => { 293 | this.ws.on('connect', () => resolve(true)); 294 | }); 295 | } 296 | 297 | handleMessage = (data) => { 298 | const message = JSON.parse(data); 299 | 300 | switch (message.type) { 301 | case 'event': 302 | this.onDapEvent(message); 303 | break; 304 | } 305 | } 306 | 307 | onDapEvent = (event) => { 308 | const eventCb = this.eventCallback.get(event.event); 309 | if (eventCb) { 310 | try { 311 | store.runSaga(eventCb, event); 312 | } catch (e) { 313 | console.log(e.message); 314 | } 315 | } 316 | } 317 | ``` 318 | 319 | 可以看到上面代码中接收到事件类型的消息时,从 `eventCallback` 中获取到 Saga 提供的事件处理函数,而使用 `store.runSaga` 来调用。这是因为这些事件处理函数都是 Saga 或者说 generator 函数的形式存在的,而这里的 store.runSaga 实际上就是 redux-saga 中的 `sagaMiddleware.run` 函数。我们知道 Saga 本身应该是由 redux 的 action 来驱动的,而我们想接收到调试器的事件时来运行 Saga ,所以借助 sagaMiddleware.run 来实现了 Saga 的外部调用。 320 | 321 | 我们可以这样注册这些外部调用的 Saga 322 | 323 | ```javascript 324 | // 发送 startDebugSession 并成功返回后连接 WebSocket 325 | const success = yield call(webSocketApi.connect, port); 326 | 327 | if (success) { 328 | // 发送初始化配置 329 | yield put(debugInitialize(initializeParams)); 330 | // 调用注册 saga 事件 331 | yield fork(registerEventCallback); 332 | } 333 | 334 | // 注册事件 335 | function* registerEventCallback() { 336 | try { 337 | webSocketApi.registerEventCallback('initialized', initializedEventSaga); 338 | webSocketApi.registerEventCallback('stopped', stoppedEventSaga); 339 | webSocketApi.registerEventCallback('output', outputEventSaga); 340 | webSocketApi.registerEventCallback('thread', threadEventSaga); 341 | webSocketApi.registerEventCallback('continued', continuedEventSaga); 342 | } 343 | } 344 | 345 | // stopped 事件的 saga 实现 346 | function* stoppedEventSaga(params) { 347 | try { 348 | const { body } = params; 349 | 350 | // set button state. 351 | yield put(setStoppedStatus(true)); 352 | // set stoppedThread 353 | yield put(setStoppedThread(body.threadId)); 354 | // set stoppedDetails 355 | yield put(updateStoppedDetails(body)); 356 | 357 | yield put(fetchThreads()); 358 | const stackParams = { 359 | threadId: body.threadId, 360 | startFrame: 0, 361 | levels: 20, 362 | }; 363 | const response = yield call(webSocketApi.sendRequest, 'stackTrace', stackParams); 364 | 365 | if (response.success) { 366 | const { 367 | body: { stackFrames }, 368 | } = response; 369 | for (const sf of stackFrames) { 370 | yield put(updateStackFreams(body.threadId, sf)); 371 | } 372 | 373 | if (stackFrames.length > 0) { 374 | // The request returns the variable scopes for a given stackframe ID. 375 | yield put(fetchVariableScopesByFrameID(stackFrames[0].id)); 376 | } 377 | // @TODO UI change 378 | } 379 | } catch (e) { 380 | // 381 | } 382 | } 383 | ``` 384 | 385 | 通过这种机制,我们可以在接收到指定事件之后借助 redux-saga 强大的异步任务调度能力来执行相应的逻辑,同时还可以调用同步的 action 来对 UI以及编辑器 做相应的更新。 386 | 387 | ## 最后 388 | 389 | 调试是日常开发中非常重要的一部分,了解常用编辑器/IDE 的调试原理有助于我们更好的使用调试功能 390 | 。这篇文章内容较长,首先介绍了 VSCODE 中调试协议的概念,进而以 Java 为例解析了 VSCODE 中是如何启动调试器,以及简单介绍了一下 Java 调试器的实现原理。最后介绍了在线编辑器调试实现的思路,同时借助 redux-saga 实现了一个简单的事件机制来实现 WebSocket 消息的转化处理。 391 | 392 | ## 相关参考链接 393 | 394 | * [howto-launch-and-debug-in-vscode-using](http://pydev.blogspot.com/2018/05/howto-launch-and-debug-in-vscode-using.html?m=1) 395 | * [深入 Java 调试体系](https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html) 396 | * [vscode-debug-protocol](https://github.com/Microsoft/vscode-debugadapter-node/blob/768e505c7d362f733a29c89fa973c6285ce8fb27/protocol/src/debugProtocol.ts) 397 | * [vscode-debugging-api](https://code.visualstudio.com/docs/extensionAPI/api-debugging) 398 | * [java-debug](https://github.com/Microsoft/java-debug) 399 | * [vscode](https://github.com/Microsoft/vscode) 400 | -------------------------------------------------------------------------------- /article/二叉树.MD: -------------------------------------------------------------------------------- 1 | # 树 2 | 树是一种分层数据的抽象模型,一个树结构包含一系列存在父子关系的节点,每个节点都有一个父节点(除了根节点)以及多个子节点. 3 | * 树顶部的节点叫做`根节点`. 4 | * 由一个子节点以及其后代组成`子树`. 5 | * 节点有个`深度`属性,表示当前节点在树的层级. 6 | * 所有节点深度的最大值被称为树的`高度`. 7 | 8 | ## 二叉树和二叉搜索树 9 | `二叉树`中一个节点最多只能有两个子节点,分别为左侧节点以及右侧节点 10 | 11 | `二叉搜索树`(BST)是另一种二叉树,但是它只允许左侧子节点存储比父节点小的值,而右侧子节点存储比父元素大的值 12 | 13 | 二叉树的节点`Node`类有一个指向左侧子节点的属性`left`以及一个指向右侧子节点的属性`right`,以及表示自身的属性`key` 14 | ```typescript 15 | class Node { 16 | constructor(public key: Node, public left?: Node, public right?: Node){} 17 | } 18 | 19 | class BinarySearchTree { 20 | root: Node; 21 | constructor() { 22 | this.root = null; 23 | } 24 | } 25 | ``` 26 | 27 | ### 向树中插入一个键 28 | ```typescript 29 | public insert(key: any) { 30 | const node = new Node(key); 31 | if (this.root === null) { 32 | this.root = node; 33 | } else { 34 | insertNode(this.root, node); 35 | } 36 | } 37 | ``` 38 | 向树中插入一个键分为三部 39 | * 实例化一个新的node 40 | * 如果不存在根节点,则新插入的节点赋值给根节点 41 | * 如果已存在根节点,则调用`insertNode`函数递归插入 42 | 43 | ```typescript 44 | function insertNode(node: Node, newNode: Node) { 45 | // 当新节点的key小于当前节点时,从当前节点左侧递归查找空位 46 | if (newNode.key < node.key) { 47 | if (node.left === null) { 48 | node.left = newNode; 49 | } else { 50 | insertNode(node.left, newNode); 51 | } 52 | } else { 53 | // 新节点的key大于当前节点时,从当前节点右侧递归查找空位 54 | if (node.right === null) { 55 | node.right = newNode; 56 | } else { 57 | insertNode(node.right, newNode); 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ### 树的遍历 64 | 树的遍历分为`先序`,`中序`以及`后序` 65 | 66 | #### 中序遍历 67 | 68 | 中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从小到大的方式顺序访问所有节点,可以使用递归的方式依次遍历左侧节点和右侧节点,以节点为null作为递归终止条件 69 | ```typescript 70 | public inOrderTraverse(callback: Function) { 71 | inOrderTraverse(this.root, callback); 72 | } 73 | 74 | function inOrderTraverse(node: Node, callback: Function) { 75 | if (node !== null) { 76 | inOrderTraverse(node.left, callback); 77 | callback(node.key); 78 | inOrderTraverse(node.right, callback); 79 | } 80 | } 81 | ``` 82 | 83 | #### 先序遍历 84 | 先序遍历是以优先于后代节点的顺序访问树 85 | ```typescript 86 | public preOrderTraverse(callback: Function) { 87 | preOrderTraverseNode(this.root, callback); 88 | } 89 | 90 | function preOrderTraverseNode(node: Node, callback: Function) { 91 | if (node !== null) { 92 | callback(node.key); 93 | preOrderTraverseNode(node.left, callback); 94 | preOrderTraverseNode(node.right, callback); 95 | } 96 | } 97 | ``` 98 | 99 | #### 后序遍历 100 | 后序遍历是指先访问节点的后代节点,再访问节点本身 101 | ```typescript 102 | public postOrderTraverse(callback: Function) { 103 | postOrderTraverseNode(this.root, callback); 104 | } 105 | 106 | function postOrderTraverseNode(node: Node, callback: Function) { 107 | if (node !== null) { 108 | postOrderTraverseNode(node.left, callback); 109 | postOrderTraverseNode(node.right, callback); 110 | callback(node.key); 111 | } 112 | } 113 | ``` -------------------------------------------------------------------------------- /article/利用PureComponent+ImmutableJS实现Pure-render.md: -------------------------------------------------------------------------------- 1 | ## 利用ImmutableJS实现Pure-Render 2 | `PureRender`是react应用中最常见的优化方式之一,顾名思义是纯·渲染,React的核心思想可以用一个表达式来概括 3 | 4 | `view = f(model)` 5 | 6 | 这个很简单的表达式阐述了一个最基本的思想,数据的更新触发视图的更新,如果把它看做一个[纯函数](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976),那么给定相同的输入必定得到相同的输出,简而言之就是如果state&props没有改变,理论上讲组件不会重新渲染 7 | 8 | React生命周期有一个函数`shouldComponentUpdate`,看名字就知道这个函数决定了组件要不要更新(重新渲染),默认情况下这个函数始终返回`true` 9 | 10 | 但是过多的rerender势必会引起性能问题,所以在必要的情况下开发者需要自己手动实现shouldComponentUpdate: 11 | ```javascript 12 | shouldComponentUpdate(nextProps, nextState) { 13 | return this.props.value !== nextProps.value; 14 | } 15 | ``` 16 | 事实上在较新版本的React中内置了一个已经实现shouldComponentUpdate方法的类,叫做`PureComponent`,使用时只要将原先的Component替换为PureComponent即可 17 | ```javascript 18 | import react, { PureComponent } from 'react'; 19 | class MyPage extends PureComponent { 20 | // your code 21 | } 22 | ``` 23 | 然而不管是上面那个简单的例子,还是PureComponent,它们实现的方式很简单,都是浅比较 24 | 25 | 对于基本数据类型,只需要对比值,而引用类型则只对比引用地址,试想一下如果有一个长度为50+的数组,单纯的`!==`浅对比就完全没有任何用处了,因为数组是引用类型,每次传来新的数组都是不同的引用,始终还是返回true,但是深对比带来的开销更大,到底如何取舍? 26 | 27 | 答案是[ImmutableData](https://github.com/facebook/immutable-js) 28 | ### 什么是ImmutableData? 29 | > ImmutableData(不可变数据)就是指一旦创建就不能被改变的数据 30 | 上对ImmutableData的任何修改都会返回一个新的immutable对象 31 | 32 | Immutablejs实现了List、Map等常用数据类型,分别对应js中的数组和对象 33 | 34 | 简单来说,当对一个immutabledata进行增删改操作时,并不会修改原本的数据,而是生成了新的immutable对象,如果没有任何修改则返回原对象 35 | 36 | 基本用法可参考[官方文档](http://facebook.github.io/immutable-js/) 37 | ### React中怎么用? 38 | 可以将组件state中的数据转为immutabledata,也可以将redux的state转为immutabledata 39 | ```javascript 40 | // state 41 | import React from 'react'; 42 | import { fromJS } from 'immutable'; 43 | 44 | class Dmoe extends PureComponent { 45 | state = { 46 | someDeepData: fromJS({ 47 | name: 'misaka', 48 | age: 20, 49 | }), 50 | } 51 | 52 | handleChangeAge = () => { 53 | const { someDeepData } = this.state; 54 | const prevAge = someDeepData.get('age'); 55 | this.setState({ 56 | someDeepData: someDeepData.set('age', prevAge - 1), 57 | }); 58 | } 59 | 60 | render() { 61 | const { someDeepData } = this.state; 62 | return (
63 |

{someDeepData.get('name')}

64 | 65 |
); 66 | } 67 | } 68 | ``` 69 | [CodeSandbox在线示例](https://codesandbox.io/s/3rq3orqom6) 70 | 71 | 例如一个父子组件嵌套,父组件数据改变导致自身rerender从而引发子组件一起rerender,这种情况使用ImmutableData + PureComponent则可以很好的避免子组件的重复渲染 72 | ```javascript 73 | class Child extends PureComponent { 74 | render() { 75 | const { info } = this.props; 76 | console.log("render"); 77 | return ( 78 |
79 |

my name is {info.name}

80 |

I am {info.age} years old!

81 |
82 | ); 83 | } 84 | } 85 | 86 | class Parent extends PureComponent { 87 | state = { 88 | info: fromJS({ 89 | name: "misaka", 90 | age: 10 91 | }), 92 | age: 20 93 | }; 94 | 95 | handleChangeAge = () => { 96 | const { age } = this.state; 97 | this.setState({ 98 | age: age + 1 99 | }); 100 | }; 101 | render() { 102 | const { info, age } = this.state; 103 | return ( 104 |
105 | 106 | I am Sakura, {age} years old. this is my child! 107 | 108 |
109 | ); 110 | } 111 | } 112 | 113 | ``` 114 | 115 | [CodeSanBox在线示例](https://codesandbox.io/s/n9z3z2k0op) 116 | 117 | ### refs: 118 | * [Master the JavaScript Interview: What is a Pure Function?](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976) 119 | * [ReactShallowRenderer.js](https://github.com/facebook/react/blob/2a1b1f3094e524338b3eb3de51b23921576f02f5/packages/react-test-renderer/src/ReactShallowRenderer.js#L170) 120 | * [什么时候要在React组件中写shouldComponentUpdate?](http://www.infoq.com/cn/news/2016/07/react-shouldComponentUpdate) 121 | * [Should I use shouldComponentUpdate?](http://jamesknelson.com/should-i-use-shouldcomponentupdate/) 122 | * [Optimizing Performance](https://reactjs.org/docs/optimizing-performance.html) 123 | * [Immutable详解及React中实践](https://github.com/camsong/blog/issues/3) 124 | -------------------------------------------------------------------------------- /article/用于创建高阶组件的React辅助库---recompose.MD: -------------------------------------------------------------------------------- 1 | ## Recompose 用于创建函数式组件和高阶组件的react工具库 2 | [Recompose项目地址](https://github.com/acdlite/recompose) 3 | 4 | `recompose`可以看做React技术栈的`lodash`,提供了许多用于创建react函数式组件和高阶组件的工具函数,包括`compose`、`branch`、`withState`、`withStateHandlers`等 5 | 6 | ### 基本用法 7 | 8 | #### withState 9 | ```javascript 10 | // 接收三个参数,第一个参数为注入state的key名,第二个参数为修改state的函数名,第三个参数为默认值 11 | const enhance = withState('counter', 'setCounter', 0); 12 | const Counter = enhance(({ counter, setCounter }) => 13 |
14 | Count: {counter} 15 | 16 | 17 |
18 | ) 19 | ``` 20 | #### 使用pure和onlyUpdateForKeys实现函数式组件的shouldComponentUpdate 21 | ```javascript 22 | 23 | // 无状态组件 24 | const ExpensiveComponent = ({ propA, propB }) =>
{/*xxx*/}
25 | 26 | // 使用pure函数(效果等同于 extends PureComponent) 27 | const OptimizedComponent = pure(ExpensiveComponent) 28 | 29 | // 指定props更新 30 | const HyperOptimizedComponent = onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent) 31 | ``` 32 | 33 | ### 高级用法 34 | 35 | #### compose组合 36 | `compose`方法和之前redux源码中的compose方法一模一样,因为react的高阶组件实际上是`接受组件作为参数并最终返回一个组件的高阶函数`,所以对于相同的组件支持使用compose函数直接组合不同的高阶组件,类似于这样一种方式 37 | ```javascript 38 | func0 = (component) => finalComponent; 39 | func1 = (component) => finalComponent; 40 | func2 = (component) => finalComponent; 41 | 42 | func0(func1(func2(Component))); 43 | ``` 44 | 直接看recompose的源代码实现会发现很多方法比如`withState`、`withProps`、`withHandles`、`withContext`都是直接返回一个高阶组件的,他们都长这样 45 | 46 | ```javascript 47 | const withXXX = (...args) => (BaseComponent) => { 48 | class WithXXX extends PureComponent { 49 | // 内部实现 50 | } 51 | return WithXXX; 52 | } 53 | ``` 54 | 55 | 对于这样的高阶组件,就可以使用compose方法来进行组合,compose函数的参数个数没有限制,但必须是一个高阶组件(满足输入一个组件并返回一个新组件),使用compose函数就可以组合这些方法,包装一个纯函数组件,利用这个办法可以把组件的副作用剥离出来,使外部业务逻辑不会干扰组件自身 56 | 57 | ```javascript 58 | // Toggle.js 59 | const Toggle = ({ title, message, toggleVisibility, isVisible, name }) => ( 60 |
61 |

{title}

62 | {isVisible ?

{"I'm visible"}

:

{'Not Visible'}

} 63 |

{message}

64 |

{name}

65 | 66 |
67 | ); 68 | 69 | 70 | // 使用recompose包装Toggle 71 | export default compose( 72 | withState('isVisible', 'toggleVis', false), 73 | withHandlers({ 74 | toggleVisibility: ({ toggleVis, isVisible }) => (event) => 75 | toggleVis(!isVisible), 76 | }), 77 | withProps(({ isVisible }) => ({ 78 | title: isVisible 79 | ? 'This is the visible title' 80 | : 'This is the default title', 81 | message: isVisible 82 | ? 'Hello I am Visible' 83 | : 'I am not visible yet, click the button!', 84 | })), 85 | )(Toggle); 86 | ``` 87 | 88 | #### 生命周期函数 89 | 90 | recompose推崇函数式无状态组件,因为这样既方便书写也可以把无用的逻辑抽离出来,使用`lifecycle`函数可以为无状态组件添加生命周期函数 91 | ```javascript 92 | import { lifecycle } from 'recompose'; 93 | 94 | const cycle = { 95 | componentDidMount() { 96 | // didMount 97 | }, 98 | componentWillReceiveProps(nextProps) { 99 | // nextProps 100 | }, 101 | componentWillUnmount() { 102 | // willUnmount 103 | } 104 | }; 105 | 106 | const Toggle = (props) =>
{props.value}
; 107 | 108 | export default lifecycle(cycle)(Toggle); 109 | ``` 110 | 111 | #### nest组件层级嵌套 112 | `nest`函数的作用是将传入的组件按照顺序一层一层嵌套起来,形成这样的jsx结构 113 | ```javascript 114 | 115 | 116 | 117 | {/*xxx*/} 118 | 119 | 120 | 121 | ``` 122 | 使用nest可以将这些组件自动嵌套在一起 123 | ```javascript 124 | import { nest } from 'recompose'; 125 | 126 | const A = ({data, children}) => ( 127 |
128 | this is some node 129 | {children} 130 |
131 | ); 132 | const B = ({data, children}) => ( 133 |
134 | this is some node 135 | {children} 136 |
137 | ); 138 | const C = ({data, children}) => ( 139 |
140 | this is some node 141 | {children} 142 |
143 | ); 144 | export default nest(A, B, C); 145 | ``` 146 | 147 | 需要注意的是组件传入的顺序决定嵌套层级,以及父组件需要使用{children}引用子组件 148 | -------------------------------------------------------------------------------- /article/链表.MD: -------------------------------------------------------------------------------- 1 | # 链表 2 | > 一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据 3 | 4 | ## 单项链表 5 | 单项链表的基本结构 6 | ```typescript 7 | class Node { 8 | constructor(public element = element, public next = null) {} 9 | } 10 | class LinkedList { 11 | constructor() { 12 | this.length = 0; 13 | this.head = null; 14 | } 15 | public append(element: any) {} 16 | public insert(position: number, element: Node) {} 17 | public removeAt(position: number){} 18 | public remove(element: Node){} 19 | public indexOf(element: Node){} 20 | public isEmpty(): boolean{} 21 | public size(): number{} 22 | public getHead(): Node{} 23 | public toString(): string {} 24 | public print() {} 25 | } 26 | ``` 27 | 28 | ### 向链表尾部追加元素 29 | 向链表对象尾部添加一个元素时,可能有两种场景: 列表空时,添加的是第一个元素,列表不为空时,向其追加元素 30 | ```typescript 31 | // 省略部分代码 32 | public append(element: Node) { 33 | const node = new Node(element); 34 | let current; 35 | // 列表为空时添加为第一个元素 36 | if (this.head === null) { 37 | head = node; 38 | } else { 39 | current = this.head; 40 | while(current.next) { 41 | current = current.next; 42 | } 43 | current.next = node; 44 | } 45 | this.length += 1; 46 | } 47 | // 省略部分代码 48 | ``` 49 | ### 从链表中移除元素 50 | 移除元素也分两种场景: 移除第一个元素或移除第一个以外的任一元素 51 | ```typescript 52 | // 省略部分代码 53 | /** 54 | * 移除指定位置的元素 55 | * */ 56 | public removeAt(position: number) { 57 | if (position > -1 && position < length) { 58 | let current = this.head; 59 | let previous; 60 | let index = 0; 61 | 62 | // 移除第一项 63 | if (position === 0) { 64 | this.head = current.next; 65 | } else { 66 | while (index++ < position) { 67 | previous = current; 68 | current = current.next; 69 | } 70 | previous.next = current.next; 71 | } 72 | this.length -= 1; 73 | return current.element; 74 | } else { 75 | return null; 76 | } 77 | } 78 | // 省略部分代码 79 | ``` 80 | 移除列表最后一项或中间某一项时, 需要依靠一个细节来迭代列表,直到到达目标位置,使用一个内部递增的`index`变量, `current`变量为所 81 | 82 | 循环列表的当前元素进行引用,以及一个对当前元素的前一个元素引用的变量`previous` 83 | 84 | 从列表中删除当前元素,要做的就是将previous.next和current.next连接起来 85 | 86 | ### 在任意位置插入一个元素 87 | `insert`方法用于在任意位置插入一个元素 88 | ```typescript 89 | public insert(position: number, element: any): boolean { 90 | if (position >= 0 && position <= this.length) { 91 | const node = new Node(element); 92 | let current = this.head; 93 | let previous; 94 | let index = 0; 95 | 96 | // 当position为0 则在第一个位置添加新元素 97 | if (position === 0) { 98 | node.next = current; 99 | head = node; 100 | } else { 101 | while(index++ < position) { 102 | previous = current; 103 | current = current.next; 104 | } 105 | node.next = current; 106 | previous.next = node; 107 | } 108 | this.length += 1; 109 | return true; 110 | } else { 111 | return false; 112 | } 113 | } 114 | ``` 115 | 当插入位置为0时,先把新插入node的next设为当前head,再把head修改为node,就成功把新元素插入到了列表头部 116 | 117 | 当插入位置在中间或尾部时,循环遍历列表,找到目标位置,在previous和current中间添加新元素,将新元素的next设为current,previous的next设为新元素,这样就成功在列表中间插入了新元素 118 | 119 | ### 其他方法 120 | #### toString 121 | toString方法需要将LinkedList对象转换成一个字符串 122 | ```typescript 123 | public toString() { 124 | let current = this.head; 125 | let string = ''; 126 | 127 | while(current) { 128 | string += current.element + (current.next ? 'n' : ''); 129 | current = current.next; 130 | } 131 | return string; 132 | } 133 | ``` 134 | 135 | #### indexOf 136 | indexOf方法接受一个元素的值,如果在列表中找到它,就返回元素的位置,否则返回-1 137 | ```typescript 138 | public indexOf(element: any) { 139 | let current = this.head; 140 | let index = -1; 141 | while(current) { 142 | if (element === current.element) { 143 | return index; 144 | } 145 | index += 1; 146 | current = current.next; 147 | } 148 | return -1; 149 | } 150 | ``` 151 | 152 | #### remove 153 | remove方法可以依赖indexOf方法先找到要移除元素的index,然后调用`this.removeAt`将index传进去从而删除元素 154 | ```typescript 155 | public remove(element: any) { 156 | const index = this.indexOf(element); 157 | return this.removeAt(index); 158 | } 159 | ``` 160 | 161 | #### isEmpty && size && getHead 162 | ```typescript 163 | public isEmpty() { 164 | return this.length === 0; 165 | } 166 | 167 | public size() { 168 | return this.length; 169 | } 170 | 171 | public getHead() { 172 | return this.head; 173 | } 174 | 175 | ``` 176 | 177 | ## 双向链表 178 | 双向链表与单项链表的区别是,双向链表有两个分别指向上一个元素以及下一个元素的链接 179 | 180 | 因此,双向链表除了`head`属性外,还有一个表示尾部元素的`tail`属性 181 | 182 | ```typescript 183 | class DoublyLinkedList { 184 | constructor { 185 | this.length = 0; 186 | this.head = null; 187 | this.tail = null; 188 | } 189 | } 190 | ``` 191 | 双向链表还提供了两种迭代列表的方法: 从头到尾以及从尾到头.我们也可以访问一个特定节点的下一个或上一个元素 192 | ### 任意位置插入新元素 193 | 双向链表插入操作和单项链表相似,唯一的区别是单项链表只需要维护一个next指针,而双向链表需要同时维护next和prev指针 194 | 195 | ```typescript 196 | public insert(position: number, element: any): boolean { 197 | if (position >= 0 && position <= this.length) { 198 | const node = new Node(element); 199 | let current = this.head; 200 | let previous; 201 | let index = 0; 202 | 203 | if (position === 0) { 204 | if (!this.head) { 205 | this.head = node; 206 | this.tail = node; 207 | } else { 208 | node.next = current; 209 | current.prev = node; 210 | this.head = node; 211 | } 212 | } else if (positon === length) { 213 | current = this.tail; 214 | current.next = node; 215 | node.prev = current; 216 | this.tail = node; 217 | } else { 218 | whild (index++ < position) { 219 | previous = current; 220 | current = current.next; 221 | } 222 | node.next = current; 223 | previous.next = node; 224 | current.prev = node; 225 | node.prev = previous; 226 | } 227 | this.length += 1; 228 | return true; 229 | } else { 230 | return false; 231 | } 232 | } 233 | ``` 234 | * 头部插入,如果头部为null,则把head和tail都设为新元素. 如果头部已经存在,则把头部元素赋值给current,新元素的next设为current,current的prev设为新元素,同时把尾部指针设为新元素 235 | * 尾部插入, 将尾部元素赋值给current, current的next指向新元素,新元素的prev再指向current,最后把尾部指针指向新元素 236 | * 中间插入, 循环遍历列表,将当前元素赋值给previous,当前元素则改为current的next,再把新元素的next指向当前元素,previous的next指向新元素,最后把当前元素的prev指向新元素,新元素的prev指向previous 237 | 238 | 239 | ### 从任意位置移除元素 240 | ```typescript 241 | public removeAt(position: number) { 242 | if (position > -1 && position < this.length) { 243 | let current = this.head; 244 | let previous; 245 | let index = 0; 246 | 247 | if (position === 0) { 248 | this.head = current.next; 249 | if (this.length === 1) { 250 | this.tail = null; 251 | } else { 252 | this.head.prev = null; 253 | } 254 | } else if (position === this.length - 1) { 255 | current = this.tail; 256 | this.tail = current.prev; 257 | this.tail.next = null; 258 | } else { 259 | while (index++ < position) { 260 | previous = current; 261 | current = current.next; 262 | } 263 | 264 | previous.next = current.next; 265 | current.next.prev = previous; 266 | } 267 | this.length -= 1; 268 | return current.element; 269 | } else { 270 | return null; 271 | } 272 | } 273 | ``` 274 | 同样需要处理三种场景 275 | * 头部移除, 将当前头部赋值给current, 之后将头部设为current的next, 如果列表只有一项,则把tail设为null,否则将头部的prev设为null 276 | * 尾部移除, 将当前尾部赋值给current, 之后将尾部设为current的prev, 尾部的next设为null 277 | * 中间移除, 循环遍历列表, 将当前项赋值给previous, current修改为current的next, 最后将previous的next设为current的next, current的next的prev设为previous, 完成中间指定位置移除 278 | 279 | ## 循环链表 280 | 循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用,循环链表和链表之间唯一的区别是,最后一个元素的next指针并不指向null,而是头部元素 281 | -------------------------------------------------------------------------------- /article/面试题整理.md: -------------------------------------------------------------------------------- 1 | # 操作系统 2 | 3 | ## 进程和线程的区别 4 | 5 | * 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元 6 | * 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。 7 | * 进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束 8 | * 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的 9 | * 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源 10 | * 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志 11 | 12 | # HTTP 13 | 14 | ## HTTP是什么 15 | 16 | > 全称HyperText Transfer Protocol,超文本传输协议 17 | 18 | 是一个客户端和服务器端请求和应答的标准(TCP),属于应用层的通信协议 19 | 20 | ## 请求报文 21 | 22 | * 方法(GET,POST,PUT,DELETE等) 23 | 24 | * URI(统一资源标识符) 25 | 26 | > URI表示资源是什么,URL表示资源的位置 27 | 28 | * HTTP协议版本 29 | 30 | * 请求头部字段 31 | 32 | > 包含Host,Connection,Content-type,Content-length等 33 | 34 | * 内容实体 35 | 36 | ## 响应报文 37 | 38 | * HTTP协议版本 39 | 40 | * 状态码 41 | 42 | * 状态码原因短语 43 | 44 | * 响应头部 45 | 46 | > 包含Date,Content-type,Content-length等 47 | 48 | * 内容主体 49 | 50 | > HTML字符串,JSON及XML数据等 51 | 52 | ## HTTP方法 53 | 54 | * GET 55 | 56 | > 主要用于获取资源 57 | > 58 | > 使用 GET 方法,浏览器会把 HTTP Header 和 Data 一并发送出去,服务器响应 200(OK)并返回数据 59 | 60 | * POST 61 | 62 | > 主要目的是传输存储在内容实体中的数据 63 | > 64 | > 使用 POST 方法,浏览器先发送 Header,服务器响应 100(Continue)之后,浏览器再发送 Data,最后服务器响应 200(OK)并返回数据 65 | 66 | * HEAD 67 | 68 | > 获取报文首部 69 | > 70 | > 不返回报文实体主体部分 71 | > 72 | > 主要用于确认 URL 的有效性以及资源更新的日期时间等 73 | 74 | * PUT 75 | 76 | > 上传文件,不带验证机制 77 | 78 | * DELETE 79 | 80 | > 删除文件,同样不带验证机制 81 | 82 | * OPTIONS 83 | 84 | > 查询指定的 URL 能够支持的方法 85 | 86 | ## HTTP状态码 87 | 88 | > 服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 89 | 90 | ### 1XX 91 | 92 | 表示接受的请求正在处理 93 | 94 | ### 2XX 95 | 96 | * 200 ok 97 | * 204 No Content:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用 98 | * 206 Partial Content: 表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容 99 | 100 | 101 | 102 | ### 3XX 103 | 104 | 重定向 105 | 106 | * 301 Moved Permanently: 永久重定向 107 | * 302 Found: 临时重定向 108 | 109 | ### 4XX 110 | 111 | 客户端错误 112 | 113 | * 400 Bad Request: 请求报文中存在语法错误 114 | * 401 Unauthorized: 请求需要携带认证信息 115 | * 404 Not Found 116 | 117 | ### 5XX 118 | 119 | 服务器错误 120 | 121 | 122 | 123 | ## 为什么要有HTTPS 124 | 125 | HTTP有一些安全问题 126 | 127 | * 使用明文进行通信,内容有可能被抓包窃听 128 | * 不验证通信双方的身份,通信方身份可能遭遇伪装(电信劫持) 129 | * 无法验证报文的完整性,报文有可能被篡改 130 | 131 | > HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Socket Layer)通信,再由 SSL 和 TCP 通信。通过使用 SSL,HTTPs 提供了加密、认证和完整性保护 132 | 133 | 134 | 135 | ## HTTPS如何加密的 136 | 137 | HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信 138 | 139 | # JavaScript 140 | 141 | ## JavaScript是单线程还是多线程 142 | 143 | 单线程,因为作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM.如果是多线程,一个线程添加了一个DOM节点,另一个线程删除了DOM节点,浏览器无法判断以哪个线程为主 144 | 145 | 通过WebWorker标准可以简单模拟多线程操作,但子线程完全受主线程控制,且无法操作DOM,本质上还是单线程 146 | 147 | ## 为什么单线程的JavaScript可以实现异步 148 | 149 | 单线程同步的情况下一些耗时较长的任务将会导致页面渲染阻塞甚至卡死,浏览器是多线程的,因此浏览器为这些耗时较长的任务比如HTTP,定时器以及事件监听等单独开一个线程,由于JavaScript单线程的特性,所有任务会形成一个任务队列等待被执行,**当主线程中没有执行任何同步任务时**,异步线程就会通过`回调函数`将执行完毕的异步任务放到JavaScript主线程的任务队列里 150 | 151 | ## EventLoop 152 | 153 | JavaScript主线程会一直循环查找任务队列里是否还有其他任务 154 | 155 | ```javascript 156 | function fn2(){ 157 | //dosomething... 158 | } 159 | function fn1() { 160 | fn2() 161 | // dosomething... 162 | } 163 | function foo(){ 164 | fn1(); 165 | } 166 | foo(); 167 | ``` 168 | 169 | 比如这段代码,函数`foo`在执行时,主线程发现它还需要执行函数`fn1`,就把它推入一个栈中,执行到fn1时又发现它需要先执行fn2,于是又将fn1推入栈中,等到fn2执行结束,继续执行fn1,fn1执行结束将继续执行foo,待所有函数执行完成,主线程会让这些函数出栈,此时说明一个任务已经执行完毕,主线程会从下一个任务队列(callback equeue)中寻找下一个任务推入栈中,这个过程就叫做`EventLoop` 170 | 171 | ## 用C语言实现一个事件循环 172 | 173 | > 扑街 174 | 175 | 176 | 177 | ## 以下代码输入结果是什么,为什么? 178 | 179 | ```javascript 180 | var a = { 181 | value: 'aaa', 182 | say: function(){ 183 | console.log(this.vlaue); 184 | } 185 | } 186 | 187 | var b = { 188 | value: 'bbb', 189 | say: function() { 190 | console.log(this.value); 191 | } 192 | } 193 | 194 | a.say = b.say; 195 | a.say(); // 输出结果是什么 196 | ``` 197 | 198 | 输出字符串`aaa`,因为函数中this的指向始终是运行时调用它的对象 199 | 200 | 201 | 202 | ## 多个异步请求,如何使所有请求结束后才执行下一步 203 | 204 | 将请求包装为一个Promise数组,使用Promise.all执行,之后调用.then即可 205 | 206 | 207 | 208 | ## React 209 | 210 | ### React生命周期函数有哪些 211 | 212 | * componentWillMount 213 | * componentDidMount 214 | * render 215 | * componentWillUpdate 216 | * componentWillReceiveProps 217 | * render 218 | * componentDidUpdate 219 | * componentUnMount 220 | 221 | ### React有几种组件 222 | 223 | * 有状态组件(包含生命周期方法,在组件渲染-更新-卸载阶段会自动执行) 224 | * 无状态组件(纯UI展示型组件,无法使用this以及生命周期方法) 225 | * 高阶组件(使用一个高阶函数包装,接受指定一个组件并进行修改后返回新的组件) 226 | 227 | ### 高阶组件具体使用场景是什么,解决了什么问题 228 | 229 | 高阶组件可以将多个组件中逻辑相同的部分抽离出来,由一个函数包装后形成一个新的组件,并且不影响原组件 230 | 231 | ```jsx 232 | class A extends PureComponent { 233 | componentWillMount() { 234 | console.log(this.props.name); 235 | } 236 | render(){ 237 | return
I am {this.props.name}
238 | } 239 | } 240 | 241 | class B extends PureComponent { 242 | componentWillMount() { 243 | console.log(this.props.name); 244 | } 245 | render(){ 246 | return
I am {this.props.name}
247 | } 248 | } 249 | 250 | function HighOrderLog(Component) { 251 | class Com extends PureComponent { 252 | 253 | componentWillMount() { 254 | console.log(this.props.name); 255 | } 256 | 257 | return 258 | } 259 | return Com; 260 | } 261 | ``` 262 | 263 | 264 | 265 | ### setState是同步还是异步, 为什么,调用后怎么拿到新的state 266 | 267 | 异步,因为每次调用setState之后,React内部不光要更新state,还要进行一系列比如diff算法来决定下一次render,setState多次调用会造成一定程度上性能的损耗,所以React会将多个setState先合并再执行,这样一来避免了不必要的性能损失 268 | 269 | 拿到新的state有两种方法 270 | 271 | * setState的第二个参数是一个回调函数,会在state更新后自动执行,这个函数里就可以拿到最新的state 272 | * 利用componentDidUpdate函数 273 | 274 | ### 父组件更新state后,子组件会不会rerender 275 | 276 | 分两种情况 277 | 278 | * 如果子组件没有用到父组件的state,则不会rerender 279 | * 如果用到 280 | * 使用PureComponent,且父组件更新的state不是子组件使用的,则子组件不会rerendr 281 | * 使用Component,不管子组件是否用到更新的state,都会触发rerender 282 | 283 | ### 为什么PureComponent不会引起子组件重渲染 284 | 285 | 因为PureComponent实现了`shouldComponentUpdate`方法,收到新的props后会做一次浅对比,既仅对比引用是否相同,shouldComponentUpdate方法返回布尔值,将决定组件是否进行重渲染 286 | 287 | ### 谈谈对Redux的理解 288 | 289 | Redux是单Store的思想,通过view->action->reducer->view的单向数据流管理页面状态 290 | 291 | * action只返回一个简单对象,包含一个type属性及执行动作所需的数据 292 | * reducer是一个纯函数,利用switch/case根据不同的action对state进行相应的修改并返回新的state 293 | * store是一个包含`getState`,`dispatch`,`subscribe`等方法的对象,它接受reducer作为参数 294 | * dispatch负责触发action,store内部会将当前state和触发的action传递给reducer函数,state被修改并返回 295 | * subscribe函数负责订阅一个更新后的回调函数并存放在store内部的listener中,当reducer执行完毕则执行这个回调函数 296 | 297 | ### Redux的State与React组件本身的State是否冲突 298 | 299 | 不冲突,业务层面上可以将React的state作为内部状态,既不依赖父组件或外部环境的组件可以使用state,而一些后端返回的数据,可能需要在多个组件中共享的,则可以作为全局的状态存放在redux的state中 300 | 301 | ### Redux的中间件机制如何实现的 302 | 303 | [applyMIddleware源码解读](https://github.com/SakuraAsh/blog/issues/3) 304 | 305 | 306 | 307 | # 算法 308 | 309 | ## 算法的时间复杂度是什么意思 310 | 311 | 简单来讲就是指一个算法解决相应问题其代码执行基本语句需要的次数 312 | 313 | > 算法中某个特定步骤的执行次数/对于总执行时间的估算成本,随着「问题规模」的增大时,增长的形式。 314 | 315 | 决定算法复杂度的两个重要因素是 316 | 317 | * 问题规模 318 | * 算法策略 319 | 320 | ## 实现冒泡排序,并说明其时间复杂度 321 | 322 | ```javascript 323 | function swap(arr, i, j) { 324 | const tmp = arr[i]; 325 | arr[i] = arr[j]; 326 | arr[j] = tmp; 327 | } 328 | const arrays = [21,454,6578,784534,443565,87978,4567]; 329 | function bubbleSort(arr) { 330 | const length = arr.length; 331 | for (let i = 0; i < length; i += 1) { 332 | for (let j = 0; j < length - 1; j += 1) { 333 | if (arr[j] > arr[j + 1]) { 334 | swap(arr, j, j + 1); 335 | } 336 | } 337 | } 338 | } 339 | 340 | bubbleSort(arrays); // [21, 454, 4567, 6578, 87978, 443565, 784534] 341 | ``` 342 | 343 | 冒泡排序的策略是依次循环比较两个相邻的项,如果第一个比第二个大,则交换他们的位置,较小的项会逐渐向上移动到正确的位置 344 | 345 | 由于无论数组是否已经排序,都会对每一项进行双重的循环遍历,所以冒泡排序的时间复杂度是O(n²) 346 | 347 | ## 实现插入排序,并说明其时间复杂度 348 | 349 | ```javascript 350 | const arrays = [21,454,6578,784534,443565,87978,4567]; 351 | function insertionSort(arr) { 352 | const length = arr.length; 353 | for (let i = 1; i < length; i += 1) { 354 | for (let j = i - 1; j >= 0; j -= 1) { 355 | if (arr[j] > arr[j + 1]) { 356 | swap(arr, j, j + 1); 357 | } 358 | } 359 | } 360 | } 361 | 362 | insertionSort(arrays); // [21, 454, 4567, 6578, 87978, 443565, 784534] 363 | ``` 364 | 365 | 插入排序的策略是,将数组分为两个部分, 假设原第一项是已经排好序的一个数组,那么用其余数组项依次与这个已经排好序的数组对比,小的交换位置,直到其余数组为空,排序结束 366 | 367 | 这段代码中,外循环从i = 1开始,即表示不论第一项多大,它被看做一个已经排好序的数组,从原数组第二项开始遍历,由于第一轮循环,有序数组只有一项,所以直接与第二项对比后交换位置,依次类推 368 | 369 | 插入排序是不稳定的排序,最好的情况下时间复杂度为O(n),最坏情况下为O(n²) 370 | 371 | ## 数组扁平化 372 | 373 | 将`[123, [2,32,445,[32,54,4]]]`转为`[123,2,32,445,32,54,4]` 374 | 375 | 使用isArray方法判断是否为数组,递归调用即可 376 | 377 | ```javascript 378 | const arrays = [123, [2, 32, 445, [32, 54, 4]]]; 379 | 380 | function flatten(arr) { 381 | return arr.reduce((pre, cur) => { 382 | return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); 383 | },[]) 384 | } 385 | flatten(arrays); // [123, 2, 32, 445, 32, 54, 4] 386 | ``` 387 | 388 | ## 翻转二叉树 389 | 390 | ```JavaScript 391 | function reversalBST(root) { 392 | if (root === null) return null; 393 | const temp = root.left; 394 | root.left = root.right; 395 | root.right = temp; 396 | temp(root.left); 397 | temp(root.right); 398 | } 399 | ``` 400 | 401 | -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/0752c7392981e2f65281b9cdb792dcf8.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./Node","loc":{"line":3,"column":21}},{"name":"./insertNode","loc":{"line":4,"column":27}},{"name":"./inOrderTraverseNode","loc":{"line":5,"column":36}},{"name":"./preOrderTraverseNode","loc":{"line":6,"column":37}},{"name":"./postOrderTraverseNode","loc":{"line":7,"column":38}}],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar Node_1 = require(\"./Node\");\nvar insertNode_1 = require(\"./insertNode\");\nvar inOrderTraverseNode_1 = require(\"./inOrderTraverseNode\");\nvar preOrderTraverseNode_1 = require(\"./preOrderTraverseNode\");\nvar postOrderTraverseNode_1 = require(\"./postOrderTraverseNode\");\nvar BinarySearchTree = /** @class */ (function () {\n function BinarySearchTree() {\n this.root = null;\n }\n BinarySearchTree.prototype.insert = function (key) {\n var node = new Node_1[\"default\"](key);\n if (this.root === null) {\n this.root = node;\n }\n else {\n insertNode_1[\"default\"](this.root, node);\n }\n };\n BinarySearchTree.prototype.inOrderTraverse = function (callback) {\n inOrderTraverseNode_1[\"default\"](this.root, callback);\n };\n BinarySearchTree.prototype.preOrderTraverse = function (callback) {\n preOrderTraverseNode_1[\"default\"](this.root, callback);\n };\n BinarySearchTree.prototype.postOrderTraverse = function (callback) {\n postOrderTraverseNode_1[\"default\"](this.root, callback);\n };\n return BinarySearchTree;\n}());\nvar tree = new BinarySearchTree();\ntree.insert(10);\ntree.insert(23);\ntree.insert(27);\ntree.insert(1);\ntree.insert(7);\ntree.insert(231);\nconsole.log('中序遍历');\ntree.inOrderTraverse(function (key) {\n console.log(key);\n});\nconsole.log('先序遍历');\ntree.preOrderTraverse(function (key) {\n console.log(key);\n});\nconsole.log('后序遍历');\ntree.postOrderTraverse(function (key) {\n console.log(key);\n});\n// console.log(tree);\n"},"hash":"be9a2573a8877e56c424af89f077bc12"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/1138c81668eebadec234d5b337ea23cc.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nfunction inOrderTraverse(node, callback) {\n if (node !== null) {\n inOrderTraverse(node.left, callback);\n callback(node.key);\n inOrderTraverse(node.right, callback);\n }\n}\nexports[\"default\"] = inOrderTraverse;\n"},"hash":"404b9b516de963d4130a58530a879c1f"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/71df19c220b62a8761ad8e68b602611f.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./index.ts","dynamic":true}],"generated":{"html":"\n\n\n \n \n \n BinarySearchTree\n\n\n \n\n\n"},"hash":"8d6c85c8c167954e8e1e86a598b7f8d7"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/7d1ce5460eb03f9742f8b5b0436ce881.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nfunction postOrderTraverseNode(node, callback) {\n if (node !== null) {\n postOrderTraverseNode(node.left, callback);\n postOrderTraverseNode(node.right, callback);\n callback(node.key);\n }\n}\nexports[\"default\"] = postOrderTraverseNode;\n"},"hash":"fa86676198e8fc90f748342cb053fbd9"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/acbebe7ba772c1f2ab6e7f4649a117fc.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar Node = /** @class */ (function () {\n function Node(key, left, right) {\n if (left === void 0) { left = null; }\n if (right === void 0) { right = null; }\n this.key = key;\n this.left = left;\n this.right = right;\n }\n return Node;\n}());\nexports[\"default\"] = Node;\n"},"hash":"9cbb6cfa4e70b6843c0895e45d216ec6"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/d19b650e0f36f4fb8a50a5d0bbaa4a87.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nfunction insertNode(node, newNode) {\n if (newNode.key < node.key) {\n if (node.left === null) {\n node.left = newNode;\n }\n else {\n insertNode(node.left, newNode);\n }\n }\n else {\n if (node.right === null) {\n node.right = newNode;\n }\n else {\n insertNode(node.right, newNode);\n }\n }\n}\nexports[\"default\"] = insertNode;\n"},"hash":"a2354e1bd78e60938a17412e6572647d"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/eb3b882b0eea59dfd1b2c991a709ffa9.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nfunction inOrderTraverseNode(node, callback) {\n if (node !== null) {\n inOrderTraverseNode(node.left, callback);\n callback(node.key);\n inOrderTraverseNode(node.right, callback);\n }\n}\nexports[\"default\"] = inOrderTraverseNode;\n"},"hash":"5cf845e8a26887629496467a2364cba3"} -------------------------------------------------------------------------------- /src/BinarySearchTree/.cache/f605b6c2309f88802bc2f2e70f35957a.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nfunction preOrderTraverseNode(node, callback) {\n if (node !== null) {\n callback(node.key);\n preOrderTraverseNode(node.left, callback);\n preOrderTraverseNode(node.right, callback);\n }\n}\nexports[\"default\"] = preOrderTraverseNode;\n"},"hash":"d01bfbdf5bef09ddeab432c85ef74b68"} -------------------------------------------------------------------------------- /src/BinarySearchTree/Node.ts: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(public key: any, public left: Node = null, public right: Node = null) {} 3 | } 4 | 5 | export default Node; 6 | -------------------------------------------------------------------------------- /src/BinarySearchTree/dist/a9e8758779a7f02c0273e555b3d60a05.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | 9 | require = (function (modules, cache, entry) { 10 | // Save the require from previous bundle to this closure if any 11 | var previousRequire = typeof require === "function" && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof require === "function" && require; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | var err = new Error('Cannot find module \'' + name + '\''); 33 | err.code = 'MODULE_NOT_FOUND'; 34 | throw err; 35 | } 36 | 37 | localRequire.resolve = resolve; 38 | 39 | var module = cache[name] = new newRequire.Module; 40 | 41 | modules[name][0].call(module.exports, localRequire, module, module.exports); 42 | } 43 | 44 | return cache[name].exports; 45 | 46 | function localRequire(x){ 47 | return newRequire(localRequire.resolve(x)); 48 | } 49 | 50 | function resolve(x){ 51 | return modules[name][1][x] || x; 52 | } 53 | } 54 | 55 | function Module() { 56 | this.bundle = newRequire; 57 | this.exports = {}; 58 | } 59 | 60 | newRequire.Module = Module; 61 | newRequire.modules = modules; 62 | newRequire.cache = cache; 63 | newRequire.parent = previousRequire; 64 | 65 | for (var i = 0; i < entry.length; i++) { 66 | newRequire(entry[i]); 67 | } 68 | 69 | // Override the current require with this new one 70 | return newRequire; 71 | })({4:[function(require,module,exports) { 72 | "use strict"; 73 | exports.__esModule = true; 74 | var Node = /** @class */ (function () { 75 | function Node(key, left, right) { 76 | if (left === void 0) { left = null; } 77 | if (right === void 0) { right = null; } 78 | this.key = key; 79 | this.left = left; 80 | this.right = right; 81 | } 82 | return Node; 83 | }()); 84 | exports["default"] = Node; 85 | 86 | },{}],5:[function(require,module,exports) { 87 | "use strict"; 88 | exports.__esModule = true; 89 | function insertNode(node, newNode) { 90 | if (newNode.key < node.key) { 91 | if (node.left === null) { 92 | node.left = newNode; 93 | } 94 | else { 95 | insertNode(node.left, newNode); 96 | } 97 | } 98 | else { 99 | if (node.right === null) { 100 | node.right = newNode; 101 | } 102 | else { 103 | insertNode(node.right, newNode); 104 | } 105 | } 106 | } 107 | exports["default"] = insertNode; 108 | 109 | },{}],6:[function(require,module,exports) { 110 | "use strict"; 111 | exports.__esModule = true; 112 | function inOrderTraverseNode(node, callback) { 113 | if (node !== null) { 114 | inOrderTraverseNode(node.left, callback); 115 | callback(node.key); 116 | inOrderTraverseNode(node.right, callback); 117 | } 118 | } 119 | exports["default"] = inOrderTraverseNode; 120 | 121 | },{}],7:[function(require,module,exports) { 122 | "use strict"; 123 | exports.__esModule = true; 124 | function preOrderTraverseNode(node, callback) { 125 | if (node !== null) { 126 | callback(node.key); 127 | preOrderTraverseNode(node.left, callback); 128 | preOrderTraverseNode(node.right, callback); 129 | } 130 | } 131 | exports["default"] = preOrderTraverseNode; 132 | 133 | },{}],8:[function(require,module,exports) { 134 | "use strict"; 135 | exports.__esModule = true; 136 | function postOrderTraverseNode(node, callback) { 137 | if (node !== null) { 138 | postOrderTraverseNode(node.left, callback); 139 | postOrderTraverseNode(node.right, callback); 140 | callback(node.key); 141 | } 142 | } 143 | exports["default"] = postOrderTraverseNode; 144 | 145 | },{}],2:[function(require,module,exports) { 146 | "use strict"; 147 | exports.__esModule = true; 148 | var Node_1 = require("./Node"); 149 | var insertNode_1 = require("./insertNode"); 150 | var inOrderTraverseNode_1 = require("./inOrderTraverseNode"); 151 | var preOrderTraverseNode_1 = require("./preOrderTraverseNode"); 152 | var postOrderTraverseNode_1 = require("./postOrderTraverseNode"); 153 | var BinarySearchTree = /** @class */ (function () { 154 | function BinarySearchTree() { 155 | this.root = null; 156 | } 157 | BinarySearchTree.prototype.insert = function (key) { 158 | var node = new Node_1["default"](key); 159 | if (this.root === null) { 160 | this.root = node; 161 | } 162 | else { 163 | insertNode_1["default"](this.root, node); 164 | } 165 | }; 166 | BinarySearchTree.prototype.inOrderTraverse = function (callback) { 167 | inOrderTraverseNode_1["default"](this.root, callback); 168 | }; 169 | BinarySearchTree.prototype.preOrderTraverse = function (callback) { 170 | preOrderTraverseNode_1["default"](this.root, callback); 171 | }; 172 | BinarySearchTree.prototype.postOrderTraverse = function (callback) { 173 | postOrderTraverseNode_1["default"](this.root, callback); 174 | }; 175 | return BinarySearchTree; 176 | }()); 177 | var tree = new BinarySearchTree(); 178 | tree.insert(10); 179 | tree.insert(23); 180 | tree.insert(27); 181 | tree.insert(1); 182 | tree.insert(7); 183 | tree.insert(231); 184 | console.log('中序遍历'); 185 | tree.inOrderTraverse(function (key) { 186 | console.log(key); 187 | }); 188 | console.log('先序遍历'); 189 | tree.preOrderTraverse(function (key) { 190 | console.log(key); 191 | }); 192 | console.log('后序遍历'); 193 | tree.postOrderTraverse(function (key) { 194 | console.log(key); 195 | }); 196 | // console.log(tree); 197 | 198 | },{"./Node":4,"./insertNode":5,"./inOrderTraverseNode":6,"./preOrderTraverseNode":7,"./postOrderTraverseNode":8}],0:[function(require,module,exports) { 199 | var global = (1, eval)('this'); 200 | var OldModule = module.bundle.Module; 201 | function Module() { 202 | OldModule.call(this); 203 | this.hot = { 204 | accept: function (fn) { 205 | this._acceptCallback = fn || function () {}; 206 | }, 207 | dispose: function (fn) { 208 | this._disposeCallback = fn; 209 | } 210 | }; 211 | } 212 | 213 | module.bundle.Module = Module; 214 | 215 | if (!module.bundle.parent && typeof WebSocket !== 'undefined') { 216 | var ws = new WebSocket('ws://localhost:60216/'); 217 | ws.onmessage = function(event) { 218 | var data = JSON.parse(event.data); 219 | 220 | if (data.type === 'update') { 221 | data.assets.forEach(function (asset) { 222 | hmrApply(global.require, asset); 223 | }); 224 | 225 | data.assets.forEach(function (asset) { 226 | if (!asset.isNew) { 227 | hmrAccept(global.require, asset.id); 228 | } 229 | }); 230 | } 231 | 232 | if (data.type === 'reload') { 233 | ws.close(); 234 | ws.onclose = function () { 235 | window.location.reload(); 236 | } 237 | } 238 | 239 | if (data.type === 'error-resolved') { 240 | console.log('[parcel] ✨ Error resolved'); 241 | } 242 | 243 | if (data.type === 'error') { 244 | console.error('[parcel] 🚨 ' + data.error.message + '\n' + 'data.error.stack'); 245 | } 246 | }; 247 | } 248 | 249 | function getParents(bundle, id) { 250 | var modules = bundle.modules; 251 | if (!modules) { 252 | return []; 253 | } 254 | 255 | var parents = []; 256 | var k, d, dep; 257 | 258 | for (k in modules) { 259 | for (d in modules[k][1]) { 260 | dep = modules[k][1][d]; 261 | if (dep === id || (Array.isArray(dep) && dep[dep.length - 1] === id)) { 262 | parents.push(+k); 263 | } 264 | } 265 | } 266 | 267 | if (bundle.parent) { 268 | parents = parents.concat(getParents(bundle.parent, id)); 269 | } 270 | 271 | return parents; 272 | } 273 | 274 | function hmrApply(bundle, asset) { 275 | var modules = bundle.modules; 276 | if (!modules) { 277 | return; 278 | } 279 | 280 | if (modules[asset.id] || !bundle.parent) { 281 | var fn = new Function('require', 'module', 'exports', asset.generated.js); 282 | asset.isNew = !modules[asset.id]; 283 | modules[asset.id] = [fn, asset.deps]; 284 | } else if (bundle.parent) { 285 | hmrApply(bundle.parent, asset); 286 | } 287 | } 288 | 289 | function hmrAccept(bundle, id) { 290 | var modules = bundle.modules; 291 | if (!modules) { 292 | return; 293 | } 294 | 295 | if (!modules[id] && bundle.parent) { 296 | return hmrAccept(bundle.parent, id); 297 | } 298 | 299 | var cached = bundle.cache[id]; 300 | if (cached && cached.hot._disposeCallback) { 301 | cached.hot._disposeCallback(); 302 | } 303 | 304 | delete bundle.cache[id]; 305 | bundle(id); 306 | 307 | cached = bundle.cache[id]; 308 | if (cached && cached.hot && cached.hot._acceptCallback) { 309 | cached.hot._acceptCallback(); 310 | return true; 311 | } 312 | 313 | return getParents(global.require, id).some(function (id) { 314 | return hmrAccept(global.require, id) 315 | }); 316 | } 317 | },{}]},{},[0,2]) -------------------------------------------------------------------------------- /src/BinarySearchTree/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BinarySearchTree 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/BinarySearchTree/inOrderTraverseNode.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | function inOrderTraverseNode(node: Node, callback: Function) { 4 | if (node !== null) { 5 | inOrderTraverseNode(node.left, callback); 6 | callback(node.key); 7 | inOrderTraverseNode(node.right, callback); 8 | } 9 | } 10 | 11 | export default inOrderTraverseNode; 12 | -------------------------------------------------------------------------------- /src/BinarySearchTree/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BinarySearchTree 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/BinarySearchTree/index.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | import insertNode from './insertNode'; 3 | import inOrderTraverseNode from './inOrderTraverseNode'; 4 | import preOrderTraverseNode from './preOrderTraverseNode'; 5 | import postOrderTraverseNode from './postOrderTraverseNode'; 6 | 7 | class BinarySearchTree { 8 | root: Node; 9 | constructor() { 10 | this.root = null; 11 | } 12 | 13 | public insert(key: any) { 14 | const node = new Node(key); 15 | if (this.root === null) { 16 | this.root = node; 17 | } else { 18 | insertNode(this.root, node); 19 | } 20 | } 21 | 22 | public inOrderTraverse(callback: Function) { 23 | inOrderTraverseNode(this.root, callback); 24 | } 25 | 26 | public preOrderTraverse(callback: Function) { 27 | preOrderTraverseNode(this.root, callback); 28 | } 29 | 30 | public postOrderTraverse(callback: Function) { 31 | postOrderTraverseNode(this.root, callback); 32 | } 33 | } 34 | 35 | const tree = new BinarySearchTree(); 36 | 37 | tree.insert(10); 38 | tree.insert(23); 39 | tree.insert(27); 40 | tree.insert(1); 41 | tree.insert(7); 42 | tree.insert(231); 43 | console.log('中序遍历'); 44 | tree.inOrderTraverse((key: any) => { 45 | console.log(key); 46 | }); 47 | console.log('先序遍历'); 48 | tree.preOrderTraverse((key: any) => { 49 | console.log(key); 50 | }); 51 | console.log('后序遍历'); 52 | tree.postOrderTraverse((key: any) => { 53 | console.log(key); 54 | }); 55 | // console.log(tree); 56 | -------------------------------------------------------------------------------- /src/BinarySearchTree/insertNode.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | function insertNode(node: Node, newNode: Node) { 4 | if (newNode.key < node.key) { 5 | if (node.left === null) { 6 | node.left = newNode; 7 | } else { 8 | insertNode(node.left, newNode); 9 | } 10 | } else { 11 | if (node.right === null) { 12 | node.right = newNode; 13 | } else { 14 | insertNode(node.right, newNode); 15 | } 16 | } 17 | } 18 | 19 | export default insertNode; 20 | -------------------------------------------------------------------------------- /src/BinarySearchTree/postOrderTraverseNode.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | 4 | function postOrderTraverseNode(node: Node, callback: Function) { 5 | if (node !== null) { 6 | postOrderTraverseNode(node.left, callback); 7 | postOrderTraverseNode(node.right, callback); 8 | callback(node.key); 9 | } 10 | } 11 | 12 | export default postOrderTraverseNode; 13 | -------------------------------------------------------------------------------- /src/BinarySearchTree/preOrderTraverseNode.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | function preOrderTraverseNode(node: Node, callback: Function) { 4 | if (node !== null) { 5 | callback(node.key); 6 | preOrderTraverseNode(node.left, callback); 7 | preOrderTraverseNode(node.right, callback); 8 | } 9 | } 10 | 11 | export default preOrderTraverseNode; 12 | -------------------------------------------------------------------------------- /src/DoublyLinkedList/.cache/080dc902d6fa4ae70f724f2417e1d094.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./DoublyNode","loc":{"line":3,"column":27}}],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar DoublyNode_1 = require(\"./DoublyNode\");\nvar DoublyLinkedList = /** @class */ (function () {\n function DoublyLinkedList() {\n this.length = 0;\n this.head = null;\n this.tail = null;\n }\n DoublyLinkedList.prototype.insert = function (position, element) {\n if (position >= 0 && position <= this.length) {\n var node = new DoublyNode_1[\"default\"](element);\n var current = this.head;\n var previous = void 0;\n var index = 0;\n if (position === 0) {\n if (!this.head) {\n this.head = node;\n this.tail = node;\n }\n else {\n node.next = current;\n current.prev = node;\n this.head = node;\n }\n }\n else if (position === this.length) {\n current = this.tail;\n current.next = node;\n node.prev = current;\n this.tail = node;\n }\n else {\n while (index++ < position) {\n previous = current;\n current = current.next;\n }\n node.next = current;\n previous.next = node;\n debugger;\n current.prev = node;\n node.prev = previous;\n }\n this.length += 1;\n return true;\n }\n else {\n return false;\n }\n };\n return DoublyLinkedList;\n}());\nvar doublyLinkedList = new DoublyLinkedList();\ndoublyLinkedList.insert(0, { name: 'sakura' });\nconsole.log(doublyLinkedList);\ndoublyLinkedList.insert(1, { name: 'misaka' });\nconsole.log(doublyLinkedList);\n"},"hash":"7e23f7706a8e583208136554807e2f09"} -------------------------------------------------------------------------------- /src/DoublyLinkedList/.cache/0bc1c7ed26503281c778a01b0cef91e5.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar DoublyNode = /** @class */ (function () {\n function DoublyNode(element, next, prev) {\n this.element = element;\n this.next = next;\n this.prev = prev;\n }\n return DoublyNode;\n}());\nexports[\"default\"] = DoublyNode;\n"},"hash":"4f24fcb5a1f36f56f2b7215b8f53386c"} -------------------------------------------------------------------------------- /src/DoublyLinkedList/.cache/158a03e5a2e4585ff7fdc0a23bb30a11.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./index.ts","dynamic":true}],"generated":{"html":"\n\n\n \n \n \n DoublyLinkedList\n\n\n \n\n"},"hash":"60016f1e642f053195a72a6b7ab435a8"} -------------------------------------------------------------------------------- /src/DoublyLinkedList/DoublyNode.ts: -------------------------------------------------------------------------------- 1 | class DoublyNode { 2 | constructor(public element: any, public next?: any, public prev?: any) {} 3 | } 4 | 5 | export default DoublyNode; 6 | -------------------------------------------------------------------------------- /src/DoublyLinkedList/dist/42e86c00ec971c65acc323f3f2982061.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | 9 | require = (function (modules, cache, entry) { 10 | // Save the require from previous bundle to this closure if any 11 | var previousRequire = typeof require === "function" && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof require === "function" && require; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | var err = new Error('Cannot find module \'' + name + '\''); 33 | err.code = 'MODULE_NOT_FOUND'; 34 | throw err; 35 | } 36 | 37 | localRequire.resolve = resolve; 38 | 39 | var module = cache[name] = new newRequire.Module; 40 | 41 | modules[name][0].call(module.exports, localRequire, module, module.exports); 42 | } 43 | 44 | return cache[name].exports; 45 | 46 | function localRequire(x){ 47 | return newRequire(localRequire.resolve(x)); 48 | } 49 | 50 | function resolve(x){ 51 | return modules[name][1][x] || x; 52 | } 53 | } 54 | 55 | function Module() { 56 | this.bundle = newRequire; 57 | this.exports = {}; 58 | } 59 | 60 | newRequire.Module = Module; 61 | newRequire.modules = modules; 62 | newRequire.cache = cache; 63 | newRequire.parent = previousRequire; 64 | 65 | for (var i = 0; i < entry.length; i++) { 66 | newRequire(entry[i]); 67 | } 68 | 69 | // Override the current require with this new one 70 | return newRequire; 71 | })({5:[function(require,module,exports) { 72 | "use strict"; 73 | exports.__esModule = true; 74 | var DoublyNode = /** @class */ (function () { 75 | function DoublyNode(element, next, prev) { 76 | this.element = element; 77 | this.next = next; 78 | this.prev = prev; 79 | } 80 | return DoublyNode; 81 | }()); 82 | exports["default"] = DoublyNode; 83 | 84 | },{}],4:[function(require,module,exports) { 85 | "use strict"; 86 | exports.__esModule = true; 87 | var DoublyNode_1 = require("./DoublyNode"); 88 | var DoublyLinkedList = /** @class */ (function () { 89 | function DoublyLinkedList() { 90 | this.length = 0; 91 | this.head = null; 92 | this.tail = null; 93 | } 94 | DoublyLinkedList.prototype.insert = function (position, element) { 95 | if (position >= 0 && position <= this.length) { 96 | var node = new DoublyNode_1["default"](element); 97 | var current = this.head; 98 | var previous = void 0; 99 | var index = 0; 100 | if (position === 0) { 101 | if (!this.head) { 102 | this.head = node; 103 | this.tail = node; 104 | } 105 | else { 106 | node.next = current; 107 | current.prev = node; 108 | this.head = node; 109 | } 110 | } 111 | else if (position === this.length) { 112 | current = this.tail; 113 | current.next = node; 114 | node.prev = current; 115 | this.tail = node; 116 | } 117 | else { 118 | while (index++ < position) { 119 | previous = current; 120 | current = current.next; 121 | } 122 | node.next = current; 123 | previous.next = node; 124 | debugger; 125 | current.prev = node; 126 | node.prev = previous; 127 | } 128 | this.length += 1; 129 | return true; 130 | } 131 | else { 132 | return false; 133 | } 134 | }; 135 | return DoublyLinkedList; 136 | }()); 137 | var doublyLinkedList = new DoublyLinkedList(); 138 | doublyLinkedList.insert(0, { name: 'sakura' }); 139 | console.log(doublyLinkedList); 140 | doublyLinkedList.insert(1, { name: 'misaka' }); 141 | console.log(doublyLinkedList); 142 | 143 | },{"./DoublyNode":5}],0:[function(require,module,exports) { 144 | var global = (1, eval)('this'); 145 | var OldModule = module.bundle.Module; 146 | function Module() { 147 | OldModule.call(this); 148 | this.hot = { 149 | accept: function (fn) { 150 | this._acceptCallback = fn || function () {}; 151 | }, 152 | dispose: function (fn) { 153 | this._disposeCallback = fn; 154 | } 155 | }; 156 | } 157 | 158 | module.bundle.Module = Module; 159 | 160 | if (!module.bundle.parent && typeof WebSocket !== 'undefined') { 161 | var ws = new WebSocket('ws://localhost:55300/'); 162 | ws.onmessage = function(event) { 163 | var data = JSON.parse(event.data); 164 | 165 | if (data.type === 'update') { 166 | data.assets.forEach(function (asset) { 167 | hmrApply(global.require, asset); 168 | }); 169 | 170 | data.assets.forEach(function (asset) { 171 | if (!asset.isNew) { 172 | hmrAccept(global.require, asset.id); 173 | } 174 | }); 175 | } 176 | 177 | if (data.type === 'reload') { 178 | ws.close(); 179 | ws.onclose = function () { 180 | window.location.reload(); 181 | } 182 | } 183 | 184 | if (data.type === 'error-resolved') { 185 | console.log('[parcel] ✨ Error resolved'); 186 | } 187 | 188 | if (data.type === 'error') { 189 | console.error('[parcel] 🚨 ' + data.error.message + '\n' + 'data.error.stack'); 190 | } 191 | }; 192 | } 193 | 194 | function getParents(bundle, id) { 195 | var modules = bundle.modules; 196 | if (!modules) { 197 | return []; 198 | } 199 | 200 | var parents = []; 201 | var k, d, dep; 202 | 203 | for (k in modules) { 204 | for (d in modules[k][1]) { 205 | dep = modules[k][1][d]; 206 | if (dep === id || (Array.isArray(dep) && dep[dep.length - 1] === id)) { 207 | parents.push(+k); 208 | } 209 | } 210 | } 211 | 212 | if (bundle.parent) { 213 | parents = parents.concat(getParents(bundle.parent, id)); 214 | } 215 | 216 | return parents; 217 | } 218 | 219 | function hmrApply(bundle, asset) { 220 | var modules = bundle.modules; 221 | if (!modules) { 222 | return; 223 | } 224 | 225 | if (modules[asset.id] || !bundle.parent) { 226 | var fn = new Function('require', 'module', 'exports', asset.generated.js); 227 | asset.isNew = !modules[asset.id]; 228 | modules[asset.id] = [fn, asset.deps]; 229 | } else if (bundle.parent) { 230 | hmrApply(bundle.parent, asset); 231 | } 232 | } 233 | 234 | function hmrAccept(bundle, id) { 235 | var modules = bundle.modules; 236 | if (!modules) { 237 | return; 238 | } 239 | 240 | if (!modules[id] && bundle.parent) { 241 | return hmrAccept(bundle.parent, id); 242 | } 243 | 244 | var cached = bundle.cache[id]; 245 | if (cached && cached.hot._disposeCallback) { 246 | cached.hot._disposeCallback(); 247 | } 248 | 249 | delete bundle.cache[id]; 250 | bundle(id); 251 | 252 | cached = bundle.cache[id]; 253 | if (cached && cached.hot && cached.hot._acceptCallback) { 254 | cached.hot._acceptCallback(); 255 | return true; 256 | } 257 | 258 | return getParents(global.require, id).some(function (id) { 259 | return hmrAccept(global.require, id) 260 | }); 261 | } 262 | },{}]},{},[0,4]) -------------------------------------------------------------------------------- /src/DoublyLinkedList/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DoublyLinkedList 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/DoublyLinkedList/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DoublyLinkedList 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/DoublyLinkedList/index.ts: -------------------------------------------------------------------------------- 1 | import DoublyNode from './DoublyNode'; 2 | 3 | interface Element { 4 | name: string; 5 | } 6 | 7 | class DoublyLinkedList { 8 | length: number; 9 | head: DoublyNode; 10 | tail: DoublyNode; 11 | constructor() { 12 | this.length = 0; 13 | this.head = null; 14 | this.tail = null; 15 | } 16 | 17 | public insert(position: number, element: Element): boolean { 18 | if (position >= 0 && position <= this.length) { 19 | const node = new DoublyNode(element); 20 | let current = this.head; 21 | let previous; 22 | let index = 0; 23 | 24 | if (position === 0) { 25 | if (!this.head) { 26 | this.head = node; 27 | this.tail = node; 28 | } else { 29 | node.next = current; 30 | current.prev = node; 31 | this.head = node; 32 | } 33 | } else if (position === this.length) { 34 | current = this.tail; 35 | current.next = node; 36 | node.prev = current; 37 | this.tail = node; 38 | } else { 39 | while (index++ < position) { 40 | previous = current; 41 | current = current.next; 42 | } 43 | node.next = current; 44 | previous.next = node; 45 | debugger; 46 | current.prev = node; 47 | node.prev = previous; 48 | } 49 | this.length += 1; 50 | return true; 51 | } else { 52 | return false; 53 | } 54 | } 55 | } 56 | 57 | const doublyLinkedList = new DoublyLinkedList(); 58 | doublyLinkedList.insert(0, {name: 'sakura'}); 59 | console.log(doublyLinkedList); 60 | doublyLinkedList.insert(1, {name: 'misaka'}); 61 | console.log(doublyLinkedList); 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/LinkedList/.cache/394ef263b0cffd3f1cd0e5de1214c20d.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./index.ts","dynamic":true}],"generated":{"html":"\n\n\n \n \n \n Document\n\n\n \n\n"},"hash":"5b0eac7992fd5c1ba43b966250c96b25"} -------------------------------------------------------------------------------- /src/LinkedList/.cache/68e76d4a9b84942d8e8546b5e904aaec.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar Node = /** @class */ (function () {\n function Node(element, next) {\n this.element = element;\n this.next = next;\n }\n return Node;\n}());\nexports[\"default\"] = Node;\n"},"hash":"d51ea35f0b9d1737df95aad5e68a740e"} -------------------------------------------------------------------------------- /src/LinkedList/.cache/e09955fecb90623ab1878c09b54cf4a2.json: -------------------------------------------------------------------------------- 1 | {"dependencies":[{"name":"./Node","loc":{"line":3,"column":21}}],"generated":{"js":"\"use strict\";\nexports.__esModule = true;\nvar Node_1 = require(\"./Node\");\nvar LinkedList = /** @class */ (function () {\n function LinkedList() {\n this.length = 0;\n this.head = null;\n }\n LinkedList.prototype.append = function (element) {\n var node = new Node_1[\"default\"](element);\n var current;\n // 列表为空时添加为第一个元素\n if (this.head === null) {\n this.head = node;\n }\n else {\n current = this.head;\n while (current.next) {\n current = current.next;\n }\n current.next = node;\n }\n this.length += 1;\n };\n LinkedList.prototype.insert = function (position, element) {\n if (position >= 0 && position <= this.length) {\n var node = new Node_1[\"default\"](element);\n var current = this.head;\n var previous = void 0;\n var index = 0;\n // 当position为0 则在第一个位置添加新元素\n if (position === 0) {\n node.next = current;\n this.head = node;\n }\n else {\n while (index++ < position) {\n previous = current;\n current = current.next;\n }\n node.next = current;\n previous.next = node;\n }\n this.length += 1;\n return true;\n }\n else {\n return false;\n }\n };\n LinkedList.prototype.removeAt = function (position) {\n if (position > -1 && position < this.length) {\n var current = this.head;\n var previous = void 0;\n var index = 0;\n // 移除第一项\n if (position === 0) {\n this.head = current.next;\n }\n else {\n /**\n * 移除列表最后一项或中间某一项时, 需要依靠一个细节来迭代列表,直到到达目标位置\n * 使用一个内部递增的index变量, current变量为所循环列表的当前元素进行引用\n */\n while (index++ < position) {\n previous = current;\n current = current.next;\n }\n previous.next = current.next;\n }\n this.length -= 1;\n return current.element;\n }\n else {\n return null;\n }\n };\n LinkedList.prototype.toString = function () {\n var current = this.head;\n var string = '';\n while (current) {\n string += current.element.name + (current.next ? '\\n' : '');\n current = current.next;\n }\n return string;\n };\n LinkedList.prototype.indexOf = function (element) {\n var current = this.head;\n var index = -1;\n while (current) {\n if (element === current.element) {\n return index;\n }\n index += 1;\n current = current.next;\n }\n return -1;\n };\n LinkedList.prototype.remove = function (element) {\n var index = this.indexOf(element);\n return this.removeAt(index);\n };\n LinkedList.prototype.isEmpty = function () {\n return this.length === 0;\n };\n LinkedList.prototype.size = function () {\n return this.length;\n };\n LinkedList.prototype.getHead = function () {\n return this.head;\n };\n return LinkedList;\n}());\nvar linkedList = new LinkedList();\nlinkedList.append({ name: 'sakura' });\nlinkedList.append({ name: 'misaka' });\nlinkedList.append({ name: 'mikoto' });\nlinkedList.append({ name: 'yahaha' });\nconsole.log(linkedList);\n// const data = linkedList.removeAt(0);\nlinkedList.insert(2, { name: 'javascript' });\nconsole.log(linkedList);\nconsole.log(linkedList.toString());\n"},"hash":"b780066d8fb53b9c991fe055be3e8ce8"} -------------------------------------------------------------------------------- /src/LinkedList/Node.ts: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(public element: Item, public next?: any) {} 3 | } 4 | 5 | export default Node; 6 | -------------------------------------------------------------------------------- /src/LinkedList/dist/339c3ca8229ad11106d1f8fd0b86f537.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | 9 | require = (function (modules, cache, entry) { 10 | // Save the require from previous bundle to this closure if any 11 | var previousRequire = typeof require === "function" && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof require === "function" && require; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | var err = new Error('Cannot find module \'' + name + '\''); 33 | err.code = 'MODULE_NOT_FOUND'; 34 | throw err; 35 | } 36 | 37 | localRequire.resolve = resolve; 38 | 39 | var module = cache[name] = new newRequire.Module; 40 | 41 | modules[name][0].call(module.exports, localRequire, module, module.exports); 42 | } 43 | 44 | return cache[name].exports; 45 | 46 | function localRequire(x){ 47 | return newRequire(localRequire.resolve(x)); 48 | } 49 | 50 | function resolve(x){ 51 | return modules[name][1][x] || x; 52 | } 53 | } 54 | 55 | function Module() { 56 | this.bundle = newRequire; 57 | this.exports = {}; 58 | } 59 | 60 | newRequire.Module = Module; 61 | newRequire.modules = modules; 62 | newRequire.cache = cache; 63 | newRequire.parent = previousRequire; 64 | 65 | for (var i = 0; i < entry.length; i++) { 66 | newRequire(entry[i]); 67 | } 68 | 69 | // Override the current require with this new one 70 | return newRequire; 71 | })({5:[function(require,module,exports) { 72 | "use strict"; 73 | exports.__esModule = true; 74 | var Node = /** @class */ (function () { 75 | function Node(element, next) { 76 | this.element = element; 77 | this.next = next; 78 | } 79 | return Node; 80 | }()); 81 | exports["default"] = Node; 82 | 83 | },{}],4:[function(require,module,exports) { 84 | "use strict"; 85 | exports.__esModule = true; 86 | var Node_1 = require("./Node"); 87 | var LinkedList = /** @class */ (function () { 88 | function LinkedList() { 89 | this.length = 0; 90 | this.head = null; 91 | } 92 | LinkedList.prototype.append = function (element) { 93 | var node = new Node_1["default"](element); 94 | var current; 95 | // 列表为空时添加为第一个元素 96 | if (this.head === null) { 97 | this.head = node; 98 | } 99 | else { 100 | current = this.head; 101 | while (current.next) { 102 | current = current.next; 103 | } 104 | current.next = node; 105 | } 106 | this.length += 1; 107 | }; 108 | LinkedList.prototype.insert = function (position, element) { 109 | if (position >= 0 && position <= this.length) { 110 | var node = new Node_1["default"](element); 111 | var current = this.head; 112 | var previous = void 0; 113 | var index = 0; 114 | // 当position为0 则在第一个位置添加新元素 115 | if (position === 0) { 116 | node.next = current; 117 | this.head = node; 118 | } 119 | else { 120 | while (index++ < position) { 121 | previous = current; 122 | current = current.next; 123 | } 124 | node.next = current; 125 | previous.next = node; 126 | } 127 | this.length += 1; 128 | return true; 129 | } 130 | else { 131 | return false; 132 | } 133 | }; 134 | LinkedList.prototype.removeAt = function (position) { 135 | if (position > -1 && position < this.length) { 136 | var current = this.head; 137 | var previous = void 0; 138 | var index = 0; 139 | // 移除第一项 140 | if (position === 0) { 141 | this.head = current.next; 142 | } 143 | else { 144 | /** 145 | * 移除列表最后一项或中间某一项时, 需要依靠一个细节来迭代列表,直到到达目标位置 146 | * 使用一个内部递增的index变量, current变量为所循环列表的当前元素进行引用 147 | */ 148 | while (index++ < position) { 149 | previous = current; 150 | current = current.next; 151 | } 152 | previous.next = current.next; 153 | } 154 | this.length -= 1; 155 | return current.element; 156 | } 157 | else { 158 | return null; 159 | } 160 | }; 161 | LinkedList.prototype.toString = function () { 162 | var current = this.head; 163 | var string = ''; 164 | while (current) { 165 | string += current.element.name + (current.next ? '\n' : ''); 166 | current = current.next; 167 | } 168 | return string; 169 | }; 170 | LinkedList.prototype.indexOf = function (element) { 171 | var current = this.head; 172 | var index = -1; 173 | while (current) { 174 | if (element === current.element) { 175 | return index; 176 | } 177 | index += 1; 178 | current = current.next; 179 | } 180 | return -1; 181 | }; 182 | LinkedList.prototype.remove = function (element) { 183 | var index = this.indexOf(element); 184 | return this.removeAt(index); 185 | }; 186 | LinkedList.prototype.isEmpty = function () { 187 | return this.length === 0; 188 | }; 189 | LinkedList.prototype.size = function () { 190 | return this.length; 191 | }; 192 | LinkedList.prototype.getHead = function () { 193 | return this.head; 194 | }; 195 | return LinkedList; 196 | }()); 197 | var linkedList = new LinkedList(); 198 | linkedList.append({ name: 'sakura' }); 199 | linkedList.append({ name: 'misaka' }); 200 | linkedList.append({ name: 'mikoto' }); 201 | linkedList.append({ name: 'yahaha' }); 202 | console.log(linkedList); 203 | // const data = linkedList.removeAt(0); 204 | linkedList.insert(2, { name: 'javascript' }); 205 | console.log(linkedList); 206 | console.log(linkedList.toString()); 207 | 208 | },{"./Node":5}],0:[function(require,module,exports) { 209 | var global = (1, eval)('this'); 210 | var OldModule = module.bundle.Module; 211 | function Module() { 212 | OldModule.call(this); 213 | this.hot = { 214 | accept: function (fn) { 215 | this._acceptCallback = fn || function () {}; 216 | }, 217 | dispose: function (fn) { 218 | this._disposeCallback = fn; 219 | } 220 | }; 221 | } 222 | 223 | module.bundle.Module = Module; 224 | 225 | if (!module.bundle.parent && typeof WebSocket !== 'undefined') { 226 | var ws = new WebSocket('ws://localhost:56526/'); 227 | ws.onmessage = function(event) { 228 | var data = JSON.parse(event.data); 229 | 230 | if (data.type === 'update') { 231 | data.assets.forEach(function (asset) { 232 | hmrApply(global.require, asset); 233 | }); 234 | 235 | data.assets.forEach(function (asset) { 236 | if (!asset.isNew) { 237 | hmrAccept(global.require, asset.id); 238 | } 239 | }); 240 | } 241 | 242 | if (data.type === 'reload') { 243 | ws.close(); 244 | ws.onclose = function () { 245 | window.location.reload(); 246 | } 247 | } 248 | 249 | if (data.type === 'error-resolved') { 250 | console.log('[parcel] ✨ Error resolved'); 251 | } 252 | 253 | if (data.type === 'error') { 254 | console.error('[parcel] 🚨 ' + data.error.message + '\n' + 'data.error.stack'); 255 | } 256 | }; 257 | } 258 | 259 | function getParents(bundle, id) { 260 | var modules = bundle.modules; 261 | if (!modules) { 262 | return []; 263 | } 264 | 265 | var parents = []; 266 | var k, d, dep; 267 | 268 | for (k in modules) { 269 | for (d in modules[k][1]) { 270 | dep = modules[k][1][d]; 271 | if (dep === id || (Array.isArray(dep) && dep[dep.length - 1] === id)) { 272 | parents.push(+k); 273 | } 274 | } 275 | } 276 | 277 | if (bundle.parent) { 278 | parents = parents.concat(getParents(bundle.parent, id)); 279 | } 280 | 281 | return parents; 282 | } 283 | 284 | function hmrApply(bundle, asset) { 285 | var modules = bundle.modules; 286 | if (!modules) { 287 | return; 288 | } 289 | 290 | if (modules[asset.id] || !bundle.parent) { 291 | var fn = new Function('require', 'module', 'exports', asset.generated.js); 292 | asset.isNew = !modules[asset.id]; 293 | modules[asset.id] = [fn, asset.deps]; 294 | } else if (bundle.parent) { 295 | hmrApply(bundle.parent, asset); 296 | } 297 | } 298 | 299 | function hmrAccept(bundle, id) { 300 | var modules = bundle.modules; 301 | if (!modules) { 302 | return; 303 | } 304 | 305 | if (!modules[id] && bundle.parent) { 306 | return hmrAccept(bundle.parent, id); 307 | } 308 | 309 | var cached = bundle.cache[id]; 310 | if (cached && cached.hot._disposeCallback) { 311 | cached.hot._disposeCallback(); 312 | } 313 | 314 | delete bundle.cache[id]; 315 | bundle(id); 316 | 317 | cached = bundle.cache[id]; 318 | if (cached && cached.hot && cached.hot._acceptCallback) { 319 | cached.hot._acceptCallback(); 320 | return true; 321 | } 322 | 323 | return getParents(global.require, id).some(function (id) { 324 | return hmrAccept(global.require, id) 325 | }); 326 | } 327 | },{}]},{},[0,4]) -------------------------------------------------------------------------------- /src/LinkedList/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/LinkedList/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LinkedList 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/LinkedList/index.ts: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | interface Element { 4 | name: string; 5 | } 6 | 7 | class LinkedList { 8 | length: number; 9 | head: Node; 10 | constructor() { 11 | this.length = 0; 12 | this.head = null; 13 | } 14 | public append(element: Element) { 15 | let node = new Node(element); 16 | let current; 17 | // 列表为空时添加为第一个元素 18 | if (this.head === null) { 19 | this.head = node; 20 | } else { 21 | current = this.head; 22 | while(current.next) { 23 | current = current.next; 24 | } 25 | current.next = node; 26 | } 27 | this.length += 1; 28 | } 29 | 30 | public insert(position: number, element: Element): boolean { 31 | if (position >= 0 && position <= this.length) { 32 | const node = new Node(element); 33 | let current = this.head; 34 | let previous; 35 | let index = 0; 36 | 37 | // 当position为0 则在第一个位置添加新元素 38 | if (position === 0) { 39 | node.next = current; 40 | this.head = node; 41 | } else { 42 | while(index++ < position) { 43 | previous = current; 44 | current = current.next; 45 | } 46 | node.next = current; 47 | previous.next = node; 48 | } 49 | this.length += 1; 50 | return true; 51 | } else { 52 | return false; 53 | } 54 | } 55 | 56 | public removeAt(position: number): Element{ 57 | if (position > -1 && position < this.length) { 58 | let current = this.head; 59 | let previous; 60 | let index = 0; 61 | 62 | // 移除第一项 63 | if (position === 0) { 64 | this.head = current.next; 65 | } else { 66 | /** 67 | * 移除列表最后一项或中间某一项时, 需要依靠一个细节来迭代列表,直到到达目标位置 68 | * 使用一个内部递增的index变量, current变量为所循环列表的当前元素进行引用 69 | */ 70 | while (index++ < position) { 71 | previous = current; 72 | current = current.next; 73 | } 74 | previous.next = current.next; 75 | } 76 | this.length -= 1; 77 | return current.element; 78 | } else { 79 | return null; 80 | } 81 | } 82 | 83 | public toString(): string { 84 | let current = this.head; 85 | let string = ''; 86 | 87 | while(current) { 88 | string += current.element.name + (current.next ? '\n' : ''); 89 | current = current.next; 90 | } 91 | return string; 92 | } 93 | 94 | 95 | public indexOf(element: Element) { 96 | let current = this.head; 97 | let index = -1; 98 | while(current) { 99 | if (element === current.element) { 100 | return index; 101 | } 102 | index += 1; 103 | current = current.next; 104 | } 105 | return -1; 106 | } 107 | 108 | public remove(element: Element) { 109 | const index = this.indexOf(element); 110 | return this.removeAt(index); 111 | } 112 | 113 | public isEmpty() { 114 | return this.length === 0; 115 | } 116 | 117 | public size() { 118 | return this.length; 119 | } 120 | 121 | public getHead() { 122 | return this.head; 123 | } 124 | 125 | // public remove(element: Node){} 126 | // public indexOf(element: Node){} 127 | // public isEmpty(): boolean{} 128 | // public size(): number{} 129 | // public getHead(): Node{} 130 | // public toString(): string {} 131 | // public print() {} 132 | } 133 | 134 | const linkedList = new LinkedList(); 135 | linkedList.append({name: 'sakura'}); 136 | linkedList.append({name: 'misaka'}); 137 | linkedList.append({name: 'mikoto'}); 138 | linkedList.append({name: 'yahaha'}); 139 | console.log(linkedList); 140 | // const data = linkedList.removeAt(0); 141 | linkedList.insert(2, {name: 'javascript'}); 142 | console.log(linkedList); 143 | console.log(linkedList.toString()); 144 | --------------------------------------------------------------------------------