diff --git a/2019/12/activity_lifecycle.md b/2019/12/activity_lifecycle.md index bfcff1a..0e0ff54 100644 --- a/2019/12/activity_lifecycle.md +++ b/2019/12/activity_lifecycle.md @@ -1,6 +1,6 @@ # [Activity Lifecycle](/2019/12/activity_lifecycle.md) -[https://developer.android.com/guide/components/activities/activity-lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle) + 英文专业名词 diff --git a/2019/12/association_without_primary_key.md b/2019/12/association_without_primary_key.md index 61dc214..c174722 100644 --- a/2019/12/association_without_primary_key.md +++ b/2019/12/association_without_primary_key.md @@ -36,6 +36,5 @@ markets表没有一个外键是currencies表的外键,好在belongs_to有选 参考链接 -[https://stackoverflow.com/questions/16071735/active-record-association-without-using-id](https://stackoverflow.com/questions/16071735/active-record-association-without-using-id) - -[https://stackoverflow.com/questions/21466726/rails-has-many-relationship-without-using-id](https://stackoverflow.com/questions/21466726/rails-has-many-relationship-without-using-id) +- +- diff --git a/2019/12/except_array_include_hash.md b/2019/12/except_array_include_hash.md index 210326c..b448703 100644 --- a/2019/12/except_array_include_hash.md +++ b/2019/12/except_array_include_hash.md @@ -16,5 +16,5 @@ expect(response_body["data"]) 参考链接 -- [https://stackoverflow.com/questions/52000714/rspec-check-if-array-contains-element-with-specified-id](https://stackoverflow.com/questions/52000714/rspec-check-if-array-contains-element-with-specified-id) -- [https://stackoverflow.com/questions/23815944/rspec-match-array-of-hashes](https://stackoverflow.com/questions/23815944/rspec-match-array-of-hashes) \ No newline at end of file +- +- diff --git a/2019/12/redirect_to_anchor.md b/2019/12/redirect_to_anchor.md index edba48f..9d4ce38 100644 --- a/2019/12/redirect_to_anchor.md +++ b/2019/12/redirect_to_anchor.md @@ -11,4 +11,4 @@ 参考链接 -[https://stackoverflow.com/questions/13791556/how-to-redirect-to-a-certain-location-in-a-page](https://stackoverflow.com/questions/13791556/how-to-redirect-to-a-certain-location-in-a-page) + diff --git a/2020/01/android_notes.md b/2020/01/android_notes.md index 7a67996..ac9a2b1 100644 --- a/2020/01/android_notes.md +++ b/2020/01/android_notes.md @@ -38,7 +38,7 @@ public static HomeFragment newInstance() { 一个TextView+ImageView可以写成一个TextView(加上一个drawable属性),而且渲染性能更好 -[https://stackoverflow.com/questions/3214424/android-layout-this-tag-and-its-children-can-be-replaced-by-one-textview-and](https://stackoverflow.com/questions/3214424/android-layout-this-tag-and-its-children-can-be-replaced-by-one-textview-and) + ## Snackbar diff --git a/2020/10/rfc_const_generics.md b/2020/10/rfc_const_generics.md index 6b37a1e..a11de65 100644 --- a/2020/10/rfc_const_generics.md +++ b/2020/10/rfc_const_generics.md @@ -2,7 +2,7 @@ 我的翻译并不会按原文顺序逐字翻译,因为除了RFC的网页,还涉及相关PR和issues,以下是我对RFC#2000 const generic个人理解下的翻译 -[https://rust-lang.github.io/rfcs/2000-const-generics.html](https://rust-lang.github.io/rfcs/2000-const-generics.html) + 首先动态语言为什么没有常量,因为常量是编译时编译器将常量的字面量替换到使用该常量的地方,这也叫「内联优化」 diff --git a/2021/03/statically_linked_executable.md b/2021/03/statically_linked_executable.md index e1f433f..0716728 100644 --- a/2021/03/statically_linked_executable.md +++ b/2021/03/statically_linked_executable.md @@ -37,7 +37,7 @@ rustc 生成的汇编默认是 Intel 语法,可以传入 llvm 参数让 rustc 可以用 ldd 工具校验编译生成的可执行文件是不是 statically linked (没有引入任何动态链接库) -汇编的劣势在于代码跟硬件架构绑定,gcc 编译这段代码时加上`-m32`参数指定生成32位的可执行文件时就会报错 +汇编的劣势在于代码跟硬件架构绑定,gcc 编译这段汇编代码时加上`-m32`参数指定生成32位的可执行文件时就会报错 ## C 编译 no_std 可执行文件 diff --git a/2021/04/kfind_lev_distance.png b/2021/04/kfind_lev_distance.png new file mode 100644 index 0000000..21b895a Binary files /dev/null and b/2021/04/kfind_lev_distance.png differ diff --git a/2021/04/rustc_edit_distance_and_typo_checker.md b/2021/04/rustc_edit_distance_and_typo_checker.md index 6045efc..406244c 100644 --- a/2021/04/rustc_edit_distance_and_typo_checker.md +++ b/2021/04/rustc_edit_distance_and_typo_checker.md @@ -1,16 +1,22 @@ -# []() +# [用 rustc 源码实现拼写错误候选词建议](/2021/04/rustc_edit_distance_and_typo_checker.md) - +--- +pub_date: Sat, 27 Mar 2021 16:00:00 GMT +description: Executable file under `no_std` environment -笔者最近在做一个聊天应用,想给聊天消息输入框加上拼写错误检查,毕竟 word, keynote 等涉及文本输入的软件都有拼写错误检查和纠错功能 +--- + +# 用 rustc 源码实现拼写错误候选词建议 + +作者: 吴翱翔@pymongo + +> 原文: [用 rustc 源码实现拼写错误候选词建议](https://pymongo.github.io/#/2021/04/rustc_edit_distance_and_typo_checker.md) + +最近想给一个聊天应用的聊天消息输入框加上拼写错误检查,毕竟 word, keynote 等涉及文本输入的软件都有拼写错误检查和纠错功能 于是想到开发中经常用的 rustup, cargo, rustc 不就内置了拼写错误时纠错建议的功能么? -在 rustup 输入错误的单词时例如`rustup dog`,此时 rustup 就会提示把`dog`改成`doc` +在 rustup 输入错误的单词时例如 `rustup dog`,此时 rustup 就会提示把 `dog` 改成 `doc` ``` [w@w-manjaro ~]$ rustup dog @@ -18,9 +24,11 @@ error: The subcommand 'dog' wasn't recognized Did you mean 'doc'? ``` -## rustup 的拼写纠错建议的实现 +## 字符串的编辑距离 + +### rustup 的拼写纠错建议的实现 -以`Did you mean`的关键词全文搜索 rustup 源码,找到出处在`src/cli/error.rs` +以 `Did you mean` 的关键词全文搜索 rustup 源码,找到出处在 `src/cli/error.rs` ```rust fn maybe_suggest_toolchain(bad_name: &str) -> String { @@ -61,44 +69,357 @@ fn maybe_suggest_toolchain(bad_name: &str) -> String { } ``` -`damerau_levenshtein`其实就是描述两个字符串之间的差异,`damerau_levenshtein`距离越小则两个字符串越接近 +`damerau_levenshtein` 其实就是描述两个字符串之间的差异,`damerau_levenshtein` 距离越小则两个字符串越接近 -该函数的将输入的错误单词跟正确的候选词挨个计算`damerau_levenshtein`距离, +该函数的将输入的错误单词跟正确的候选词挨个计算 `damerau_levenshtein` 距离, -最后排序下`damerau_levenshtein`距离输出最小的候选词 +最后排序下 `damerau_levenshtein` 距离输出最小的候选词 -rustup的`damerau_levenshtein`来自 strsim 库,除了 rustup, darling 等知名库也导入了 strsim 库 +rustup的 `damerau_levenshtein` 来自 ***strsim*** 库,除了 rustup, darling 等知名库也导入了 strsim 库 -查阅维基百科的`damerau_levenshtein`词条后发现`damerau_levenshtein`的同义词是`levenshtein_distance`和`edit_distance` +查阅维基百科的 `damerau_levenshtein` 词条后发现 `damerau_levenshtein` 的同义词是 `levenshtein_distance` 和 `edit_distance` -## 用 rustc 源码竟然过了算法题 +### 用 rustc 源码竟然过了算法题 -edit_distance 是个动态规划算法中的一个经典问题,果然 [leetcode](https://leetcode-cn.com/problems/edit-distance/) 上有 edit_distance 的算法题 +rustc 源码会尽量不用第三方库,所以我猜测 rustc 不会像 rustup 那样用 strsim 源码,那就看看 rustc 的实现会不会更好 -我拿 strsim 库的`damerau_levenshtein`去 leetcode 上提交能通过,但是运行耗时 4ms,不太理想 +在 Rust 的 github 仓库中搜索`edit distance`关键字能找到[Make the maximum edit distance of typo suggestions](https://github.com/rust-lang/rust/commit/93d01eb443d0f871716c9d7faa3b69dc49662663) 的 commit -原因是 strsim 的 edit_distance 算法动态规划的空间复杂度是 O(n^2) +顺着这个 commit 的改动在 `find_best_match_for_name` 函数内调用了 `lev_distance` 函数去计算两个字符串的编辑距离 -rustc 源码会尽量不用第三方库,所以我猜测 rustc 没有用 strsim 源码,那就看看 rustc 的实现会不会更好 +edit_distance 是个动态规划算法或字符串算法的经典问题,果然 leetcode 上有 [edit_distance 的算法题](https://leetcode.com/problems/edit-distance/) +我拿 rustc 源码的 lev_distance 函数在 leetcode上通过 edit_distance 一题 -在 Rust 的 github 仓库中搜索`edit distance`关键字能找到[这个 commit](https://github.com/rust-lang/rust/commit/93d01eb443d0f871716c9d7faa3b69dc49662663) +![](rustc_edit_distance_leetcode_submit.png) -在`find_best_match_for_name`函数内调用了`lev_distance`函数去计算两个字符串 +用 strsim 的相关函数也能通过编辑距离这题,但是运行耗时 4ms 会比 rustc 源码运行耗时 0ms 慢点 -![](rustc_edit_distance_leetcode_submit.png) +原因是 strsim 的 edit_distance 算法动态规划的空间复杂度是 O(n^2),而 rustc 的实现空间复杂度是 O(n) + +### edit_distance 算法 + +从 rustc 源码的 lev_distance 函数签名 `fn lev_distance(a: &str, b: &str) -> usize` 来看 -## edit_distance 算法 +输入的是两个字符串 a 和 b, 返回值表示 a 和 b 的 edit_distance -为了进一步验证带`rustc-ap`前缀的库是不是从 rustc 源码导出的,我们看看很可能用到部分 rustc 源码的 rust-analyzer +edit_distance 表示从字符串 a 修改成 b 或从字符串 b 修改成 a 至少需要的操作(插入/删除/替换一个字母)次数 + +例如一个拼写错误的单词 `bpple` 需要一次替换操作,将第一个字母 `b` 替换成 `a` 才能变成 `apple` + +所以字符串 `bpple` 和 `apple` 之间的 edit_distance 就是 1 + +以下是一段 edit_distance 的二维数组 dp 状态的实现,可以结合代码注释进行理解,详细的推断和动态规划状态转移方程可以看 leetcode 的官方题解 + +```rust +/// 从字符串word1修改成word2至少需要多少次操作(replace/insert/delete) +#[allow(clippy::needless_range_loop)] +fn edit_distance_dp(word1: String, word2: String) -> i32 { + let (word1, word2) = (word1.into_bytes(), word2.into_bytes()); + let (word1_len, word2_len) = (word1.len(), word2.len()); + // # dp[i][j]表示word1[..i]至少需要多少次操作(replace/insert/delete)替换成B[..j] + // 很容易想到的其中一种状态转移的情况: 如果word1[i]==word2[j],那么dp[i][j]==dp[i-1][j-1] + let mut dp = vec![vec![0; word2_len+1]; word1_len+1]; + for i in 0..=word1_len { + // 需要i次删除操作才能让word1[..i]修改成空的字符串word2[..0] + dp[i][0] = i; + } + for j in 0..=word2_len { + // 需要j次插入操作才能让空字符串word1[..0]修改成word2[..j] + dp[0][j] = j; + } + for i in 1..=word1_len { + for j in 1..=word2_len { + if word1[i-1] == word2[j-1] { + dp[i][j] = dp[i-1][j-1]; + } else { + // dp[i-1][j-1] + 1: word1[i-1]和word2[i-2]不同,所以替换次数+1, + // 如果dp的决策层选择replace操作,dp[i][j]总共操作数等于dp[i-1][j-1]+1 + // d[i-1][j]表示往word1末尾插入word2[j],dp[i][j-1]表示word1删掉末尾的字母让word1和word2更接近 + dp[i][j] = dp[i-1][j-1].min(dp[i-1][j]).min(dp[i][j-1]) + 1; + } + } + } + dp[word1_len][word2_len] as i32 +} +``` + +由于 rustc 源码为了性能选用了一位数组存储动态规划的状态,用到了很多状态压缩、滚动数组之类的技巧,较难理解,本文就不对 rustc 的动态规划 edit_distance 算法做解释 + +--- + +## 引用 rustc 编辑距离的函数 + +### rustc 动态链接库? + +考虑到 rustc 源码的 lev_distance 会比 strsim 库性能略微好点,所以就直接调 rustc 源码的 lev_distance 就行了 + +当我尝试在代码中加入 `extern crate rustc` 时就出现以下报错: + +> error[E0462]: found staticlib `rustc` instead of rlib or dylib + +然后 rustc 会提示找到个类似的静态链接库文件 + +> /home/w/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc-nightly_rt.asan.a + +然后我试着用 nm 命令去读取库文件的函数符号表 + +``` +$ nm -D librustc-nightly_rt.tsan.a +... +sanitizer_linux_s390.cpp.o: +nm: sanitizer_linux_s390.cpp.o: no symbols + +sanitizer_mac.cpp.o: +nm: sanitizer_mac.cpp.o: no symbols + +sanitizer_netbsd.cpp.o: +nm: sanitizer_netbsd.cpp.o: no symbols +... +``` + +发现里面有一个 `sanitizer_netbsd.cpp` 的文件,网上搜索得知这是 llvm 的源文件 + +所以这些 `librustc-nightly_rt` 开头的库全是 llvm 相关的静态链接库,并不是 rustc 的库 + +### rustc-ap-rustc_span + +我相信我编译过很多像 rust-analyzer, racer 等静态分析的库,说不定电脑本地的 cargo 缓存就有 rustc 源码的 lev_distance.rs + +![](kfind_lev_distance.png) + +果然发现 rustc-ap-rustc_span 这个 crate 就有 lev_distance 函数 + +再参考 StackoverFlow 的问题 [How to use `rustc` crate?](https://stackoverflow.com/questions/48372993/how-to-use-rustc-crate?rq=1) 和 racer 源码后发现 + +而以 `rustc-ap-rustc_` 命名开头的库都是由 Rust 官方团队的 [alexcrichton](https://github.com/alexcrichton/rustc-auto-publish) +定期从 rustc 源码中同步代码并发布到 crates.io 中 + +为了进一步验证带`rustc-ap`前缀的库是不是从 rustc 源码导出的,再看看很可能用到部分 rustc 源码的 rust-analyzer ``` [w@w-manjaro rust-analyzer]$ grep -r --include="*.toml" "rustc-ap" . ./crates/syntax/Cargo.toml:rustc_lexer = { version = "714.0.0", package = "rustc-ap-rustc_lexer" } ``` -果然发现 rust-analyzer 用到了`rustc-ap-rustc_lexer`这个库,毕竟 rust-analyzer 是做静态分析的,跟编译器的部分功能有点重合很正常 +果然发现 rust-analyzer 用到了 `rustc-ap-rustc_lexer` 这个库,毕竟 rust-analyzer 是做静态分析的,跟编译器的部分功能有点重合很正常 + +其实像 rust-analyzer 和 racer 等静态分析工具都会用到 rustc-ap-rustc_* 这样命名开头的 rustc 编译器组件库 + +我参考 racer 源码可以在 Cargo.toml 中这么引入 rustc_span,进而使用 rustc_span 的 lev_distance 函数 + +> rustc_span = { package="rustc-ap-rustc_span", version="714.0.0" } + +### rustc-dev component -## 如何引用 rust 源码 +阅读 rustup component 相关文档得知,rustc-dev 组件包含了 rustc 的动态链接库和源码(方便静态分析) > rustup component add rustc-dev + +然后就可以使用 rustc 编译器的各种组件 + +```rust +#![feature(rustc_private)] +extern crate rustc_span; +``` + +### rust-analyzer 对 rustc 源码静态分析 + +然后在 Cargo.toml 中加入以下内容, + +```toml +[package.metadata.rust-analyzer] +rustc_private = true +``` + +然后 rust-analyzer 能对 rustc API 的使用进行静态分析 + +然后参考 rust-analyzer 的这两个 [#6714](https://github.com/rust-analyzer/rust-analyzer/issues/6714), [#7589](https://github.com/rust-analyzer/rust-analyzer/issues/7589) + +想让 rust-analyzer 对 rustc 的使用进行静态分析,需要设置 rustc 源码的路径: + +> "rust-analyzer.rustcSource": "/home/w/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/rustc-src/rust/compiler/rustc_driver/Cargo.toml" + +rustc-dev component 会提供 rustc-src 也就是 rustc 源码 + +目前 rust-analyzer 还不支持 `extern crate test` 的静态分析,但我看 rust-src component 提供了 test crate 的源码: + +> /home/w/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/Cargo.toml + +所以 rust-analyzer 和 intellij-rust 将来有望支持 test crate 的静态分析 + +不过像 libc 虽然 rustup 每个 toolchain 都装了 libc 的 rlib 类型的动态链接库,可惜 rust-src component 没有包括 libc 源码 + +所以用 `extern crate libc` 的方式引入 toolchain 自带的 libc 还是不能做静态分析的 + +--- + +## 语料库 + +拼写错误候选词建议需求的实现思路可以是: 对常用英语单词的每个单词跟拼写错误的单词去计算编辑距离,取编辑距离最近的 5 个单词作为获选词建议 + +字符串间编辑距离的算法可以直接用 rustc 源码的 lev_distance,常用英语单词表则需要一个语料库 + +### /usr/share/dict/words + +mac 和树莓派的 respbian 系统都内置了英语语料库,方便系统预装的记事本等应用进行拼写错误检查 + +ubuntu 等 linux 系统可以装 gnu 的 aspell 获取常用英语单词的语料库,archlinux 用户可以装 `sudo pacman -S words` 这个包 + +除了用操作系统自带的语料库,还可以选用 github 的 [english-words](https://github.com/dwyl/english-words) 仓库作为语料库 + +### 语料库用 Rust 什么数据结构去存 + + + +### 拼写错误建议的设计思路 + +看 rustup 源码发现,输入一个错误的单词会从候选词中返回最近的几个词, + +在 rustup 源码中一般 2 个参数控制返回的候选词,一个参数决定 edit_distance 小于多少的候选词会被选中,这个参数在 rustup 源码中是个常量 + +另一个参数则是决定需要返回几个候选词 + +所以代码实现也很简单,将 40 万个单词从 txt 文件中读取,再存储成`Vec`,然后将拼写错误的单词跟语料库的单词挨个计算 edit_distance,然后返回 edit_distance 最接近的 5 个词作为建议 + +### 初版拼写检查工具的不足 + +按照上述的设计思路很容易就写出了以下拼写检查工具,输入一个错误单词能找出最接近的 5 个正确单词 + +```rust +#![feature(rustc_private)] +extern crate rustc_span; + +type MyResult = Result>; + +struct TypoChecker { + /// TODO use tire(prefix tree) data structure to store words for better performance + words: Vec, +} + +#[allow(dead_code)] +impl TypoChecker { + fn new() -> Self { + use std::io::{BufRead, BufReader, Write}; + const WORDS_FILENAME: &str = "english_words.txt"; + + fn download_words_list() -> MyResult<()> { + let mut http_response = Vec::new(); + let mut easy = curl::easy::Easy::new(); + // english words corpus: github.com/dwyl/english-words + easy.url( + "https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt", + )?; + let mut transfer = easy.transfer(); + transfer.write_function(|data| { + http_response.extend_from_slice(data); + Ok(data.len()) + })?; + transfer.perform()?; + drop(transfer); + // cache words list to file + let mut file = std::fs::File::create(WORDS_FILENAME)?; + file.write_all(&http_response)?; + Ok(()) + } + + if !std::path::Path::new(WORDS_FILENAME).exists() { + download_words_list().unwrap(); + } + let mut words = vec![]; + let word_file = BufReader::new(std::fs::File::open(WORDS_FILENAME).unwrap()); + for line in word_file.lines().flatten() { + words.push(line); + } + Self { words } + } + + fn typo_suggestions(&self, input: &str) -> Vec { + let mut suggestions = vec![]; + for word in self.words.iter() { + let edit_distance = rustc_span::lev_distance::lev_distance(input, word); + if edit_distance < 2 { + suggestions.push(word.clone()); + } + if suggestions.len() > 5 { + break; + } + } + suggestions + } +} +``` + +测试代码如下: + +```rust +#[test] +fn test_typo_checker() { + let typo_checker = TypoChecker::new(); + let input = "doo"; + println!( + "Unknown word `{}`, did you mean one of {:?}?", + input, + typo_checker.typo_suggestions(input) + ); +} +``` + +测试代码的输出结果示例: + +> Unknown word `doo`, did you mean one of ["boo", "coo", "dao", "do", "doa", "dob"]? + +其实用长度为 40 多万的数组去存储语料库的每个单词的内存利用率是很低的,很多单词都是重复部分很多 + +于是业界用 trie 的数据结构去存单词,也叫前缀树,广泛用于输入法,搜索引擎候选词,代码自动补全等领域 + +例如 doc, dot, dog 三个单词,如果用 `Vec` 去存储,大约需要 9 个字节 + +但是如果用"链表"去存储,这三个单词链表的前两个节点 'd' 和 'o' 可以共用,这样只需要 5 个链表节点大约 5 个字节的内存空间 + +现在只需要解决两个问题就可以优化拼写检查的过程 + +1. 前缀树存储语料库的实现 +2. (重点)针对前缀树数据结构的 edit_distance 算法 + +### trie 前缀树的实现 + +正好 leetcode 上也有 [Implement Trie (Prefix Tree) 这种实现 trie 的算法题](https://leetcode.com/problems/implement-trie-prefix-tree/) + +```rust +#[derive(Default)] +struct Trie { + children: [Option>; 26], + is_word: bool, +} + +impl Trie { + fn new() -> Self { + Self::default() + } + + fn insert(&mut self, word: String) { + let mut node = self; + for letter in word.into_bytes().into_iter().map(|ch| (ch - b'a') as usize) { + node = node.children[letter].get_or_insert_with(|| Box::new(Trie::default())) + } + node.is_word = true; + } + + fn find_node(&self, word: &str) -> Option<&Self> { + let mut node = self; + for letter in word.into_iter().map(|ch| (ch - b'a') as usize) { + node = node.children[letter].as_ref()?; + } + Some(node) + } + + fn search(&self, word: String) -> bool { + self.find_node(&word).map_or(false, |node| node.is_word) + } + + fn starts_with(&self, prefix: String) -> bool { + self.find_node(&prefix).is_some() + } +} +``` diff --git a/2021/04/rustup_install_rust_analyzer.md b/2021/04/rustup_install_rust_analyzer.md index def4242..cee9d32 100644 --- a/2021/04/rustup_install_rust_analyzer.md +++ b/2021/04/rustup_install_rust_analyzer.md @@ -22,3 +22,4 @@ rustup安装ra有几点好处: > rustup run nightly-2021-04-12 rust-analyzer 虽然toolchain是04-12版本,但是实际上ra会用距离04-12最近的一个stable release也就是ra的04-05版本 +djtt \ No newline at end of file diff --git a/archive/database/oracle_database/index.md b/archive/database/oracle_database/index.md index 6efd5af..829365e 100644 --- a/archive/database/oracle_database/index.md +++ b/archive/database/oracle_database/index.md @@ -14,7 +14,7 @@ Oracle的服务进程是开机自动启动的, 而且很占内存 OEM(Oracle Enterprise Manager)是Oracle数据库的Web端管理软件 -网址是:[https://localhost:1158/em](https://localhost:1158/em) +网址是: 注意只能用https协议访问,由于无SSL证书,需要浏览器单独允许 diff --git a/notes/async/green_thread_and_coroutinue.md b/notes/async/green_thread_and_coroutinue.md index 4347741..a47ba5c 100644 --- a/notes/async/green_thread_and_coroutinue.md +++ b/notes/async/green_thread_and_coroutinue.md @@ -103,6 +103,6 @@ sync blocking IO可能大学教科书里较多,现实工程中,Go/Ruby语言 ### async evented non-blocking IO -nginx, Node.js, 在await没出现时,js的异步写法容易陷入回调函数地狱(callback hell)[http://callbackhell.com/] +nginx, Node.js, 在await没出现时,js的异步写法容易陷入回调函数地狱 后来回调地狱改成了ES6的promise语法,不停的.then,但是不会像回调那样多层嵌套