前端代码规范以及git校验

前端代码规范以及git校验

目的

  • 前端代码保存自动校验并修改成规范代码
  • git commit 时自动校验,修改后若是代码还不规范时阻止提交

需要的工具

"eslint": "^7.11.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.2",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"pre-commit": "^1.2.2",
"prettier": "^2.1.2"

上面是所需要的依赖包,airbnb是集成的代码规范,prettier是自动修改代码为符合规范的代码,husky、lint-staged、pre-commit是用来实现git提交校验代码还有其他的是react有关的

依赖安装完成之后

执行 ./node_modules/.bin/eslint –init 后会在文件根目录下出现.eslintrc.js文件,之后修改成下面的配置

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: ["react-app", "airbnb", "plugin:prettier/recommended"],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: "module",
  },
  plugins: ["react"],
  rules: {
    "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
    "no-use-before-define": ["error", { functions: false, classes: false }],
    "no-console": 0,
  },
};

里面的rules配置项是自定义的代码规则, 如果需要可以在根目录下新增.eslintignore文件,配置校验代码需要忽略的文件

package.json

"scripts":{
 "lint": "lint-staged",
}
"pre-commit": [
    "lint"
  ],
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "echo $HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "src/**/*.js": [
      "eslint --fix --ext .js",
      "prettier --write",
      "git add"
    ]
  },

完成上面配置后,git提交代码时就可自动校验修改代码

vscode配置

setting 配置中添加

{
  //自动格式化
  "editor.formatOnSave": true,
  // eslint配置项,保存时自动修复
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  // 默认使用prettier格式化支持的文件
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "vetur.format.defaultFormatter.html": "prettier"
}

以上完成之后 就实现了最开始的两个目的

css-BFC

先了解盒模型

box-sizing又分为.content-box和border-box,所造成的就是width表示的返回不同

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域

以下方法都可以创建BFC

  • 根元素(<html>)
  • 浮动元素(元素的 float 不是 none
  • 绝对定位元素(元素的 position 为 absolute 或 fixed
  • 行内块元素(元素的 display 为 inline-block
  • 表格单元格(元素的 display为 table-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 display 为 table-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 display为 table、table-row、 table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或 inline-table
  • overflow 值不为 visible 的块元素
  • display 值为 flow-root 的元素
  • contain 值为 layoutcontent或 paint 的元素
  • 弹性元素(display为 flex 或 inline-flex元素的直接子元素)
  • 网格元素(display为 grid 或 inline-grid 元素的直接子元素)
  • 多列容器(元素的 column-count 或 column-width 不为 auto,包括 column-count 为 1
  • column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更Chrome bug)。

创建一个会包含这个浮动的BFC,通常的做法是设置父元素 overflow: auto 或者设置其他的非默认的 overflow: visible 的值。

块格式化上下文对浮动定位与清除浮动都很重要。浮动定位和清除浮动时只会应用于同一个BFC内的元素。浮动不会影响其它BFC中元素的布局,而清除浮动只能清除同一BFC中在它前面的元素的浮动。外边距折叠也只会发生在属于同一BFC的块级元素之间。

  • 大家请记住下面这个表现原则:如果一个元素具有 BFC,内部子元素再怎么翻江倒海、翻云覆雨,都不会影响外部的元素。所以,BFC 元素与相邻元素之间是不可能发生 margin 重叠的,因为 margin 重叠是会影响外部的元素的;BFC 元素也可以用来清除浮动的影响,因为如果不清除,子元素浮动则父元素高度塌陷,必然会影响后面元素布局和定位,这显然有违 BFC 元素的子元素不会影响外部元素的设定。
  • block-level 表示可以被放入 bfc
  • block-container 表示可以容纳 bfc
  • block-box = block-level + block-container
  • block-box 如果 overflow 是 visible, 那么就跟父 bfc 合并

除此之外还有IFC(inline-formatting-content) FFC( flex formatting contexts )

参考: https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flow_Layout/Intro_to_formatting_contexts

toy browser

这篇博客主要是熟悉浏览器的大致工作流程(不涉及代码,代码在最后的链接里)

如上图所示:

  • url回车后,先是解析http
  • 再解析html生成dom树
  • 再解析css
  • 再依据dom和css,进行layout获取position
  • 最后进行render 绘制bitmap

第一步(解析http)

response:

要想解析http 报文,我们可以利用状态机,使用多个状态来区分报文里的每个部分

比如设置一个 WAITTING_STATUS_LINE 状态,在第一个’\n’之前我们都可以视为在 WAITTING_STATUS_LINE 状态,’\n’之后用 WAITTING_STATUS_LINE_END 表示status line 的部分结束,以此类推使用各个状态来筛选响应报文的内容

这样我们就可以拿到response body 里面的html文本

第二步,解析html生成dom树

要想利用html文本最后返回一颗dom树,还是创建状态机来实现,其实html规范里面已经定义了很多很多个状态了,我们只选取一些 状态来模拟一下大致过程,'<‘表示 tagOpen ,在tagOpen之后就是tagName如果后面有个空格后面又有字符串那就是 attributeName 紧跟着’=’,后面就是 attributeValue,简而言之就是分析每一个token,找到每一个token所表达的语义

最后获得多个平级的对象,这个对象如下图结构

接下来就是如何把同级对象构建成一颗dom树

  • 从标签构建DOM树的基本技巧是使用栈
  • 遇到开始标签时创建元素并入栈,遇到结束标签时出栈
  • 自封闭节点可视为入栈后立刻出栈
  • 任何元素的父元素是它入栈前的栈顶

如此操作之后会获得如下图包含children的一个树结构(ast)

第三步 给每个dom树节点匹配对应的css rules

首先利用css库将<style>里的css表达式转换成css rules(其实现方式也是利用状态机生成ast),rule结构如下,declartions里面包含了css属性名和属性值,

然后根据当前的css rule的 selectors以及它的parent 的selectors 和第二步我们获得dom树进行比对,得到当前的css rule 应该添加到哪一个元素上,把这些css rule 添加到元素的computedStyle属性上

第四步 进行layout 计算每个元素在坐标轴上的位置

首先我们需要抽象出一个坐标轴

主轴:mainSize, mainStart, mainEnd, mainSign, mainBase,       

交叉轴:  crossSize, crossStart, crossEnd, crossSign, crossBase

例如flex布局中的 flexDirection === ‘row’ ,那么上面的变量就是如此:

        mainSize = 'width';
        mainStart = 'left';
        mainEnd = 'right';
        mainSign = +1;
        mainBase = 0;

        crossSize = 'height';
        crossStart = 'top';
        crossEnd = 'bottom'

然后把每个元素放入一行(行可以用数组实现)根据主轴的尺寸和css规则判断换不换行,再计算交叉轴的高度(取每一行的元素的最高交叉轴尺寸),获得每个元素在坐标轴上的坐标表示,如element[mainSize],element[crossSize]的具体数值

第五步根据坐标点render进行绘制

这里使用images库( Node.js轻量级跨平台图像编解码库 ),把上面的元素传入images的绘制方法就可以了,循环调用子元素的绘制方法完成dom树的 绘制(这里没有实现文字的绘制和图层的一些处理)

完整的代码地址: https://github.com/dz333333/Frontend-01-Template/tree/master/week07/layout

Event Loop里的task queue

首先 js 是单线程(当然 web worker 可以创建新的线程), js单线程任务被分为同步任务异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行(如下图)。

task queue(上图的任务队列) 分为:

  • macro-task (task/宏任务)由js外部宿主提供的 如: 渲染事件(如解析 DOM、计算布局、绘制);用户交互事件(如鼠标点击、滚动页面、放大缩小等);JavaScript 脚本执行事件;网络请求完成、文件读写完成事件。
  • micro-task (jobs/微任务)由js自己提供的,例如: process.nextTick, Promise, Object.observe(已废弃), MutationObserver( 核心就是采用了微任务机制,有效地权衡了实时性和执行效率的问题。 )

setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务

task queue的入队顺序如下面的代码

for (const macroTask of macroTaskQueue) {  
  handleMacroTask();   
  for (const microTask of microTaskQueue) {
    handleMicroTask(microTask);  
  }
}

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

以下是案例:

setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

以上代码根据规则 :

  • timeout1首先进入宏任务队列,
  • Promise实例第一个参数不会进入其他队列,在当前任务执行,then里面的函数进入微任务队列
  • 执行console.log(‘global1’);
  • 这时候微任务队列里有 console.log(‘then1’); ,所以执行它,然后微任务队列为null
  • 当前微任务队列是null,所以执行宏任务里的代码console.log(‘timeout1’);

所以最后的打印结果是:

  • promise1
  • promise2
  • global1
  • then1
  • timeout1

有关async 和await

我们经常使用上面两个关键字来写同步代码,这个时候要了解的是他们做了什么

async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

await 将等待 Promise 正常处理完成并返回其处理结果。 如果该值不是一个 Promise,await 会把该值转换为已正常处理的Promise,然后等待其处理结果。

示例:

async function async1() {
    console.log('async 1 start');
    await async2();
    console.log('async 1 end');
  }
  
  async function async2() {
    console.log('async 2');
  } 
  
  async1();
  new Promise(function(resolve) {
    console.log('promise1');
    resolve();
  }).then(function() {
    console.log('promise2');
  })

上面的代码可以理解为:

  • async1里的 console.log(“async 1 start”)可视为promise里面的同步代码,在当前执行栈中直接执行
  • 执行到await时,async2里的console.log(“async 2”)与上面逻辑一样,而console.log(‘async 1 end’);可视为promise().then()里执行,进入微任务队列
  • 再执行console.log(“promise1”),console.log(“promise2”)进入微任务队列
  • 此时微任务队列按照进入的先后顺序进入执行栈执行,依次打印async 1 end 和promise2

所以最后结果为:

  • async 1 start
  • async 2
  • promise1
  • async 1 end
  • promise2

ps: promise里的then的实现根据规范可以当做微任务,也可以是宏任务,以上代码都是在chrome里执行的,各个浏览器可能实现不一样

参考:

https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html

https://juejin.im/post/5c3d8956e51d4511dc72c200

执行上下文

先简单说下执行栈是什么:

js执行代码时会创造一个执行上下文(里面包括执行代码所需要的信息),比如下面代码

function foo(){
    let a=1
    return function b(){
        console.log(a)
    }
    b()
}
foo()

调用函数foo时先创建了函数foo的执行上下文,执行到return 的时候又在执行栈里添加了新的函数b的执行上下文,等函数b执行完了之后就把函数b的执行上下文从执行栈里踢出去,再然后foo的执行上下文也踢出去

再详细说一下执行上下文

在es5里它包含了下面三个内容

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this 值。

但在es6之后是这个样子的

  • lexical environment:词法环境(环境记录+外部词法引用),当获取变量或者 this 值时使用。
  • variable environment:变量环境,当声明变量时使用。
  • code evaluation state:用于恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示正在被执行的函数。
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  • Realm:使用的基础库和内置对象实例。
  • Generator:仅生成器上下文有这个属性,表示当前生成器。

词法环境会通过内部的环境记录来存储标识符与实际变量的映射关系,一个外部词法引用用于查找非当前作用域的变量时进行逐级溯源查找,以及绑定当前作用域的this指针

再聊一聊let/const/var

var 会将变量提升到当前函数作用域最顶端,let和const则不会,而且let碰到以下语句

  • for;
  • if;
  • switch;
  • try/catch/finally

会生成块级作用域

所以:

for(var i=0;i<5;i++){
    setTimeout(()=>{console.log(i)},1000) // i打印出来的都是5
}
for(let i=0;i<5;i++){
    setTimeout(()=>{console.log(i)},1000) // i打印出来的是0,1,2,3,4
}

并且

var、let、const class和function 在执行上下文的创建阶段时,js会在创建阶段执行上下文就将声明的变量、函数和类都创建,但是var 声明的变量会被初始化为undefined,而 let / const / class 声明的变量不会被初始化 状态为 uninitialized ,并且禁止访问,function 关键字的声明会直接将函数体赋值给对应的函数名。而且除了var之外,后面几个关键字声明的标识符映射是存放在词法环境里,而var声明的标识符映射是存放在变量环境里

所以:
console.log(c);
let c =1;
//Uncaught ReferenceError: Cannot access 'c' before initialization
//但是var
console.log(d) //undefiend
var d=1;

再谈谈闭包

根据古典闭包定义( closure )包含:

  • 环境部分(环境和标识符列表)
  • 表达式部分

我们在对应到js里面

  • 环境(函数的词法环境)
  • 标识符列表( 函数中用到的未声明的变量 )
  • 表达式(函数体)

其实闭包就是一个带有词法环境的函数

ABNF 描述四则运算

文法分为:

无限制文法、上下文相关文法、上下文无关文法、正则文法

BNF 巴科斯诺尔范式是一种用于表示上下文无关文法的语言 ,一类形式语言

BNF规定是推导规则(产生式)的集合 表示为 : 符号 ::= <使用符号的表达式>

终结符 是指一个形式语言的基本符号 不能被分解为更小的单位,产生式的组合必须完全由终结符组成

非终结符 是可以被替代的符号

< >     : 内包含的为必选项。
[ ]     : 内包含的为可选项。
{ }     : 内包含的为可重复0至无数次的项。
|       : 表示在其左右两边任选一项,相当于"OR"的意思。
::=     : 是“被定义为”的意思
"..."   : 术语符号
[...]   : 选项,最多出现一次
{...}   : 重复项,任意次数,包括 0 次
(...)   : 分组
|       : 并列选项,只能选一个

ABNF 是对bnf 的扩充 写为

 规则 = 定义 ; 注释 CR LF  

更多参考 https://www.jianshu.com/p/15efcb0c06c8

abnf表示四则运算

Expr   = Term   *Sum   ; 继续绕圈子, *Sum 有或者没有, 先写求和是有原因的
Term   = Factor *Mul   ; 再写乘积, *Sum 不匹配, 就尝试乘积
Sum    = SumOp  Term   ; 求和的运算, 有运算符必定要有后续表达式
Mul    = MulOp  Factor ; 乘积的运算,
Factor = Num /         ; 引向终结
         "(" Expr ")"  ; 括号永远都在

Num    = 1*(0-9)      ; 数字, 这可以是独立的终结符
SumOp  = "+" / "-"    ; 加或者减, 可以叫做求和, 小技巧
MulOp  = "*" / "/"    ; 乘或者除, 可以叫做乘积

更多参考 https://wizardforcel.gitbooks.io/go-blog-in-action/Chapter07.html

前端知识图谱

1. Range 接口表示一个包含节点与文本节点的一部分的文档片段。

2.

Node 是一个接口,许多 DOM API 对象的类型会从这个接口继承。它允许我们使用相似的方式对待这些不同类型的对象;比如, 继承同一组方法,或者用同样的方式测试。

以下接口都从 Node 继承其方法和属性:DocumentElementAttrCharacterData (which TextComment, and CDATASection inherit), ProcessingInstructionDocumentFragmentDocumentTypeNotationEntityEntityReference