followWinter

followWinter 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

暂时没有

个人动态

followWinter 发布了文章 · 2020-06-13

活用 Git 撤销,提升工程质量(文末招聘)

0x001 概述

一个优秀的前端工程师,必先是个优秀的工程师;一个优秀的工程师,要为自己写的每一行代码负责,要为自己提交的每一个 commit 负责。话虽这么说,但是不想写一篇十分大而全的文章,就想写一些小东西。

0x002 撤销 git add

我们知道,要提交一个 commit,必须要先 add,那如果把一些不想要 add 的东西 add 进去了,该咋办?下面提供几个场景和解决方案:

场景1:添加某个误创建文件,这个文件是不想要的

  • 环境:误创建了一个 new.txt,并使用 add
$ echo new > new.txt
$ git add .
$ git status

On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   new.txt # 👈
  • 解决方案:直接删除就好了
$ rm new.txt
$ git add .
$ git status

On branch master
nothing to commit, working tree clean # 👈

场景 2:新建了一个组件,但是不希望在这次 commit 中一起提交

  • 环境:创建了一个 Product 组件,但是这个组件不在当前使用,本次的修改只应该包含一个 index.js
$ git status

 On branch master
 Changes to be committed:
     (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css # 👈 多余
    new file:   Product/index.js # 👈 多余
    new file:   index.js
  • 解决方案 1:使用git rm移除,-r表示文件夹递归,--cache表示从index移除,working tree还保留着
$ git rm -r --cache Product/
rm 'Product/index.css'
rm 'Product/index.js'

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   index.js  # 👈 index 只剩下一个文件了

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    Product/ # 👈但是 working tree 还有
$ git commit -m 'feat: index'
\[master 3bf1398\] feat: index
 1 file changed, 1 insertion(+)
 create mode 100644 index.js
  • 解决方案 2:使用git stashProduct组件先暂存,提交index.js,再放出Product
  1. 暂存Product
$ git stash -- Product
Saved working directory and index state WIP on master: b426e7e feat: initial
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   index.js  # 👈 只剩下一个文件了
  1. 提交 index.js
$ git commit -m 'feat: index'
\[master 3bf1398\] feat: index
 1 file changed, 1 insertion(+)
 create mode 100644 index.js
  1. 取出 stash 中的Product
$ git stash pop
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css  # 👈 又拿出来了
    new file:   Product/index.js  # 👈 又拿出来了

Dropped refs/stash@{0} (28675d4accc329da3e54b0a839967471f21c7102)
  1. 作为独立commit提交
$ git commit -m "feat: product"
\[master de42d60\] feat: product
 2 files changed, 2 insertions(+)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

0x003 撤销不想要的修改

场景1:修改了某个文件,但是又不想要其中一些或者全部

  • 环境
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  modified:   Product/index.js  # 👈 不想要
    modified:   index.js  # 👈 不想要
  • 解决方案1:git stash,然后就不用管它了,适用于全部抛弃或者部分
$ git stash -- Product/index.js
Saved working directory and index state WIP on master: de42d60 feat: product
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js   # 👈 只剩下这个了
no changes added to commit (use "git add" and/or "git commit -a")
  • 解决方案2:git reset,适用于当前全部修改都不要了
$ git reset --hard HEAD
HEAD is now at de42d60 feat: product

$ git status
On branch master
nothing to commit, working tree clean   # 👈 全没了
  • 解决方案3:git checkout,适用于部分抛弃
$ git checkout  -- Product/index.js
$ git status
 On branch master
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js   # 👈 只剩下这个了
 
 no changes added to commit (use "git add" and/or "git commit -a")

0x004 撤销/修改 git commit

场景1:commit 已经提交,但是 commit message 有错误

  • 场景展示
$ git add .
$ git commit -m 'feat: 错误的 message'
\[master ed31f58\] feat: 错误的 message # 👈 
 2 files changed, 2 insertions(+), 2 deletions(-)
  • 解决方案:git commit --amend
$ git commit --amend -m 'feat: 正确的 message'
\[master 325e290\] feat: 正确的 message # 👈 
 Date: Tue May 26 12:39:07 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)

场景2:commit 已经提交,但是有文件遗漏了

场景展示:

  1. 查看状态,当前有两个文件被修改
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   Product/index.js  # 👈 修改
    modified:   index.js  # 👈 修改
  1. 只提交一个文件
$ git add Product/index.js  # 👈 提交一个
$ git commit -m 'feat: 完成产品功能'
\[master 8f3f605\] feat: 完成产品功能
 1 file changed, 1 insertion(+), 1 deletion(-)
  1. 查看状态,还有一个文件还没提交
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js  # 👈 剩余一个

解决方案1:git commit --amend

$ git add .
$ git commit --amend -m 'feat: 完成 产品功能'   # 👈 提交
\[master 1ae5752\] feat: 完成 产品功能
 Date: Tue May 26 12:49:00 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)
 
$ git status
On branch master
nothing to commit, working tree clean  # 👈 全提交了

解决方案2:git reset --soft HEAD^

$ git reset head^
Unstaged changes after reset:
M   Product/index.js  # 👈 上次提交的又回来了

$ git add .
$ git commit -m 'feat: 完成 产品功能'  # 👈 再次提交
\[master 1ae5752\] feat: 完成 产品功能
 Date: Tue May 26 12:49:00 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)

场景3:commit 已经提交了,但是多了一些文件

  • 场景展示:
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css
    new file:   Product/index.js
    modified:   index.js

# 原本只想提交 Product 相关文件,但是误把 index.js 提交了
$ git add .
$ git commit -m 'feat: 完成 Product 组件'
\[master 6c0f3c8\] feat: 完成 Product 组件
 3 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

解决方案1: git reset head^ 之后重新提交

$ git reset head^
Unstaged changes after reset:
M   index.js

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js  # 👈 提交的又回来了

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    Product/  # 👈 提交的又回来了

no changes added to commit (use "git add" and/or "git commit -a")

$ git add Product/\*   # 👈 只提交 Product
$ git commit -m 'feat: 完成 Product 组件'
\[master 9b9d085\] feat: 完成 Product 组件
 2 files changed, 2 insertions(+)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

0x005 删除一个 commit(最新的一个)

有时候我们不想要一个 commit 了怎么办,这里提供几个解决方案

  • 场景展示:
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   index.js

$ git commit -m 'feat: 不想要的提交'
\[master aa47ff2\] feat: 不想要的提交
 1 file changed, 1 insertion(+)
 
$ git log --pretty=oneline
aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 (HEAD -> master) feat: 不想要的提交   # 👈 这个提交不想要
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
...
  • 解决方案1:reset
$ git reset --hard HEAD^
HEAD is now at 0dc03f1 feat: 完成首页

$ git log --pretty=oneline
0dc03f1970ed670b97bccffc92ab5598b5049bf8 (HEAD -> master) feat: 完成首页 # 👈 没了
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
  • 执行以下命令
$ git rebase 7064a1652cda00d4a57d24d3661b018771a49387 # 倒二个
  • 此时会进入 vim
pick 5acd6e2 feat(project): 添加 version 脚本

# Rebase 7064a16..5acd6e2 onto 7064a16 (1 command)
  • 修改为以下内容并保存
drop 5acd6e2 feat(project): 添加 version 脚本
  • 查看结果
Successfully rebased and updated refs/heads/feat/A.

$  git log --pretty=oneline
7064a1652cda00d4a57d24d3661b018771a49387 (HEAD -> feat/A) feat(changeLost): 添加 changeLog  # 第一个没啦
c45cbb73e79d460473dc941ab062ff7e11d099fa (tag: v1.0.3) 1.0.3
671757d69b98e3d6cae6e4dce5717fac4f77a3a7 feat(project): 添加 ChangeLog
fd369e89072279a7057e236ba1707f2a19709b03 (tag: v1.0.2) 1.0.2
4e9f32cc30525a70c07f3c544594f105c48d8d0f (tag: v1.0.1) 1.0.1
  • 解决方案2:rebase
$ git log --pretty=oneline
5acd6e27a85b7e728454374b9fe516387e2e7450 (HEAD -> feat/A) feat(project): 添加 version 脚本
7064a1652cda00d4a57d24d3661b018771a49387 feat(changeLost): 添加 changeLog
c45cbb73e79d460473dc941ab062ff7e11d099fa (tag: v1.0.3) 1.0.3
671757d69b98e3d6cae6e4dce5717fac4f77a3a7 feat(project): 添加 ChangeLog
fd369e89072279a7057e236ba1707f2a19709b03 (tag: v1.0.2) 1.0.2
4e9f32cc30525a70c07f3c544594f105c48d8d0f (tag: v1.0.1) 1.0.1
  • 解决方案3: revert
$ git revert aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 --no-edit
\[master ae7eae8\] Revert "feat: 不想要的提交"
 Date: Tue May 26 13:45:07 2020 +0800
 1 file changed, 1 deletion(-)

$ git log --pretty=oneline
ae7eae86691a90e81b68b85e0f3cb7ebaee4312c (HEAD -> master) Revert "feat: 不想要的提交"   # 👈 没了
aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 feat: 不想要的提交
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
  • 解决方案4:reflog + reset
$ git reflog
6fa3df5 (HEAD -> master) HEAD@{0}: commit: feat: 不想要的提交
0dc03f1 HEAD@{1}: reset: moving to HEAD^   # 👈 这个
aa47ff2 HEAD@{2}: reset: moving to HEAD
aa47ff2 HEAD@{3}: reset: moving to HEAD@{6}

$ git reset --hard HEAD@{1}
HEAD is now at 0dc03f1 feat: 完成首页

0x006 取消一次 merge

  • 场景展示
有 feat/A 和 feat/B 分支合并到 master,如下图现在想撤销 feat/B 到 master 的合并

![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2020/png/281707/1590472368645-536b45c9-ebc2-4f6e-858a-f906614ba5cb.png)  
  • 解决方案1:reset
$ git log --pretty=oneline
aeca903ffad7140b2d1c854c936290c006542f05 (HEAD -> master) Merge branch 'feat/B'
7c8f46806ffb2ee5961e24b4a435d68f893c78c4 (feat/A) feat: 添加功能a   # 👈 回退到这个就行了
1b2dd75b7193a9b37e20a269f5490971adfe4b50 (feat/B) feat: 添加功能b
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
$ git reset --hard 7c8f46806ffb2ee5961e24b4a435d68f893c78c4
HEAD is now at 7c8f468 feat: 添加功能a

看看结果

![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2020/png/281707/1590472911298-e89e3453-14ff-4ad6-bf32-3c28e4e26699.png?x-oss-process=image%2Fresize%2Cw_1500 "image.png")
  • 解决方案2:revert
$ git log --pretty=oneline
aeca903ffad7140b2d1c854c936290c006542f05 (HEAD -> master) Merge branch 'feat/B'
7c8f46806ffb2ee5961e24b4a435d68f893c78c4 (feat/A) feat: 添加功能a  # 👈 回退到这个就行了
1b2dd75b7193a9b37e20a269f5490971adfe4b50 (feat/B) feat: 添加功能b
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件

$ git revert aeca903ffad7140b2d1c854c936290c006542f05 -m 1   # 👈 1是只分支序号
\[master a26fa95\] Revert "Merge branch 'feat/B'"
 1 file changed, 1 deletion(-)
 delete mode 100644 b.txt
  • 解决方案3:reflog + reset
$ git reflog
aeca903 (HEAD -> master) HEAD@{0}: merge feat/B: Merge made by the 'recursive' strategy.
7c8f468 (feat/A) HEAD@{1}: merge feat/A: Fast-forward   # 👈 回退到这个就行了
0dc03f1 HEAD@{2}: checkout: moving from feat/A to master
7c8f468 (feat/A) HEAD@{3}: commit: feat: 添加功能a
0dc03f1 HEAD@{4}: checkout: moving from feat/B to feat/A
1b2dd75 (feat/B) HEAD@{5}: commit: feat: 添加功能b
0dc03f1 HEAD@{6}: checkout: moving from feat/A to feat/B
0dc03f1 HEAD@{7}: checkout: moving from master to feat/A

$ git reset --hard HEAD@{1}
HEAD is now at 7c8f468 feat: 添加功能a

结果:

image.png

0x007 删除提交历史中的一个大文件/敏感文件

这个功能在我看来有两个用处:

  1. 过去提交了敏感文件,比如密钥,期望把它从提交历史中移除
  2. 提交了大文件,导致整个项目变大,虽然后期把它删除了,但是历史中还是存在的
  • 场景展示
\# 不小心把一个安装包给提交进来了
$ git add .
$ git commit -m 'feat: 完成首页'
\[master aa23c6a\] feat: 完成首页
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 XMind-for-macOS-10.1.0-202003221812.dmg    # 👈不小心添加的大文件
 
 # 尝试删除 安装包
 $ rm XMind-for-macOS-10.1.0-202003221812.dmg
 $ git add .
 $ git commit -m 'feat: 删除安装包'
\[master 68a3be8\] feat: 删除安装包
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 XMind-for-macOS-10.1.0-202003221812.dmg  # 👈删除

明面上看是删除了,但是看看提交历史,这个文件依旧是存在的,如果是密钥文件,那不免有泄露风险。

image.png

并且文件特别大,看看这时候我们项目有多大吧:

image.png

解决方案1:filter-branch

$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch XMind-for-macOS-10.1.0-202003221812.dmg' --prune-empty --tag-name-filter cat -- --all
Rewrite b426e7e5fe1633d4321403e36be5656a99ba4ad8 (1/5) (0 seconds passed, remainRewrite 3bf1398f9010f2358ddb9bb6c29669c11a2fea01 (2/5) (0 seconds passed, remainRewrite 9b9d085b932043a2d9250b864a1cbb1535f82184 (3/5) (0 seconds passed, remainRewrite aa23c6ad093baa8a90264e5d8c761d8cc7b062f8 (4/5) (0 seconds passed, remaining 0 predicted)    rm 'XMind-for-macOS-10.1.0-202003221812.dmg'
Rewrite 68a3be8e878eaba59af800c63856d488de3f3df3 (5/5) (0 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten

再看看历史:

image.png

再看看大小,还是没变啊,咋回事

image.png

继续往下:

$ git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
$ git reflog expire --expire=now --all
$ git gc --prune=now
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (16/16), done.
Total 16 (delta 3), reused 14 (delta 3)

image.png

注意:git filter-branch 有很多陷阱,不再推荐使用它来重写历史。 请考虑使用 git-filter-repo,它是一个 Python 脚本,相比大多数使用 filter-branch 的应用来说,它做得要更好。它的文档和源码可访问https://github.com/newren/git-filter-repo获取。

0x008 招聘

新零售技术事业群-CCO技术部的前端团队,我们致力于Web前端技术的研究,同时也重视全栈的能力、产品的推动。Glue即为胶水,我们粘合了设计与技术、用户和产品的关系,用最酷的技术提升客户服务的体验和小二工作的幸福感。FGT也可看成是Fighting的缩写。我们在听得到炮火的地方战斗,我们要把好阿里用户体验的最后一关!

我们负责的产品线非常丰富,PC、无线、内部CRM、Native等等,无论你是什么技术特质,总能找到合适的应用场景。

我们的业务也很有挑战,整个BU致力于通过产品技术创新驱动服务变革,改变人力密集型的传统服务模式。通过打造一站式服务平台,为企业提供全套客户服务解决方案,提升服务体验的同时极大降低服务成本。 通过输出阿里平台沉淀,构建服务生态圈,为电商服务体系贡献水电煤。

如果你来到我们团队,你将负责: 

  • 各业务前端开发 - 平台易用性与用户体验的持续改进 
  • Web前沿技术研究和新技术调研  

职位要求:  

  • 精通各种前端技术,同时具备 PC/无线 端的开发能力,有复杂产品的开发经验。  
  • 掌握常见性能优化的方法,以及衡量产品性能的方法。  
  • 对 MVC/MVVM 等模式有一定的理解,熟悉 React/Vue 等热门框架,有实际项目经验。  
  • 对前端工程化有一定理解,熟练掌握 Webpack/Grunt/Gulp 等构建工具的使用和配置。 ‘
  • 熟悉 ES6/Node.js 并且具备一定的开发能力。

简历投递地址:

  • cangxiu.lyx@alibaba-inc.com

微信(不投简历交个朋友也行哦):

IMG_2725.JPG

查看原文

赞 0 收藏 0 评论 0

followWinter 发布了文章 · 2020-04-17

每一场面试都是人生的一次战役(最后有福利)

〇、概述

一个优秀的工程师,必先是个优秀的程序员,一个优秀的程序员,必先是个优秀的人。这篇文章并不讲述如何在短时间内拿到 offer,而是讲述我作为一个普通的技术人,如何将面试当作人生规划的一部分,一步一步达到自己的目标。其时间跨度是一年、三年、五年,而不是一个月、两个月。

一、产品化思维分析自我,建立能力模型

苏格拉底提出“认识你自己”,在这里,我们要做的也是“认识我们自己”。当然不是为了解答“你是谁,你从哪里来,往哪里去?”的哲学问题,而是使用产品化的思维去分析你自己,将自己作为人才市场上竞争的产品,从而建立能力模型。

  1. 要分析什么

    其实就是分析自己的亮点、缺点。作为技术人,分析的就是我们在技术上所擅长的,比如,框架熟悉的是 React、Vue 还是 Angular?其熟悉程度如何?能否解答大部分常见问题?是否有自己的独立见解和实践沉淀?和市场上其他人同质化能力相比,我的能力有何竞争力?

  2. 为何要分析

    分析的目标是为了找出自己的竞争点,明白自己的价值在哪里,从而建立自己的能力模型。在学习过程中加强优点,补足缺点。在面试过程扬长避短,紧抓优点,规避不足和缺点。

  3. 如何分析

    • 列出自己的亮点
    • 找出自己亮点中的不足
    • 列出市场需要的能力你却没有或者不够的(缺点)
    • 针对前面 a、b、c 列出提升方案或者解决方案

亮点和不足的 ?:我觉得我的亮点是对 React 很熟悉,但是对 React 的实现原理不够了解。那么我就可以列一个关于 React 的提升计划,专攻 React,比如攻读源码,让这个能力更加具备稀缺性。

缺点的 ?:项目开发中常常需要使用网络相关的知识,但是我除了 axios 调用啥也不会。那么我可以列一个关于网络知识的学习计划,不足这块的不足。

二、工程化方法规划方向,重在扬长补短

其实分析出了我们的能力模型之后,我们就知道下一步该怎么做了,四个字,扬长补短

想要让亮点成为真正在市场上有竞争力的亮点,那就必须提高它的稀缺性。要提高他的稀缺性,首先就是要消灭亮点中的不足。

就像亮点和不足的 ? 中的亮点是熟悉 React,而不足是对 React 的原理不够熟悉。那么我们可以用工厂化的方法规划,并执行落地(以下是一个 ?):

熟悉 React.png

注意 3 点:

  1. 树立里程碑,也就是每一根鱼骨,达成之后让自己有成就感

2. 要有沉淀作为规划的支撑,可以是项目,也可以是文章

  1. 规划都在纸上,实践要在脚下

三、重视简历维护,让简历可持续发展

很多人认为简历不过是面试时候的一张纸,但在我看来,简历是你的人生在某个时间点的切面。简历描绘的是你的能力模型,简历是什么样子的,那么你就是什么样子的。那么如何维护简历呢?

  1. 简历中寸土寸金,每一句话都要有意义,有内容,要紧扣你的亮点,重点突出,有实践支撑。 比如亮点是熟悉 React,那么可以有 React 博客、React 项目、React 开源工具作为沉淀输出
  2. 结合规划,跨越一个里程碑,就更新你的简历相关信息。 比如不足是对 React 原理不熟悉,但是当你补足这个不足的时候,就可以更新对 React 的描述了
  3. 宁缺毋滥,宜少不宜多,宜精不宜泛。 以前我喜欢在简历上罗列一大堆技术栈,显示自己经验丰富,但是发现,列得越多,打击面越广。所以“了解 Spring Cloud,MySql,RabbitMQ...”之类的和前端不太相关的技术描述我就变成了“有过后端开发经验,了解常见技术后端技术”,将可能的坑变成了优势。
  4. 面试之后复盘也要更新简历。 当我们面试之后发现简历上的一些描述给自己埋坑或者不符合,面试之后即刻修改。如 3 的 ?。

四、面试前的准备

  1. 通用题

    通用题其实就是体现个人基本素质的地方。所谓通用题,就是自我介绍,项目介绍,职业规划说明等。看过去像是闲聊,但其实考察已经开始了。每一句话都体现了一个人的知识,素养。所以,推荐打好草稿,并练习,记住,时刻围绕自己建立的能力模型和简历。

  2. 技术题
  • 常见面试题必会
  • 技术要有自己的见解、实践、总结
  • 算法、设计模式之类的属于积硅步致千里,需要始于足下,慢慢沉淀
  1. 询问题
    面试官通常会给你一个机会,让你问他一些问题,这些问题可以准备一下,比如:

    • 团队情况
    • 项目情况
    • 你的职位定位
    • 公司福利
    • ...
  2. 题库

    整理自己的题库,包含上面提到的三种类型的题目

五、面试中的表达

接触过一些面试官,他们说其实有些候选人很优秀,但是不知道如何去表达。我在面试中的表达总结出了以下三条:

  1. 回答前想几秒,打草稿

    得到问题之后先不急于作答,而是先分析面试官所问的问题的目的是什么?然后打个小草稿,如果之前总结过那就更好了

  2. 回答时有条理,可以按总分总

    通常面试官会做一些记录,那么如果你的回答太过泛,而没有中心,则就不知道该记录啥了。举个例子,我喜欢这么说:关于这个问题,我有三个看法,1、xxx,2、xxx,3、xxx,其中 1 说的是 xxx,2 说的是 xxx,3 说的是 xxx。总之 xxx。

    先抛结论,再详细说明,最后再总结。临时发挥其实有点困难,但是如果养成习惯,或者打草稿并刻意练习,还是可以的。

  3. 始终围绕自己的亮点,不要给自己埋坑

    面试中最怕的就是给自己挖坑,比如回答问题的时候引用了自己不熟悉的技术,让整个面试的走向走到了自己不熟悉的领域。所以我们才要分析自己的能力模型,明白自己的优势和劣势,同时对于常见问题需要打个草稿并练习。要将整场面试掌控在自己的领域内,首先先不要给自己挖坑,然后是引领面试官到自己的领域。

六、面试后的复盘

面试时候记录,面试后回顾:

  • 如果是通用题答得不好,就修改好以后添加到自己的题库去
  • 如果是技术题答得不好,就再去深入这个技术之后,总结、沉淀,确保下次能够很好的回答

七、心态

  1. 失败

    失败是常态,成功是运气,或者是趟过了所有的坑积累出来的,就和 bug 一样,重在复盘和解决,让自己不会再一次踏入这个坑

  2. 放弃

    放弃是常态,始终坚持才是不正常的,我基本一周要对人生放弃一次,但是务必找到方法让自己重新振作。比如我用规划来约束自己,达到了目标,那放弃一个周末也没啥大事。

八、总结

  1. 长期有规划
  2. 中期产沉淀
  3. 短期多准备

九、福利

内推海报 copy.png

查看原文

赞 1 收藏 0 评论 0

followWinter 收藏了文章 · 2020-01-06

前端通用国际化解决方案

文章首发于个人blog,欢迎大家关注。

DI18n

前端通用国际化解决方案

背景

前端技术日新月异,技术栈繁多。以前端框架来说有React, Vue, Angular等等,再配以webpack, gulp, Browserify, fis等等构建工具去满足日常的开发工作。同时在日常的工作当中,不同的项目使用的技术栈也会不一样。当需要对部分项目进行国际化改造时,由于技术栈的差异,这时你需要去寻找和当前项目使用的技术栈相匹配的国际化的插件工具。比如:

  • vue + vue-i18n
  • angular + angular-translate
  • react + react-intl
  • jquery + jquery.i18n.property

等等,同时可能有些页面没有使用框架,或者完全是没有进行工程化的静态前端页面。

为了减少由于不同技术栈所带来的学习相关国际化插件的成本及开发过程中可能遇到的国际化坑,在尝试着分析前端国际化所面临的主要问题及相关的解决方案后,我觉得是可以使用更加通用的技术方案去完成国际化的工作。

国际化所面临的问题

1.语言翻译

  • 静态文案翻译(前端静态模板文案)
  • 动态文案翻译(server端下发的动态数据)

2.样式

  • 不同语言文案长度不一样造成的样式错乱
  • 图片的替换

3.map表维护

4.第三方服务

  • SDK

5.本地化

  • 货币单位
  • 货币汇率
  • 时间格式

6.打包方案

  • 运行时
  • 编译后

解决方案

在日常的开发过程当中,遇到的最多的需要国际化的场景是:语言翻译,样式,map表维护打包方案。接下来针对这几块内容并结合日常的开发流程说明国际化的通用解决方案。

首先来看下当前开发环境可能用的技术栈:

1.使用了构建工具

  • webpack
  • gulp
  • fis
  • browserify
  • ...

基于这些构建工具,使用:

  • Vue
  • Angular
  • React
  • Backbone
  • ...
  • 未使用任何framework

2.未使用构建工具

  • 使用了jqueryzepto等类库
  • 原生js

其中在第一种开发流程当中,可用的国际化的工具可选方案较多:

从框架层面来看,各大框架都会有相对应的国际化插件,例如:vue-i18n, angular-translate, react-intl等,这些插件可以无缝接入当前的开发环节当中。优点是这些框架层面的国际化插件使用灵活,可以进行静态文案的翻译,动态文案的翻译。缺点就是开发过程中使用不同的框架还需要去学习相对应的插件,存在一定的学习成本,同时在业务代码中可能存在不同语言包判断逻辑。

从构建工具层面来看, webpack有相对应的i18n-webpack-plugin, gulpgulp-static-i18n等相应的插件。这些插件的套路一般都是在你自定义map语言映射表,同时根据插件定义好的需要被编译的代码格式,然后在代码的编译阶段,通过字符串匹配的形式去完成静态文案的替换工作。这些插件仅仅解决了静态文案的问题,比如一些样式,图片替换,class属性,以及动态文案的翻译等工作并没有做。
事实上,这些插件在编译过程中对于样式图片替换, class属性等替换工作是非常容易完成的,而动态文案的翻译因为缺少context,所以不会选择使用这些编译插件去完成动态文案的翻译工作。相反,将动态文案的翻译放到运行时去完成应该是更加靠谱的。

但是换个角度,抛开基于这些构建工具进行开发的框架来说,构建工具层面的国际化插件可以很好的抹平使用不同框架的差异,通过将国际化的过程从运行时转到编译时,在编译的过程中就完成大部分的国际化任务,降低学习相对应国际化插件的成本,同时在构建打包环节可实现定制化。不过也存在一定的缺点,就是这些构建工具层面的国际化插件只能完成一些基本的静态文案的翻译,因为缺少context,并不能很好的去完成动态文案的翻译工作,它比较适用于一些纯静态,偏展示性的网页。

在第二种开发流程当中,可使用的国际化工具较少,大多都会搭配jquery这些类库及相对应的jquery.i18ni18next等插件去完成国际化。

综合不同的构建工具,开发框架及类库,针对不同的开发环境似乎是可以找到一个比较通用的国际化的方案的。

这个方案的大致思路就是:通过构建工具去完成样式, 图片替换, class属性等的替换工作,在业务代码中不会出现过多的因国际化而多出的变量名,同时使用一个通用的翻译函数去完成静态文案动态文案的翻译工作,而不用使用不同框架提供的相应的国际化插件。简单点来说就是:

  • 依据你使用的构建工具 + 一个通用的翻译函数去完成前端国际化

首先,这个通用的语言翻译函数: di18n-translate。它所提供的功能就是静态和动态文案的翻译, 不依赖开发框架及构建工具。

  npm install di18n-translate
// 模块化写法
  const LOCALE = 'en'
  const DI18n = require('di18n-translate')
  const di18n = new DI18n({
    locale: LOCALE,     // 语言环境 
    isReplace: false,   // 是否开始运行时(适用于没有使用任何构建工具开发流程) 
    messages: {         // 语言映射表 
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

  di18n继承于一个翻译类,提供了2个方法`$t`, `$html`:
 
  di18n.$t('你好', {person: 'xl'})   // 输出: Hello, xl
  di18n.$html(htmlTemp)   // 传入字符串拼接的dom, 返回匹配后的字符串,具体示例可见下文

// 外链形式
  <script data-original="./lib/di18n-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: false,
      messages: {
        // 语言包
      }
    })
  </script>

这个时候你只需要将这个通用的翻译函数以适当的方式集成到你的开发框架当中去。

接下来会结合具体的不同场景去说明下相应的解决方案:

使用MVVM类的framework

使用了MVVM类的framework时,可以借助framework帮你完成view层的渲染工作, 那么你可以在代码当中轻松的通过代码去控制class的内容, 以及不同语言环境下的图片替换工作.

例如vue, 示例(1):


main.js文件:

window.LOCALE = 'en'

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`)
        }
      }
    }
  </script>

这个时候你再加入翻译函数,就可以满足大部分的国际化的场景了,现在在main.js中添加对翻译函数di18n-translate的引用:

main.js文件:

import Vue from 'vue'

window.LOCALE = 'en'
const DI18n = require('di18n-translate')
const di18n = new DI18n({
    locale: LOCALE,       // 语言环境
    isReplace: false,   // 是否进行替换(适用于没有使用任何构建工具开发流程)
    messages: {         // 语言映射表
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

Vue.prototype.d18n = di18n

翻译函数的基本使用, 当然你还可以使用其他的方式集成到你的开发环境当中去:

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
    <p>{{title}}</p>
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`),
          title: this.di18n.$t('你好')
        }
      }
    }
  </script>

使用mvvm framework进行国际化,上述方式应该是较为合适的,主要是借助了framework帮你完成view层的渲染工作, 然后再引入一个翻译函数去完成一些动态文案的翻译工作

这种国际化的方式算是运行时处理,不管是开发还是最终上线都只需要一份代码。

当然在使用mvvm framework的情况下也是可以不借助framework帮我们完成的view层的这部分的功能,而通过构建工具去完成, 这部分的套路可以参见下午的示例3

未使用mvvm框架,使用了构建工具(如webpack/gulp/browserify/fis)

使用了前端模板

国际化的方式和上面说的使用mvvm框架的方式一致,因为有模板引擎帮你完成了view层的渲染.所以对于样式图片class属性的处理可以和上述方式一致, 动态文案的翻译需引入翻译函数。

这种国际化的方式也算是运行时处理,开发和最终上线都只需要一份代码。

没有使用前端模板

因为没用使用前端模板,便少了对于view层的处理。这个时候你的DOM结构可能是在html文件中一开始就定义好的了,也可能是借助于webpack这样能允许你使用模块化进行开发,通过js动态插入DOM的方式。

接下来我们先说说没有借助webpack这样允许你进行模块化开发的构建工具,DOM结构直接是在html文件中写死的项目。这种情况下你失去了对view层渲染能力。那么这种情况下有2种方式去处理这种情况。

第一种方式就是可以在你自己的代码中添加运行时的代码。大致的思路就是在DOM层面添加属性,这些属性及你需要翻译的map表所对应的key值:

示例(2):

html文件:

  <div class="wrapper" i18n-class="${locale}">
    <img i18n-img="/images/${locale}/test.png">
    <input i18n-placeholder="你好">
    <p i18n-content="你好"></p>
  </div>

运行时:

  <script data-original="[PATH]/di18-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: true,   // 开启运行时
      messages: {
        en: {
          你好: 'Hello'
        },
        zh: {
          你好: '你好'
        }
      }
    })
  </script>

最后html会转化为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <input placeholder="Hello">
    <p>Hello</p>
  </div>

第二种方式就是借助于构建工具在代码编译的环节就完成国际化的工作,以webpack为例:

示例(3):

html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

这个时候使用了一个webpackpreloader: locale-path-loader,它的作用就是在编译编译前,就通过webpack完成语言环境的配置工作,在你的业务代码中不会出现过多的关于语言环境变量以及很好的解决了运行时作为cssbackground的图片替换工作, 具体的locale-path-loader文档请戳我

使用方法:

  npm install locale-path-loader

webpack 1.x 配置:

  module.exports = {
    ....
    preLoaders: [
      {
        test: /\.*$/,
        exclude: /node_modules/,
        loaders: [
          'eslint',
          'locale-path?outputDir=./src/common&locale=en&inline=true'
        ]
      } 
    ]
    ....
  }

webpack 2 配置:

  module.exports = {
    ....
    module: {
      rules: [{
        test: /\.*$/,
        enforce: 'pre',
        exclude: /node_modules/,
        use: [{
          loader: 'locale-path-loader',
          options: {
            locale: 'en',
            outputDir: './src/common',
            inline: true
          }
        }]
      }]
    }
    ....
  }

经过webpackpreloader处理后,被插入到页面中的DOM最后成为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

但是使用这种方案需要在最后的打包环节做下处理,因为通过preloader的处理,页面已经被翻译成相对应的语言版本了,所以需要通过构建工具以及改变preloader的参数去输出不同的语言版本文件。当然构建工具不止webpack这一种,不过这种方式处理的思路是一致的。
这种方式属于编译时处理,开发时只需要维护一份代码,但是最后输出的时候会输出不同语言包的代码。当然这个方案还需要服务端的支持,根据不同语言环境请求,返回相对应的入口文件。关于这里使用webpack搭配locale-path-loader进行分包的内容可参见vue-demo:

|--deploy
  |   |
  |   |---en
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---zh
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---jp
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |----lang.json

接下来继续说下借助构建工具进行模块化开发的项目, 这些项目可能最后页面上的DOM都是通过js去动态插入到页面当中的。那么,很显然,可以在DOM被插入到页面前即可以完成静态文案翻译样式, 图片替换, class属性等替换的工作。

示例(4):
html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

js文件:

  let tpl = require('html!./index.html')
  let wrapper = document.querySelector('.box-wrapper')
  
  // di18n.$html方法即对你所加载的html字符串进行replace,最后相对应的语言版本
  wrapper.innerHTML = di18n.$html(tpl)

最后插入到的页面当中的DOM为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

这个时候动态翻译再借助引入的di18n上的$t方法

  di18n.$t('你好')

这种开发方式也属于运行时处理,开发和上线后只需要维护一份代码。

没有使用任何framework构建工具的纯静态,偏展示性的网页

这类网页的国际化,可以用上面提到的通过在代码中注入运行时来完成基本的国际化的工作, 具体内容可以参见示例(2)以及仓库中的html-demo文件夹。

语言包map表的维护

建议将语言包单独新建文件维护,通过异步加载的方式去获取语言包.

项目地址(如果觉得文章不错,请不要吝啬你的star~~)

请戳我

最后需要感谢 @kenberkeley 同学,之前和他有过几次关于国际化的探讨,同时关于编译时这块的内容,他的有篇文章(请戳我)也给了我一些比较好的思路。

查看原文

followWinter 赞了文章 · 2020-01-06

前端通用国际化解决方案

文章首发于个人blog,欢迎大家关注。

DI18n

前端通用国际化解决方案

背景

前端技术日新月异,技术栈繁多。以前端框架来说有React, Vue, Angular等等,再配以webpack, gulp, Browserify, fis等等构建工具去满足日常的开发工作。同时在日常的工作当中,不同的项目使用的技术栈也会不一样。当需要对部分项目进行国际化改造时,由于技术栈的差异,这时你需要去寻找和当前项目使用的技术栈相匹配的国际化的插件工具。比如:

  • vue + vue-i18n
  • angular + angular-translate
  • react + react-intl
  • jquery + jquery.i18n.property

等等,同时可能有些页面没有使用框架,或者完全是没有进行工程化的静态前端页面。

为了减少由于不同技术栈所带来的学习相关国际化插件的成本及开发过程中可能遇到的国际化坑,在尝试着分析前端国际化所面临的主要问题及相关的解决方案后,我觉得是可以使用更加通用的技术方案去完成国际化的工作。

国际化所面临的问题

1.语言翻译

  • 静态文案翻译(前端静态模板文案)
  • 动态文案翻译(server端下发的动态数据)

2.样式

  • 不同语言文案长度不一样造成的样式错乱
  • 图片的替换

3.map表维护

4.第三方服务

  • SDK

5.本地化

  • 货币单位
  • 货币汇率
  • 时间格式

6.打包方案

  • 运行时
  • 编译后

解决方案

在日常的开发过程当中,遇到的最多的需要国际化的场景是:语言翻译,样式,map表维护打包方案。接下来针对这几块内容并结合日常的开发流程说明国际化的通用解决方案。

首先来看下当前开发环境可能用的技术栈:

1.使用了构建工具

  • webpack
  • gulp
  • fis
  • browserify
  • ...

基于这些构建工具,使用:

  • Vue
  • Angular
  • React
  • Backbone
  • ...
  • 未使用任何framework

2.未使用构建工具

  • 使用了jqueryzepto等类库
  • 原生js

其中在第一种开发流程当中,可用的国际化的工具可选方案较多:

从框架层面来看,各大框架都会有相对应的国际化插件,例如:vue-i18n, angular-translate, react-intl等,这些插件可以无缝接入当前的开发环节当中。优点是这些框架层面的国际化插件使用灵活,可以进行静态文案的翻译,动态文案的翻译。缺点就是开发过程中使用不同的框架还需要去学习相对应的插件,存在一定的学习成本,同时在业务代码中可能存在不同语言包判断逻辑。

从构建工具层面来看, webpack有相对应的i18n-webpack-plugin, gulpgulp-static-i18n等相应的插件。这些插件的套路一般都是在你自定义map语言映射表,同时根据插件定义好的需要被编译的代码格式,然后在代码的编译阶段,通过字符串匹配的形式去完成静态文案的替换工作。这些插件仅仅解决了静态文案的问题,比如一些样式,图片替换,class属性,以及动态文案的翻译等工作并没有做。
事实上,这些插件在编译过程中对于样式图片替换, class属性等替换工作是非常容易完成的,而动态文案的翻译因为缺少context,所以不会选择使用这些编译插件去完成动态文案的翻译工作。相反,将动态文案的翻译放到运行时去完成应该是更加靠谱的。

但是换个角度,抛开基于这些构建工具进行开发的框架来说,构建工具层面的国际化插件可以很好的抹平使用不同框架的差异,通过将国际化的过程从运行时转到编译时,在编译的过程中就完成大部分的国际化任务,降低学习相对应国际化插件的成本,同时在构建打包环节可实现定制化。不过也存在一定的缺点,就是这些构建工具层面的国际化插件只能完成一些基本的静态文案的翻译,因为缺少context,并不能很好的去完成动态文案的翻译工作,它比较适用于一些纯静态,偏展示性的网页。

在第二种开发流程当中,可使用的国际化工具较少,大多都会搭配jquery这些类库及相对应的jquery.i18ni18next等插件去完成国际化。

综合不同的构建工具,开发框架及类库,针对不同的开发环境似乎是可以找到一个比较通用的国际化的方案的。

这个方案的大致思路就是:通过构建工具去完成样式, 图片替换, class属性等的替换工作,在业务代码中不会出现过多的因国际化而多出的变量名,同时使用一个通用的翻译函数去完成静态文案动态文案的翻译工作,而不用使用不同框架提供的相应的国际化插件。简单点来说就是:

  • 依据你使用的构建工具 + 一个通用的翻译函数去完成前端国际化

首先,这个通用的语言翻译函数: di18n-translate。它所提供的功能就是静态和动态文案的翻译, 不依赖开发框架及构建工具。

  npm install di18n-translate
// 模块化写法
  const LOCALE = 'en'
  const DI18n = require('di18n-translate')
  const di18n = new DI18n({
    locale: LOCALE,     // 语言环境 
    isReplace: false,   // 是否开始运行时(适用于没有使用任何构建工具开发流程) 
    messages: {         // 语言映射表 
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

  di18n继承于一个翻译类,提供了2个方法`$t`, `$html`:
 
  di18n.$t('你好', {person: 'xl'})   // 输出: Hello, xl
  di18n.$html(htmlTemp)   // 传入字符串拼接的dom, 返回匹配后的字符串,具体示例可见下文

// 外链形式
  <script data-original="./lib/di18n-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: false,
      messages: {
        // 语言包
      }
    })
  </script>

这个时候你只需要将这个通用的翻译函数以适当的方式集成到你的开发框架当中去。

接下来会结合具体的不同场景去说明下相应的解决方案:

使用MVVM类的framework

使用了MVVM类的framework时,可以借助framework帮你完成view层的渲染工作, 那么你可以在代码当中轻松的通过代码去控制class的内容, 以及不同语言环境下的图片替换工作.

例如vue, 示例(1):


main.js文件:

window.LOCALE = 'en'

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`)
        }
      }
    }
  </script>

这个时候你再加入翻译函数,就可以满足大部分的国际化的场景了,现在在main.js中添加对翻译函数di18n-translate的引用:

main.js文件:

import Vue from 'vue'

window.LOCALE = 'en'
const DI18n = require('di18n-translate')
const di18n = new DI18n({
    locale: LOCALE,       // 语言环境
    isReplace: false,   // 是否进行替换(适用于没有使用任何构建工具开发流程)
    messages: {         // 语言映射表
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

Vue.prototype.d18n = di18n

翻译函数的基本使用, 当然你还可以使用其他的方式集成到你的开发环境当中去:

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
    <p>{{title}}</p>
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`),
          title: this.di18n.$t('你好')
        }
      }
    }
  </script>

使用mvvm framework进行国际化,上述方式应该是较为合适的,主要是借助了framework帮你完成view层的渲染工作, 然后再引入一个翻译函数去完成一些动态文案的翻译工作

这种国际化的方式算是运行时处理,不管是开发还是最终上线都只需要一份代码。

当然在使用mvvm framework的情况下也是可以不借助framework帮我们完成的view层的这部分的功能,而通过构建工具去完成, 这部分的套路可以参见下午的示例3

未使用mvvm框架,使用了构建工具(如webpack/gulp/browserify/fis)

使用了前端模板

国际化的方式和上面说的使用mvvm框架的方式一致,因为有模板引擎帮你完成了view层的渲染.所以对于样式图片class属性的处理可以和上述方式一致, 动态文案的翻译需引入翻译函数。

这种国际化的方式也算是运行时处理,开发和最终上线都只需要一份代码。

没有使用前端模板

因为没用使用前端模板,便少了对于view层的处理。这个时候你的DOM结构可能是在html文件中一开始就定义好的了,也可能是借助于webpack这样能允许你使用模块化进行开发,通过js动态插入DOM的方式。

接下来我们先说说没有借助webpack这样允许你进行模块化开发的构建工具,DOM结构直接是在html文件中写死的项目。这种情况下你失去了对view层渲染能力。那么这种情况下有2种方式去处理这种情况。

第一种方式就是可以在你自己的代码中添加运行时的代码。大致的思路就是在DOM层面添加属性,这些属性及你需要翻译的map表所对应的key值:

示例(2):

html文件:

  <div class="wrapper" i18n-class="${locale}">
    <img i18n-img="/images/${locale}/test.png">
    <input i18n-placeholder="你好">
    <p i18n-content="你好"></p>
  </div>

运行时:

  <script data-original="[PATH]/di18-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: true,   // 开启运行时
      messages: {
        en: {
          你好: 'Hello'
        },
        zh: {
          你好: '你好'
        }
      }
    })
  </script>

最后html会转化为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <input placeholder="Hello">
    <p>Hello</p>
  </div>

第二种方式就是借助于构建工具在代码编译的环节就完成国际化的工作,以webpack为例:

示例(3):

html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

这个时候使用了一个webpackpreloader: locale-path-loader,它的作用就是在编译编译前,就通过webpack完成语言环境的配置工作,在你的业务代码中不会出现过多的关于语言环境变量以及很好的解决了运行时作为cssbackground的图片替换工作, 具体的locale-path-loader文档请戳我

使用方法:

  npm install locale-path-loader

webpack 1.x 配置:

  module.exports = {
    ....
    preLoaders: [
      {
        test: /\.*$/,
        exclude: /node_modules/,
        loaders: [
          'eslint',
          'locale-path?outputDir=./src/common&locale=en&inline=true'
        ]
      } 
    ]
    ....
  }

webpack 2 配置:

  module.exports = {
    ....
    module: {
      rules: [{
        test: /\.*$/,
        enforce: 'pre',
        exclude: /node_modules/,
        use: [{
          loader: 'locale-path-loader',
          options: {
            locale: 'en',
            outputDir: './src/common',
            inline: true
          }
        }]
      }]
    }
    ....
  }

经过webpackpreloader处理后,被插入到页面中的DOM最后成为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

但是使用这种方案需要在最后的打包环节做下处理,因为通过preloader的处理,页面已经被翻译成相对应的语言版本了,所以需要通过构建工具以及改变preloader的参数去输出不同的语言版本文件。当然构建工具不止webpack这一种,不过这种方式处理的思路是一致的。
这种方式属于编译时处理,开发时只需要维护一份代码,但是最后输出的时候会输出不同语言包的代码。当然这个方案还需要服务端的支持,根据不同语言环境请求,返回相对应的入口文件。关于这里使用webpack搭配locale-path-loader进行分包的内容可参见vue-demo:

|--deploy
  |   |
  |   |---en
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---zh
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---jp
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |----lang.json

接下来继续说下借助构建工具进行模块化开发的项目, 这些项目可能最后页面上的DOM都是通过js去动态插入到页面当中的。那么,很显然,可以在DOM被插入到页面前即可以完成静态文案翻译样式, 图片替换, class属性等替换的工作。

示例(4):
html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

js文件:

  let tpl = require('html!./index.html')
  let wrapper = document.querySelector('.box-wrapper')
  
  // di18n.$html方法即对你所加载的html字符串进行replace,最后相对应的语言版本
  wrapper.innerHTML = di18n.$html(tpl)

最后插入到的页面当中的DOM为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

这个时候动态翻译再借助引入的di18n上的$t方法

  di18n.$t('你好')

这种开发方式也属于运行时处理,开发和上线后只需要维护一份代码。

没有使用任何framework构建工具的纯静态,偏展示性的网页

这类网页的国际化,可以用上面提到的通过在代码中注入运行时来完成基本的国际化的工作, 具体内容可以参见示例(2)以及仓库中的html-demo文件夹。

语言包map表的维护

建议将语言包单独新建文件维护,通过异步加载的方式去获取语言包.

项目地址(如果觉得文章不错,请不要吝啬你的star~~)

请戳我

最后需要感谢 @kenberkeley 同学,之前和他有过几次关于国际化的探讨,同时关于编译时这块的内容,他的有篇文章(请戳我)也给了我一些比较好的思路。

查看原文

赞 91 收藏 146 评论 19

followWinter 收藏了文章 · 2019-12-03

布隆过滤器你值得拥有的开发利器

在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问我的个人博客 ——全栈修仙之路

一、布隆过滤器简介

当你往简单数组或列表中插入新数据时,将不会根据插入项的值来确定该插入项的索引值。这意味着新插入项的索引值与数据值之间没有直接关系。这样的话,当你需要在数组或列表中搜索相应值的时候,你必须遍历已有的集合。若集合中存在大量的数据,就会影响数据查找的效率。

针对这个问题,你可以考虑使用哈希表。利用哈希表你可以通过对 “值” 进行哈希处理来获得该值对应的键或索引值,然后把该值存放到列表中对应的索引位置。这意味着索引值是由插入项的值所确定的,当你需要判断列表中是否存在该值时,只需要对值进行哈希处理并在相应的索引位置进行搜索即可,这时的搜索速度是非常快的。

bf-array-vs-hashtable.jpg

根据定义,布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。“可能” 表示有一定的概率,也就是说可能存在一定为误判率。那为什么会存在误判呢?下面我们来分析一下具体的原因。

布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0,如下图所示。

bf-bit-vector.jpg

为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。在前面所提到的哈希表中,我们使用的是单个哈希函数,因此只能输出单个索引值。而对于布隆过滤器来说,我们将使用多个哈希函数,这将会产生多个索引值。

bf-input-hash.jpg

如上图所示,当输入 “semlinker” 时,预设的 3 个哈希函数将输出 2、4、6,我们把相应位置 1。假设另一个输入 ”kakuqo“,哈希函数输出 3、4 和 7。你可能已经注意到,索引位 4 已经被先前的 “semlinker” 标记了。此时,我们已经使用 “semlinker” 和 ”kakuqo“ 两个输入值,填充了位向量。当前位向量的标记状态为:

bf-input-hash-1.jpg

当对值进行搜索时,与哈希表类似,我们将使用 3 个哈希函数对 ”搜索的值“ 进行哈希运算,并查看其生成的索引值。假设,当我们搜索 ”fullstack“ 时,3 个哈希函数输出的 3 个索引值分别是 2、3 和 7:

bf-input-hash-2.jpg

从上图可以看出,相应的索引位都被置为 1,这意味着我们可以说 ”fullstack“ 可能已经插入到集合中。事实上这是误报的情形,产生的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):

bf-fpp.jpg

  • n 是已经添加元素的数量;
  • k 哈希的次数;
  • m 布隆过滤器的长度(如比特数组的大小)。

极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n

实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:

bf-bit-vector-length.jpg

对于 m/n 比率表示每一个元素需要分配的比特位的数量,也就是哈希函数 k 的数量可以调整误判率。通过如下公式来选择最佳的 k 可以减少误判率(FPP):

bf-hash-fn-count.jpg

了解完上述的内容之后,我们可以得出一个结论,当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。

二、布隆过滤器应用

在实际工作中,布隆过滤器常见的应用场景如下:

  • 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
  • Google Chrome 使用布隆过滤器识别恶意 URL;
  • Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。

除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。

利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。

三、布隆过滤器实战

布隆过滤器有很多实现和优化,由 Google 开发著名的 Guava 库就提供了布隆过滤器(Bloom Filter)的实现。在基于 Maven 的 Java 项目中要使用 Guava 提供的布隆过滤器,只需要引入以下坐标:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.0-jre</version>
</dependency>

在导入 Guava 库后,我们新建一个 BloomFilterDemo 类,在 main 方法中我们通过 BloomFilter.create 方法来创建一个布隆过滤器,接着我们初始化 1 百万条数据到过滤器中,然后在原有的基础上增加 10000 条数据并判断这些数据是否存在布隆过滤器中:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDemo {
    public static void main(String[] args) {
        int total = 1000000; // 总数量
        BloomFilter<CharSequence> bf = 
          BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 1000000 条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }
}

当以上代码运行后,控制台会输出以下结果:

已匹配数量 1000309

很明显以上的输出结果已经出现了误报,因为相比预期的结果多了 309 个元素,误判率为:

309/(1000000 + 10000) * 100 ≈ 0.030594059405940593

如果要提高匹配精度的话,我们可以在创建布隆过滤器的时候设置误判率 fpp:

BloomFilter<CharSequence> bf = BloomFilter.create(
  Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);

在 BloomFilter 内部,误判率 fpp 的默认值是 0.03:

// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
  return create(funnel, expectedInsertions, 0.03D);
}

在重新设置误判率为 0.0002 之后,我们重新运行程序,这时控制台会输出以下结果:

已匹配数量 1000003

通过观察以上的结果,可知误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。

四、简易版布隆过滤器

为了便于大家理解布隆过滤器,我们来看一下下面简易版布隆过滤器。

package com.semlinker.bloomfilter;

import java.util.BitSet;

public class SimpleBloomFilter {
    private static final int DEFAULT_SIZE = 2 << 24;
    private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61};

    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[seeds.length];

    public SimpleBloomFilter() {
        // 创建多个哈希函数
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    /**
     * 添加元素到布隆过滤器中
     *
     * @param value
     */
    public void put(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断布隆过滤器中是否包含指定元素
     *
     * @param value
     * @return
     */
    public boolean mightContain(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public static void main(String[] args) {
        SimpleBloomFilter bf = new SimpleBloomFilter();
        for (int i = 0; i < 1000000; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < 1000000 + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }

    /**
     * 简单哈希类
     */
    public static class SimpleHash {
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }
}

在 SimpleBloomFilter 类的实现中,我们使用到了 Java util 包中的 BitSet,BitSet 是位操作的对象,值只有 0 或 1 ,内部维护了一个 long 数组,初始只有一个 long,所以 BitSet 最小的容量是 64 位。当随着存储的元素越来越多,BitSet 内部会动态扩容,最终内部是由 N 个 long 值来存储。默认情况下,BitSet 的所有位都是 0。

五、总结

本文主要介绍的布隆过滤器的概念和常见的应用场合,在实战部分我们演示了 Google 著名的 Guava 库所提供布隆过滤器(Bloom Filter)的基本使用,同时我们也介绍了布隆过滤器出现误报的原因及如何提高判断准确性。最后为了便于大家理解布隆过滤器,我们介绍了一个简易版的布隆过滤器 SimpleBloomFilter。

六、参考资源

本人的全栈修仙之路订阅号,会定期分享 Angular、TypeScript、Node.js/Java 、Spring 相关文章,欢迎感兴趣的小伙伴订阅哈!

full-stack-logo

查看原文

followWinter 赞了文章 · 2019-12-03

布隆过滤器你值得拥有的开发利器

在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问我的个人博客 ——全栈修仙之路

一、布隆过滤器简介

当你往简单数组或列表中插入新数据时,将不会根据插入项的值来确定该插入项的索引值。这意味着新插入项的索引值与数据值之间没有直接关系。这样的话,当你需要在数组或列表中搜索相应值的时候,你必须遍历已有的集合。若集合中存在大量的数据,就会影响数据查找的效率。

针对这个问题,你可以考虑使用哈希表。利用哈希表你可以通过对 “值” 进行哈希处理来获得该值对应的键或索引值,然后把该值存放到列表中对应的索引位置。这意味着索引值是由插入项的值所确定的,当你需要判断列表中是否存在该值时,只需要对值进行哈希处理并在相应的索引位置进行搜索即可,这时的搜索速度是非常快的。

bf-array-vs-hashtable.jpg

根据定义,布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。“可能” 表示有一定的概率,也就是说可能存在一定为误判率。那为什么会存在误判呢?下面我们来分析一下具体的原因。

布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0,如下图所示。

bf-bit-vector.jpg

为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。在前面所提到的哈希表中,我们使用的是单个哈希函数,因此只能输出单个索引值。而对于布隆过滤器来说,我们将使用多个哈希函数,这将会产生多个索引值。

bf-input-hash.jpg

如上图所示,当输入 “semlinker” 时,预设的 3 个哈希函数将输出 2、4、6,我们把相应位置 1。假设另一个输入 ”kakuqo“,哈希函数输出 3、4 和 7。你可能已经注意到,索引位 4 已经被先前的 “semlinker” 标记了。此时,我们已经使用 “semlinker” 和 ”kakuqo“ 两个输入值,填充了位向量。当前位向量的标记状态为:

bf-input-hash-1.jpg

当对值进行搜索时,与哈希表类似,我们将使用 3 个哈希函数对 ”搜索的值“ 进行哈希运算,并查看其生成的索引值。假设,当我们搜索 ”fullstack“ 时,3 个哈希函数输出的 3 个索引值分别是 2、3 和 7:

bf-input-hash-2.jpg

从上图可以看出,相应的索引位都被置为 1,这意味着我们可以说 ”fullstack“ 可能已经插入到集合中。事实上这是误报的情形,产生的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):

bf-fpp.jpg

  • n 是已经添加元素的数量;
  • k 哈希的次数;
  • m 布隆过滤器的长度(如比特数组的大小)。

极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n

实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:

bf-bit-vector-length.jpg

对于 m/n 比率表示每一个元素需要分配的比特位的数量,也就是哈希函数 k 的数量可以调整误判率。通过如下公式来选择最佳的 k 可以减少误判率(FPP):

bf-hash-fn-count.jpg

了解完上述的内容之后,我们可以得出一个结论,当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。

二、布隆过滤器应用

在实际工作中,布隆过滤器常见的应用场景如下:

  • 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
  • Google Chrome 使用布隆过滤器识别恶意 URL;
  • Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。

除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。

利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。

三、布隆过滤器实战

布隆过滤器有很多实现和优化,由 Google 开发著名的 Guava 库就提供了布隆过滤器(Bloom Filter)的实现。在基于 Maven 的 Java 项目中要使用 Guava 提供的布隆过滤器,只需要引入以下坐标:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.0-jre</version>
</dependency>

在导入 Guava 库后,我们新建一个 BloomFilterDemo 类,在 main 方法中我们通过 BloomFilter.create 方法来创建一个布隆过滤器,接着我们初始化 1 百万条数据到过滤器中,然后在原有的基础上增加 10000 条数据并判断这些数据是否存在布隆过滤器中:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDemo {
    public static void main(String[] args) {
        int total = 1000000; // 总数量
        BloomFilter<CharSequence> bf = 
          BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 1000000 条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }
}

当以上代码运行后,控制台会输出以下结果:

已匹配数量 1000309

很明显以上的输出结果已经出现了误报,因为相比预期的结果多了 309 个元素,误判率为:

309/(1000000 + 10000) * 100 ≈ 0.030594059405940593

如果要提高匹配精度的话,我们可以在创建布隆过滤器的时候设置误判率 fpp:

BloomFilter<CharSequence> bf = BloomFilter.create(
  Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);

在 BloomFilter 内部,误判率 fpp 的默认值是 0.03:

// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
  return create(funnel, expectedInsertions, 0.03D);
}

在重新设置误判率为 0.0002 之后,我们重新运行程序,这时控制台会输出以下结果:

已匹配数量 1000003

通过观察以上的结果,可知误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。

四、简易版布隆过滤器

为了便于大家理解布隆过滤器,我们来看一下下面简易版布隆过滤器。

package com.semlinker.bloomfilter;

import java.util.BitSet;

public class SimpleBloomFilter {
    private static final int DEFAULT_SIZE = 2 << 24;
    private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61};

    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[seeds.length];

    public SimpleBloomFilter() {
        // 创建多个哈希函数
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    /**
     * 添加元素到布隆过滤器中
     *
     * @param value
     */
    public void put(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断布隆过滤器中是否包含指定元素
     *
     * @param value
     * @return
     */
    public boolean mightContain(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public static void main(String[] args) {
        SimpleBloomFilter bf = new SimpleBloomFilter();
        for (int i = 0; i < 1000000; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < 1000000 + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }

    /**
     * 简单哈希类
     */
    public static class SimpleHash {
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }
}

在 SimpleBloomFilter 类的实现中,我们使用到了 Java util 包中的 BitSet,BitSet 是位操作的对象,值只有 0 或 1 ,内部维护了一个 long 数组,初始只有一个 long,所以 BitSet 最小的容量是 64 位。当随着存储的元素越来越多,BitSet 内部会动态扩容,最终内部是由 N 个 long 值来存储。默认情况下,BitSet 的所有位都是 0。

五、总结

本文主要介绍的布隆过滤器的概念和常见的应用场合,在实战部分我们演示了 Google 著名的 Guava 库所提供布隆过滤器(Bloom Filter)的基本使用,同时我们也介绍了布隆过滤器出现误报的原因及如何提高判断准确性。最后为了便于大家理解布隆过滤器,我们介绍了一个简易版的布隆过滤器 SimpleBloomFilter。

六、参考资源

本人的全栈修仙之路订阅号,会定期分享 Angular、TypeScript、Node.js/Java 、Spring 相关文章,欢迎感兴趣的小伙伴订阅哈!

full-stack-logo

查看原文

赞 53 收藏 32 评论 1

followWinter 收藏了文章 · 2019-11-27

html2image原理简述

前言

看到 TJ 大神 star了dom-to-image,也一直很好奇html怎么转 image

那么就翻下源码,看下是如何实现的,其实一共就不到800行代码,还蛮容易读懂的

工作原理

使用svg的一个特性,允许在<foreignobject>标签中包含任意的html内容。(主要是 XMLSerializer | MDN这个apidom转为svg
所以,为了渲染那个dom节点,你需要采取以下步骤:

  1. 递归 clone 原始的 dom 节点
  2. 获取 节点以及子节点 上的 computed style,并将这些样式添加进新建的style标签中(不要忘记了clone 伪元素的样式)
  3. 嵌入网页字体
  • 找到所有的@font-face
  • 解析URL资源,并下载对应的资源
  • base64编码和内联资源 作为 data: URLS引用
  • 把上面处理完的css rules全部都放进<style>中,并把标签加入到clone的节点中去
  1. 内嵌图片
  • 内联图片src 的url 进 <img>元素
  • 背景图片 使用 background css 属性,类似fonts的使用方式
  1. 序列化 clone 的 dom 节点 为 svg
  2. 将xml包装到<foreignobject>标签中,放入svg中,然后将其作为data: url
  3. 将png内容或原始数据作为uint8array获取,使用svg作为源创建一个img标签,并将其渲染到新创建的canvas上,然后把canvas转为base64
  4. 完成

核心API

import domtoimage from 'dom-to-image'

domtoimage 有如下一些方法:

    * toSvg (`dom` 转 `svg`)
    * toPng (`dom` 转 `png`)
    * toJpeg (`dom` 转 `jpg`)
    * toBlob (`dom` 转 `blob`)
    * toPixelData (`dom` 转 像素数据)

见名知意,名字取得非常好

下面我挑一个toPng来简单解析一下原理,其他的原理也都是类似的


分析 toPng 原理

尽量挑最核心的讲,希望不会显得很繁琐,了解核心思想就好

下面介绍几个核心函数:

  • toPng (包装了draw函数,没啥意义)
  • Draw (dom => canvas)
  • toSvg (dom => svg)
  • cloneNode (clone dom树和css样式)
  • makeSvgDataUri (dom => svg => data:uri)

调用顺序为

toPng 调用 Draw
Draw 调用 toSvg
toSvg 调用 cloneNode

toPng方法:

// 里面其实就是调用了 draw 方法,promise返回的是一个canvas对象
function toPng(node, options) {
    return draw(node, options || {})
        .then(function (canvas) {
            return canvas.toDataURL();
        });
}

Draw方法

function draw(domNode, options) {
    // 将 dom 节点转为 svg(data: url形式的svg)
    return toSvg(domNode, options)    
        // util.makeImage 将 canvas 转为 new Image(uri)
        .then(util.makeImage)
        .then(util.delay(100))
        .then(function (image) {
            var canvas = newCanvas(domNode);
            canvas.getContext('2d').drawImage(image, 0, 0);
            return canvas;
        });

    // 创建一个空的 canvas 节点
    function newCanvas(domNode) {
        var canvas = document.createElement('canvas');
        canvas.width = options.width || util.width(domNode);
        canvas.height = options.height || util.height(domNode);
          ......
        return canvas;
    }
}

toSvg方法

  function toSvg (node, options) {
    options = options || {}
    // 设置一些默认值,如果option是空的话
    copyOptions(options)

    return (
      Promise.resolve(node)
        .then(function (node) {
          // clone dom 树
          return cloneNode(node, options.filter, true)
        })
        // 把字体相关的csstext 全部都新建一个 stylesheet 添加进去
        .then(embedFonts)
        // clone 处理图片啊,background url('')里面的资源,顺便加载好
        .then(inlineImages)
        // 把option 里面的一些 style 放进stylesheet里面
        .then(applyOptions)
        .then(function (clone) {
          // node 节点序列化成 svg
          return makeSvgDataUri(
            clone,
            // util.width 就是 getComputedStyle 获取节点的宽
            options.width || util.width(node),
            options.height || util.height(node)
          )
        })
    )
      // 设置一些默认值
    function applyOptions (clone) {
        ......
      return clone
    }
  }

cloneNode 方法

  function cloneNode (node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve()

    return (
      Promise.resolve(node)
        .then(makeNodeCopy)
        .then(function (clone) {
          return cloneChildren(node, clone, filter)
        })
        .then(function (clone) {
          return processClone(node, clone)
        })
    )
    // makeNodeCopy
    // 如果不是canvas 节点的话,就clone
    // 是的话,就返回 canvas转image的 img 对象
    function makeNodeCopy (node) {
      if (node instanceof HTMLCanvasElement) { return util.makeImage(node.toDataURL()) }
      return node.cloneNode(false)
    }
    // clone 子节点 (如果存在的话)
    function cloneChildren (original, clone, filter) {
      var children = original.childNodes
      if (children.length === 0) return Promise.resolve(clone)

      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone
        }
      )
      // 递归 clone 节点
      function cloneChildrenInOrder (parent, children, filter) {
        var done = Promise.resolve()
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter)
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone)
            })
        })
        return done
      }
    }
    
    // 处理添加dom的css,处理svg
    function processClone (original, clone) {
      if (!(clone instanceof Element)) return clone

      return Promise.resolve()
        // 读取节点的getComputedStyle,添加进css中
        .then(cloneStyle)
        // 获取伪类的css,添加进css
        .then(clonePseudoElements)
        // 读取 input textarea 的value
        .then(copyUserInput)
        // 设置svg 的 xmlns
        // 命名空间声明由xmlns属性提供。此属性表示<svg>标记及其子标记属于名称空间为“http://www.w3.org/2000/svg”的XML方言
        .then(fixSvg)
        .then(function () {
          return clone
        })
下面是这篇的重点 把 html 节点序列化成 svg
  // node 节点序列化成 svg
  function makeSvgDataUri (node, width, height) {
    return Promise.resolve(node)
      .then(function (node) {
        node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')

        // XMLSerializer 对象使你能够把一个 XML 文档或 Node 对象转化或“序列化”为未解析的 XML 标记的一个字符串。
        // 要使用一个 XMLSerializer,使用不带参数的构造函数实例化它,然后调用其 serializeToString() 方法:
        return new XMLSerializer().serializeToString(node)
      })
      // escapeXhtml代码是string.replace(/#/g, '%23').replace(/\n/g, '%0A')
      .then(util.escapeXhtml)
      .then(function (xhtml) {
        return (
          '<foreignObject x="0" y="0" width="100%" height="100%">' +
          xhtml +
          '</foreignObject>'
        )
      })
      // 变成svg
      .then(function (foreignObject) {
        return (
          '<svg xmlns="http://www.w3.org/2000/svg" width="' +
          width +
          '" height="' +
          height +
          '">' +
          foreignObject +
          '</svg>'
        )
      })
      // 变成 data: url
      .then(function (svg) {
        return 'data:image/svg+xml;charset=utf-8,' + svg
      })
  }

参考链接

查看原文

followWinter 赞了文章 · 2019-11-27

html2image原理简述

前言

看到 TJ 大神 star了dom-to-image,也一直很好奇html怎么转 image

那么就翻下源码,看下是如何实现的,其实一共就不到800行代码,还蛮容易读懂的

工作原理

使用svg的一个特性,允许在<foreignobject>标签中包含任意的html内容。(主要是 XMLSerializer | MDN这个apidom转为svg
所以,为了渲染那个dom节点,你需要采取以下步骤:

  1. 递归 clone 原始的 dom 节点
  2. 获取 节点以及子节点 上的 computed style,并将这些样式添加进新建的style标签中(不要忘记了clone 伪元素的样式)
  3. 嵌入网页字体
  • 找到所有的@font-face
  • 解析URL资源,并下载对应的资源
  • base64编码和内联资源 作为 data: URLS引用
  • 把上面处理完的css rules全部都放进<style>中,并把标签加入到clone的节点中去
  1. 内嵌图片
  • 内联图片src 的url 进 <img>元素
  • 背景图片 使用 background css 属性,类似fonts的使用方式
  1. 序列化 clone 的 dom 节点 为 svg
  2. 将xml包装到<foreignobject>标签中,放入svg中,然后将其作为data: url
  3. 将png内容或原始数据作为uint8array获取,使用svg作为源创建一个img标签,并将其渲染到新创建的canvas上,然后把canvas转为base64
  4. 完成

核心API

import domtoimage from 'dom-to-image'

domtoimage 有如下一些方法:

    * toSvg (`dom` 转 `svg`)
    * toPng (`dom` 转 `png`)
    * toJpeg (`dom` 转 `jpg`)
    * toBlob (`dom` 转 `blob`)
    * toPixelData (`dom` 转 像素数据)

见名知意,名字取得非常好

下面我挑一个toPng来简单解析一下原理,其他的原理也都是类似的


分析 toPng 原理

尽量挑最核心的讲,希望不会显得很繁琐,了解核心思想就好

下面介绍几个核心函数:

  • toPng (包装了draw函数,没啥意义)
  • Draw (dom => canvas)
  • toSvg (dom => svg)
  • cloneNode (clone dom树和css样式)
  • makeSvgDataUri (dom => svg => data:uri)

调用顺序为

toPng 调用 Draw
Draw 调用 toSvg
toSvg 调用 cloneNode

toPng方法:

// 里面其实就是调用了 draw 方法,promise返回的是一个canvas对象
function toPng(node, options) {
    return draw(node, options || {})
        .then(function (canvas) {
            return canvas.toDataURL();
        });
}

Draw方法

function draw(domNode, options) {
    // 将 dom 节点转为 svg(data: url形式的svg)
    return toSvg(domNode, options)    
        // util.makeImage 将 canvas 转为 new Image(uri)
        .then(util.makeImage)
        .then(util.delay(100))
        .then(function (image) {
            var canvas = newCanvas(domNode);
            canvas.getContext('2d').drawImage(image, 0, 0);
            return canvas;
        });

    // 创建一个空的 canvas 节点
    function newCanvas(domNode) {
        var canvas = document.createElement('canvas');
        canvas.width = options.width || util.width(domNode);
        canvas.height = options.height || util.height(domNode);
          ......
        return canvas;
    }
}

toSvg方法

  function toSvg (node, options) {
    options = options || {}
    // 设置一些默认值,如果option是空的话
    copyOptions(options)

    return (
      Promise.resolve(node)
        .then(function (node) {
          // clone dom 树
          return cloneNode(node, options.filter, true)
        })
        // 把字体相关的csstext 全部都新建一个 stylesheet 添加进去
        .then(embedFonts)
        // clone 处理图片啊,background url('')里面的资源,顺便加载好
        .then(inlineImages)
        // 把option 里面的一些 style 放进stylesheet里面
        .then(applyOptions)
        .then(function (clone) {
          // node 节点序列化成 svg
          return makeSvgDataUri(
            clone,
            // util.width 就是 getComputedStyle 获取节点的宽
            options.width || util.width(node),
            options.height || util.height(node)
          )
        })
    )
      // 设置一些默认值
    function applyOptions (clone) {
        ......
      return clone
    }
  }

cloneNode 方法

  function cloneNode (node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve()

    return (
      Promise.resolve(node)
        .then(makeNodeCopy)
        .then(function (clone) {
          return cloneChildren(node, clone, filter)
        })
        .then(function (clone) {
          return processClone(node, clone)
        })
    )
    // makeNodeCopy
    // 如果不是canvas 节点的话,就clone
    // 是的话,就返回 canvas转image的 img 对象
    function makeNodeCopy (node) {
      if (node instanceof HTMLCanvasElement) { return util.makeImage(node.toDataURL()) }
      return node.cloneNode(false)
    }
    // clone 子节点 (如果存在的话)
    function cloneChildren (original, clone, filter) {
      var children = original.childNodes
      if (children.length === 0) return Promise.resolve(clone)

      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone
        }
      )
      // 递归 clone 节点
      function cloneChildrenInOrder (parent, children, filter) {
        var done = Promise.resolve()
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter)
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone)
            })
        })
        return done
      }
    }
    
    // 处理添加dom的css,处理svg
    function processClone (original, clone) {
      if (!(clone instanceof Element)) return clone

      return Promise.resolve()
        // 读取节点的getComputedStyle,添加进css中
        .then(cloneStyle)
        // 获取伪类的css,添加进css
        .then(clonePseudoElements)
        // 读取 input textarea 的value
        .then(copyUserInput)
        // 设置svg 的 xmlns
        // 命名空间声明由xmlns属性提供。此属性表示<svg>标记及其子标记属于名称空间为“http://www.w3.org/2000/svg”的XML方言
        .then(fixSvg)
        .then(function () {
          return clone
        })
下面是这篇的重点 把 html 节点序列化成 svg
  // node 节点序列化成 svg
  function makeSvgDataUri (node, width, height) {
    return Promise.resolve(node)
      .then(function (node) {
        node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')

        // XMLSerializer 对象使你能够把一个 XML 文档或 Node 对象转化或“序列化”为未解析的 XML 标记的一个字符串。
        // 要使用一个 XMLSerializer,使用不带参数的构造函数实例化它,然后调用其 serializeToString() 方法:
        return new XMLSerializer().serializeToString(node)
      })
      // escapeXhtml代码是string.replace(/#/g, '%23').replace(/\n/g, '%0A')
      .then(util.escapeXhtml)
      .then(function (xhtml) {
        return (
          '<foreignObject x="0" y="0" width="100%" height="100%">' +
          xhtml +
          '</foreignObject>'
        )
      })
      // 变成svg
      .then(function (foreignObject) {
        return (
          '<svg xmlns="http://www.w3.org/2000/svg" width="' +
          width +
          '" height="' +
          height +
          '">' +
          foreignObject +
          '</svg>'
        )
      })
      // 变成 data: url
      .then(function (svg) {
        return 'data:image/svg+xml;charset=utf-8,' + svg
      })
  }

参考链接

查看原文

赞 9 收藏 20 评论 3

followWinter 发布了文章 · 2019-11-18

[前端漫谈]Git 内部原理 - Git 对象

### 导读
这篇文章是对Git Pro 10.2 Git 内部原理 - Git 对象章节的解读和转化,主要介绍两个东西:1)使用 Git 底层命令完成提交,2)尝试使用 NodeJS 解析 Git 对象(文章中提供的是 Ruby)。

### 0x001 初始化
初始化一个本地仓库:

$ mkdir git-test
$ cd git-test
$ git init
Initialized empty Git repository in ...

查看文件结构:

+ git-test
   + .git
       + branches
       - config
       - description
       - HEAD
       + hooks
       + info
       + objects
           + info
           + pack
       + refs

其他暂时不关注,只关注objects,此时只有infopack两个文件夹,我们也不关注它,我们只关注objects下除了infopack之外的变化。

0x002 hash-object

这个命令用来为一个文件计算对象 ID 并可能创建一个 blob 文件。这里有两层含义:1)计算对象 ID,对象 ID 是什么呢?2)blob 文件是啥?为啥是可能?接下来将给出答案。

执行命令:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w指示hash-object存储数据对象,如果不指定,则返回计算的 objectId。--stdin指示从标准输入读取内容,也就是将test content作为内容计算。

当我们执行这个这个命令的时候,会返回一个 40 个字符长度的 SHA1 哈希值。d670460b4b4aece5915caf5c68d12f560a9fe3e4,由于制定了-wgit 会存储这次的计算,查看objects下的文件:

+ objects
    + d6
        - 70460b4b4aece5915caf5c68d12f560a9fe3e4

会发现,多了一个文件夹d6,而d6中有一个文件70460b4b4aece5915caf5c68d12f560a9fe3e4,这两者拼接起来正好是刚刚生成的objectID

如果我们多次执行这个命令,会发现,这个文件没有发生变化,因为已经存在,这也就是之前说可能生成的原因了。

如果我们改变内容,则会生成一个新的objectID和一个新的blob文件。

0x003 cat-file

我们已经直到如何存储文件,那如何读取呢?可以使用cat-file

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

0x004 文件存储和版本恢复

接下来我们使用文件,而不直接使用内容

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

然后更新这个文件并存储

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

此时的objects

+ objects
    + 1f
        - 7a7a472abf3dd9643fd615f6da379c4acb3e3a
    + 83
        - baae61804e65cc73a7201a7252750c76066a30
    + d6
        - 70460b4b4aece5915caf5c68d12f560a9fe3e4

然后吧文件内容恢复到第一个版本

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

或者第二个版本

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

0x005 树对象 和 write-tree

将文件加入缓存区

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

将缓存区内容写入树对象

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

新建一个新的数对象,包含test.txt的第二个版本和一个新的文件:

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

0x006 提交 commit 和 commit-tree

有了树对象之后,就可以提交该树对象,生成一个commit

$ echo 'first commit' | git commit-tree d8329f
b51096bf62fa145c0b95ce18dc3020daa1f2556e

查看这个commit

$ git cat-file -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

接着提交第二个树对象,并使用-p指定这个提交的上一个commit

$ echo 'second commit' | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
bf41fa3700a67914b3b45eefced02fffcdaf4464

使用git log查看记录

commit bf41fa3700a67914b3b45eefced02fffcdaf4464
Author: lyxxxx <lyxxxx@yeah.net>
Date:   Sun Nov 17 22:14:36 2019 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit b51096bf62fa145c0b95ce18dc3020daa1f2556e
Author: lyxxxx <lyxxxx@yeah.net>
Date:   Sun Nov 17 22:07:01 2019 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

以上,便是使用底层命令创建 Git 提交历史的过程,主要涉及 5 个命令:

  • hash-object:计算objectID,创建blob文件
  • cat-file:读取生产的object文件
  • update-index:更新暂存区文件
  • write-tree:将暂存区文件写入tree文件
  • commit-tree:提交tree文件

0x007 object文件类型

object文件类型一共有三种:

  • blobhash-object生成,表示一个文件
  • treewrite-tree生成,表示缓存区的文件列表
  • commitcommit-tree生成,表示本次提交的文件列表

他们的关系是:

  • commit包含一个tree对象
  • tree包含多个blob对象和tree对象

0x008 objectID如何生成

接下来使用NodeJS演示如何生成objectID,

假设我们要存储的内容是what is up, doc?

const content = 'what is up, doc?'
const type = 'blob'

object对象的存储的格式是:

const store = `${type} ${content.length}\0${content}`

然后计算sh1 值:

const crypto = require('crypto');

const hash = crypto.createHash('sha1');
hash.update(store);
const objectID = hash.digest('hex');

最后计算的结果是:

bd9dbf5aae1a3862dd1526723246b20206e5fc37

接着是存储,在存储的时候,会执行压缩之后再存储:

const zlib = require('zlib');

const result = zlib.deflateSync(Buffer.from(store))

然后按照objectID分割存储到objects文件夹下就行了:

+ objects
    + bd
        - 9dbf5aae1a3862dd1526723246b20206e5fc37

完整源码:

const zlib = require('zlib');
const fs = require('fs');
const Buffer = require('buffer').Buffer
const crypto = require('crypto');

const type = 'blob'
const content = process.argv[2]

const store = `${type} ${content.length}\0${content}`

const hash = crypto.createHash('sha1');
hash.update(store)
const objectID = hash.digest('hex')
const result = zlib.deflateSync(Buffer.from(store))

const path = '.git/objects'
const [a, b, ...file] = objectID
const dirPath = `${path}/${a}${b}`
const filePath = `${dirPath}/${file.join('')}`
fs.mkdirSync(dirPath)
fs.writeFileSync(filePath)

0x009 资源

0x010 带货

最近发现一个好玩的库,作者是个大佬啊--基于 React 的现象级微场景编辑器

查看原文

赞 0 收藏 0 评论 0

followWinter 回答了问题 · 2019-11-17

自调用函数里的this都是指向window吗?

不是,和 this 的指向规则一致。
比如这样:

new function(){
    this.name='inner';
    (()=>{console.log(this===window, this.name)})()
}
// 输出
false "inner"

指向函数实例的 this

关注 4 回答 3

followWinter 收藏了文章 · 2019-11-12

javascript中词法环境、领域、执行上下文以及作业详解

网上有很多文章讲到了javascript词法环境以及执行环境,但是大多数都是说的ES5时期的词法环境,很少是提到了ES6以及最新的ES8中有关词法环境的介绍。相比ES5,ES6以及之后的规范对词法环境有了不一样的说明,甚至在词法环境之外新增了领域(Realms)、作业(Jobs)这两全新概念。这导致我在阅读ES8的规范时遇到了不少问题,虽然最后都解决了,但为此付出不少时间。所以我在这专门把我对词法环境以及领域的理解写出了。我希望通过这篇文章能对正在了解这一方面或对javascript有兴趣的人有所帮助。好了,废话不多说了,开始进入正题。

词法环境(Lexical Environments)

官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。通常,词法环境与ECMAScript代码的特定语法结构相关联,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,并且每次执行这样的代码时都会创建新的词法环境。
环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。它被称为词法环境的环境记录。环境记录也是一种规范类型。规范类型对应于在算法中用来描述ECMAScript语言结构和ECMAScript语言类型的语义的元值。
全局环境是一个没有外部环境的词法环境。全局环境的外部环境引用为null。
模块环境是一个包含模块顶层声明绑定的词法环境。模块环境的外部环境是一个全局环境。
函数环境是一个对应于ECMAScript函数对象调用的词法环境。
上面这些话是官方的说明,我只是稍微简单的翻译了一下(原谅我英语学的不好,都是谷歌的功劳)。
可能光这么说一点都不形象,我举个例子:

var a,b=1;
function foo(){
   var a1,b1;
};
foo();

看上面这一简单的代码,js在执行这段代码的时候做了如下操作:

  1. 创建了一个词法环境我把它记为LE1(这里的LE1其实是一个global environment)。
  2. 确定LE1的环境记录(我在这不细说环境记录,只知道它里面包含了{a,b,foo}标识符的记录,我会在之后详细介绍)。
  3. 设置外部词法环境引用,因为LE1已经在最外面了,于是外部词法环境引用就是null,到此LE1就确立完毕了。
  4. 接着执行代码,当执行到foo()这句话时,js调用了foo函数。此时foo函数是一个FunctionDeclaration,于是js开始执行foo函数。
  5. 创建了一个新的词法环境记为LE2.
  6. 设置LE2的外部词法环境引用,很明显LE2的外部词法环境引用就是LE1
  7. 确定LE2的环境记录{a1,b1} 。
  8. 最后继续执行foo函数,知道函数执行完毕。

注意:所有创建词法环境以及环境记录都是不可见的,编译器内部实现。

用图简单解释一下LE1LE2的关系就是如下:
图画的真是丑

上面的步骤都是简化步骤,当讲解完之后的环境记录、领域、执行上下文、作业时,我会给出一个详细的步骤。

环境记录(Environment Record)

ES8规范中主要使用两种环境记录值:声明性环境记录和对象环境记录。环境记录是一个抽象类,它具有三个具体的子类,分别是声明式环境记录,对象环境记录和全局环境记录。其中全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合。

对象环境记录(Object Environment Record)

每个对象环境记录都与一个对象联系在一起,这个对象被称为绑定对象(binding object)。一个对象环境记录绑定一组字符串标识符名称,直接对应于其绑定对象的属性名称。无论绑定对象自己的和继承的属性的[[Enumerable]]设置如何,它们都包含在集合中。由于可以动态地从对象中添加和删除属性,因此对象环境记录绑定的一组标识符可能会因为任何添加或删除对象属性操作的副作用而改变。即使相应属性的Writable的值为false。因此由于这种副作用而创建的任何绑定都将被视为可变绑定。对象环境记录不存在不可变的绑定。
with语句用到的就是对象环境记录,我们看一下简单的例子:

var withObject={
    a:1,
    foo:function(){
        console.log(this.a);
    }
}

with(withObject){
    a=a+1;
    foo();                    //2
}

在js代码执行到with语句的时候,

  1. 创建新的词法环境。
  2. 接着创建了一个对象环境记录即为OEROER包含withObject这个绑定对象,OER中的字符串标识符名称列表为withObject中的属性«a,foo»,在with语句中的变量操作默认在绑定对象中的属性中优先查找。
  3. OER设置外部词法环境引用。

注意:对象环境记录不是指Object里面的环境记录。普通的Object内部不存在新的环境记录,它的环境记录就是定义该对象所在的环境记录。

声明性环境记录(Declarative Environment Record)

每个声明性环境记录都与包含变量,常量,let,class,module,import和/或function的声明的ECMAScript程序作用域相关联。声明性环境记录绑定了包含在其作用域内声明定义的标识符集。这句话很好理解,举个例子如下:

import x from '***';
var a=1;
let b=1;
const c=1;
function foo(){};
class Bar{};
//这时声明性环境记录中就有了«x,a,b,c,foo,Bar»这样一组标识符,当然实际存放的结构肯定不是这个样子的,还要复杂。

函数环境记录(Function Environment Record)

函数环境记录是一个声明性环境记录,它用来表示function中的顶级作用域,此外如果函数不是一个箭头函数(ArrowFunction),则为这个函数提供一个this绑定。如果一个函数不是一个ArrowFunction函数并引用了super,则它的函数环境记录还包含从该函数内执行super方法调用的状态。
函数环境记录有下列附加的字段

字段名称含义
[[ThisValue]]Any用于该函数调用的this值
[[ThisBindingStatus]]"lexical" ,"initialized" ,"uninitialized"如果值是“lexical”,这是一个ArrowFunction,并且没有一个本地的this值。
[[FunctionObject]]Object一个函数对象,它的调用导致创建该环境记录
[[HomeObject]]Object或者undefined如果关联的函数具有super属性访问权限,并且不是一个ArrowFunction,则[[HomeObject]]是该函数作为方法绑定的对象。 [[HomeObject]]的默认值是undefined。
[[NewTarget]]Object或者undefined如果该环境记录是由[[Construct]]的内部方法创建的,则[[NewTarget]]就是[[Construct]]的newTarget参数的值。否则,它的值是undefined。

我简单介绍一下这些字段,[[ThisValue]]这个字段的值就是函数中的this对象,[[ThisBindingStatus]]中"initialized" ,"uninitialized"看字面意思也知道了,主要是“lexical”这个状态为什么是代表ArrowFunction,我的理解是ArrowFunction中是没有一个本地的this值,所以ArrowFunction中的this引用不是指向调用该函数的对象,而是根据词法环境进行查找,本地没有就向外部词法环境中查找this值,不断向外查找,直到查到this值,所以[[ThisBindingStatus]]的值是“lexical”。看下面例子:

var a = 'global.a';
var obj1 = {
    a:'obj1.a',
    foo: function(){
     console.log(this.a);
    }
}
var obj2 = {
    a:'obj2.a',
    arrow:()=>{
     console.log(this.a);
    }
}
obj1.foo()                  //obj1.a
obj2.arrow()                //global.a不是obj2.a
obj1.foo.bind(obj2)()       //obj2.a
obj2.arrow.bind(obj1)()     //global.a  强制绑定对ArrowFunction没有作用

对ArrowFunction中this的有趣的说法就是:我没有this,你送我个this我也不要,我就喜欢拿别人的this用,this还是别人的好。
[[FunctionObject]]:在上一个例子中指得就是obj1.foo、obj1.arrow。
[[HomeObject]]:只有函数有super访问权限且不是ArrowFunction才有值。看个MDN上的例子:

var obj1 = {
  method1() {
      console.log("method 1");
  }
}

var obj2 = {
  method2() {
      super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2();                          //method 1

//在这里obj2就是[[HomeObject]]
//注意不能这么写:
var obj2 = {
  foo:function method2() {
      super.method1();                 //error,function定义下不能出现super关键字,否则报错。
  }
}                 

[[NewTarget]]:构造函数才有[[Construct]]这个内部方法,如用new关键词调用的函数就会有[[Construct]],newTarget参数我们可以通过new.target在函数中看到。

function newTarget(){
   console.log(new.target);
}

newTarget()             //undefined
new newTarget()         /*function newTarget(){
                              console.log(new.target);
                        }
                        new.target指代函数本身*/

全局环境记录(Global Environment Records)

全局环境记录用于表示在共同领域(Realms)中处理所有共享最外层作用域的ECMAScript Script元素。全局环境记录提供了内置全局绑定,全局对象的属性以及所有在脚本中发生的顶级声明。
全局环境记录有下表额外的字段。

字段名称含义
[[ObjectRecord]]Object Environment Record绑定对象是一个全局对象。它包含全局内置绑定以及关联领域的全局代码中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定。
[[GlobalThisValue]]Object在全局作用域内返回的this值。宿主可以提供任何ECMAScript对象值。
[[DeclarativeRecord]]Declarative Environment Record包含在关联领域的全局代码中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定之外的所有声明的绑定
[[VarNames]]List of String关联领域的全局代码中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration声明绑定的字符串名称。

这里提一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,这也解释了为什么在全局代码中用var、function声明的变量自动的变为全局对象的属性而let、const、class等声明的变量却不会成为全局对象的属性。

模块环境记录(Module Environment Records)

模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部作用域。除了正常的可变和不可变绑定之外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另一个环境记录中存在的目标绑定。

领域(Realms)

在执行ECMAScript代码之前,所有ECMAScript代码都必须与一个领域相关联。从概念上讲,一个领域由一组内部对象,一个ECMAScript全局环境,在该全局环境作用域内加载的所有ECMAScript代码以及其他相关的状态和资源组成。通俗点讲领域就是老大哥,在领域下的小弟都必须等大哥把事情干完才能做。领域被表示为领域记录(Realm Record),有下表的字段:

字段名称含义
[[Intrinsics]]一个记录,它的字段名是内部键,其值是对象与此领域相关的代码使用的内在值。
[[GlobalObject]]Object这个领域的全局对象。
[[GlobalEnv]]Lexical Environment这个领域的全局环境。
[[TemplateMap]]一个记录列表 { [[Strings]]: List, [[Array]]: Object}.模板对象使用Realm Record的[[TemplateMap]]分别对每个领域进行规范化。
[[HostDefined]]Any, 默认值是undefined.保留字段以供需要将附加信息与Realm Record关联的宿主环境使用。

[[Intrinsics]]:我举几个在[[Intrinsics]]中对你来说很熟悉的字段名%Object%(Object构造器),%ObjectPrototype%(%Object%的原型数据属性的初始值),相似的有%Array%(Array构造器),%ArrayPrototype%、%String%、%StringPrototype%、%Function%、%FunctionPrototype%等等的内部方法,可以说全局对象上的属性和方法的值基本都是从[[Intrinsics]]来的(不包括宿主环境提供的属性和方法如:console、location等)。想查看所有的内部方法请查看官方文档内部方法列表

[[GlobalObject]]和[[GlobalEnv]]一目了然,在浏览器中[[GlobalObject]]就是值window了,node中[[GlobalObject]]就是值global。[[HostDefined]] 值宿主环境提供的附加信息。我在这重点说一下[[TemplateMap]]。

[[TemplateMap]]

[[TemplateMap]]是模板在领域中的存储信息,每个模板文字在领域中对应一个唯一的模板对象。具体的模板存储方式我简单说明一下:
在js中模板是用两个反引号(`)进行引用;在js进行解析时模板文字被解释为一系列的Unicode代码点。,具体看如下例子:

var tpObject = {name:'fqf',desc:'programmer'};
var template=`My name is${tpObject.name}. I am a ${tpObject.desc}.`;
//根据模板语法这个模板分三个部分组成:
//TemplateHead:(`My name is${),TemplateMiddle:(}. I am a ${),TemplateTail:(}.)
//tpObject.name,tpObject.desc是表达式,不存储在模板中。
//其中如果模板文字是纯字符串,则这是个NoSubstitutionTemplate。
//js是按顺序解析模板文字,其中`、${、} ${、}、`被认为是空的代码单元序列。
//模板文字被解析成TV(模板值),TRV(模板原始值),它们之间的区别在于TRV中的转义序列被逐字解释,如果你的模板中不带有(\)转义符,你可以认为TV与TRV是一样的。
//具体字符对应的编码存储你可以先对字符做charCodeAt(0),然后通过toString(16)转化为16进制,你就知道对应的编码单元了。

//比如字符a
('a').charCodeAt(0).toString(16);              //61,对应编码就是0x0061

模板文字变成Unicode代码点后,会将Unicode代码点分段存入List,按TemplateHead,TemplateMiddleList,TemplateTail顺序存入(TemplateMiddleList是多个TemplateMiddle组成的顺序列表),具体表示可以是这样«TemplateHead,TemplateMiddle1,TemplateMiddle2,...,TemplateTail»。了解这个之后再来看模板信息具体是如何存入Realms的[[TemplateMap]]中的,步骤如下:

  1. 让rawStrings成为模板按TRV进行解析返回的结果。
  2. 让cookedStrings成为模板按TV进行解析返回的结果。
  3. 让count成为cookedStrings这个List中的元素数量。
  4. 让template成为ArrayCreate(count)。(ArrayCreate)是js用来创建数组的内部方法
  5. 让rawObj成为ArrayCreate(count)。
  6. 让index=0。
  7. 循环,while index<count

    1. 让prop成为ToString(index)。
    2. cookedValue成为cookedStrings[index]。
    3. 调用template.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    4. 让rawValue成为rawStrings[index]。
    5. 调用rawObj.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    6. 让index=index+1。
  8. 冻结rawObj,类似于调用了Object.frozen(rawObj)。
  9. 调用template.[[DefineOwnProperty]]("raw", PropertyDescriptor{[[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false})。
  10. 冻结template。
  11. 添加Record{[[Strings]]: rawStrings, [[Array]]: template}到领域的[[TemplateMap]]中。

每个模板都对应一个唯一且不可变的模板对象,每次获取模板对象都是先从Realms中寻找,如果有返回模板对象,如果没有按上面步骤添加到领域中,再返回模板对象。
所以下列tp1和tp2模板其实对应的是同一个模板对象:

var template='template';
var othertemplate='othertemplate';
var tp1=`This is a ${template}.`;
var tp2=`This is a ${othertemplate}.`;

注:我不是很清楚为什么要把模板信息存入[[TemplateMap]]中,可能是考虑性能的原因。如果有了解这方面的,希望能留言告知。

想进一步了解TV(模板值)和TRV(模板原始值)的不同请戳这里查看具体说明。
到这里领域的描述就告一段落了。开始进入执行上下文也称执行环境的讲解了。

执行上下文(Execution Contexts)

执行上下文是一种规范设备,通过ECMAScript编译器来跟踪代码的运行时评估。在任何时候,每个代理(agent)最多只有一个正在执行代码的执行上下文。这被称为代理的运行执行上下文(running execution context)。本规范中对正在运行的执行上下文(running execution context)的所有引用都表示周围代理的正在运行的执行上下文(running execution context)。
这看起来有点混乱,在这里需要明白一个东西:执行上下文不是表示正在执行的上下文,你可以把它看成一个名词就比较好理解了。
执行上下文栈用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶层元素。每当从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关的可执行代码时新的执行上下文被创建。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
用代码加步骤说明:

1. var a='running execution context';
2. function foo(){
3.     console.log('new running execution context');4.
4. }
5.
6. foo();
7. console.log(a);

我把全局的执行上下文记为ec1,
我把foo函数的执行上下文记为ec2,
执行上下文栈记为recList;
正在运行的执行上下文rec

  1. 首先recList是空的,rec=recList[0]。
  2. 运行全局代码时ec1被创建,并unshift到recList中,recList=[ec1],rec=recList[0]。
  3. 当执行到第6句,进入foo函数里时,ec2被创建并unshift到recList中,recList=[ec2,ec1],rec=recList[0]。
  4. foo函数执行完毕,recList.shift(),ec2从recList中删除,recList=[ec1],rec=recList[0]。
  5. 到第7句执行完毕,ec1从recList中删除,recList又变为空了,rec=recList[0]。

在这里我们可以看到执行上下文之间的转换通常以堆栈式的后进/先出(LIFO)方式进行。
所有执行上下文都有下表的组件:

组件含义
代码评估状态任何需要去执行,暂停和恢复与此执行上下文相关的代码评估状态。
Function如果这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。如果上下文正在评估脚本或模块的代码,则该值为空。
Realm关联代码访问ECMAScript资源的领域记录。
ScriptOrModule模块记录(Module Record)或脚本记录(Script Record)相关代码的来源。如果不存在来源的脚本或模块,则值为null。

正在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具有下表列出的其他状态组件。

组件含义
LexicalEnvironment标识在此执行上下文中用于解析有代码所做的标识符引用的词法环境。
VariableEnvironment标识在此执行上下文中的词法环境,它的环境记录保存了由VariableStatements创建的绑定。

当创建执行上下文时,它的LexicalEnvironment和VariableEnvironment组件最初具有相同的值。

作业和作业队列(Jobs and Job Queues)

作业和领域一样都是ES6新增的东西。作业是一个抽象操作,当没有其他ECMAScript计算正在进行时,它将启动ECMAScript计算。一个作业抽象操作可以被定义为接受任意一组作业参数。只有当没有正在运行的执行上下文并且执行上下文堆栈为空时,才能启动作业的执行。一旦启动了一个作业的执行,作业将始终执行完成。在当前正在运行的作业完成之前,不能启动其他作业。PendingJob是未来执行Job的请求。PendingJob是内部记录,其字段如下表:

字段名称含义
[[Job]]作业抽象操作的名称这是在执行此PendingJob时执行的抽象操作。
[[Arguments]]一个List当[[Job]]激活时要传递给[[Job]]的参数值的列表。
[[Realm]]一个领域记录此PendingJob启动时,最初执行上下文的领域记录。
[[ScriptOrModule]]一个Script Record或Module Record此PendingJob启动时,用于初始执行上下文的脚本或模块。
[[HostDefined]]any,默认undefined保留字段供需要将附加信息与 pending Job相关联的宿主环境使用。

我们可以把[[Job]]看成一个函数,[[Arguments]]是这个函数的参数。
一个作业队列是一个PendingJob记录的FIFO队列。每个作业队列都有一个名称和由ECMAScript编译器定义的一整套可用的作业队列。每个ECMAScript编译器至少具有下表中定义的作业队列。

名称目的
ScriptJobs验证和评估ECMAScript脚本和模块源文本的作业。
PromiseJobs回应一个承诺的解决的作业

Promise的回调就是与PromiseJobs有关。

执行流程

有关javascript中词法环境、领域、执行上下文以及作业,基本简单的介绍了一下。那么ECMAScript编译器怎么把它们之间关联起来的呢,下面我大致写了一个简单的流程:
ECMAScript中有一个RunJobs ( )方法,所有东西的确立都是从这个方法出来的。

  1. 让realm成为CreateRealm()。CreateRealm()主要是创建了一个领域,初始化了领域中字段的值,并返回创建的领域。
  2. 让newContext成为一个新的执行上下文。
  3. 设置newContext的Function为null,newContext的Realm为realm,newContext的ScriptOrModule为null。
  4. 把newContext放到执行上下文栈,现在newContext是一个正在运行的执行上下文。
  5. 执行SetRealmGlobalObject(realm, global, thisValue)方法,正常情况下global为undefined,thisValue为undefined。

    • SetRealmGlobalObject方法执行,我在这里默认global和thisValue为undefined:
    1. 让intrinsics成为realmRec.[[Intrinsics]]。
    2. 让globalObj等于ObjectCreate(intrinsics.[[%ObjectPrototype%]])。
    3. 让thisValue等于globalObj。
    4. 设置realmRec.[[GlobalObject]]是globalObj。
    5. 设置newGlobalEnv为新的词法环境。
    6. 让objRec成为一个新的包含globalObj为绑定对象的对象环境记录。
    7. 让dclRec成为没有任何绑定的新的声明性环境记录。
    8. 让globalRec成为一个新的全局环境记录。
    9. 设置globalRec.[[ObjectRecord]]为objRec,设置globalRec.[[GlobalThisValue]]为 thisValue,设置globalRec.[[DeclarativeRecord]]为dclRec,设置globalRec.[[VarNames]]是一个空的List,设置newGlobalEnv的环境记录为globalRec,newGlobalEnv的外部词法环境为null。
  6. 设置realmRec.[[GlobalEnv]]为newGlobalEnv。
  7. 让globalObj变为SetDefaultGlobalBindings(realm)得返回值。SetDefaultGlobalBindings的方法主要是把realm的[[Intrinsics]]中的内部方法拷贝到全局对象中。
  8. 在globalObj上创建任何编译器定义的全局对象属性。
  9. 依赖编译器方式,在零个或多个ECMAScript脚本和/或ECMAScript模块中获取ECMAScript源文本和任何关联的host-defined的值。为每一个sourceText和hostDefined做如下操作:

    1. 如果sourceText是script的源代码, 那么执行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »)。
    2. 如果sourceText是module的源代码,那么执行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)。
  10. 循环

    1. 挂起正在运行的执行上下文并将其从执行上下文堆栈中移除。
    2. 确定:执行上下文堆栈现在是空的。
    3. 让nextQueue是以编译器定义的方式选择的非空作业队列。如果所有作业队列都为空,则结果是编译器定义的,nextQueue里的记录是上面通过EnqueueJob方法放到作业队列中的记录。
    4. 让nextPending成为nextQueue前面的PendingJob记录。从nextQueue中删除该记录。
    5. 让newContext成为一个新的执行上下文。
    6. 设置newContext的Function为null,newContext的Realm为nextPending.[[Realm]],newContext的ScriptOrModule为nextPending.[[ScriptOrModule]]。
    7. 将newContext推入执行上下文堆栈; newContext现在是正在运行的执行上下文。
    8. 使用nextPending执行任何编译器或宿主环境定义的作业初始化。
    9. 让result成为使用nextPending.[[Arguments]]元素作为nextPending.[[Job]]的参数进行抽象操作的结果,这里指运行上面EnqueueJob中的ScriptEvaluationJob或TopLevelModuleEvaluationJob方法。
    10. 如果result是突然完成的,比如throw扔出异常, 执行HostReportErrors(« result.[[Value]] »),HostReportErrors方法就是报错误的,比如SyntaxError和ReferenceError等。

2017-11-27新增
突然发现这么一长串的步骤不易阅读和理解,我在这做一些笼统的说明:

领域(Realm)只创建一次,领域创建后开始创建全局词法环境(包括全局词法环境中的声明性环境记录和对象环境记录以及全局对象),SetDefaultGlobalBindings方法中global和thisValue为undefined意味着全局环境记录中的[[GlobalThisValue]]就是全局对象(这也表示了在浏览器中全局环境下this就是window对象)。

步骤9中的script中的sourceText表示用<script></script>引入的js代码的Unicode编码。EnqueueJob方法你可以认为是把脚本信息按执行顺序放到队列中。

步骤10,你可以认为是从队列中拿出脚本进行执行(该循环的第9步就是执行脚本(指ScriptEvaluationJob方法),脚本的执行都是在领域和全局词法环境创建之后的)。


我这里说一下ScriptEvaluationJob方法的执行过程(TopLevelModuleEvaluationJob方法只在评估module时运行)正常都是运行的ScriptEvaluationJob方法。
ScriptEvaluationJob ( sourceText, hostDefined ):

  1. 确定: sourceText是ECMAScript源文本。
  2. 让realm成为当前的领域记录。
  3. 让s成为ParseScript(sourceText, realm, hostDefined)。
  4. 如果s是一个errors列表, 那么执行HostReportErrors(s),返回 NormalCompletion(undefined)(一个完成记录值,值为undefined)。
  5. 返回ScriptEvaluation(s)。

ParseScript(sourceText, realm, hostDefined):

  1. 使用脚本解析sourceText作为目标符号,并分析任何早期错误条件的解析结果。如果解析成功并且没有发现早期错误,那么让body成为所得到的分析树,否则body是一个包含一个或多个早期错误的列表。
  2. 如果body是错误列表,则返回body。
  3. 返回脚本记录(Script Record){[[Realm]]: realm, [[Environment]]: undefined, [[ECMAScriptCode]]: body, [[HostDefined]]: hostDefined}。

早期错误有很多,我举个例子:使用关键词作为标识符就是典型的早期错误。

ScriptEvaluation ( scriptRecord )大致流程:

  1. 让globalEnv成为scriptRecord.[[Realm]].[[GlobalEnv]]。
  2. 让scriptCxt成为一个新的ECMAScript代码执行上下文。
  3. 设置scriptCxt的Function为null, scriptCxt的Realm为scriptRecord.[[Realm]],设置scriptCxt的ScriptOrModule为scriptRecord。
  4. 设置VariableEnvironment和LexicalEnvironment为scriptCxt的globalEnv
  5. 挂起当前正在运行的执行上下文。
  6. 把scriptCxt放到执行上下文栈中,scriptCxt是一个正在运行的执行上下文。
  7. 让scriptBody成为scriptRecord.[[ECMAScriptCode]]。
  8. 让result成为运行GlobalDeclarationInstantiation(scriptBody, globalEnv)返回的结果。
  9. 如果result.[[Type]]是normal,那么设置result是执行scriptBody的结果.
  10. 如果result.[[Type]]是normal且result.[[Value]]是empty, 那么设置result为NormalCompletion(undefined).
  11. 挂起scriptCxt并将其从执行上下文堆栈中删除。
  12. 将当前位于执行上下文堆栈顶部的上下文恢复为正在运行的执行上下文。
  13. 返回Completion(result),一个记录值。

GlobalDeclarationInstantiation()方法是对全局环境中的标识符定义进行实例化。比如var、function、let、const、class声明的标识符。该方法执行成功返回的result.[[Type]]为normal。注意这时候的我们能看到的js代码还没有执行,真正执行我们的代码的是步骤9。这也是为什么我们用var和function声明的标识符会出现变量提升(Hoisting)现象。let、const、class声明也在步骤9之前,之所以没有变量提升是因为let、const、class声明的标识符只进行实例化而没有初始化,在下一篇文章中我会重点介绍它们之间的不同之处(所以我认为那些说var和function声明存在变量提升,而let、const、class声明的变量不提升的说法是不对的)。


2017-11-27新增
ScriptEvaluation你可以简单的认为它做了两件:1.对标识符实例化以及初始化,2.执行javascript脚本。

GlobalDeclarationInstantiation方法只对当前脚本的标识符定义进行实例化,不能跨脚本。比如script1在script2之前引用,那么script2中的声明的变量只有通过GlobalDeclarationInstantiation实例化后才能在script1中引用,这也表示var和function声明的标识符不能跨脚本进行变量提升。


结束语

到这里本篇文章也快结束了,本文章所有的说法都是以最新的ECMAScript的语言规范(ES8)为基础。希望这篇文章可以帮助大家更加深入的了解javascript,如果本文有不当之处请指出。还有我不得不吐槽一下ECMAScript的语言规范写得真是太不友好了,看得我心好累啊(说到底还是自己当初在英语课上睡觉的锅)。最后如果你想看ECMAScript的语言规范,那么第5章和第6章一定要看!一定要看!这是一个过来人的忠告。

查看原文

followWinter 赞了文章 · 2019-11-12

javascript中词法环境、领域、执行上下文以及作业详解

网上有很多文章讲到了javascript词法环境以及执行环境,但是大多数都是说的ES5时期的词法环境,很少是提到了ES6以及最新的ES8中有关词法环境的介绍。相比ES5,ES6以及之后的规范对词法环境有了不一样的说明,甚至在词法环境之外新增了领域(Realms)、作业(Jobs)这两全新概念。这导致我在阅读ES8的规范时遇到了不少问题,虽然最后都解决了,但为此付出不少时间。所以我在这专门把我对词法环境以及领域的理解写出了。我希望通过这篇文章能对正在了解这一方面或对javascript有兴趣的人有所帮助。好了,废话不多说了,开始进入正题。

词法环境(Lexical Environments)

官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。通常,词法环境与ECMAScript代码的特定语法结构相关联,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,并且每次执行这样的代码时都会创建新的词法环境。
环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。它被称为词法环境的环境记录。环境记录也是一种规范类型。规范类型对应于在算法中用来描述ECMAScript语言结构和ECMAScript语言类型的语义的元值。
全局环境是一个没有外部环境的词法环境。全局环境的外部环境引用为null。
模块环境是一个包含模块顶层声明绑定的词法环境。模块环境的外部环境是一个全局环境。
函数环境是一个对应于ECMAScript函数对象调用的词法环境。
上面这些话是官方的说明,我只是稍微简单的翻译了一下(原谅我英语学的不好,都是谷歌的功劳)。
可能光这么说一点都不形象,我举个例子:

var a,b=1;
function foo(){
   var a1,b1;
};
foo();

看上面这一简单的代码,js在执行这段代码的时候做了如下操作:

  1. 创建了一个词法环境我把它记为LE1(这里的LE1其实是一个global environment)。
  2. 确定LE1的环境记录(我在这不细说环境记录,只知道它里面包含了{a,b,foo}标识符的记录,我会在之后详细介绍)。
  3. 设置外部词法环境引用,因为LE1已经在最外面了,于是外部词法环境引用就是null,到此LE1就确立完毕了。
  4. 接着执行代码,当执行到foo()这句话时,js调用了foo函数。此时foo函数是一个FunctionDeclaration,于是js开始执行foo函数。
  5. 创建了一个新的词法环境记为LE2.
  6. 设置LE2的外部词法环境引用,很明显LE2的外部词法环境引用就是LE1
  7. 确定LE2的环境记录{a1,b1} 。
  8. 最后继续执行foo函数,知道函数执行完毕。

注意:所有创建词法环境以及环境记录都是不可见的,编译器内部实现。

用图简单解释一下LE1LE2的关系就是如下:
图画的真是丑

上面的步骤都是简化步骤,当讲解完之后的环境记录、领域、执行上下文、作业时,我会给出一个详细的步骤。

环境记录(Environment Record)

ES8规范中主要使用两种环境记录值:声明性环境记录和对象环境记录。环境记录是一个抽象类,它具有三个具体的子类,分别是声明式环境记录,对象环境记录和全局环境记录。其中全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合。

对象环境记录(Object Environment Record)

每个对象环境记录都与一个对象联系在一起,这个对象被称为绑定对象(binding object)。一个对象环境记录绑定一组字符串标识符名称,直接对应于其绑定对象的属性名称。无论绑定对象自己的和继承的属性的[[Enumerable]]设置如何,它们都包含在集合中。由于可以动态地从对象中添加和删除属性,因此对象环境记录绑定的一组标识符可能会因为任何添加或删除对象属性操作的副作用而改变。即使相应属性的Writable的值为false。因此由于这种副作用而创建的任何绑定都将被视为可变绑定。对象环境记录不存在不可变的绑定。
with语句用到的就是对象环境记录,我们看一下简单的例子:

var withObject={
    a:1,
    foo:function(){
        console.log(this.a);
    }
}

with(withObject){
    a=a+1;
    foo();                    //2
}

在js代码执行到with语句的时候,

  1. 创建新的词法环境。
  2. 接着创建了一个对象环境记录即为OEROER包含withObject这个绑定对象,OER中的字符串标识符名称列表为withObject中的属性«a,foo»,在with语句中的变量操作默认在绑定对象中的属性中优先查找。
  3. OER设置外部词法环境引用。

注意:对象环境记录不是指Object里面的环境记录。普通的Object内部不存在新的环境记录,它的环境记录就是定义该对象所在的环境记录。

声明性环境记录(Declarative Environment Record)

每个声明性环境记录都与包含变量,常量,let,class,module,import和/或function的声明的ECMAScript程序作用域相关联。声明性环境记录绑定了包含在其作用域内声明定义的标识符集。这句话很好理解,举个例子如下:

import x from '***';
var a=1;
let b=1;
const c=1;
function foo(){};
class Bar{};
//这时声明性环境记录中就有了«x,a,b,c,foo,Bar»这样一组标识符,当然实际存放的结构肯定不是这个样子的,还要复杂。

函数环境记录(Function Environment Record)

函数环境记录是一个声明性环境记录,它用来表示function中的顶级作用域,此外如果函数不是一个箭头函数(ArrowFunction),则为这个函数提供一个this绑定。如果一个函数不是一个ArrowFunction函数并引用了super,则它的函数环境记录还包含从该函数内执行super方法调用的状态。
函数环境记录有下列附加的字段

字段名称含义
[[ThisValue]]Any用于该函数调用的this值
[[ThisBindingStatus]]"lexical" ,"initialized" ,"uninitialized"如果值是“lexical”,这是一个ArrowFunction,并且没有一个本地的this值。
[[FunctionObject]]Object一个函数对象,它的调用导致创建该环境记录
[[HomeObject]]Object或者undefined如果关联的函数具有super属性访问权限,并且不是一个ArrowFunction,则[[HomeObject]]是该函数作为方法绑定的对象。 [[HomeObject]]的默认值是undefined。
[[NewTarget]]Object或者undefined如果该环境记录是由[[Construct]]的内部方法创建的,则[[NewTarget]]就是[[Construct]]的newTarget参数的值。否则,它的值是undefined。

我简单介绍一下这些字段,[[ThisValue]]这个字段的值就是函数中的this对象,[[ThisBindingStatus]]中"initialized" ,"uninitialized"看字面意思也知道了,主要是“lexical”这个状态为什么是代表ArrowFunction,我的理解是ArrowFunction中是没有一个本地的this值,所以ArrowFunction中的this引用不是指向调用该函数的对象,而是根据词法环境进行查找,本地没有就向外部词法环境中查找this值,不断向外查找,直到查到this值,所以[[ThisBindingStatus]]的值是“lexical”。看下面例子:

var a = 'global.a';
var obj1 = {
    a:'obj1.a',
    foo: function(){
     console.log(this.a);
    }
}
var obj2 = {
    a:'obj2.a',
    arrow:()=>{
     console.log(this.a);
    }
}
obj1.foo()                  //obj1.a
obj2.arrow()                //global.a不是obj2.a
obj1.foo.bind(obj2)()       //obj2.a
obj2.arrow.bind(obj1)()     //global.a  强制绑定对ArrowFunction没有作用

对ArrowFunction中this的有趣的说法就是:我没有this,你送我个this我也不要,我就喜欢拿别人的this用,this还是别人的好。
[[FunctionObject]]:在上一个例子中指得就是obj1.foo、obj1.arrow。
[[HomeObject]]:只有函数有super访问权限且不是ArrowFunction才有值。看个MDN上的例子:

var obj1 = {
  method1() {
      console.log("method 1");
  }
}

var obj2 = {
  method2() {
      super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2();                          //method 1

//在这里obj2就是[[HomeObject]]
//注意不能这么写:
var obj2 = {
  foo:function method2() {
      super.method1();                 //error,function定义下不能出现super关键字,否则报错。
  }
}                 

[[NewTarget]]:构造函数才有[[Construct]]这个内部方法,如用new关键词调用的函数就会有[[Construct]],newTarget参数我们可以通过new.target在函数中看到。

function newTarget(){
   console.log(new.target);
}

newTarget()             //undefined
new newTarget()         /*function newTarget(){
                              console.log(new.target);
                        }
                        new.target指代函数本身*/

全局环境记录(Global Environment Records)

全局环境记录用于表示在共同领域(Realms)中处理所有共享最外层作用域的ECMAScript Script元素。全局环境记录提供了内置全局绑定,全局对象的属性以及所有在脚本中发生的顶级声明。
全局环境记录有下表额外的字段。

字段名称含义
[[ObjectRecord]]Object Environment Record绑定对象是一个全局对象。它包含全局内置绑定以及关联领域的全局代码中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定。
[[GlobalThisValue]]Object在全局作用域内返回的this值。宿主可以提供任何ECMAScript对象值。
[[DeclarativeRecord]]Declarative Environment Record包含在关联领域的全局代码中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定之外的所有声明的绑定
[[VarNames]]List of String关联领域的全局代码中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration声明绑定的字符串名称。

这里提一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,这也解释了为什么在全局代码中用var、function声明的变量自动的变为全局对象的属性而let、const、class等声明的变量却不会成为全局对象的属性。

模块环境记录(Module Environment Records)

模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部作用域。除了正常的可变和不可变绑定之外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另一个环境记录中存在的目标绑定。

领域(Realms)

在执行ECMAScript代码之前,所有ECMAScript代码都必须与一个领域相关联。从概念上讲,一个领域由一组内部对象,一个ECMAScript全局环境,在该全局环境作用域内加载的所有ECMAScript代码以及其他相关的状态和资源组成。通俗点讲领域就是老大哥,在领域下的小弟都必须等大哥把事情干完才能做。领域被表示为领域记录(Realm Record),有下表的字段:

字段名称含义
[[Intrinsics]]一个记录,它的字段名是内部键,其值是对象与此领域相关的代码使用的内在值。
[[GlobalObject]]Object这个领域的全局对象。
[[GlobalEnv]]Lexical Environment这个领域的全局环境。
[[TemplateMap]]一个记录列表 { [[Strings]]: List, [[Array]]: Object}.模板对象使用Realm Record的[[TemplateMap]]分别对每个领域进行规范化。
[[HostDefined]]Any, 默认值是undefined.保留字段以供需要将附加信息与Realm Record关联的宿主环境使用。

[[Intrinsics]]:我举几个在[[Intrinsics]]中对你来说很熟悉的字段名%Object%(Object构造器),%ObjectPrototype%(%Object%的原型数据属性的初始值),相似的有%Array%(Array构造器),%ArrayPrototype%、%String%、%StringPrototype%、%Function%、%FunctionPrototype%等等的内部方法,可以说全局对象上的属性和方法的值基本都是从[[Intrinsics]]来的(不包括宿主环境提供的属性和方法如:console、location等)。想查看所有的内部方法请查看官方文档内部方法列表

[[GlobalObject]]和[[GlobalEnv]]一目了然,在浏览器中[[GlobalObject]]就是值window了,node中[[GlobalObject]]就是值global。[[HostDefined]] 值宿主环境提供的附加信息。我在这重点说一下[[TemplateMap]]。

[[TemplateMap]]

[[TemplateMap]]是模板在领域中的存储信息,每个模板文字在领域中对应一个唯一的模板对象。具体的模板存储方式我简单说明一下:
在js中模板是用两个反引号(`)进行引用;在js进行解析时模板文字被解释为一系列的Unicode代码点。,具体看如下例子:

var tpObject = {name:'fqf',desc:'programmer'};
var template=`My name is${tpObject.name}. I am a ${tpObject.desc}.`;
//根据模板语法这个模板分三个部分组成:
//TemplateHead:(`My name is${),TemplateMiddle:(}. I am a ${),TemplateTail:(}.)
//tpObject.name,tpObject.desc是表达式,不存储在模板中。
//其中如果模板文字是纯字符串,则这是个NoSubstitutionTemplate。
//js是按顺序解析模板文字,其中`、${、} ${、}、`被认为是空的代码单元序列。
//模板文字被解析成TV(模板值),TRV(模板原始值),它们之间的区别在于TRV中的转义序列被逐字解释,如果你的模板中不带有(\)转义符,你可以认为TV与TRV是一样的。
//具体字符对应的编码存储你可以先对字符做charCodeAt(0),然后通过toString(16)转化为16进制,你就知道对应的编码单元了。

//比如字符a
('a').charCodeAt(0).toString(16);              //61,对应编码就是0x0061

模板文字变成Unicode代码点后,会将Unicode代码点分段存入List,按TemplateHead,TemplateMiddleList,TemplateTail顺序存入(TemplateMiddleList是多个TemplateMiddle组成的顺序列表),具体表示可以是这样«TemplateHead,TemplateMiddle1,TemplateMiddle2,...,TemplateTail»。了解这个之后再来看模板信息具体是如何存入Realms的[[TemplateMap]]中的,步骤如下:

  1. 让rawStrings成为模板按TRV进行解析返回的结果。
  2. 让cookedStrings成为模板按TV进行解析返回的结果。
  3. 让count成为cookedStrings这个List中的元素数量。
  4. 让template成为ArrayCreate(count)。(ArrayCreate)是js用来创建数组的内部方法
  5. 让rawObj成为ArrayCreate(count)。
  6. 让index=0。
  7. 循环,while index<count

    1. 让prop成为ToString(index)。
    2. cookedValue成为cookedStrings[index]。
    3. 调用template.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    4. 让rawValue成为rawStrings[index]。
    5. 调用rawObj.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    6. 让index=index+1。
  8. 冻结rawObj,类似于调用了Object.frozen(rawObj)。
  9. 调用template.[[DefineOwnProperty]]("raw", PropertyDescriptor{[[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false})。
  10. 冻结template。
  11. 添加Record{[[Strings]]: rawStrings, [[Array]]: template}到领域的[[TemplateMap]]中。

每个模板都对应一个唯一且不可变的模板对象,每次获取模板对象都是先从Realms中寻找,如果有返回模板对象,如果没有按上面步骤添加到领域中,再返回模板对象。
所以下列tp1和tp2模板其实对应的是同一个模板对象:

var template='template';
var othertemplate='othertemplate';
var tp1=`This is a ${template}.`;
var tp2=`This is a ${othertemplate}.`;

注:我不是很清楚为什么要把模板信息存入[[TemplateMap]]中,可能是考虑性能的原因。如果有了解这方面的,希望能留言告知。

想进一步了解TV(模板值)和TRV(模板原始值)的不同请戳这里查看具体说明。
到这里领域的描述就告一段落了。开始进入执行上下文也称执行环境的讲解了。

执行上下文(Execution Contexts)

执行上下文是一种规范设备,通过ECMAScript编译器来跟踪代码的运行时评估。在任何时候,每个代理(agent)最多只有一个正在执行代码的执行上下文。这被称为代理的运行执行上下文(running execution context)。本规范中对正在运行的执行上下文(running execution context)的所有引用都表示周围代理的正在运行的执行上下文(running execution context)。
这看起来有点混乱,在这里需要明白一个东西:执行上下文不是表示正在执行的上下文,你可以把它看成一个名词就比较好理解了。
执行上下文栈用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶层元素。每当从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关的可执行代码时新的执行上下文被创建。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
用代码加步骤说明:

1. var a='running execution context';
2. function foo(){
3.     console.log('new running execution context');4.
4. }
5.
6. foo();
7. console.log(a);

我把全局的执行上下文记为ec1,
我把foo函数的执行上下文记为ec2,
执行上下文栈记为recList;
正在运行的执行上下文rec

  1. 首先recList是空的,rec=recList[0]。
  2. 运行全局代码时ec1被创建,并unshift到recList中,recList=[ec1],rec=recList[0]。
  3. 当执行到第6句,进入foo函数里时,ec2被创建并unshift到recList中,recList=[ec2,ec1],rec=recList[0]。
  4. foo函数执行完毕,recList.shift(),ec2从recList中删除,recList=[ec1],rec=recList[0]。
  5. 到第7句执行完毕,ec1从recList中删除,recList又变为空了,rec=recList[0]。

在这里我们可以看到执行上下文之间的转换通常以堆栈式的后进/先出(LIFO)方式进行。
所有执行上下文都有下表的组件:

组件含义
代码评估状态任何需要去执行,暂停和恢复与此执行上下文相关的代码评估状态。
Function如果这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。如果上下文正在评估脚本或模块的代码,则该值为空。
Realm关联代码访问ECMAScript资源的领域记录。
ScriptOrModule模块记录(Module Record)或脚本记录(Script Record)相关代码的来源。如果不存在来源的脚本或模块,则值为null。

正在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具有下表列出的其他状态组件。

组件含义
LexicalEnvironment标识在此执行上下文中用于解析有代码所做的标识符引用的词法环境。
VariableEnvironment标识在此执行上下文中的词法环境,它的环境记录保存了由VariableStatements创建的绑定。

当创建执行上下文时,它的LexicalEnvironment和VariableEnvironment组件最初具有相同的值。

作业和作业队列(Jobs and Job Queues)

作业和领域一样都是ES6新增的东西。作业是一个抽象操作,当没有其他ECMAScript计算正在进行时,它将启动ECMAScript计算。一个作业抽象操作可以被定义为接受任意一组作业参数。只有当没有正在运行的执行上下文并且执行上下文堆栈为空时,才能启动作业的执行。一旦启动了一个作业的执行,作业将始终执行完成。在当前正在运行的作业完成之前,不能启动其他作业。PendingJob是未来执行Job的请求。PendingJob是内部记录,其字段如下表:

字段名称含义
[[Job]]作业抽象操作的名称这是在执行此PendingJob时执行的抽象操作。
[[Arguments]]一个List当[[Job]]激活时要传递给[[Job]]的参数值的列表。
[[Realm]]一个领域记录此PendingJob启动时,最初执行上下文的领域记录。
[[ScriptOrModule]]一个Script Record或Module Record此PendingJob启动时,用于初始执行上下文的脚本或模块。
[[HostDefined]]any,默认undefined保留字段供需要将附加信息与 pending Job相关联的宿主环境使用。

我们可以把[[Job]]看成一个函数,[[Arguments]]是这个函数的参数。
一个作业队列是一个PendingJob记录的FIFO队列。每个作业队列都有一个名称和由ECMAScript编译器定义的一整套可用的作业队列。每个ECMAScript编译器至少具有下表中定义的作业队列。

名称目的
ScriptJobs验证和评估ECMAScript脚本和模块源文本的作业。
PromiseJobs回应一个承诺的解决的作业

Promise的回调就是与PromiseJobs有关。

执行流程

有关javascript中词法环境、领域、执行上下文以及作业,基本简单的介绍了一下。那么ECMAScript编译器怎么把它们之间关联起来的呢,下面我大致写了一个简单的流程:
ECMAScript中有一个RunJobs ( )方法,所有东西的确立都是从这个方法出来的。

  1. 让realm成为CreateRealm()。CreateRealm()主要是创建了一个领域,初始化了领域中字段的值,并返回创建的领域。
  2. 让newContext成为一个新的执行上下文。
  3. 设置newContext的Function为null,newContext的Realm为realm,newContext的ScriptOrModule为null。
  4. 把newContext放到执行上下文栈,现在newContext是一个正在运行的执行上下文。
  5. 执行SetRealmGlobalObject(realm, global, thisValue)方法,正常情况下global为undefined,thisValue为undefined。

    • SetRealmGlobalObject方法执行,我在这里默认global和thisValue为undefined:
    1. 让intrinsics成为realmRec.[[Intrinsics]]。
    2. 让globalObj等于ObjectCreate(intrinsics.[[%ObjectPrototype%]])。
    3. 让thisValue等于globalObj。
    4. 设置realmRec.[[GlobalObject]]是globalObj。
    5. 设置newGlobalEnv为新的词法环境。
    6. 让objRec成为一个新的包含globalObj为绑定对象的对象环境记录。
    7. 让dclRec成为没有任何绑定的新的声明性环境记录。
    8. 让globalRec成为一个新的全局环境记录。
    9. 设置globalRec.[[ObjectRecord]]为objRec,设置globalRec.[[GlobalThisValue]]为 thisValue,设置globalRec.[[DeclarativeRecord]]为dclRec,设置globalRec.[[VarNames]]是一个空的List,设置newGlobalEnv的环境记录为globalRec,newGlobalEnv的外部词法环境为null。
  6. 设置realmRec.[[GlobalEnv]]为newGlobalEnv。
  7. 让globalObj变为SetDefaultGlobalBindings(realm)得返回值。SetDefaultGlobalBindings的方法主要是把realm的[[Intrinsics]]中的内部方法拷贝到全局对象中。
  8. 在globalObj上创建任何编译器定义的全局对象属性。
  9. 依赖编译器方式,在零个或多个ECMAScript脚本和/或ECMAScript模块中获取ECMAScript源文本和任何关联的host-defined的值。为每一个sourceText和hostDefined做如下操作:

    1. 如果sourceText是script的源代码, 那么执行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »)。
    2. 如果sourceText是module的源代码,那么执行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)。
  10. 循环

    1. 挂起正在运行的执行上下文并将其从执行上下文堆栈中移除。
    2. 确定:执行上下文堆栈现在是空的。
    3. 让nextQueue是以编译器定义的方式选择的非空作业队列。如果所有作业队列都为空,则结果是编译器定义的,nextQueue里的记录是上面通过EnqueueJob方法放到作业队列中的记录。
    4. 让nextPending成为nextQueue前面的PendingJob记录。从nextQueue中删除该记录。
    5. 让newContext成为一个新的执行上下文。
    6. 设置newContext的Function为null,newContext的Realm为nextPending.[[Realm]],newContext的ScriptOrModule为nextPending.[[ScriptOrModule]]。
    7. 将newContext推入执行上下文堆栈; newContext现在是正在运行的执行上下文。
    8. 使用nextPending执行任何编译器或宿主环境定义的作业初始化。
    9. 让result成为使用nextPending.[[Arguments]]元素作为nextPending.[[Job]]的参数进行抽象操作的结果,这里指运行上面EnqueueJob中的ScriptEvaluationJob或TopLevelModuleEvaluationJob方法。
    10. 如果result是突然完成的,比如throw扔出异常, 执行HostReportErrors(« result.[[Value]] »),HostReportErrors方法就是报错误的,比如SyntaxError和ReferenceError等。

2017-11-27新增
突然发现这么一长串的步骤不易阅读和理解,我在这做一些笼统的说明:

领域(Realm)只创建一次,领域创建后开始创建全局词法环境(包括全局词法环境中的声明性环境记录和对象环境记录以及全局对象),SetDefaultGlobalBindings方法中global和thisValue为undefined意味着全局环境记录中的[[GlobalThisValue]]就是全局对象(这也表示了在浏览器中全局环境下this就是window对象)。

步骤9中的script中的sourceText表示用<script></script>引入的js代码的Unicode编码。EnqueueJob方法你可以认为是把脚本信息按执行顺序放到队列中。

步骤10,你可以认为是从队列中拿出脚本进行执行(该循环的第9步就是执行脚本(指ScriptEvaluationJob方法),脚本的执行都是在领域和全局词法环境创建之后的)。


我这里说一下ScriptEvaluationJob方法的执行过程(TopLevelModuleEvaluationJob方法只在评估module时运行)正常都是运行的ScriptEvaluationJob方法。
ScriptEvaluationJob ( sourceText, hostDefined ):

  1. 确定: sourceText是ECMAScript源文本。
  2. 让realm成为当前的领域记录。
  3. 让s成为ParseScript(sourceText, realm, hostDefined)。
  4. 如果s是一个errors列表, 那么执行HostReportErrors(s),返回 NormalCompletion(undefined)(一个完成记录值,值为undefined)。
  5. 返回ScriptEvaluation(s)。

ParseScript(sourceText, realm, hostDefined):

  1. 使用脚本解析sourceText作为目标符号,并分析任何早期错误条件的解析结果。如果解析成功并且没有发现早期错误,那么让body成为所得到的分析树,否则body是一个包含一个或多个早期错误的列表。
  2. 如果body是错误列表,则返回body。
  3. 返回脚本记录(Script Record){[[Realm]]: realm, [[Environment]]: undefined, [[ECMAScriptCode]]: body, [[HostDefined]]: hostDefined}。

早期错误有很多,我举个例子:使用关键词作为标识符就是典型的早期错误。

ScriptEvaluation ( scriptRecord )大致流程:

  1. 让globalEnv成为scriptRecord.[[Realm]].[[GlobalEnv]]。
  2. 让scriptCxt成为一个新的ECMAScript代码执行上下文。
  3. 设置scriptCxt的Function为null, scriptCxt的Realm为scriptRecord.[[Realm]],设置scriptCxt的ScriptOrModule为scriptRecord。
  4. 设置VariableEnvironment和LexicalEnvironment为scriptCxt的globalEnv
  5. 挂起当前正在运行的执行上下文。
  6. 把scriptCxt放到执行上下文栈中,scriptCxt是一个正在运行的执行上下文。
  7. 让scriptBody成为scriptRecord.[[ECMAScriptCode]]。
  8. 让result成为运行GlobalDeclarationInstantiation(scriptBody, globalEnv)返回的结果。
  9. 如果result.[[Type]]是normal,那么设置result是执行scriptBody的结果.
  10. 如果result.[[Type]]是normal且result.[[Value]]是empty, 那么设置result为NormalCompletion(undefined).
  11. 挂起scriptCxt并将其从执行上下文堆栈中删除。
  12. 将当前位于执行上下文堆栈顶部的上下文恢复为正在运行的执行上下文。
  13. 返回Completion(result),一个记录值。

GlobalDeclarationInstantiation()方法是对全局环境中的标识符定义进行实例化。比如var、function、let、const、class声明的标识符。该方法执行成功返回的result.[[Type]]为normal。注意这时候的我们能看到的js代码还没有执行,真正执行我们的代码的是步骤9。这也是为什么我们用var和function声明的标识符会出现变量提升(Hoisting)现象。let、const、class声明也在步骤9之前,之所以没有变量提升是因为let、const、class声明的标识符只进行实例化而没有初始化,在下一篇文章中我会重点介绍它们之间的不同之处(所以我认为那些说var和function声明存在变量提升,而let、const、class声明的变量不提升的说法是不对的)。


2017-11-27新增
ScriptEvaluation你可以简单的认为它做了两件:1.对标识符实例化以及初始化,2.执行javascript脚本。

GlobalDeclarationInstantiation方法只对当前脚本的标识符定义进行实例化,不能跨脚本。比如script1在script2之前引用,那么script2中的声明的变量只有通过GlobalDeclarationInstantiation实例化后才能在script1中引用,这也表示var和function声明的标识符不能跨脚本进行变量提升。


结束语

到这里本篇文章也快结束了,本文章所有的说法都是以最新的ECMAScript的语言规范(ES8)为基础。希望这篇文章可以帮助大家更加深入的了解javascript,如果本文有不当之处请指出。还有我不得不吐槽一下ECMAScript的语言规范写得真是太不友好了,看得我心好累啊(说到底还是自己当初在英语课上睡觉的锅)。最后如果你想看ECMAScript的语言规范,那么第5章和第6章一定要看!一定要看!这是一个过来人的忠告。

查看原文

赞 17 收藏 17 评论 3

followWinter 回答了问题 · 2019-11-11

解决移动端(手机端)三列布局

关注 3 回答 2

followWinter 收藏了文章 · 2019-11-11

V8引擎深入研究目录贴

对于每个前端程序员来讲都有一个终极理想,那就是搞懂 javascript 引擎是如何工作的。

从我的网络 ID(justjavac)可以看出来,当我开始使用这个 ID 的时候并不是一个前端程序员,我主攻的语言是 java 和 C,当我开始决定阅读 JVM 源码时,他们告诉我说当时进步最大、性能提升最快的是 V8,于是就走上了前端的不归路。

javascript 性能经过了两次飞跃:

javascript 性能经过了两次飞跃

第 1 次飞跃是 2008 年 V8 发布,第 2 次则是 2017 年的 WebAssembly。不过 WebAssembly 到底能不能掀起前端的波澜还是未知数,但是 V8 对前端的贡献大家都有目共睹。

从去年底开始连载《V8源码分析》,记录一下自己学习 V8 源码的点点滴滴。

此文是索引贴,随时更新

这几天 SF 增加了新的板块——直播。我也收到了官方的邀请。4月15(星期六)晚8点和大家一起聊聊 V8 引擎:前端程序员应该懂点 V8 知识 - SegmentFault 讲堂

最后是鸡汤时间:“精通 one,学习 another,关注 next”。

查看原文

followWinter 赞了文章 · 2019-11-11

V8引擎深入研究目录贴

对于每个前端程序员来讲都有一个终极理想,那就是搞懂 javascript 引擎是如何工作的。

从我的网络 ID(justjavac)可以看出来,当我开始使用这个 ID 的时候并不是一个前端程序员,我主攻的语言是 java 和 C,当我开始决定阅读 JVM 源码时,他们告诉我说当时进步最大、性能提升最快的是 V8,于是就走上了前端的不归路。

javascript 性能经过了两次飞跃:

javascript 性能经过了两次飞跃

第 1 次飞跃是 2008 年 V8 发布,第 2 次则是 2017 年的 WebAssembly。不过 WebAssembly 到底能不能掀起前端的波澜还是未知数,但是 V8 对前端的贡献大家都有目共睹。

从去年底开始连载《V8源码分析》,记录一下自己学习 V8 源码的点点滴滴。

此文是索引贴,随时更新

这几天 SF 增加了新的板块——直播。我也收到了官方的邀请。4月15(星期六)晚8点和大家一起聊聊 V8 引擎:前端程序员应该懂点 V8 知识 - SegmentFault 讲堂

最后是鸡汤时间:“精通 one,学习 another,关注 next”。

查看原文

赞 40 收藏 145 评论 4

followWinter 回答了问题 · 2019-11-08

解决单元测试的意义是什么

我说几点
比如对于下面的函数:

function add(numA, numB){
    numA = Math.max(numA, 0)
    numB = Math.min(numB, 0)
    return numA + numB
}

我们看看需要考虑几种情况:

numAnumB
11
>=0<0
<0>=0
>=0>=0
......

此外,是否我们还需要考虑 numA 和 numB 的类型呢?
此时,如果需要添加一个需求,要求在 =0 的时候做特殊操作,请问:

  1. 修改了这个函数是否会影响函数执行的结果呢?那么这几种情况都需要尝试一遍。
  2. 这个函数的影响面多大,换句话说,会不会影响上层的调用方呢?

所谓的单元测试是考虑一个函数所有的情况,任何一次修改也会重新在所有情况之下修改,而这些所有情况就覆盖了上层的所有调用。这个函数单元测试过了,就说明不影响上层功能。

按照你的说法,结果不变的确不影响。但是单元测试不是一个单元一个测试,而是一个单元无限的测试。所以你的这种情况是单元测试覆盖不够,除了要测试 1.2 和 2.2,还需要考虑 1.9 和 2.9,等等等。

所以吧,大概有以下优点:

  1. 写一个函数,考虑所有情况,一次测试所有情况。
  2. 修改函数功能,更新单元测试,一次测试更多的情况,单测覆盖面越来越广,避免引入新功能导致旧的功能出 bug。
  3. 单元测试层层覆盖,定位快,从顶层到底层,单测覆盖面越广,项目质量越稳定,每一次修改代码,跑一边测试,就能保证不影响旧功能。

关注 3 回答 1

followWinter 回答了问题 · 2019-11-06

基于jQuery mobile的Web app怎么连接后台的?然后怎么打包成apk在手机上运行呢?

  1. 连接后台通常用 HTTP,技术用 jQuery ajax,或者其他网络库,fetch、axios
  2. 打包 app 可以使用一些跨端方案,比如 Cordava,或者使用在线打包工具,或者创建一个安卓壳利用 WebView 本地加载文件或者远程加载地址

背景不明确,不知道该如何详细描述,要描述到哪一个层级。

关注 2 回答 1

followWinter 回答了问题 · 2019-11-06

需要授权才能调用某些api,如何做到批量调用api的时候等待授权成功后再执行调用?异步锁?

做个锁就好啦

const auth = undefined

function callApi(options, api) {
    if(!auth){
        auth = f9Config()
    }
    auth.then(function () {
        api(options);
    });
}

关注 3 回答 2

followWinter 发布了文章 · 2019-11-05

[前端漫谈] 一巴掌拍平Git中的各种概念

0x000 导读

git的文章很多,但是大部分都是一个套路,讲概念,讲命令,太多的概念和命令总是让人有一种稀里糊涂的感觉,讲的很对,但似乎没能讲透,没有醍醐灌顶的感觉,大概是我的悟性差吧。所以这几天一直在做各种git的实验,并且阅读了一些博客、文档、资料,综合下来整理出了这么一篇文章。注意:

  • 本篇文章旨在说明我对git的理解,只是一家之言,聊以分享。
  • 本片文章不是一篇命令教程,或者概念说明,需要一定的git使用经验和踩坑经验。
  • 为了阅读方便,commitID只保留4位

0x001 总结[提前]

这是一篇比较乱七八糟的文章,不从传统出发,尝试用自己的思想去理解git这一神奇的工具。以前我觉得git命运石之门,我们在不同的时间线(分支)上跳跃,所有的事件都必须且只能发生在时间线上。但是现在我觉得git无限的可能性的集合,一个事件可以引申出无限的可能性。而我们要做的是用工具(branch、tag、reset、rebase、merge....)来组织这些可能性,从而构成一个有序的、向前发展的历史,引导整个历史的发展,构建可以走向未来的工程。

0x002 存档和读档

  • 存档

    其实吧,版本就是存档,就是游戏中的存档,我们不断的推进项目,完成一个又一个任务,然后中途不断的存档,就形成了版本迭代。而版本多了,自然就需要管理了,自然就有了版本管理系统。

    在游戏中,存档可以手动存档,也可以到指定点存档,也可以自动定场景存档。在游戏中,存档之后形成的东西叫做档案,而在git中,叫做commit。我们可以使用git add+git commit完成一个档案的创建,或者说版本的创建。

    一个commit拥有许多的属性,比如IDMessageDateAuthor:等等,这些信息都有助于我们了解这个版本,就像游戏中的存档会以关卡名/图片之类的信息来提示你这个存档所代表的进度,比如使用git log可以获取以下信息:

    commit 4963 (HEAD -> master)
    Author: **********
    Date:   Thu Jan 10 15:22:12 2019 +0800
    
        版本H
    
    commit 1a42
    Author: **********
    Date:   Thu Jan 10 15:25:01 2019 +0800
    
        版本G
    
    commit 931b
    Author: **********
    Date:   Thu Jan 10 15:24:50 2019 +0800
    
        版本F
    ....
  • 读档

    既然有存档,那就有读档。游戏中直接选择一个档案就行了,那git中呢?如果有可视化操作工具,那我们直接点击也是可以的,但现在不使用工具,而使用命令行,该如何呢。读取一个存档说白了在git中就是读取一个commit而已,所以我们可以使用git checkoutgit reset两个命令来做到,那如何指定版本呢?前面提到的commit属性中的ID可以帮我们实现这个目标。

    • 环境说明:我在仓库中git commit了8个,每个commit都添加了一个从a-h的文件,并在commit信息中添加版本标记
      2019-01-10-18-04-08
    • 使用git checkout切到版本A,可以发现,此时只有文件a

      $ git checkout 401e
      Note: checking out '401e'.
      
      ...
      
      HEAD is now at 401e1b6 版本A
      
      $ ls
      README.md       a.txt
    • 使用git reset切换到版本G,可以发现,此时有了a-g几个文件了

      $ git reset 1a42
      Unstaged changes after reset:
      D       b.txt
      D       c.txt
      D       d.txt
      D       e.txt
      D       f.txt
      D       g.txt
      
      $ git stash
      Saved working directory and index state WIP on (no branch): 1a4268d 版本G
      
      l$ ls
      README.md       a.txt           b.txt           c.txt           d.txt           e.txt           f.txt           g.txt
  • 总结:
    我们通过commitID属性配合其他命令来达到了任意读档的目的,可以在各种版本中随意穿梭,快乐的很啊。而读档的姿势其实还有很多,但不外乎是对commit操作方式的不同,在git中,我觉得commit 才是构成整个版本管理的基本栗子。每一个commit都是独立的个体,虽然和其他commit存在着关联,但是依旧是独立的,而我们在commit构成节点序列中来回移动和操作,就可以达到所有对版本管理的目的。

0x003 别名系统

在上一个章节中,我们已经可以依靠一些命令和commit ID做到在版本中自由的穿梭,但是却带来一个问题,那就是commit ID的记忆难度。commit IDhash值,尽管git支持只提供前几位就能匹配到hash,并且也提供了commit message来说明commit,但是依旧存在commit的辨识和记忆问题。而这个问题,可以通过别名系统来解决。

所谓的别名系统,其实是我自己归纳的概念,指的其实就是HEADbranchtag这三个东西。在我看来,这三个东西都是一样的东西,都是别名,也就是标记一个commit的东西而已,只是在行为表现上有一些区别。

  1. HEAD

    一个仓库只有一个HEAD,指向你当前所在的commit。如果你创建了一个commitHEAD将会指向这个新的commit。也可以通过命令,强制HEAD指向某个commit,比如resetcheckout。也就是不论你在哪个commit之上,那么HEAD就在哪儿,或者说,其实你能在哪个commit,是通过修改HEAD指向的commit实现的。

    • 通过修改HEAD在各个版本之间旋转跳跃

      2019-01-11-13-20-54

  1. branch

    一开始我觉得这个东西才是git的核心,因为创建项目的时候,我们就处于master分支之上,并且我们在工作中,往往也是基于分支工作的。但是后来发现,分支在本质上毫无意义,并不需要真的基于branch去工作,基于commit就行了。而branch只是提供了一个方式来管理这些commit。branchHEAD相同点是随着新的commit的创建,branch指向的commit会不断更新,当然前提是你需要在这个branch所在的commit上创建新的commit。而branchHEAD的不同点在于HEAD只能有一个,branch可以有多个。

    实验一:用branch来实现切换版本

    • 目前的库情况

      $ git log --pretty=oneline
      1a42 (HEAD) 版本G
      931b 版本F
      071d 版本E
      0caa 版本D
      7855 版本C
      1295 版本B
      401e 版本A
    • 为版本A-G分别创建一个分支

      $ git checkout 1a42 -b G
      Switched to a new branch 'G'
      $ git checkout 931b -b F
      Switched to a new branch 'F'
      $ git checkout 071d -b E
      Switched to a new branch 'E'
      $ git checkout 0caa -b D
      Switched to a new branch 'D'
      $ git checkout 7855 -b C
      Switched to a new branch 'C'
      $ git checkout 1295 -b B
      Switched to a new branch 'B'
      $ git checkout 401e -b A
      Switched to a new branch 'A'
      
      $ git log --pretty=oneline
      1a42 (HEAD -> G) 版本G
      931b (F) 版本F
      071d (E) 版本E
      0caa (D) 版本D
      7855 (C) 版本C
      1295 (B) 版本B
      401e (A) 版本A
    • 接下来就可以换一种方式在版本之间跳跃了,并且不需要记住或者查询冗长的commit ID

      $ git checkout A
      Switched to branch 'A'
      $ git checkout B
      Switched to branch 'B'
      $ git checkout C
      Switched to branch 'C'
      $ git checkout E
      Switched to branch 'E'
      $ git checkout F
      Switched to branch 'F'
      $ git checkout G
      Switched to branch 'G'
实验二:分支跟随新的`commit`
- 当前库的情况,注意:这里的`HEAD -> G`表示`HEAD`指向了`branch G`,而`branch G`指向了`版本G`
    ```
    $ git log --pretty=oneline
    1a42 (HEAD -> G) 版本G
    931b (F) 版本F
    071d (E) 版本E
    0caa (D) 版本D
    7855 (C) 版本C
    1295 (B) 版本B
    401e (A) 版本A
    ```
- 添加一个文件,创建一个`commit`
    ```
    $ echo 'h'> h.txt
    $ git add h.txt
    $ git commit -m '版本H'
    [G d346d27] 版本H
    1 file changed, 1 insertion(+)
    create mode 100644 h.txt
    ```
- 此时查看`log`,可以看到`HEAD`和`G`都指向了`版本H`,就是所谓的`branch`跟着`commit`动,但是它真的是跟着`commit`动吗?
    ```
    $ git log --pretty=oneline
    d346 (HEAD -> G) 版本H
    1a42 版本G
    931b (F) 版本F
    071d (E) 版本E
    0caa (D) 版本D
    7855 (C) 版本C
    1295 (B) 版本B
    401e (A) 版本A
    ```
实验三:分支跟着啥动

- 将`HEAD`指向`版本G`的`commit`,而不是分支`G`,也就是使用`git checkout commitID`,而不是使用`git checkout branchName`,可以看到,此时`HEAD`不指向`G`,而是`HEAD`和`G`同时指向了`版本H`的`commit`。
    ```
    $ git checkout d346 # 版本 H 的 commitID
    $ git log --pretty=oneline
    d346 (HEAD, G) 版本H
    1a42 版本G
    931b (F) 版本F
    071d (E) 版本E
    0caa (D) 版本D
    7855 (C) 版本C
    1295 (B) 版本B
    401e (A) 版本A
    ```
- 继续创建一个`commit`,可以看到,这个时候分支`G`不再跟着`commit`移动了,所以,只有在`HEAD`指向`branch`的时候,`branch`才会向前移动,也就是只要`HEAD`来到`branch`身边,`branch`就会跟着`HEAD`跑。
    ```
    $ echo 'i'> i.txt
    $ git add i.txt
    $ git commit -m "版本I"
    [detached HEAD 2e836eb] 版本I
    1 file changed, 1 insertion(+)
    create mode 100644 i.txt
    $ git log --pretty=oneline
    2e83 (HEAD) 版本I
    d346 (G) 版本H
    1a42 版本G
    931b (F) 版本F
    071d (E) 版本E
    0caa (D) 版本D
    7855 (C) 版本C
    1295 (B) 版本B
    401e (A) 版本A
    ```
  1. tag

    tag是比较特殊的一个别名类型,他无法移动,或者说不推荐移动。一旦一个tag和指向某个coimmit,就不希望它移动,因为tag就是用来标记这个commit的,他是一个孤独而忠诚的守望者,而不像branch,花间游龙似的浪子。

    • 现在库的情况

      $ git log --pretty=oneline
      1a42 (HEAD, G) 版本G
      931b (F) 版本F
      071d (E) 版本E
      0caa (D) 版本D
      7855 (C) 版本C
      1295 (B) 版本B
      401e (A) 版本A
    • 为每个版本添加一个tag,为了区别分支名,统统加了个T

      $ git tag TA A
      $ git tag TB B
      $ git tag TC C
      $ git tag TD D
      $ git tag TE E
      $ git tag TF F
      $ git tag TG G
      
      $ git log --pretty=oneline
      1a42 (HEAD, tag: G, G) 版本G
      931b (tag: TF, F) 版本F
      071d (tag: TE, E) 版本E
      0caa (tag: TD, D) 版本D
      7855 (tag: TC, C) 版本C
      1295 (tag: TB, B) 版本B
      401e (tag: TA, A) 版本A
    • 现在又多了一种旋转跳跃的方式了

      $ git checkout TA
      Previous HEAD position was 1a4268d 版本G
      HEAD is now at 401e1b6 版本A
      $ git checkout TB
      Previous HEAD position was 401e1b6 版本A
      HEAD is now at 1295260 版本B
      $ git checkout TC
      Previous HEAD position was 1295260 版本B
      HEAD is now at 7855905 版本C
      $ git checkout TD
      Previous HEAD position was 7855905 版本C
      HEAD is now at 0caa2b7 版本D
      $ git checkout TE
      Previous HEAD position was 0caa2b7 版本D
      HEAD is now at 071d00a 版本E
      $ git checkout TF
      Previous HEAD position was 071d00a 版本E
      HEAD is now at 931b3c9 版本F
      $ git checkout TG
      Previous HEAD position was 931b3c9 版本F
      HEAD is now at 1a4268d 版本G
    • 总结
所以,不管是`HEAD`、`tag`、`branch`,都是一种别名,除了行为表现上的差别,没有太大的不同,特别是`branch`和`tag`,不过都只是提供了一种管理`commit`的方式。

0x004 分叉

在上一章节中,我们揭开了别名系统的红盖头,这一章,我们就开始探索一下分叉的神秘。

和游戏中的存档一样,有时候一个游戏有许多的选择,这些选择指向了不同的结果。而作为游戏玩家,我们希望能够走完所有的选择,以探索更多的游戏乐趣。所以我们会在做选择的时候存档,而当我们走完一个选择,就会读取这个存档,继续往另一个选择探索。这个时候,就产生了两个不同的剧情走向,这就是分叉。

git中,其实我们可以有无数的选择,每一个commit可以创建无数的commit,就会引申出无数的可能。

  • 我们遇到了一个抉择,所以需要创建版本,暂时称为版本X

    $ git log --pretty=oneline
    2cae (HEAD) 版本X
    ....
  • 然后我们选择了走Y,并且沿着Y1一直走到Y3,这是尽头

    $ git log --pretty=oneline
    d2e0 (HEAD) 版本Y3
    4ca8 版本Y2
    fcff 版本Y1
    2cae 版本X
    ...
  • 接着我们返回X,并选择另一个选择Z,从Z1走到Z3

    $ git checkout 2cae # 切到`版本X`
    $ git log --pretty=oneline
    16ff (HEAD) 版本Z3
    0ca5 版本Z2
    b4a7 版本Z1
    2cae 版本X
    ...
  • 总结

可以看到,我们顺着两个选择一直往下发展,在这发展的过程中,我们完全没有用到tagbranch,也就是为了印证commit 是构成 git 世界的基本栗子这一说明。

git log中,我们看不到了Y走向,那Y真的消失了吗?不是的,我们依旧可以通过Ycommit ID来寻回Y的记录。当然为了方便在YZ中切换,我们可以使用branch来标记一下YZ两个走向,这样就形成了YZ两个branch了,也就是分叉!

那那些没有被branch或者tag标记的commit呢?他们会消失吗?会,也不会。不会是因为不被标记的commit将变成dangling commit,我称之为游离的commit,是git中最孤独的存在,只要我们知道commitID,就会可唤回它。但是很大的可能是我们永远不会记得这么一个不被引用的commit,所以我呼吁,善待每一个commit。会是因为还是可能会被回收的,看这里,git 也有 gc

0x003 合并

和游戏的存档不同的是,git中的版本可以合并,也就是说我可以在分支Y中做完任务Y1Y2Y3,然后分支Z中完成任务Z1Z2Z3,然后合并这两个分支,结果回到了X,但是却完成了Y1-y3Z1-Z3,并拿到了神器YZ,这让boss怎么活?

  • 实验一:使用merge合并commit

    • 创建版本O

      $ echo O >> o.txt
      $ git add o.txt
      $ git commit -m '版本O'
      [detached HEAD 478fa6d] 版本O
      1 file changed, 1 insertion(+)
      create mode 100644 o.txt
    • 基于版本O创建版本P1

      $ echo P >>p1.txt
      $ git add p1.txt
      $ git commit -m '版本P1'
      [detached HEAD a3ab178] 版本P1
      1 file changed, 1 insertion(+)
      create mode 100644 p1.txt
      $ git log --pretty=oneline
      a3ab (HEAD) 版本P1
      478f 版本O
    • 基于版本O创建版本P2

      $ git checkout 478f # 版本O 的 commitID
      $ echo p2 >> p2.txt
      $ git add p2.txt
      $ git commit -m '版本P2'
      [detached HEAD cbccf52] 版本P2
      1 file changed, 1 insertion(+)
      create mode 100644 p2.txt
      $ git log --pretty=oneline
      cbcc (HEAD) 版本P2
      478f 版本O
    • 合并版本P1版本P2

      $ git merge a3ab # 版本P1 的 commitID
      $ git log --pretty=oneline
      656a (HEAD) Merge commit 'a3ab' into HEAD
      cbcc 版本P2
      a3ab 版本P1
      478f 版本O
  • 实验三:使用rebase合并

    切换到版本P2,在版本P2中使用rebase

    $ git checkout cbcc # 版本P2 的 commitID
    ....
    HEAD is now at cbccf52 版本P2
    $ git rebase a3ab # 版本P1 的 commitID
    First, rewinding head to replay your work on top of it...
    Applying: 版本P2
    $ git log --pretty=oneline
    3bd7 (HEAD) 版本P2
    a3ab 版本P1
    478f 版本O
  • 实验四:使用cherry-pick合并

    • 切换到版本O,新建版本P3

      $ echo 'p3'>> p3.txt
      $ git add p3.txt
      $ git commig -m '版本P3'
      git: 'commig' is not a git command. See 'git --help'.
      
      The most similar command is
              commit
      $ git commit -m '版本P3'
      [detached HEAD ae09e94] 版本P3
      1 file changed, 1 insertion(+)
      create mode 100644 p3.txt
      $ git log --pretty=oneline
      ae09 (HEAD) 版本P3
      478f 版本O
    • 切换到版本P2中使用cherry-pick合并版本P3的东西

      $ git checkout 3bd7 # 版本P2 的commitID
      ...
      HEAD is now at 3bd7820 版本P2
      $ git cherry-pick ae09 # 版本P3 的 commitID
      [detached HEAD f9dfba2] 版本P3
      Date: Sat Jan 12 11:35:27 2019 +0800
      1 file changed, 1 insertion(+)
      create mode 100644 p3.txt
      $ git log --pretty=oneline
      f9df (HEAD) 版本P3
      3bd7 版本P2
      a3ab 版本P1
      478f 版本O
  • 注意:合并中的冲突解决

    合并的过程中可能会出现冲突,比如同时拿到神器P1P2,但是在P1中卖掉了O之前拿到的装备S,而在P2中则为S镶上了宝石,那么合并之后要怎么处理?是卖掉S?还是保留镶宝石的S?还是镶了宝石再卖掉?深井冰啊!我不要面子的啊...
    所以这里就涉及到了合并的冲突解决,这里不再赘述,不是我要讲的内容。

0x005 变基

这个名词让人想入菲菲啊,每次项目新成员加入,总是会提醒他们注意要变基....

这里不去说mergerebase的爱恨情仇,只说一些rebase的操作,用rebase来整理commit

上面说到commit 是构成 git 世界的基本栗子,所以,我们需要掌握一些栗子的操作方式

  • 查看commit,可以使用git log,如果需要寻回忘记的commit,可以使用reflog来尝试看看是否能够找到

    $ git log --pretty=oneline
    68de (HEAD -> X) Merge branches 'Y' and 'Z' into X
    16ff (Z) 版本Z3
    ...
    $ git reflog
    23e799e (HEAD) HEAD@{0}: rebase -i (pick): 版本Z
    01a10d6 HEAD@{1}: rebase -i (squash): 版本Y
    a15dd72 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
    b6f2ea3 HEAD@{3}: rebase -i (start): checkout 1004
    f4c4ccc HEAD@{4}: rebase -i (abort): updating HEAD
    ...
  • 创建

    创建使用git add+git commit就行了

    $ echo error > error.txt
    $ git add error.txt
    $ git commit -m '一个错误的版本'
    [X bc90774] 一个错误的版本
    1 file changed, 1 insertion(+)
    create mode 100644 error.txt
    $ git log --pretty=oneline
    bc90 (HEAD -> X) 一个错误的版本
    68de Merge branches 'Y' and 'Z' into X
    ...
  • 更新上一个commit,直接使用git commit --amend

    $ echo error2 >> error.txt
    $ git add error.txt
    $ git commit --amend
    // 这里将打开一个vim窗口
    一个错误的版本
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    #
    # Date:      Fri Jan 11 17:21:18 2019 +0800
    #
    # On branch X
    # Changes to be committed:
    #       new file:   error.txt
    #
    // 保存退出之后输出
    [X d5c4487] 一个错误的版本
    Date: Fri Jan 11 17:21:18 2019 +0800
    1 file changed, 2 insertions(+)
    create mode 100644 error.txt
  • 要更新历史中的commit也是可以做到的,例如需要在版本X中加入文件x1.txt,要使用交互式模式

    git rebase -i 2e83 # 指向 版本X 的前一个 commit

    此时将打开一个交互式窗口

    pick 913b571 版本X
    pick 0eca5e3 版本Y1
    pick 33a9ca3 版本Y2
    pick b95b3ca 版本Y3
    pick 839c481 版本Z1
    pick 6fb6cb3 版本Z2
    pick c28d3e0 版本Z3
    ...

    版本X前面的pick改为e或者edit,保存,然后退出,这个时候,仓库将会回到版本X的状态,并输出

    Stopped at 913b571...  版本X
    You can amend the commit now, with
    
    git commit --amend
    
    Once you are satisfied with your changes, run
    
    git rebase --continue

    添加文件x1

    $ echo x1 > x1.txt
    $ git add x*
    $ git commit --amend
    // 打开交互式窗口可以修改 commit message
    $ git rebase --comtinue
    Successfully rebased and updated detached HEAD.

    此时又回会到原本的版本并且多出了文件x1,就像是在版本X中就已经加入一样

  • 插入一个新的commit,上面的栗子中不使用--amend,就会在XY1之间插入一个新的commit

    $ git rebase -i 2e83
    // 交互式窗口,吧`pick`改为`e`
    $ echo x2 > x2.txt
    $ git add x2.txt
    $ git commit -m '插入一个版本X2'
    [detached HEAD 1b4821f] 插入一个版本X2
    1 file changed, 1 insertion(+)
    create mode 100644 x2.txt
    $ git rebase --continue
    Successfully rebased and updated detached HEAD.
    $ git log --pretty=oneline
    30a5 (HEAD) 版本Z3
    4b00 版本Z2
    cc1d 版本Z1
    595e 版本Y3
    4456 版本Y2
    b6f2 版本Y1
    1b48 插入一个版本X2
    1004 版本X
  • 删除

    删除一个分支可以使用交互式rebase,命令:git rebase -i commitID,这里的commitID必须是你要删除的commit的前一个commit

    $ git rebase -i 68de

    此时将会打开一个vim窗口

    pick bf2c542 版本Y1
    pick 588feec 版本Y2
    pick 1b2ae37 版本Y3
    pick 38f7cf3 版本Z1
    pick 080e442 版本Z2
    pick 206a7ae 版本Z3
    pick 6b01f70 一个错误的版本
    
    # Rebase 2caeda3..6b01f70 onto 2caeda3 (7 commands)
    #
    # Commands:
    # p, pick = use commit
    # r, reword = use commit, but edit the commit message
    # e, edit = use commit, but stop for amending
    # s, squash = use commit, but meld into previous commit
    # f, fixup = like "squash", but discard this commit's log message
    # x, exec = run command (the rest of the line) using shell
    # d, drop = remove commit
    ...

    要删除一个错误的版本,将前面的pick改为d或者drop

    d 6b01f70 一个错误的版本

    保存退出,输出

    Successfully rebased and updated refs/heads/X.
  • 合并多个commit,比如合并Z1-Z3,打开交互式窗口之后,将Z2Z3pick改为s

    $ git rebase -i 100468330c7819173760938d9e6d4b02f37ba001
    // 打开了交互式窗口
    pick bf2c542 版本Y1
    pick 588feec 版本Y2
    pick 1b2ae37 版本Y3
    pick 38f7cf3 版本Z1
    s 080e442 版本Z2
    s 206a7ae 版本Z3

    保存退出以后,又打开交互式窗口,显示要合并的commitmessage,这里可以修改commit

    # This is a combination of 3 commits.
    # This is the 1st commit message:
    
    版本Z1
    
    # This is the commit message #2:
    
    版本Z2
    
    # This is the commit message #3:
    
    版本Z3

    这里修改为Z,保存,退出,输出,可以看到,Z1-Z3消失了,取而代之的是Z,对Y1-Y3做操作

    detached HEAD f4c4ccc] 版本Z
    Date: Fri Jan 11 16:27:00 2019 +0800
    1 file changed, 3 insertions(+)
    create mode 100644 z.txt
    Successfully rebased and updated detached HEAD.
    $ git log --pretty=oneline
    f4c4 (HEAD) 版本Z
    595e 版本Y3
    4456 版本Y2
    b6f2 版本Y1
    
    $ git rebase -i 1004
    [detached HEAD 01a10d6] 版本Y
    Date: Fri Jan 11 16:24:37 2019 +0800
    1 file changed, 3 insertions(+)
    create mode 100644 y.txt
    Successfully rebased and updated detached HEAD.
    $ git rebase --continue
  • 重新排序commit顺序,比如重排版本Y版本Z,交换一下顺序就好了

    $ git log --pretty=oneline
    23e7 (HEAD) 版本Z
    01a1 版本Y
    $ git rebase -i 1b48

    这时候打开交互式窗口,显示

    pick a1942a3 版本Y
    pick eeabc6c 版本Z

    将它交换顺序,保存,退出

    pick eeabc6c 版本Z
    pick a1942a3 版本Y

    查看结果

    Successfully rebased and updated detached HEAD.
    $ git log --pretty=oneline
    a194 (HEAD) 版本Y
    eeab 版本Z

0x006 资料

0x007 带货

帮大佬带货:【随心秀】开篇 - 开源微场景编辑器介绍

查看原文

赞 1 收藏 1 评论 0

followWinter 发布了文章 · 2019-11-05

[前端漫谈]浅谈 HTTP 学习路径

0x000 概述

最近专注于 HTTP 的学习,这篇文章浅谈一下我学习 HTTP 过程中的心得,当作过渡性总结。如果能对各位有所帮助,最好不过。

0x001 HTTP 协议

说起 HTTP,自然逃不过 HTTP 协议本身,以下是我的学习资料:

《图解 HTTP》是入门精品,是初学者的福音。

《HTTP 权威指南》对于深入学习的需求也可以满足。

深入学习之后我觉得应该回归本质,也就是 HTTP 规范自身,rfc7230-7235 就是这接下来的不二选择。

其实前两本书已经够了,可是我发现单纯看书无法掌握技术发展的脉络,譬如 HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3
是如何发展的。规范虽然晦涩,却有很明显的历史发展性;虽然不够详细,却非常全面。

http/0.9 协议很简单,只有一行,就是simple-request

SimpleRequest     =        GET <uri> CrLf

http/1.0 和 和 HTTP/1.1 变化不大。

  • 多部署服务器
  • 默认 keep-alive 持久连接
  • 引入传输编码

有人说作为前端,HTTP 协议使用的并不深,不需要掌握太深太细。但是我在学习之后却发现并不是这样,学习是一回事,实践是另一回事。

比如:

  1. 在往常思考 HTTP 的时候,总是将请求分为 client 和 server 来思考。

client-network-server

对于 network 中的一切都是忽略不计的,但深入学习之后才会发现,真实世界的网络拓扑可是能是这样的

client-device-server

并且最后处理我们请求的并不一定是目标服务器,而是中间的某个设备,可能是代理,也可能是缓存服务,或者网关。

  1. 我以为 HTTP 的报文结构从来是这样的:
request-ling/status-line
header-section

request-body/response-body

但其实也有可能是(Range Request):

HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-Length: 1741
Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

看见了吗?HTTP 的 Header 并不是只能出现在 request-line/response-line 下面

  1. Last-Modified、If-Modified-Since、ETag、If-Match...

我总是在缓存相关的文章中看到这些头部,所以我以为这些头部就是专门用来做缓存控制的,但是其实这些头部应该是属于 Preconditional Headers Field,是用来发起 Conditional Request,是为了防止多人同时编辑导致“update lost”问题的。比如两个人两个了 GET 了一个资源,该资源的 ETag 是 xx001,两个人修改资源之后 POST 到服务端,这两个 POST 请求带有 If-Match: xx001,其中一个请求 a 被接收了,资源更新,则新资源的 ETag 为 xx002,第二个人请求到来的时候,就可以通过检测资源的 ETag 和 If-Match 中是否一致,如果不一致,说明资源已经被修改。则这个资源不应该被更新,否则将会覆盖第一个人的更新。相当于一个锁。当然也用来做缓存控制,但是容易产生概念上的误解。

所以为什么要深入学习 HTTP,甚至学习 HTTP 协议本身呢?原因如下:

  1. 从大局全面理解技术,避免一叶障目,不见泰山的情况,脱离信息茧房。(不能因为你只玩过游乐园的摩天轮,就认为这是一个摩天轮游乐园,毕竟人家可能还有海底世界和动物园呢)
  2. 掌握技术在历史发展中的脉络,更好理解技术的存在的原因和未来发展的方向。
  3. 不再人云亦云,对技术持有自己的观点。

0x002 基于 HTTP 之上的技术

在前端,通常我们不会直接操作 HTTP,而是通过浏览器提供的接口来实现这个过程:

URI 和 URL 是不一样,应该详细了解 URL 的构成。

同源策略是用户代理安全基石之一,许多功能都是基于同源策略之上,在理解跨域资源共享之前还是先理解一下同源策略吧。

注意:cookie 是一个痛苦的例外,一来它可以在头部中出现多次,二来它并没有使用同源策略。

XMLHTTPRequest 叫这名字存粹是历史原因,和功能无关。

Fetch 和 XMLHTTPRequest 就是我们最常用的底层对象了。

有人觉得 jQuery 已经落后于时代了,完全不需要去了解。但是作为当年最火的项目,它的每一个实现在当时都是最佳实践。我们学习历史的原因是为了在当时历史条件下,分析一个技术解决问题的思路,而不仅仅看它在当代能有啥用。

axios 真是前端瑞士军刀。

还有一些工具:

Fiddler和 Charles 一样,都是网络调试利器,我常用 Charles 抓包修改请求和响应测试各种数据情况下的交互。

wireshark 也是抓包,但是只是在需要分析底层报文的时候才用。

postman 则是测试 restful 接口、团队合作的好东西。

curl 在无界面环境是不错的选择,比如 linux。

0x003 HTTPS

HTTP 之后自然轮到 HTTPS 了:

我们需要知道浏览器如何和 HTTPS 建立连接,并传输数据的。

0x004 TSL

在 HTTPS 的底层是 TSL,本身是可以作为独立的技术存在的,有握手协议和消息协议。

0x005 X.509

浏览器在建立 HTTPS 连接的时候,需要验证证书,想要知道证书的详细格式,就需要了解 X.509。

0x006 openssl

生成、转化、验证证书的库/工具,可以用来模拟验证过程,或者自己成为 CA 颁发证书。

0x007 http 服务

Nginx 和 Apache 搭建 HTTP 服务器,配合 openSSL 做配置 HTTPS 实验。

Node http 模块可以自己搭建一个 web 服务器。

http-server 则是静态服务器的命令行工具。

0x008 HTTP2

支持帧传输、二进制流、头部压缩、服务端推送等。

0x009 TCP/IP

这里只做记录。

0x010 HTTP3

底层抛弃了 TCP 协议,转而使用基于 UDP 协议的 Quic 协议。目前还是草稿版本。

0x011 UDP

这里只做记录。

0x012 DNS

...待续

0x013 CDN

...待续

0x014 总结

以上,将随着我的学习持续更新。

0x015 带货

帮大佬带货:【随心秀】开篇 - 开源微场景编辑器介绍

查看原文

赞 1 收藏 1 评论 1