Fork me on GitHub

日常心得手记

 在日常的自我提升过程中,总会有些碎片化的知识点在经过一段时间的积累和沉淀后才能汇总出一个系统的方法论。固有此文不定期更新,专门用于汇总结论。

js基础

  1. 我们声明的function其实等价于Function构造的实例,所以如function Person(){}Person.__proto__ === Function.prototypetrue

  2. 一个async functionEL中的执行顺序可以理解成:在内部的await等到返回值前的代码块为Promise中的声明块,是同步的。await进行函数调用时,可以理解为一个Promise.resolve(),其中若是函数,没有return前的内容也是同步执行的。最后return的内容和await后的内容等价于resolvethen方法对应的回调(micro task)。

  3. Promise构造函数中,即便resolve了,后续的代码依旧会执行(正常入栈)。对于resolve回调的这一句来讲可以理解为异步,后续的同步会先执行,e.g.

1
2
3
4
5
6
7
8
9
10
let p = new Promise((resolve, reject) => {
console.log(1);
resolve(2);
console.log(3);
throw new Error(4);
console.log(5);
}).then(console.log);
console.log(6);
p.then(console.log);
// 1 3 6 2
  1. 我们知道js的没有所谓指针一说,即便是引用类型也是将地址赋值给我们的变量。所以类似var a = b = {}这种,其实只是将两者都存储了该对象地址的访问,但是ab间并没有关联关系(它们只是存了同一个地址)。那么当我们再次修改a存储别的地址空间比如a = {name: 'Leo'},此时b还是老样子{}

  2. js的动态性,就我个人直观感受而言主要体现在变量、函数声明的提升上,解析引擎会预先确认相互的关系(LHS、RHS)。

  3. 执行环境及作用域,过去理解其实就是一层套一层,逐层往外寻找,取最近一层的内容,理论上够用了但是还是不够系统。

  根据红宝书的描述:我们运行JS的环境存在一个全局执行环境(浏览器的window、Node的global),另外每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境会被推入一个环境栈中,在函数执行完毕后,栈会将该推入环境弹出,将控制权返回给之前的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链,它保证了对执行环境中有权访问的所有变量和函数的访问有序性。而作用域链的前端,始终都是当前执行代码所在环境的变量对象。那这个变量对象是什么呢?当环境是函数时,会将活动对象作为变量对象(可以理解为入参那一块的空间,初始时是我们的arguments对象)。然后再逐个去下个包含环境搜索,直到全局执行环境(同时也是作用域链中最后一个对象)。

  1. 接上面的内容,举2个例子:
1
2
3
4
5
6
7
8
9
function sayMyName(name) {
console.log(name);
var name = 'Leo';
console.log(name);
function name() {
}
console.log(name);
}
sayMyName('Tony');

  首先在函数执行前,变量和函数的声明都会被优先处理;执行函数时,活动对象AO被创建。此时活动对象中的name被传入的Tony赋值,但是由于内部的函数声明提升,函数声明提升优先级最高,高于AO和内部变量提升,于是有第一个输出function name() {};另外还有个规则是对同一个变量的重复声明,会静默失败;接着由于赋值操作不会被提升,name被赋值为Leo然后输出Leo,最后的输出中间没有影响因素,通输出Leo

1
2
3
4
5
6
7
8
 function sayMyName(name) {
console.log(name);
var name = function () {
console.log(name);
}
name();
}
sayMyName('Leo');

  此处变量提升,但由于AO中已经存在声明的name,所以静默失败不影响原值Leo,遂先输出Leo,之后namefunction () {}覆盖,最终函数被调用根据作用域链查找,输出function () {//...}

  1. 再举两个比较有意思的作用域链查找:
1
2
3
4
5
6
7
8
9
10
var b = 1;
(function b() { b = 2; console.log(b);})()
// ƒunction b() { b = 2; console.log(b);}

var a = 10;
function test() {
console.log(a);
}
(function() { var a = 20; test() })()
// 10

React

  1. ref不仅可以作用在HTML element上,亦可作用在我们的component上;

  2. React.createRef()在使用时,注意只能作用于类class,作用于函数组件会失效报warning

  1. React.forwardRef适用于函数组件,他接收一个render function并返回一个新的React组件(HOC),主要应用在ref需要向子组件内嵌套的元素传递的情景:
1
2
3
4
5
6
7
8
9
10
// 官方文档 DEMO
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  1. Reconciliation是React中处理渲染性能优化的一部分,它通过几种策略成功将原本需要消耗 O(n3) 时间复杂度的DOM树转化工作优化到 O(n)。官方文档称其是一种启发式的策略,我们基于两个前提(几乎所有场景符合):
  • 两个不同类型的元素将会产生不同的树型结构

  • 开发者能够通过设置key告知调和过程哪些内容需要保留

常说的Diffing算法可以概括成下述几种策略:

  • 不同类型的元素节点,如原生DOM、React Component发生改变时,以该变动节点为根的树将被全部销毁并重建新的树型结构。
  • 相同类型的DOM节点,React会逐步比较其属性、文本差异,仅更新有差异的部分,而不会直接销毁一整部分。
  • 相同类型的React Component节点,当更新时,其实例会保留,React通过componentWillReceivePropscomponentWillUpdate更新嵌套的子内容,然后在render中又对子内容递归应用DIFF。
  • key可以规避一些场景的整体重建,通过局部移动或者插入的方式刷新内容。
  1. 受控与非受控组件是React表单处理下的概念,其最基本的逻辑是单一数据源。然而Form本身,就有数据关联的逻辑比如valuechecked这些属性,那要使其具有唯一控制源头,最自然的做法就是用state控制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     // input
    <input type="text" value={this.state.value} onChange={this.handleChange} />
    // textarea
    <textarea value={this.state.value} onChange={this.handleChange} />
    // select
    <select multiple={true} value={['B', 'C']}>

    <select value={this.state.value} onChange={this.handleChange}>
    <option value="grapefruit">Grapefruit</option>
    </select>

       另外对于<input type="file" />,由于其value是只读的,所以file input标签是一个铁非受控组件。相较于受控的state,非受控组件就是对原生DOM表单的值获取了,我们可以通过refcurrent属性来拿。值得注意的是,在React中一些原生组件的属性都被重写了,就像合成事件那样。所以value如果不是受控组件的state设置值,我们无法再对其设置初始值,可以用React提供的另一个属性defaultValue来替代。

       简单点来说,就是React不用state或是用不了state控制的表单元素是非受控的,反之则是受控的。基于单一数据源的哲学和可追溯的考虑,React推荐大多数场景使用受控组件。

  1. @babel/plugin-proposal-class-propertiesbabel插件非常重要,可以说是我们在Class中直接写=>函数最关键的一步,因为它能够帮我们将函数直接绑定到生成的实例函数上(不是原型上),以下代码取自babel官方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Bork {
//Property initializer syntax
instanceProperty = "bork";
boundFunction = () => {
return this.instanceProperty;
};

//Static class properties
static staticProperty = "babelIsCool";
static staticFunction = function() {
return Bork.staticProperty;
};
}

let myBork = new Bork;

//Property initializers are not on the prototype.
console.log(myBork.__proto__.boundFunction); // > undefined

//Bound functions are bound to the class instance.
console.log(myBork.boundFunction.call(undefined)); // > "bork"

//Static function exists on the class.
console.log(Bork.staticFunction()); // > "babelIsCool"

git flow

  1. git rebase一般有两个主流应用场景,其一是交互模式下多条commit信息的整理合并;其二是替代使用git merge进行分支合并,最后在对Master分支进行PR后,主分支就仍是一条干净的改动路线图。可以参考知乎这篇内容Git commits历史是如何做到如此清爽的?,其中Vue维护团队也是如此规范处理的。

工程化

  1. husky提供了开发者更易介入git hooks的能力。我们主要在其中两个hooks阶段操作:pre-commit内进行ESLint校验,发生错误会输出并退出git流程;commit-msg配合git-cz进行规范化的提交message模板配置。但是要是有人使用-n这个参数就会直接跳过校验lint的步骤…所以归根到底这种限制还是一种弱约束。真想严格统一上传代码的格式可以使用CI流程中的GIT HOOKS(区分于前面的git flow,是一种自动化部署的流程)触发。

  2. package-lock.json在不同npm版本下的执行反馈不同(npm 5后开始我们根据package.json进行npm i会默认生成一个package-lock.json文件记录当时具体的各依赖版本号):

    5.0.x版本,package.json不管如何修改,都会严格根据lock中的具体版本(初始生成的)下载依赖;
    5.1.0 ~ 5.4.2版本,npm i会无视lock的配置,即每次安装依赖都会根据semver变动刷新lock配置;
    5.4.2 ~ future版本,我们通过npm i进行指定依赖安装(xxx@xxx)时,若与package.json中写入的不一致会重新覆盖其中的配置,并安装指定的依赖。lock中之前初始生成的对应配置则不会改变。但如果在package.json中手动修改了版本再npm ilock中的配置就会对饮更新。

    npm在没有lock机制出现前似乎有这么一个维护关系的文件,npm-shrinkwrap.json;本着兼容的关系,这个老配置优先级会高于pakcage-lock.json

     参考github issue

  3. package-lock.json不应写入.gitignore,应当提交到git repo上。可以通过项目级的.npmrc配置package-lock=false关闭lock机制。另外对于yarnnpm两种安装方式,可以把不使用的一方写入.gitignore,比如使用npm,把yarn.lock写入。

     参考知乎

  4. npm使用与依赖关系:

    Semver(语义化版本号)扫盲

    npm信息汇总