Giter Club home page Giter Club logo

blog's People

Contributors

liul0703 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

面试总结

面试总结

回顾一下二月到三月之间经历的面试
如下会列出面试过程中问的比较多的一些问题,希望可以帮到有需要的同学,如果都可以答上来,那么前两面应该差不多了三四面就看运气了。
⭐️ 个数用来表示频次

HTML CSS

  • H5新增的标签 ⭐️⭐️
  • flex布局 flex : 1 ⭐️⭐️⭐️⭐️⭐️
  • position ⭐️⭐️⭐️⭐️
  • 水平垂直居中 ⭐️⭐️⭐️⭐️
  • 左边固定右边自适应 ⭐️⭐️⭐️⭐️
  • 动画 animation ⭐️
  • 盒模型 ⭐️
  • rem, em, vh 等 ⭐️⭐️⭐️
  • BFC IFC ⭐️⭐️

JavaScript

  • 基本数据类型和判断方法 ⭐️⭐️⭐️
  • typeof 和 instanceof ⭐️⭐️⭐️​
  • null 和undefined ⭐️⭐️⭐️
  • hoisting ⭐️⭐️⭐️⭐️⭐️
  • scope ⭐️⭐️⭐️⭐️⭐️
  • this ⭐️⭐️⭐️⭐️
  • 原型及原型链 ⭐️⭐️
  • new 操作符 ⭐️⭐️⭐️
  • 事件代理 ⭐️⭐️⭐️
  • Promise 以及常见的api用法和一些基于Promise的扩展题 头条快手都让实现一个promise.allSettled 对比async await ⭐️⭐️⭐️⭐️
  • generator ⭐️⭐️
  • 跨域以及OPTIONS ⭐️⭐️⭐️
  • 闭包及作用 缺点 ⭐️⭐️⭐️⭐️
  • 数组的常用的方法 ⭐️⭐️⭐️⭐️
  • ES6 ⭐️⭐️⭐️⭐️
  • Event Loop 以及会给出一段代码让说一下输出顺序 ⭐️⭐️⭐️⭐️⭐️
  • call apply bind区别 实现其中一个 ⭐️⭐️⭐️
  • 防抖节流 ⭐️⭐️⭐️⭐️
  • 数组乱序 ⭐️⭐️
  • 正则 ⭐️⭐️⭐️
  • 快排 归并 ⭐️⭐️

Vue

由于对Vue不是很熟 问的比较少

  • 生命周期 ⭐️⭐️⭐️⭐️
  • 组件间通信 ⭐️⭐️⭐️⭐️⭐️
  • Vue的双向绑定 ⭐️⭐️⭐️⭐️⭐️
  • proxy和defineProperty (顺带问了Reflect) ⭐️⭐️⭐️
  • computed和watch ⭐️
  • 怎么做组件复用 slot mixin这些优缺点 ⭐️⭐️⭐️⭐️
  • Vue-router实现原理 ⭐️⭐️
  • Vue源码 ⭐️⭐️⭐️⭐️

React

  • 生命周期 以及render和commit阶段 ⭐️⭐️⭐️⭐️⭐️
  • React的合成事件 ⭐️⭐️⭐️
  • Fiber意义 解决了什么问题 怎么实现的 ⭐️⭐️⭐️⭐️
  • React Hooks的优缺点 为什么需要 ⭐️⭐️⭐️
  • React的性能优化 以及key的作用 diff的过程 ⭐️⭐️⭐️ ⭐️
  • setState同步异步问题 ⭐️⭐️⭐️⭐️ ⭐️
  • 组件复用 HOC render props mixin 对比 ⭐️⭐️⭐️ ⭐️
  • Context 及Redux等状态管理工具 ⭐️ ⭐️
  • React和Vue的区别 ⭐️⭐️⭐️
  • 给定条件实现custom hooks ⭐️ ⭐️
  • useLayoutEffect 和useEffect区别 ⭐️ ⭐️
  • Suspense 和Concurrent Mode ⭐️

网络

  • XSS和CSRF以及如果防止 可以延伸到react中的$$typeof ⭐️⭐️⭐️
  • 同源策略 ⭐️⭐️⭐️
  • HTTP请求头以及状态码等等 ⭐️⭐️⭐️⭐️
  • HTTP缓存和浏览器缓存 ⭐️⭐️⭐️⭐️
  • 发起一个HTTP请求的全过程 ⭐️⭐️⭐️⭐️
  • HTTPS建立连接的过程和作用 ⭐️⭐️⭐️
  • HTTP2 ⭐️⭐️⭐️
  • 对称加密和非对称加密(大都和HTTPS一起问了) ⭐️⭐️⭐️
  • GET POST OPTIONS ⭐️⭐️⭐️
  • Cookies Session LocalStorage Service worker ⭐️⭐️

算法和其他

算法

总体来说没有碰到hard的 基本都是easy或Meduim难度
比如 : 最大子列和 链表倒数第k个元素 二叉树反转 二叉树最大深度 树形对象中找指定个元素输出路径等等

性能优化⭐️⭐️⭐️⭐️⭐️⭐️

性能优化(因为简历有写)所以问的最多也最详细牵扯到项目 基本每个公司都会问 深挖细节很多

其他

  • 浏览器输入一个url到展现出页面发生了什么 ⭐️⭐️⭐️⭐️⭐️
  • 给定条件自己设计组件等 ⭐️⭐️⭐️
  • png jpg webp等图片区别 ⭐️
  • 看过那些源码什么的 ⭐️⭐️

优秀文章收藏

GET与POST异同

POST & GET 区别

  • GET的url 可被手动输入 可被浏览器缓存 可存为书签
  • GET是幂等
  • POST是非幂等 不可被缓存
  • POST并不比GET更加安全

何时使用:

Quick Checklist for Choosing HTTP GET or POST
Use GET if:The interaction is more like a question (i.e., it is a safe operation such as a query, read operation, or lookup).

Use POST if:The interaction is more like an order, or The interaction changes the state of the resource in a way that the user would perceive (e.g., a subscription to a service), or The user be held accountable for the results of the interaction.However, before the final decision to use HTTP GET or POST, please also consider considerations for sensitive data and practical considerations.

new操作符做了什么

new操作符创建对象可以分为四个步骤:

1 . 创建一个空对象
2 . 将所创建对象的__proto__属性值设成构造函数的prototype属性值
3 . 执行构造函数中的代码,构造函数中的this指向该对象
4 . 返回该对象(除非构造函数中返回一个对象)

function Person(a, b) {
  this.name = a;
  this.age = b;
}

Person.prototype.show = function() {
  console.log(this.name, this.age);
};

var p = new Person('cat', 1);
console.log(p);  //  Person {name: "cat", age: 1}
function Person(a, b) {
  this.name = a;
  this.age = b;
}

Person.prototype.show = function() {
  console.log(this.name, this.age);
};

// var p = new Person('cat', 1);
var p = {};
p.__proto__ = Person.prototype;
Person.call(p, 'cat', 1);

console.log(p);  //  Person {name: "cat", age: 1}    与上段代码输出表现一致

function fakeNew() {
  const obj = Object.create(null)
  const C = [].shift.call(arguments)
  obj.__proto__ = C.prototype
  const ret = C.apply(obj, arguments)
  return typeof ret === 'object' && ret !== null ? ret : obj
}

React一些细节的理解整理汇总

React为什么不应该在componentWillMount阶段获取数据

This is problematic both server rendering (where the external data won’t be used) and the upcoming async rendering mode (where the request might be initiated multiple times). There is a common misconception that fetching in componentWillMount lets you avoid the first empty rendering state. In practice this was never true because React has always executed render immediately after componentWillMount. If the data is not available by the time componentWillMount fires, the first render will still show a loading state regardless of where you initiate the fetch. This is why moving the fetch to componentDidMount has no perceptible effect in the vast majority of cases.

为什么React的getDerivedStateFromProps方法是static ?

to help ensure purity which is important because it fires during interruptible phase ----- by Dan

This proposal is intended to reduce the risk of writing async-compatible React components.
Choosing lifecycle method names that have a clearer, more limited purpose.
Making certain lifecycles static to prevent unsafe access of instance properties. ----- react-rfc

why-is-getderivedstatefromprops-is-a-static-method

Reconciliation/ Render phase 和 Commit phase

Reconciliation / Render phase: React builds the work in progress tree and finds out the changes it needs to make without flushing them to the renderer. This is interruptible.

Commit phase: all the changes are flushed to DOM. This is uninterruptible.

useEffect vs useLayoutEffect

useLayoutEffect: If you need to mutate the DOM and/or DO need to perform measurements
useLayoutEffect fires synchronously after all DOM mutations but before Paint phase. Use this to read layout(styles or layout information) from the DOM and then perform blocking custom DOM mutations based on layout.

useEffect: If you don't need to interact with the DOM at all or your DOM changes are unobservable (seriously, most of the time you should use this).
useEffect runs after the render is committed to the screen i.e. after Layout and Paint phase. Use this whenever possible to avoid blocking visual updates

why is setState asynchronous?

  • Guaranteeing Internal Consistency
  • Enabling Concurrent Updates
  • Performance optimization

RFClarification: why is setState asynchronous?

React中的 type $$typeof tag

  • type
    type代表JSX的类型,在JSX中标签是什么type就是什么,例如<div> type就是div,<Example> type就是Example,所以说这里的标签是啥type就是啥。即 type === Example

  • $$typeof
    $$typeof是对JSX中type的细化,一般能渲染出的比如<div> <Modal>等的自定义component的$$typeof都是Symbol('react.element'),而无法渲染出的(React自身定义的一些封装类型)则由具体类型决定,例如<React.Portal> 的$$typeof就为Symbol('react.portal') <React.Fragment>则为Symbol('react.fragment'),还有memo,lazy,forwardRef都类似。Why Do React Elements Have a $$typeof Property? 【PS:为了安全】

  • tag
    tag是针对fiber的类型标识(type和$$typeof是针对element或者说JSX的返回值)主要为了react处理不同任务。

Defines the type of the fiber. It’s used in the reconciliation algorithm to determine what work needs to be done. As mentioned earlier, the work varies depending on the type of React element. The function createFiberFromTypeAndProps maps a React element to the corresponding fiber node type

... 待补充

JavaScript基本类型之--BigInt

BigInt

BigInt目前已经进入Stage 4阶段 下一个版本将会作为新特性出现在ECMAScript,下面我们来一起了解一下Bigint

BigInt是什么? BigInt是JavaScript中一种可以用来表示任意精度整数的基本数据类型

BigInt可以用来表示任意精度整数的特性为JavaScript解锁了更多的*操作,使用BigInt可以告别过去因为整数运算导致溢出的痛苦。特别是金融方面因为涉及大量的数据运算,比如高精度时间戳,或者数值过大的ID,这些是无法安全的用Number类型去存储的,所以退而求其次使用String类型去存储,有了BigInt类型后就可以安全的将其存储为数值类型。

另外BigInt的实现也为实现BigDecimal打下坚实基础,那将对于以十进制精度表示货币金额并对其进行精确运算(也就是0.10 + 0.20 !== 0.30问题)非常有帮助

此前已经有不少库实现了BigInt式的整数存储,当BigInt完全可用时,就可以拿掉那些依赖了,因为相比于使用这些依赖库,Native BigInt则更具优势。因为与之相比,NativeBigInt不需要加载解析编译的额外时间,并且在性能上表现更好。

Snipaste_2019-10-11_16-15-52-3ca05966-1757-413f-a051-29c2b22e8e62

图示为BigInt与其他流行库在Chrome中的表现情况对比(值越大表现越好)

现状:Number

JavaScript中Number是以64位双精度浮点型存储,所以会有精度限制,JavaScript中可以准确表示的最大整数是Number.MAX_SAFE_INTEGER这个值是2^53-1

    const max = Number.MAX_SAFE_INTEGER;
    // → 9_007_199_254_740_991  

Tips:为了可读性使用下划线作为分隔符进行分组 The numeric literal separators proposal

当自增一次时,可以得到正确值:

    max + 1;
    // 9_007_199_254_740_992 ✅

当自增两次时,我们发现结果并非预期

    max + 2;
    // → 9_007_199_254_740_992 ❌

max+1max+2的结果一致,这就导致我们无法保证在JavaScript中获取到的这个值的准确性,JavaScript中任何超出安全值范围的计算都会丢失精度,正因为如此我们只能信任安全值范围内的整数。

新热点:BigInt

BigInt是JavaScript中一种可以用来表示任意精度(arbitrary precision)整数的基本数据类型,使用BigInt可以安全的存储和操作任意大小的整数而不受Number类型的安全值范围的限制。

生成一个BigInt类型的值只需要在任意整数后加上n做后缀即可。例如:123BigInt类型表示123n,也可以通过全局函数BigInt(number)来将Number类型转化为BigInt类型,换言之BigInt(123) === 123n,让我们用BigInt来解决一下上文所提到的问题

    BigInt(Number.MAX_SAFE_INTEGER) + 2n;
    // 9_007_199_254_740_993n ✅

再看一个两个Number类型的数值相乘的例子

    1234567890123456789*123;
    // -> 151851850485185200000 ❌

仔细看这个结果肯定是不对的,因为最低位一个是9一个是3所以正确值肯定是以7结尾的(3*9=27),但是这里却是一串0结尾,我们来用BigInt重新计算一下

    1234567890123456789n*123n;
    // -> 151851850485185185047n  ✅

很显然这次是对的,当我们用BigInt来处理时不会受到Number中的安全值范围的限制,所以不用担心精度丢失

BigInt是JavaScript中新的的基础类型,所以可以用typeof操作符去检测

    typeof 123
    // 'number'
    typeof 123n
    // 'bigint'

因为Bigint是一个单独的类型,所以BigInt类型值和Number严格模式下不相等,e.g. 4!== 4n,BigInt类型和Number类型作比较时需要将自身类型转化为相互的类型,或者直接使用严格相等(===)

    4n === BigInt(4);
    // => true
    
    4n == 4;
    // => true
    
    4n === 4;
    // => false

当强制类型转化为布尔值时(例如在使用if,&&,||,或者Boolean(int)时触发),BigInt遵循和Numebr一样的规则

    if(0n){
    	console.log('if');
    }else{
    	console.log('else');
    }
    
    // 输出:'else', 因为0n是假值
    
    0n || 12n
    // -> 12n
    0n && 12n
    // -> 0n
    Boolean(0n);
    // -> false
    Boolean(12n);
    // -> true
    !12n
    // -> false
    !0n
    // -> true

BigInt支持那些常见的运算符例如:+,-,*,/ ** %,包括一些按位运算符如|, & , <<, >> ^ ,它和Number类型值的表现一致

    (7 + 6 - 5) * 4 ** 3 / 2 % 3;
    // → 1
    (7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
    // → 1n

一元运算符-可以用来表示BigInt中的负数,如:-42n ,但是一元运算符+不支持,因为如果支持则会导致+x表示结果为非Number值,从而引起和现有逻辑的冲突。

一个值得注意的点是不要混合操作BigInt类型和Number类型,因为任何隐式强制类型转化都会导致精度丢失,看下面的例子:

    BigInt(Number.MAX_SAFE_INTEGER)+2.5
    // => ?? 🤔

可以猜一下结果,其实并没有合理的答案。因为BigInt无法表示小数,而Number则无法正确表示BigInt类型的超出安全范围的值,因此当混用BigIntNumber时会报TypeError

其中唯一的例外是比较运算符,比如 === < > <= >=等,因为这类操作符最终会返回一个布尔类型值,不存在精度丢失的情况:

    1+1n
    // -> TypeError
    
    123<124n;
    // -> true

建议:BigInt和Number一般情况下不要混合操作,BigInt对于可能操作较大整数的情况下是合理的选择,Number则对于在安全值范围内的操作更合适,所以选定一种合适的类型用下去,不要相互混用。

注意⚠️:额外需要注意的一点是无符号右移操作符>>>,因为BigInt始终是有符号的所以无符号右移操作符对于BigInt来说不会生效。

API

关于BigInt的几个API BigInt() BigInt.asIntN(width, value) BigInt.asUintN(width, value) BigInt64ArrayBigUint64Array

  1. BigInt函数,这个BigInt全局构造函数和Number的构造函数类似,将传入的参数转化为BigInt类型,如果转化失败,会报SyntaxError或者RangeError
    BigInt(123);
    // -> 123n
    BigInt(1.2);
    // -> RangeError
    BigInt('1.2');
    // -> SyntaxError
  1. BigInt.asIntN(width, value) BigInt.asUintN(width, value),通过这两个库函数,可以将BigInt值包装为有符号或无符号整数,并限制在特定位数。其中BigInt.asIntN(width,value)BigInt类型值包装为有符号二进制整数,BigInt.asUintN(width,value)将BigInt类型值包装为无符号二进制整数。例如:如果你要执行64位算术运算,则可以使用它们来将其保持在适当的范围内:
    // BigInt类型值所能表示的最大的有符号的64位整数值
    const max = 2n**(64n - 1n) - 1n;
    
    BigInt.asIntN(64,max);
    // -> 9_223_372_036_854_775_807n
    
    BigInt.asIntN(64, max+1n);
    // -> -9_223_372_036_854_775_808n
    //    ^ 变为负值 因为溢出了
    // 一旦传给超过64位整数范围(即63位的绝对数值+1位符号位)的BigInt值,就会发生溢出。
    
    BigInt.asUintN(64,max);
    // -> 9_223_372_036_854_775_807n
    
    BigInt.asUintN(64,max+1n)
    // -> 9_223_372_036_854_775_808n
  1. BigInt使得准确表示其他编程语言中常用的64位有符号和无符号整数成为可能,其中BigInt64ArrayBigUint64Array可以使我们更加容易且有效地表示和操作此类值的列表。
    const view = new BigInt64Array(4);
    // -> [0n,0n,0n,0n]
    view.length;
    // -> 4
    view[0];
    // -> 0n
    view[0] = 40n;
    view[0];
    // -> 40n

BigInt64Array可以确保其值保持在有符号的64位限制范围内。BigUint64Array则确保其值保持在无符号位的64位限制范围内

    // BigInt类型值所能表示的最大的有符号的64位整数值
    const max = 2n**(64n - 1n) - 1n;
    view[0] = max;
    view[0]
    // -> 9_223_372_036_854_775_807n
    view[0] = max + 1n;
    view[0];
    // -> -9_223_372_036_854_775_808n
    //    ^ 溢出了
    
    const view_u = new BigUint64Array(4);
    view_u[0] = max;
    view_u[0];
    // -> 9_223_372_036_854_775_807n
    view_u[0] = max+1n;
    view_u[0];
    // -> 9_223_372_036_854_775_808n

兼容性

到目前为止已经实现BigInt的有Chrome(67+),Firefox(68+),Opear(54+),Node(10.4.0
+),其中Safari正在实现中

参考链接:

想知道BigInt是如何在Chrome中的实现的吗?请看下篇BigInt在V8中的实现(还没写呢)loading...

解决chromium工具下载问题

"fatal: unable to access 'https://chromium.googlesource.com/chromium/tools/depot_tools.git/': Failed to connect to chromium.googlesource.com port 443: Operation timed out"
chromium
解决办法:git config --global http.proxy 127.0.0.1:端口号【下载时候发现又不能用了,那就去 vi .gitconfig 里把设置的http和https删掉】

fetch chromium zsh: command not found: fetch
解决办法:export PATH="$PATH:{path}/depot_tools/depot_tools"

export http_proxy=http://127.0.0.1:1087;export https_proxy=http://127.0.0.1:1087;

gclient sync You have unstaged changes
解决办法: gclient sync -f

域名收敛

域名收敛即DNS优化

  • PC 时代为了突破浏览器的域名并发限制。有了域名发散。
  • 浏览器有并发限制,是为了防止DDOS攻击。
  • 域名收敛:就是将静态资源放在一个域名下。减少DNS解析的开销。
  • 域名发散:是将静态资源放在多个子域名下,就可以多线程下载,提高并行度,使客户端加载静态资源更加迅速。
  • 域名发散是pc端为了利用浏览器的多线程并行下载能力。而域名收敛多用与移动端,提高性能,因为dns解析是是从后向前迭代解析,如果域名过多性能会下降,增加DNS的解析开销。

正则表达式之断言

正则表达式--断言

Positive Lookbehind

  • (?<=) #断言要匹配的文本前缀

let str = 'qwert&code=1243&qerfvs=6577';
str.replace(/(?<=code=)\d+/,'4321');     // "qwert&code=4321&qerfvs=6577"

Negative Lookbehind

  • (?<!) #断言位置不能匹配的文本前缀

    "(?<!(T|t)he\s)(cat)"  => The cat sat on cat.

Positive Lookahead

  • (?=) #断言要匹配的文本后缀

    "(T|t)he(?=\sfat)" => The fat cat sat on the mat.
    // 匹配 紧跟fat的 The或the 

Negative Lookahead

  • (?!) #断言位置不能匹配的文本后缀

    "(T|t)he(?!\sfat)" => The fat cat sat on the mat.
    // 匹配文本后面不紧跟fat的The 或the

总结

  • X(?=Y) Positive lookahead X if followed by Y
  • X(?!Y) Negative lookahead X if not followed by Y
  • (?<=Y)X Positive lookbehind X if after Y
  • (?<!Y)X Negative lookbehind X if not after Y

React ES6 class constructor super()

当我们在写React时候 会用到ES6中的class语法 ,比较常见的情况如下:

class MyClass extends React.Component{
    constructor(){
        super()
    }
}

这里有两个问题:

  1. 是否有必要在constructor中调用super()函数?

  2. 调用super()super(props) 有何区别 ?

解答 Q1:

Always call super() if you have a constructor and don't worry about it if you don't have a constructor

只有当你有一个constructor时候调用super()才是必须的 看代码:

class MyClass extends React.component{
    render(){
    	return <div>Hello {this.props.world}</div>
    }
}

上述代码完全符合规定所以你其实并没有必要去为你创建的每个React Component 调用super() 话分两头 如果你的代码中有constrctor你就必须调用super()

class MyClass extends React.component {
    constructor(){
        console.log(this) //Error: 'this' is not allowed before super()
    }
}

出现上述错误的原因是 super()未被调用之前 this还未被初始化 (uninitialized) [更多]
或许聪敏的你会想着 使用一个空的constructor从而摆脱super()

class MyClass extends React.component {
    constructor(){} // Error: missing super() call in constructor
}

ES6的class的constructors如果属于子类就 必须调用super()方法 所以一旦你的代码有 constructor你就必须调用用super()

解答Q 2:

Call super(props) only if you want to access this.props inside the constructor. React automatically set it for you if you want to access it anywhere else.

假使你想获取到constructor中的this.props 你就必须调用super(props) 然后React就会自动为你自动为你配置好它 以便你可以在随便什么地方调用它

看一下使用super() super(props) 的不同 :

class MyClass extends React.component{
    constructor(props){
        super();
        console.log(this.props); // this.props is undefined

    }
}

当使用super(props)时 你可以从constructor中获取到this.props

class MyClass extends React.component{
    constructor(props){
        super(props);
        console.log(this.props); // prints out whatever is inside props
    }
}

当然还有一点 当你想在其他地方使用它时 也没有必要将props传递到constructor中 React会自动为你设置好它 [更多]

class MyClass extends React.component{
    render(){
        // There is no need to call `super(props)` or even having a constructor 
        // this.props is automatically set for you by React 
        // not just in render but another where else other than the constructor
        console.log(this.props);  // it works!
    }
}

我的理解是 总之 需要绑定 this. 方法或是需要在 constructor 使用操作 props 定义 state,就需要 constructor ,否则 例如在其他方法中(如 render())使用 this.props 则没必要要使用 constructor

原文链接: React ES6 class constructor super()

HTML5中的自定义 data-* 属性

当没有合适的属性和元素时,自定义的 data 属性是能够存储页面或 App 的私有的自定义数据
这个自定义data属性的用法非常的简单, 就是你可以往HTML标签上添加任意以 "data-"开头的属性, 这些属性页面上是不显示的,它不会影响到你的页面布局和风格,但它却是可读可写的.

html5中data-*属性的定义

  • data-* 属性用于存储页面或应用程序的自定义数据。
  • data-* 属性赋予我们在所有 HTML 元素上嵌入自定义 data 属性的能力。
  • 存储的(自定义)数据能够被页面的 JavaScript 中利用,以创建更好的用户体验

data-* 属性包括两部分

  • 属性名不应该包含任何大写字母,并且在前缀 "data-" 之后必须有至少一个字符
  • 属性值可以是任意字符串

获取时通过dataset对象,使用"."来获取属性,需要去掉data-前缀。遇到自定义属性中有连字符需要转化为驼峰命名

<div id="day2-meal-expense"data-drink="coffee"data-food="sushi"data-meal="lunch">¥20.12</div>

var expenseday2 = document.getElementById('day2-meal-expense');
console.log(expenseday2.dataset.food);       // "sushi"
console.log(expenseday2.dataset.drink);      //  "coffee"
console.log(expenseday2.dataset.meal);      //  "lunch"

jquery获取

$('#day2-meal-expense').data('drink');

HTML5使用data-* 要注意的地方

  • data-其后的属性名命名,发现如果为大写字母,则会转为 “-” + “小写字母”
    DOM.dataset.newAttrHaha 将会转换为属性“data-new-attr-haha”

  • 使用setAttribute定义的属性,如果中间包含”-”,转换规则有所不同,中间所有大写字母均转换为小写之母

  • DOM.setAttribute("data-newAttr2-abc", "22222") ==> data-newattr2-abc=”2222”,获取它可以使用getAttribute(‘设定时的名称’)

  • DOM.getAttribute(“data-newAttr2-abc”)或是DOM.dataset.newattr2Abc获取。获取则刚好相反 “-” + “小写”-->“大写”,如果是“-”+ “数字”,则保持原样不变

  • 设置属性时尽量使用setAttribute(“data-xxx”),其中属性命名最好不要有大写的出现,避免出现“-”,推荐使用“char_char”,如:“favo_obj_id”

data-* 全局属性 构成一类名称为自定义数据属性的属性,允许通过脚本在HTML 和其 DOM 表示之间交换专有信息。所有这些自定义数据都可以通过属性设置的元素的 HTMLElement 接口来访问。 HTMLElement.dataset 属性可以访问它们

原型&继承&闭包

原型

我们创建每一个函数 都有一个prototype属性 该属性指向一个对象 这个对象就是原型
我们创建一个对象时候 可以根据自己的需求 选择性的将一些方法属性通过prototype属性 挂载在原型对象上 而每一个new出来的实例都有一个__proto__属性 该属性指向构造函数的原型对象 通过这个属性 让实例方法能够访问 到原型对象上的这方法


闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,这个函数持有对该词法作用域的引用,这个引用就叫做闭包
闭包本质还是函数,只不过这个函数绑定了上下文环境(函数内部引用的所有变量)
缺点:常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
作用(使用场景):可以用来管理私有变量和私有方法,将对变量(状态)的变化封装在安全的环境中,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
闭包有三个特性:
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收


继承

  • 原型链继承

function Animals(){
    this.name = ['cat','dog'];
    this.other = 'animals';
}
function Cat(){
};
// 继承
Cat.prototype = new Animals();

var c1 = new Cat();
var c2 =  new Cat();

c1.name.push('pig');
console.log(c1.name);     // ["cat", "dog", "pig"]
console.log(c2.name);    // ["cat", "dog", "pig"]

c1.other = 'cat';
console.log(c1.other);     // cat
console.log(c2.name);     // animals

缺点: 引用类型的属性被所有实例共享 一旦修改 会反应在所以实例上 创建子类型实例时 不能向超类型传参

注:通过原型链实现继承时 不能使用对象字面量创建原型方法 因为会重写整个原型链

  • 借用构造函数

function Animals(){
    this.name = ['cat','dog'];
    this.other = 'animals';
}
function Cat(){
    Animals.call(this);
};

var c1 = new Cat();
var c2 =  new Cat();

c1.name.push('pig');
console.log(c1.name);     // ["cat", "dog", "pig"]
console.log(c2.name);    // ["cat", "dog"]

优点:可以在子类型构造函数中向超类型构造函数传参

缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法 无法实现函数复用

  • 组合继承

function Animals(name){
    this.name = name;
    this.animals = ['cat','dog'];
}

Animals.prototype.sayName = function(){
    console.log(this.name);
}
function Cat(name,age){
    // 属性继承
    Animals.call(this,name);
    this.age = age;
}

Cat.prototype = new Animals();
Cat.prototype.constructor = Cat();
Cat.prototype.sayAge= function(){
    console.log(this.age);
};

var c1 = new Cat('Cat1',18);
c1.animals.push('pig');
console.log(c1.animals);    //  ["cat", "dog", "pig"]
c1.sayName();                   // Cat1
c1.sayAge();                       // 18

var c2 = new Cat('Cat2',19);
console.log(c2.animals);    //  ["cat", "dog"]
c2.sayName();                   // Cat2
c2.sayAge();                     // 19

优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

缺点:无论什么情况下都会调用两次父类型构造函数 一次是在创建子类型原型时候 另一次是在子类型构造函数内部 子类型中包含全部父类型对象的实例属性 调用子类型构造函数时会重写这些属性

  • 寄生式继承

创建一个仅用于封装继承过程的函数 该函数在内部以某种方式来增强对象 最后返回对象

function Animals(orginal){
    var c = Object(orginal);
    c.sayHi = function(){
        console.log('Hi');
    }
    return c;
}

var c = {
    name:'cat',
    others:['cat1','cat2']
}
var cat = Animals(c);
cat.sayHi();    // 'Hi'

缺点:无法做到函数复用

  • 寄生组合式继承

基本思路是:不必为了指定子类型的原型而调用父类型的构造函数 我们只需要的是一个父类原型的副本 本质上就是使用寄生式继承来继承父类的原型 然后讲结果指定给子类型的原型

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

//  继承
inheritPrototype(Child, Parent);

优点:集组合继承和寄生继承优点于一身 是实现继承最有效的方式

HTTP及HTTPS通信建立TCP连接的过程

建立TCP连接的三次握手过程

  • 第一次握手(SYN=1, Seq=X):客户端项服务器端发送一个TCP的SYN标志位置为1的包,指名客户端想连接的服务器端口,以及初始化序号X,保存在包头的序列号字段里
  • 第二次握手(SYN=1, ACK=1, Seq=Y, ACKnum=x+1): 服务器发送确认应答包(ACK),并将ACK置为1,服务器选择自己的ISN序列号,放到Seq域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1,发送完毕后,服务器端进入 SYN_RCVD 状态
  • 第三次握手(ACK=1,ACKnum=y+1):客户端再次发送确认包(ACK),并将SYN置为0,ACK置为1,并将确认序号(Acknowledgement Number)+1,并且在数据段上将ISN的+1,发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束

TLS/SSL的四次握手

  • 第一步:客户端请求建立SSL连接,并发送自身支持的加密及压缩方式和支持的协议版本以及一个随机数client random给服务器;
  • 第二步:服务器收到后,发送给客户端确认使用的加密通信协议版本,确认使用的加密方法,并且再加上另外一个随机数server random,和服务证书(其中有公钥)发送给客户端
  • 第三步:客户端确认这个数字证书是有效的,并且再生成一个新的随机数,将这个随机数用服务器发送给它的数字证书中的公钥进行加密发送给服务器;
  • 第四步:服务器收到客户端的回复,利用自己的私钥进行解密,获得这个随机数,然后通过将前面这三个随机数以及他们协商的加密方式,计算生成一个对称密钥,服务器握手结束通知,表示服务器的握手阶段已经结束

断开TCP连接的四次挥手过程

  • 第一次挥手(FIN=1,seq=x):客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据,发送完毕后,客户端进入 FIN_WAIT_1 状态
  • 第二次挥手(ACK=1,ACKnum=x+1):服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接
  • 第三次挥手(FIN=1,seq=y):服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1,发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
  • 第四次挥手(ACK=1,ACKnum=y+1):客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包,服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态,客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

渲染性能分析(上)

如今大部分设备的刷新频率数60fps,什么意思呢?意思就是每秒屏幕刷新60次。举个例子:页面上出现动画或者渐变的效果,又或者用户滚动页面,那么浏览器渲染动画或页面的每一帧的频率也需要跟设备屏幕的刷新率保持一致。每帧的预算时间是16.66ms,这个时间段中浏览器要处理很多事情,所以最好的情况是在10ms内将所有工作做完,如果超出预算时间,那么帧率会下降,就会出现常见的卡顿现象,对用户体验带来负面影响

渲染过程

要想在预期时间内完成页面更新则主要有5个关键点需要关心,这些点决定了页面的渲染时间

frame-full-a9d1a7d7-0b9e-40b9-809a-40254258e7ad

  • JavaScript : 一般来说,JavaScript通常用来实现一些视觉变化的效果,比如来处理一些动画效果,或者对一个数据集排序,又或者修改页面的DOM元素等。当然除了JavaScript,还有一些常用的方法也可以实现类似的效果,比如:CSS Animation、Transitions、Web Aninations API。
  • Style : 这个过程主要是样式计算。是根据样式匹配选择器计算出哪些元素需要应用哪些属性规则的过程,知道规则后计算出每个元素的最终的样式。
  • Layout : 知道了各个元素对应的规则后,浏览器开始计算元素要占据的空间大小和在屏幕中的位置,页面布局模式意味着一个元素可能会影响其他元素,例如元素的宽一般会影响其子元素的宽度以及树中各处的节点。
  • Paint : 绘制是填充像素的过程。它涉及绘制出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分,绘制一般在多个图层上完成
  • Composite : 由于页面的各部分内容可能被绘制在多个图层,因此需要按照正确的顺序绘制到屏幕上,特别是对于重叠在一起的元素来说,这个顺序是非常重要的。

这些部分都是很重要的,处理不当就会导致页面出现卡顿情况,所以为了做好这一点必须要确切的知道你的代码到底会影响渲染的哪个阶段

  1. JS/CSS ➔ Style ➔ Layout ➔ Paint ➔ Composite

    如果修改元素的"layout"属性,即改变元素的几何属性(例如宽高,或位置),那么浏览器必须检查其他所有元素并重新计算,受到影响的部分要重新绘制,最终进行合成。所以要重走整个过程

  2. JS/CSS ➔ Style ➔ Paint ➔ Composite

    如果修改的内容是属于“Paint”属性(比如:背景图片,文字颜色或阴影),这些不会影响到页面布局,所以浏览器会直接跳过Layout阶段。

  3. JS/CSS ➔ Style ➔ Composite

    如果修改的属性既不需要页面重新布局,也不需要重新绘制,那浏览器会跳过Paint和Layout阶段,这种情况开销最低,适合用在动画或滚动的情况

下面来详细说说每个阶段需要注意的问题

优化JavaScript的执行

JavaScript经常会触发一些页面视觉上改变,有时候是直接改变样式,有时候是更新页面数据,有时候是执行一些动画效果等等。JavaScript运行时间通常是影响性能的关键因素,所以接下来我们可以看一下如何去尽量减小这些因素的影响。

JavaScript的性能分析可能是一门艺术了,因为你所写的JavaScript代码和实际执行时的完全不同。如今的浏览器都采用的是JIT(Just In Time)编译器,同时会使用各种优化技巧以供最快速的执行,但是正因为如此却改变了代码本来的动态性。

如上所言,那么下面会给出一些建议来帮你更好的执行JavaScript代码

TL;DR

  • 使用requestAnimationFrame代替setTimeout或者setInterval来处理页面上的视觉变化
  • 考虑使用Web Worker来将需要在主线程长时间运行的JavaScript代码迁移
  • 将大的耗时任务拆分为多个task分为几帧完成
  • 使用Chrome DevTools中的Performance和JavaScript Profiler来观测对JavaScript的影响因素

使用requestAnimationFrame做动效

某些情况下在页面视觉上发生变化时,你可能想正好在每一帧开始时执行某些操作,那么requestAnimationFrame是唯一可以准确保证在每一帧执行前执行JS代码的方法

/**
 * 作为requestAnimationFrame的回调函数,会在每帧开始前执行
 */
function updateScreen(time){
    // ...
}
requestAnimationFrame(updateScreen);

一些框架或者示例可能使用setTimeout或者setInterval来做一些视觉上的变动比如动画,但是问题是无法确定这些回调函数的执行时间点,有可能恰巧是在每帧的结尾,那就可能导致帧丢失,从而导致页面卡顿,这完全不是我们想要的。
Snipaste_2019-10-21_10-24-45-5c703a8e-78ee-423c-8bd1-b5a312855092

实际上jQuery以前也用setTimeout来执行动画,在后来的版本中改用requestAnimationFrame了,如果你还在使用旧版本的话可以检查一下,有必要可以考虑升级。(应该人很少了吧)

减少复杂度的或者使用Web Worker

JavaScript运行在浏览器的主线程上,与此同时主线程还要执行样式计算,布局,绘制等等。如果JavaScript代码长时间执行则会阻塞这些任务,就可能出现帧丢失的情况。

所以需要考量JavaScript代码的执行时间点和执行时长。举个例子:如果在执行滚动操作,那么理想情况下应该保持JS代码的执行时间保持在3~4ms内,如果超过这个时间,就要考虑采取优化手段了,如果是在空闲时间段那就可以放宽时间限制了。

很多情况下如果不需要访问DOM,就可以把一些纯计算的工作交给Web Worker去执行,对于数据的处理或者搜索排序等等操作都非常适合在这里处理

const dataSortWorker = new Worker('sort-worker.js');
dataSortWorker.postMessage(dataToSort);

// 主线程则可以做其他事情
dataSortWorker.addEventListener('message',(e)=>{
    const sortedData = e.data;
    // ... 
})

也不是所有情况都适合:Web Worker无法访问DOM。在必须在主线程执行的工作,可以考虑采用批处理方法,什么意思呢?就是把较大的任务分割成多个task,每个task不超过几毫秒,并且放在requestAnimationFrame中,让其在每帧的开始去执行。

const taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTiem){
    let taskFinishTime;
    do {
        // 假设下一个task已经推到栈里了
        const nextTask = taskList.pop();
        // 执行下一个task
        processTask(nextTask);

        // 
        taskFinishTime = window.performance.now();
    } while(taskFinishTime - taskStartTime < 3);
    
    if(taskList.length > 0){
        requestAinmationFrame(processTaskList);
    }
}

这种处理方法从UI方面考虑,可以加一个进度标识图标以便让用户知晓任务正在执行。不管怎样它都可以保证程序的主线程是空闲状态,因此不会影响用户交互行为。

知晓JavaScript的帧的副作用

在评估一个库或者一个框架亦或自己的代码时,逐帧分析JS代码的执行消耗的时间是很必要的。特别是在动画或者一些过渡效果方面时尤为重要。

Chrome DevTools提供的Performance功能是查看JS每帧执行消耗时间的非常好的工具。

Snipaste_2019-10-21_11-51-28-18b91852-90a7-4f9b-9c7c-75a55a7cdc7b

通过这个工具提供的信息分析后就可以找出影响性能的原因,如我们之前所提到的,如果在主线程中长时间执行的JS代码是非必要的就可以把它移到Web Worker中来让主线程执行其他任务。【Performance的使用方法

对于Style Calculation阶段的优化

避免嵌套过深和复杂的样式计算

通过添加和删除元素,更改属性,或者通过动画来改变DOM结构等都会导致浏览器重新计算元素样式,很多情况下都会重新对整个页面或其中部分布局(layout)(或者回流[reflow]),这个过程也叫样式计算。

💡:关于repaint和reflow的区别:repaint 指元素只发生了外观的变化,但是不影响布局,页面只需要做重绘即可。会触发repaint的常见CSS属性比如:outline, visibility, background, 或 color。relfow 则是发生了几何上的变化需要对元素进行重新计算和布局。例如元素的width,height等。

样式计算的第一步就是创建一个与之对应的选择器集合,实际上就是让浏览器确定哪些类哪些伪类选择器和ID该应用于哪个元素,第二步是从匹配的选择器中获取所有样式,并计算最终样式。在Blink(Chrome和Opera的渲染引擎)中这个过程还是相当消耗性能的。

这个过程中渲染引擎大概有50%的时间在匹配选择器,剩下的一半时间在计算最终样式。

TL;DR

  • 降低选择器的复杂度,即避免嵌套太深
  • 减少在样式中的计算

降低选择器的复杂度

我们知道浏览器在解析匹配CSS规则时是从右向左查找匹配的,嵌套越深选择匹配的负担越重,最好不要超过三层。同时浏览器在解析生成页面时分别解析构建DOM Tree 和CSSOM,在DOM树构建完成CSSOM未构建完成时,是不会直接把html放出来的,所以CSS不能太大,否则会有一段白屏时间,所以把字体或者图片转成base64放在CSS里是不太推荐的做法。

有些CSS优化建议说要按照如下优先级书写:

  1. 位置属性【position,top,left,right,z-index...】
  2. 大小【width,height...】
  3. 字体相关【font,text-align...】
  4. 背景【background,border...】
  5. 其他【animation,transition...】

其实这些顺序对浏览器来说是一样的,因为浏览器在解析构建CSS规则时并不立马进行渲染,而是把这些属性值合并、归类、并将最终计算出的属性值放到computedStyle里,之后交由Layout阶段去计算实际显示值,Paint阶段才会去绘制。所以顺序并不会带来性能上的影响,对浏览器而言都是一样的。

关于样式中的计算比较典型一个例子的可能就是对元素使用rgba函数(或者使用calc),当CSS解析时就需要先去执行rgba函数计算颜色,所以直接写成16位的色值更好一些。

参考链接: Rendering Performance

事件委托

事件委托又叫事件代理 事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件
事件委托的原理:
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件 举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件

H5适配ios全面屏

适配ios全面屏

对于iPhone X等全面屏的适配问题,IOS11提出了一个安全区域的概念,安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)影响,要做好适配,必须保证页面可视、可操作区域是在安全区域内
对于适配ios全面屏,ios对现有 viewport meta 标签的一个扩展,用于设置网页在可视窗口的布局方式,可设置三个值:

  • contain: 可视窗口完全包含网页内容
  • cover:网页内容完全覆盖可视窗口
  • auto:默认值,跟 contain 表现一致

【注意:网页默认不添加扩展的表现是 viewport-fit=contain,需要适配 iPhoneX 必须设置 viewport-fit=cover,这是适配的关键步骤】

iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量

  • safe-area-inset-left:安全区域距离左边边界距离
  • safe-area-inset-right:安全区域距离右边边界距离
  • safe-area-inset-top:安全区域距离顶部边界距离
  • safe-area-inset-bottom:安全区域距离底部边界距离

【当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持env() 的浏览器,浏览器将会忽略它】
同时为了向后兼容ios11.2~ios11.0 需要使用关键字 constant 而不是env

css-attribute: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
css-attribute: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */

注意:env() 跟 constant() 需要同时存在,而且顺序不能换

更多内容可以参考文档:webkit.org

渲染性能分析(三)之Style

对于Style Calculation阶段的优化

避免嵌套过深和复杂的样式计算

通过添加和删除元素,更改属性,或者通过动画来改变DOM结构等都会导致浏览器重新计算元素样式,很多情况下都会重新对整个页面或其中部分布局(layout)(或者回流[reflow]),这个过程也叫样式计算

样式计算的第一步就是创建一个与之对应的选择器集合,实际上就是让浏览器确定哪些类哪些伪类选择器和ID该应用于哪个元素,第二步是从匹配的选择器中获取所有样式,并计算最终样式。在Blink(Chrome和Opera的渲染引擎)中这个过程还是相当消耗性能的。

这个过程中渲染引擎大概有50%的时间在匹配选择器,剩下的一半时间在计算最终样式。

TL;DR

  • 降低选择器的复杂度,即避免嵌套太深
  • 减少在样式中的计算

降低选择器的复杂度

我们知道浏览器在解析匹配CSS规则时是从右向左查找匹配的,嵌套越深选择匹配的负担越重,最好不要超过三层。同时浏览器在解析生成页面时分别解析构建DOM Tree 和CSSOM,在DOM树构建完成CSSOM未构建完成时,是不会直接把html放出来的,所以CSS不能太大,否则会有一段白屏时间,所以把字体或者图片转成base64放在CSS里是不太推荐的做法。

有些CSS优化建议说要按照如下优先级书写:

  1. 位置属性(position,top,left,right,z-index...)
  2. 大小(width,height...)
  3. 字体相关(font,text-align...)
  4. 背景(background,border...)
  5. 其他(animation,transition...)

其实这些顺序对浏览器来说是一样的,因为浏览器在解析构建CSS规则时并不立马进行渲染,而是把这些属性值进行计算将最终的属性值放到computedStyle里。所以顺序并不会带来性能上的影响,对浏览器而言都是一样的。

关于样式中的计算比较典型一个例子的可能就是对元素使用rgba函数,当CSS解析时就需要先去执行rgba函数计算颜色,所以直接写成16位的色值更好一些。

什么是Largest Contentful Paint

Largest Contentful Paint

一直以来,如何能准确的衡量一个页面从初始化到页面的主要内容已对用户可见的这个过程所耗时长是一个非常棘手的问题。以前的一些指标如load或者DOMContentLoaded都无法准确的描述用户所看到的内容,较新的一些指标如First Contentful Paint(FCP)只能捕获到首次加载时间,如果这个页面的展示的一个进度条或者开机动画,那么这个时间其实就和用户毫无关联。

在过去我们比较推荐的性能指标比如First Meaningful Paint(FMP)和Speed Index(SI)来帮助我们测算在初始渲染后页面内容加载所需的时间,但是这些指标通常都比较复杂很难解释清楚,甚至于有些时候是错误的,所以这就意味着它也不能准确表示到底什么时候页面的主要内容被加载渲染了。就FMP而言,对于不同类型的页面内容FMP其实并不相同,比如一个博客更重要的部分应该是文章的标题和内容简介对用户可见,所以其FMP应该是文本内容,但是对于一些商品站点或者电商而言,图片才是至关重要的关注点,所以它的FMP应该是图片加载完成。所以这也是为什么FMP至今无法被标准化的原因。

现在我们有一种全新的方案来衡量页面主要内容何时加载更准确,这个方案就是查看页面中最大元素的呈现时间,即Largest Contentful Paint (LCP)

什么是LCP ?

Largest Contentful Paint (LCP) 是一个非常重要的用来衡量页面加载速度的指标,它标志着页面的主要内容的加载时长,LCP所需时间越短用户体验越好

LCP这个指标是用来表述在视口中可见的最大内容元素的渲染时间。

哪些元素会被考虑 ?

根据目前的Largest Contentful Paint API 中规范(草案阶段),如下类型的元素会被考虑:

  • <img>元素
  • <svg>中的<image>元素
  • <video>元素(被使用的海报)
  • 元素样式带有background image且通过url()函数加载(比如 CSS gradients)
  • 包含文本节点或其他内联文本元素的块级元素

⚠️:现阶段将其限制在有限的元素集合内是为了简单考虑,未来会将更多的元素加入其中

如何确定元素大小?

这个元素的Largest Contentful Paint大小取决于这个元素在可视区内的可见尺寸。如果一个元素延伸到可视区外比如这个元素的溢出部分或者被裁减的不可见部分都不会被计算在内。换言之,所见即所得,可看到最大元素面积多大它的LCP就是多大。

  • 对于已从其固有尺寸调整大小后的image元素 LCP的值是可见大小和固有大小值之间较小的那个
  • 对于文本元素仅考虑其文本节点的大小(包含所有文本节点的最小矩形)
  • 另外对于任意元素通过CSS设置的margin、padding、border这些属性都不会考虑在内。

什么阶段可以认为触发了Largest Contentful Paint

页面通常是分阶段加载的,所以页面上最大的元素可能会发生变化。为了应对这种潜在变化,浏览器在绘制完第一帧后会立即调度一个类型为largest-contentful-paint的PerformanceEntry来标识最大内容元素(Largest contentful element),在后续每帧更新的过程中只要最大内容元素(Largest contentful element)发生变化,就会重新分配一个PerformanceEntry。

很重要的一点是只有当那些已经被渲染并且对用户可见的元素才会被考虑为最大内容元素(Largest Contentful Element),比如还未加载完成的图片就不会被考虑。又比如使用特殊字体的文本节点,这种情况下当较小的元素被当做Largest contentful element的时候,一旦更大的元素完成渲染,就会调度分发一个新的PerformanceEntry对象

除了有延迟加载图像和字体情况外,也可能会在页面有新的内容可用时(available),在DOM中添加新元素,如果有任意新元素比之前的最大内容元素更大,同样的也会调度一个新的PerformanceEntry

如果从页面的DOM结构中移除了某个元素,那么这个元素就不会再被考虑了,类似的如果一个元素关联的image资源发生了变化(比如通过JavaScript改变了img.src属性),那么只有当这个图片加载完成后才会被列入参考范围

一旦用户开始与页面进行交互(点击,滚动或按键),浏览器将停止上报新条目(entries),因为用户交互过程中通常会改变用户可见的内容(例如滚动)

⚠️:由于用户可以在后台选项卡中打开页面,因此,只有当用户将选项卡聚焦后,最大内容渲染(Largest contentful Paint)才会出现,这可能比他们第一次加载时晚。

加载时间 vs 渲染时间

出于安全考虑。对于请求头缺少Timing-Allow-Origin的跨域图片,不会显示图片的渲染时间点,只显示加载时间。为了使你的LCP指标更准确推荐尽可能的给加上Timing-Allow-Origin请求头

如何处理元素布局和大小更改?

为了使页面计算和分配新性能条目的性能开销更低,对于元素大小或位置的变更不会生成新的LCP,仅考虑元素的初始大小和在视口中的位置。这意味着可能无法上报最初在屏幕外通过动画显示在屏幕上的图片,这也意味着最初在视口中渲染的元素随后被移出视口范围,但仍将上报其初始时在视口内的大小

但是,(如上所述)如果某个元素已从DOM中删除或与其相关的图像资源发生了变化,则该元素将不会再被当做LCP的候选元素。

如下是几个热门网站加载过程的Timeline及何时触发Largest Contentful Paint:

Snipaste_2020-02-01_15-24-58

上面的两个例子的最大元素都随着内容的加载不断变化,第一个例子中新内容被添加到DOM结构中并且成为了最大内容元素。第二个例子中由于布局变化之前的最大内容元素被移出了视口范围。

通常情况下,延迟加载的内容要比页面上已有的内容更大,但也不一定都是这种情况。接下来的两个示例显示了在页面完全加载之前触发了Largest Contentful Paint。

Snipaste_2020-02-01_15-25-50

第一个例子中Instagram的logo加载出来相对较早,即便其他内容也逐渐加载完成,它依然是最大元素。第二个例子中最大的元素是一段文本,该文本在其他任一图片或logo加载完成之前就显示了。由于所有单个图片均小于它,所以在整个加载过程中它始终是最大元素。

你可能会注意到了,在第一个例子中的Instagram时间轴的第一帧中,相机logo没有被绿色框框起来。这是因为它是一个<svg>元素,而 <svg>元素现在还不被视为LCP候选对象。所以第一个LCP候选对象是第二帧中的那段文本。

怎样计算LCP ?

在JavaScript中通过使用Largest Contentful Paint API 计算LCP。如下的例子展示了如何通过创建一个PerformanceObserver来监听largest-contentful-paint

    let lcp;
    
    const po = new PerformanceObserver((entryList) => {
    	const entries = entryList.getEnteries();
      const lastEntry = entries[entries.length - 1];
      
      lcp = lastEntry.renderTime || lastEntry.loadTime;
    })
    
    po.observe({type: 'largest-contentful-paint', buffered: true });
    
    addEvevtListener('visibilitychangre', function fn(){
      if (lcp && document.visiblityState === 'hidden') {
        console.log('LCP', lcp);
        removeEventListener('visibilitychange',fn, true);
      }
    }, true);

如果最大元素并不是最重要的怎么办?

在某些情况下页面中最重要的元素却不是最大元素,所以对我们来说更重要的元素的渲染时间更有价值,当然这是可以通过Element Timing API 来实现自定义指标

如何改善LCP?

LCP 通常受以下三个因素影响:

  • 服务器响应时间
  • CSS阻塞时间
  • 资源加载时间

如果你的页面是客户端渲染(client-rendered),并且页面的最大内容元素是通过JavaScript添加到DOM中,那么你的脚本解析编译执行的时间都会是影响LCP的原因。

如下给出了一些优化方案:

后记

如下是一些常用的衡量性能的指标(Important metrics to measure)

  • First Contentful Paint (FCP) : 衡量页面从开始加载到页面内任意部分内容被绘制到屏幕上的时间
  • Largest Contentful Paint (LCP) : 衡量页面从开始加载到页面中最大的文本内容或者图片元素被渲染到屏幕上的时间
  • First input delay (FID) : 衡量用户首次与当前页面交互(比如:点击一个链接,或者一个按钮或者自定义事件等等)到浏览器实际可以响应交互的时间
  • Time to Interactive (TTI) : 衡量从页面开始加载到可视化呈现且初始化脚本已加载(如果有)且能快速响应用户输入的时间
  • Total blocking time (TBT) : 衡量从FCP到TTI这个时间段内主线程被阻塞无法响应用户输入的时间
  • Cumulative layout shift (CLS) : 衡量从页面开始加载到页面生命周期状态变为隐藏之间发生的所有意外布局转换的累积
参考链接

渲染性能分析(下)

上篇我们大致分析了在处理JavaScript阶段和Style阶段需要注意的问题,这篇我们就来看下在Layout、Paint、Composite阶段以及处理用户行为的时候,应该关注的问题所在。

避免大型的复杂的布局和布局限制

Layout阶段浏览器将计算元素的大小,在页面中的位置,其他元素的影响等等,与样式计算(Style calculation)类似,基本限制因素如下:

  • 需要Layout的元素数量
  • Layout的复杂度

TL;DR

  • Layout适用于整个文档流
  • DOM的数量直接影响Layout的性能消耗,尽量避免触发Layout
  • 避免强制同步修改Layout,造成反复Layout。即读取style的值然后修改style

尽可能的避免触发Layout

当更改样式时,浏览器会去检查需不需重新计算触发Layout,一般来说修改元素的几何属性(geometric properties)例如:宽高,布局定位都会触发Layout

.box {
  width: 200px;
  height: 200px;
}

// 改变元素宽高 触发Layout
.box-expanded: {
  width: 300px;
  height: 300px;
}

Layout是作用于全局整个文档流的,所以如果有大量的元素需要处理,就会消耗很长时间去计算这些元素的大小和定位。
如果无法避免触发Layout,可以通过Performance查看Layout阶段的耗时是否是影响性能的瓶颈。

Snipaste_2019-10-23_17-24-29-4fe35133-c763-4d7a-90ac-84483eb9262c

在Performance中我们可以清楚的看到Layout阶段消耗的时间,以及涉及的节点数(如图为314个元素)
https://csstriggers.com/ 列出了一些CSS属性会触发渲染的哪个阶段,可以作为对照参考。
另外使用flexbox布局要比传统的通过float或者相对定位绝对定位实现布局更快。

避免出现强制同步布局

正常情况下渲染步骤是先执行JavaScript,然后是style calculation 然后触发Layout。但是有种情况是触发Layout的时间点早于JavaScript的执行,这种情况叫强制同步布局(forced synchoronous layout)

要明确的是在JavaScript运行时,前一帧的布局属性值都是已知的。举个例子来说如果你想在帧(frame)开始前获取某个元素的高度,就可以这样写:

requestAnimtionFrame(logBoxHeight);

function logBoxHeight(){
  console.log(element.offsetHeight);
}

但是如果你先改变的元素的样式然后在获取元素高就会出问题

function logBoxHeight(){
  element.classList.add('big');
  console.log(element.offsetHeight);
}

现在的情况就变成这样,由于添加了新的class后要输入元素的offsetHeight,浏览器必须先重新进行布局计算才能拿到正确的offsetHeight的值,这完全是没必要的,而且这个例子中通常情况下都是不需要先去设置样式再去取属性值的,直接使用最后一帧的属性值完全足够了。所以一般情况下最好是先去读取需要的属性值,然后再做更改。

function logBoxHeight(){
  console.log(element.offsetHeight);
  element.classList.add('big');
}

还有一种更糟糕的情况是反复不断的强制同步触发layout。看下面的代码

function resizeAllParagraphsToMatchBlockWidth(){
    // 让浏览器陷入读写循环
  for(let i = 0; i < paragraphs.length; i++){
    paragraphs[i].style.width = element.offsetHeight + 'px';
  }
}

打眼一看好像没什么问题,其实这种问题很常见每次迭代都会去读取element.offsetHeight属性,然后用它去更新paragraph的width属性。解决办法也很常见就是读取一次做一个缓存。

const width = element.offsetHeight;

function resizeAllparagraphsToMatchBlockWidth(){
    for(let i = 0; i < paragraphs.length;i++){
        paragraphs[i].style.width = width + 'px';
    }
}

简化Paint复杂度,减少Paint的面积

Paint是一个填充像素(pixels)的过程,最终这些像素会通过合成器合成到屏幕。这个阶段通常是渲染元素整个过程中最消耗时间的阶段,所以要尽可能的避免

TL;DR

  • 除了transform和opacity属性改变其他任何属性都会触发Paint
  • 因为Paint在整个渲染过程中是最消耗时间和性能的,所以尽可能的避免触发
  • 利用Chrome DevTools来观察Paint阶段,并尽可能的降低减小对性能的消耗
  • 可以通过提升图层来减少Paint的面积大小

如果触发Layout肯定触发Paint,因为改变元素的几何属性(宽高等)意味着需要重新布局定位。当然修改一些非几何属性例如:background text-color,shadow这些也会触发paint,只不过不会触发layout所以整个渲染过程就会跳过Layout阶段。

Snipaste_2019-10-24_19-17-20-a9b6b4bf-10d0-43b9-a72f-095c69237835

利用Chrome DevTools来观察渲染过程中最消耗性能的部分,可以看到如下图绿色部分表示的是需要被重绘的区域。

Snipaste_2019-10-24_19-26-52-e3d8d020-8732-4f82-a60f-779cdef8f440

可以使用will-change属性或者类似的hack手段让浏览器创建一个新的图层来减少需要被Paint的区域。关于will-change的详细内容可以看这篇文章【关于will-change属性你需要知道的事】此处不在赘述。

尽可能简化Paint的过程,在Paint阶段有的步骤是非常消耗性能的,比如任何涉及到模糊(blur)的过程(例如:shadow属性),就CSS而言这些属性之间看上去没什么性能上的差异,但实际在Paint阶段是区别还是很明显的。

Composite

composite阶段是将Paint过程中的内容汇集起来显示在屏幕上。

这个过程中主要有两个影响页面性能的关键因素:一个是需要整合的合成层(compoaitor layers)数量,另一个是用于动画的相关属性

TL;DR

  • 使用will-change或translateZ属性做硬件加速
  • 避免创建过多图层(layer),图层会占用内存
  • 对于动画的操作使用transform和opacity做变更

渲染过程中最好的情况是避免触发Layout和Paint只需要合成(compositing)阶段处理变更。要做到这一点只需要一直使用只通过合成器处理的属性即可。(只有transform和opacity属性可以做到)

Postion   transform: translate(npx,npx);
Scale     transform: scale(n);
Rotation  transform: rotate(ndeg);
Skew      transform: skew(X|Y)(ndeg);
Matrix    transform: matrix(3d)(...);
Opacity   opacity: 0 <= n <= 1

使用transform和opacity的注意点是对应元素要在自身的合成图层,如果没有自身图层就要创建一个图层。这里涉及到创建图层和硬件加速的内容可以参考【关于will-change属性你需要知道的事

通过提升或创建图层有助于性能的提升这个技巧诱惑力是大的,所以有可能就会写出如下代码:

* {
    will-change: transform;
    transform: translateZ(0);
}

如同在 【关于will-change属性你需要知道的事】里提到的这种做法非但不能带来性能上的提升,反而会占用过多系统资源,对CPU和GPU都会带来额外的负担。

最后和我们之前提到的类似Chrome DevTools提供了供开发者查看页面图层的工具,可以看到当前页面上有多少层级,每个层级的大小、渲染的次数以及合成的原因等等,我们可以通过这些信息去分析和做对应的优化。

Snipaste_2019-10-24_22-54-43-39b79651-7d26-493d-ae0a-74384cc8fbf2

对输入处理程序做防抖

处理用户输入也是潜在的可能会影响性能的因素,因为其可能会阻塞其他内容的加载并且导致不必要的布局(layout)工作

TL;DR

  • 避免时间过长运行处理输入程序,其会阻塞页面滚动;
  • 不要在处理输入的程序中修改样式;
  • 对输入处理程序做防抖,在下一帧的requestAnimationFrame回调中存储事件值和样式的更改

避免运行时间过长的处理程序

页面交互最快的情况是,当用户与页面交互时,页面的合成器线程接受用户的触摸输入并将内容四处移动。这个过程不需要与主线程通信,而是直接提交给GPU处理。所以不需要等待主线程对JS的处理、以及布局(layout)、绘制(paint)等操作完成。

Snipaste_2019-10-21_17-27-22-296a06f0-6a55-41a7-af01-ff773c8e1f7c

但是,如果附加了输入处理程序(如touchstart,touchmove,或者touchend)后,合成器线程必须等待该处理程序执行完毕,因为有可能调用了preventDefault()来阻止触摸滚动事件的发生。即使没有调用preventDefault(),合成器也必须等待其执行完毕,这样用户的滚动操作就被阻止就可能导致帧丢失从而引起卡顿。
Snipaste_2019-10-21_17-28-10-7182cacd-7819-4f15-8957-594510110f12

总而言之,你应该确保运行的所有输入处理程序都快速执行,并允许合成器执行其工作。

避免在输入处理程序中改变样式

输入处理程序被安排在requestAnimtionFrame回调之前运行。如果在这个处理程序中做样式上的修改,那么在requestAnimationFrame开始处有需要更改的样式处理,这会触发强制同步布局。

Snipaste_2019-10-21_17-41-01-f4a205b3-b672-4a2c-9ab0-e21e97d1d2d5

输入处理程序做防抖

上面两个问题的解决方案是相同的:你应该对下一个requestAnimationFrame的回调中做样式更改的情况做防抖处理。

function onScroll(evt){
    lastScrollY = window.scrollY;

    if(scheduleAnimationFrame)
        retun;
    scheduleAnimationFrame = true;
    requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll',onScroll);

这样做还有一个好处,就是保持输入处理程序的轻量,因为这样就不会阻塞比如滚动等其他操作。

常见的面试题

CSS

左边固定,右边自适应

<div class="box">
<div class="left"></div>
<div class="right"></div>
</div>
  1. position + margin-left
.box{ display: relative }
.left{ position: absolute; width:100px }
.right{ margin-left:100px }
  1. flex
.box{display:flex;}
.left{width:100px; }
.right { flex:1;}

三栏布局【中间自适应,两边固定】

  1. flex
<div class="container">
       <div class="left"></div>
       <div class="center"></div>
       <div class="right"></div>
 </div>
   .container { display: flex }
   .container>div { height: 200px }
   .left { width: 300px }
   .center { flex: 1 }
   .right { width: 300px }
  1. float
    <div class="left"></div>
       <div class="right"></div>
       <div class="center"></div>
   </div>
.container>div{
    height: 200px;
}
.left { 
    float: left;
    width: 300px;
}
.right {
   float: right;
   width: 300px;
}
.center {
    overflow: hidden;
}

水平垂直居中

<div></div>
  1. absolute+负margin【知道元素宽高】
div { width:x;
        height: y;
        position: absolute;
        left:50%;
        top:50%;
        margin-left:-x/2px;
        margin-top:-y/2px;
}
  1. absolute + margin auto【知道元素宽高】
div {
    width:x;
    height: y;
    position: absolute;;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}
  1. absolute + calc【知道元素宽高】
div {
    width:x;
    height: y;
    position: absolute;;
    top: calc(50% - x/2);
    left: calc(50% - y/2);
}
  1. absolute + transform
div {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
  1. flex
<div class="box">
    <div></div>
</div>
.box {
    display: flex;
    justify-content: center;
    align-items: center;
}

css中类叠加相同属性的取值

<div class="red blue">okokok</div>
<div class="blue red">okokok</div>
.blue { color: blue }
.red { color: red }

优先级是由类在css文件中定义的顺序决定
不管class中类名顺序如何改变都会展示css中靠后的样式

JavaScript

Prmoise 实现一个红绿灯

function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}
function red(){
    console.log('red');
}

function light(fn,delay){
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            fn();
            resolve();
        },delay);
    })
}

const start = function(){
    Promise.resolve()
      .then(()=>light(green,3000))
      .then(()=>light(yellow,2000))
      .then(()=>light(red,1000))
      .then(()=>start())
}

start()

利用async和promise实现一个sleep

function sleep(delay){
    return new Promise(resolve=>{
        setTimeout(resolve,delay);
    })
}

const fn = async _ =>{
    let pre = new Date()
    console.log(pre);
    await sleep(2000);
    let after = new Date();
    console.log(after - pre);
}
console.log('a');

async function async1 () {
    console.log('b');
    await async2();
    console.log('c');
}
async function async2(){
    console.log('d');
}

async1();

setTimeout(()=>{
    console.log('e');
})

new Promise((resolve)=>{
    console.log('f');
    resolve();
}).then(()=>{
    console.log('g');
})

console.log('h');

// a b d f h c g e 

Cookie, LocalStorage 与 SessionStorage

特点 Cookie LocalStorage sessionStorage
存放数据大小 4k左右 一般为5M
存放周期 一般由服务器生成,可人为设置失效时间。如果在浏览器端生成,默认是关闭浏览器后失效 除非被清除,否则永久保存 仅在当前会话下有效,关闭页面或浏览器后被清除
与服务器端通信 每次都携带在HTTP请求头中 仅在客户端中保存不参与服务器通信
概念 是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。比较经典的,可以它用来确定两次请求是否来自于同一个浏览器,从而能够确认和保持用户的登录状态。Cookie的使用使得基于无状态的HTTP协议上记录稳定的状态信息成为了可能 没有时间限制的数据存储 (持久化) 针对一个 session 的数据存储,当用户关闭浏览器窗口后(不包括重载),数据会被删除。
用途 1.会话状态管理(如用户登录状态、购物车) 2.个性化设置(如用户自定义设置) 3.浏览器行为跟踪(如跟踪分析用户行为) 用于本地存储数据❶ 可以将一部分数据在当前会话中保存下来,刷新页面数据依旧存在。但当页面关闭后,sessionStorage 中的数据就会被清空可以用来存储比较重要的数据 安全性高于cookie❶
缺陷 1.cookie会被加在每个http请求中 增加了流量 2. cookie以明文传递有安全问题 3.存储大小限制 It works on same-origin policy. So, data stored will only be available on the same origin.(受同源策略的影响 数据只能在同源网站内存储和使用) 1. The data is available only inside the window/tab in which it was set.(数据只在当前网页中有效) 2. The data is not persistent i.e. it will be lost once the window/tab is closed.(数据不能持续保存 关闭网页或浏览器后数据丢失) 3. Like localStorage, tt works on same-origin policy. So, data stored will only be available on the same origin(和localStorage类似 受同源策略影响 数据只在符合同源策略的网页中使用)
相同点 localStorage, sessionStorage and cookies are all subject to "same-origin" rules which means browsers should prevent access to the data except from the domain that set the information to start with.
❶: perfect for persisting non-sensitive data needed within client scripts between pages (for example: preferences, scores in games). The data stored in localStorage and sessionStorage can easily be read or changed from within the client/browser so should not be relied upon for storage of sensitive or security related data within applications.

函数节流(throttle)与去抖(debounce)

函数去抖(debounce)

函数去抖核心是指:为了不让一个函数单位时间内执行次数太多 从而导致性能问题 限制其在一定时间的连续的函数调用只让其执行一次

应用场景

  • resize/scroll 触发统计事件
  • 文本输入验证 (输入完成后只验证一次)
  • 监听滚动事件判断是否到底部以自动加载更多 (用户停止滚动 判断是否到底部)

基本**

某些代码不可以在没有间断的情况连续重复执行,第一次调用函数创建一个定时器 在指定的时间间隔之后运行代码 当第二次调用该函数时 就会清除前一次的定时器并设置另外一个 如果前一个定时器已经执行过了这个操作就没有意义了 然而如果前一个定时器尚未执行 其实就是将其替换为一个新型的定时器 目的是只有在执行函数的请求停止一段时间后才执行(高程3 P614)
经典实现代码如下:

function debounce(fn,time){
    var timer;
    return function(){
        clearTimeout(timer);
        var context = this;
        var args = arguments;
        timer = setTimeout(function(){
            fn.apply(context,args);
        },time)
    }
}

函数去抖在一段时间只执行一次,而函数节流则是间隔时间段执行 具体如下

函数节流(throttle)

函数节流的核心是指:为了不让一个函数执行的太频繁 减少一些过快的调用 降低函数触发的回调频率

应用场景

  • DOM元素的拖拽(mousemove)
  • 计算鼠标移动的距离(mousemove)
  • Canvas模拟画板(mousemove)
  • 搜索联想(keyup)
  • 射击游戏的mousedown/keydown(单位时间)
  • 监听滚动事件判断是否到底部以自动加载更多 (页面滚动一次就间隔判断一次)
// 定时器实现
function throttle(fn,wait){
    var timer;

    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                fn.apply(context,args);
            },wait)
        }
    }
}

// 时间戳实现
function throttle(fn,wait){
    var timer;
    var prev = 0;

    return function(){
        var args = arguments;
        var now = +new Date();
        if(now - prev > wait){
            fn.apply(this,args);
            prev = now;
        }
    }
}

// swr

function throttle(fn, wait){
    let pending = false;

    return (...args) => {
        if(pending) return;
        pending = true;
        fn(...args);
        setTimeout(()=> (pending = false), wait);
    }
}

State vs Props

props是一个父组件传递给子组件的数据流,这个数据流可以一直传递到子孙组件。而state代表的是一个组件内部自身的状态

条件 state props
能否从父组件获取初始值?
能否被父组件改变?
能否在组件内设置默认值? *
能否在组件内改变?
能否设置子组件的初始值?
能否在子组件中改变?

*:props 和 state 从父组件获取的初始值都会被在组件内设置的默认值覆盖。

If a Component needs to alter one of its attributes at some point in time, that attribute should be part of its state, otherwise it should just be a prop for that Component.

如果一个组件在某个时间点需要改变自身的某个属性 那么这个属性应该用state 否则使用props

props
Props (short for properties) are a Component's configuration. They are received from above and immutable as far as the Component receiving them is concerned. A Component cannot change its props, but it is responsible for putting together the props of its child Components. Props do not have to just be data -- callback functions may be passed in as props.

props(properties的简称)是一个组件的配置选项 它们从上而下被指定且不可变 一个组件不能改变自身的props 主要负责设置子组件的props props不仅仅可以作为数据传递也当做callback函数传递

state
The state is a data structure that starts with a default value when a Component mounts. It may be mutated across time, mostly as a result of user events.
A Component manages its own state internally. Besides setting an initial state, it has no business fiddling with the state of its children. You might conceptualize state as private to that component.

state是一种数据结构 当组件挂载是时state有一个默认值 它或许会随着时间的改变而变化 主要原因是用户事件触发
一个组件在内部管理自己的state 除了设置和初始化state 与其子组件state没有关系 可以认为state是一个组件的私有属性

何时使用:

State

如果component的某些状态需要被改变,并且会影响到component的render,那么这些状态就应该用state表示。
例如:一个购物车的component,会根据用户在购物车中添加的产品和产品数量,显示不同的价格,那么“总价”这个状态,就应该用state表示。

Props

如果component的某些状态由外部所决定,并且会影响到component的render,那么这些状态就应该用props表示。
例如:一个下拉菜单的component,有哪些菜单项,是由这个component的使用者和使用场景决定的,那么“菜单项”这个状态,就应该用props表示,并且由外部传入。

关于JavaScript的深拷贝

关于浅复制很容易,想必大家都清楚,比如Object.assign() 亦或者是利用ES6的扩展运算符(object spread) ,之前也有总结过一部分,最近看了一些文章,所以这次我们主要来聊聊深复制

Recursion

function deepCopy(p,c){
  var i;
  c = c||{};
  for(i in p){
    if(p.hasOwnProperty(i)){
      if(typeof p[i]==="object"){
        c[i] = Array.isArray(p[i])?[]:{};
        deepCopy(p[i],c[i]);
      }else{
        c[i] = p[i];
      }
    }
  }
  return c;
}

上述代码在复制时会进行类型检查 如果复制对象为一个数组或者对象会递归的遍历属性对象并将属性元素复制出来,有个问题就是无法解决循环引用

const x = {};
const y = {x};
x.y = y;
const copy = deepCopy(x)     // Uncaught RangeError: Maximum call stack size exceeded

JSON.parse

算是一种比较老的方法,通过将对象字符串化然后解析转化回对象,可能看上去有些头重脚轻,但是确实有效

const obj = /*  ... */;
const copy = JSON.parse(JSON.stringify(obj));

关于此方法同样有一个潜在的缺陷是无法处理对象中的循环引用

const x = {};
const y = {x};
x.y = y;
const copy = JSON.parse(JSON.stringify(x)); //Uncaught TypeError: Converting circular structure to JSON

还有一点是它无法用于处理JS的一些内置类型比如 Maps,Sets,RegExps,Dates,ArrayBuffers...等

MessageChannel

利用postMessage API 可以解决上述JSON.parse无法解决的循环引用,和无法处理内置类型的问题

function structuralClone(obj){
    return new Promise(resolve ={
        const {port1,port2} = new MessageChannel();
        port2.onmessage = ev=> resolve(ev.data);
        port1.postMessage(obj);
    });
}

const obj = /* ... */;
const copy = await structuralClone(obj);

这个方法有一个缺点就是它是异步的,虽然不是什么大问题,但是有时候你可能需要的是一个同步的深复制办法

History API

如果你用过history.pushState()来构建SPA应用的话,那么你肯定知道你可以通过提供一个state对象来存储URL,并且它是同步的,这里我们为了避免带来其他不必要的麻烦和问题,在使用完state后记得还原它,同时我们用history.replaceState()来代替history.pushState()

function structuralClone(obj){
    const oldState = history.state;
    history.replaceState(obj,document.title);
    const copy = history.state;
    history.replaceState(oldState,document.title);
    return copy;
}

const obj = /* ... */;
const copy = structuralClone(obj);

tips: Safari浏览器对replaceState 做出了限制一个窗口30s内调次数不超过100次

Notification API

利用Notification API深复制对象

function structuralClone(obj){
    return new Notification('',{data:obj,slient:true}).data;
}

const obj = /* ... */;
const copy = structuralClone(obj);

tips: Safari 由于某些原因 总是返回undefined

Conclusion

  • 如果你不用考虑循环引用问题和处理JS内置的一些类型的问题,需要兼容不同的浏览器并考虑速度的话,推荐使用JSON.parse(JSON.stringify())
  • 如果不确定因素多,那么MessageChannel更可靠一些

杂乱的前端小知识

HTML

display:none指的是元素完全不陈列出来,不占据空间,涉及到了DOM结构,故产生reflow与repaint
visibility:hidden指的是元素不可见但存在,保留空间,不影响结构,故只产生repaint

标准的盒子模型:盒子总宽度 =margin + border + padding + content
W3C中的width = content

IE盒模型:盒子总宽度 = margin+content
IE中盒模型的Width = border+padding+content

关于onInput() getComputedStyle()

onInput 属性实时监控< input> 或者<textarea>中的value值得变化状况 Object.target.value.length 可以获取文本框中的字符长度 onInput事件类似于 onchange 事件。不同之处在于 oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发。另外一点不同是 onchange 事件也可以作用于 < keygen> 和 < select> 元素 。关于getComputedStyle() 与style直接获取元素的CSS属性值相比 style仅能获取内嵌在html代码中的css元素属性 而 getComputedStyle则都可以获取 但是是只读属性[针对IE的话要用currentStyle属性] 兼容一下的话:
(element.currentStyle? element.currentStyle : window.getComputedStyle(element, null)).height
getPropertyValue方法可以获取CSS样式申明对象上的属性值(直接属性名) 例如:window.getComputedStyle(element, null).getPropertyValue("float");

事件委托

事件委托又叫事件代理 事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。事件委托的原理:事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件 举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件

CSS

CSS Sprites

一种网页图片应用处理方式。它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去,这样一来,当访问该页面时,载入的图片就不会像以前那样一幅一幅地慢慢显示出来了。利用CSS的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,background-position可以用数字精确的定位出背景图片的位置。
利用CSS Sprites能很好地减少网页的http请求,从而大大的提高页面的性能,这也是CSS Sprites最大的优点,也是其被广泛传播和应用的主要原因;

屏幕分辨率

  • .col-xs- 超小屏幕 手机 <768px
  • .col-sm- 小屏幕 平板 >=768px
  • .col-md- 中等屏幕 >=992px
  • .col-lg- 大屏幕 >1200px

JavaScript

判断一个变量是不是对象非常简单。值类型的类型判断用typeof(Null除外),引用类型的类型判断用instanceof

var操作符

使用var操作符定义的变量将成为该变量的作用域中的局部变量,如果在函数中使用var定义一个变量,那么这个变量在函数退出后就会被销毁;省略var操作符会创建一个全局变量,但在局部作用域中定义的全局变量很难维护,也会由于相应变量不会马上就有定义而导致不必要的混乱,给未经声明的变量赋值在严格模式下会导致抛出Reference错误

(function() { var a = b = 5; })(); console.log(b); console.log(a);

输出:5
a is not defined

关于js中的类型判断

数组:Array.isArray(arr) || Object.prototype.toString.call(arr) === '[object Array]'
对象:typeof obj === 'function' || typeof obj === 'obj' && !!obj;

严格模式的限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 禁止this指向全局对象
  • 不能使用with语句
  • 增加了保留字
  • arguments不会自动反映函数参数的变化

JavaScript小技巧

使用~~向下取整 类似与Math.floor();

~~12.1  // 12
~~12.7 //12

数组去重

[...new Set(array)]

数组中的最大值

Math.max(...[1,2,3,4,5])    // 5
Math.max.apply(null, [1,4,2,5,6,0,-2]) // 6

数组中的最小值

Math.min.apply(null, [1,4,2,5,6,0,-2]) // -2

判断IE

function detectIE() {
    var ua = window.navigator.userAgent;

    var msie = ua.indexOf('MSIE ');
    if (msie > 0) {
        // IE 10 or older => return version number
        return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
    }

    var trident = ua.indexOf('Trident/');
    if (trident > 0) {
        // IE 11 => return version number
        var rv = ua.indexOf('rv:');
        return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
    }

    var edge = ua.indexOf('Edge/');
    if (edge > 0) {
       // Edge (IE 12+) => return version number
       return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
    }

    // other browser
    return false;
}

Check if user is using IE

正则处理千分位格式化数字(不能处理带小数的)

num.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,');
1234567890..toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,') // "1,234,567,890"

或者利用toLocaleString Number.prototype.toLocaleString.call(num);

乱序

const random = (arr)=>{
  for(let i =1; i < arr.length; i++){
    let random = ~~(Math.random()*(i+1));
    [arr[i], arr[random]] = [arr[random], arr[i]];
  }
  return arr;
}

判断一个对象是否为空

// because Object.keys(new Date()).length === 0;
// we have to do some additional check
Object.keys(obj).length === 0 && obj.constructor === Object

实现一个sleep

function sleep(ms){
  let template = new Promise((resolve)=>{
    setTimeout(resolve,ms);
  })
  return template;
}

sleep(1000).then(()=>{
    // do someting
})

script defer vs async

defer不会阻塞HTML解析 HTML解析时并行下载JS文件,等到HTML解析完执行,defer会始终按照顺序执行
async可能阻塞HTML解析 HTML解析时并行下载JS文件,一旦下载完成立即执行,不一定按照顺序执行
image

NetWork

域名收敛

域名收敛即DNS优化

  • PC 时代为了突破浏览器的域名并发限制。有了域名发散。
  • 浏览器有并发限制,是为了防止DDOS攻击。
  • 域名收敛:就是将静态资源放在一个域名下。减少DNS解析的开销。
  • 域名发散:是将静态资源放在多个子域名下,就可以多线程下载,提高并行度,使客户端加载静态资源更加迅速。
  • 域名发散是pc端为了利用浏览器的多线程并行下载能力。而域名收敛多用与移动端,提高性能,因为dns解析是是从后向前迭代解析,如果域名过多性能会下降,增加DNS的解析开销。

Browser

DOMContentLoaded与load

从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成
页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发
将js文件放在尾部是为了减少First Paint的时间

JavaScript排序算法实现

快排

function quickSort(arr){
    if(arr.length === 0) return [];
    var l = [];
    var r= [];
    var m = ~~(arr.length/2);
    var flag = arr.splice(m,1);
    for(let i = 0; i < arr.length; i++){
        arr[i] < flag ? l.push(arr[i]): r.push(arr[i]);
    }
    return quickSort(l).concat(flag,quickSort(r));
}

quickSort([5,4,3,2,7,1])   // [1,2,3,4,5,7]

冒泡

function bubbleSort(arr){
    for(let i = 0; i < arr.length; i++){
	for(let j = 0; j < arr.length - 1 - i; j++){
	    if(arr[j]>arr[j+1]){
		[arr[j],arr[j+1]] = [arr[j+1],arr[j]];
            }
	}
    }
    return arr;
}

选择排序

var selectSort = function(arr){
  for(var i = 0; i < arr.length; i++){
    var min_index = i;
    for(var j = i+1; j < arr.length; j++){
      if(arr[min_index]>arr[j]){
        min_index = j;
      }
    }
    [arr[min_index],arr[i]] = [arr[i],arr[min_index]];
  }
  return arr;
}

归并排序

function merge(left, right){
    const res = [];
    while(left.length > 0 && right.length > 0 ) {
         if(left[0] > right[0]) {
             res.push(right.unshift());
         }else{
             res.push(left.unshift());
        }
    }
    if(left.length > 0){
        res.concat(left);
    }
    if(right.length > 0){
        res.concat(right);
    }
    return res;
}


function mergeSort(arr){
    const lens = arr.length;
    if(len < 2) {
        return arr;
    }
    const mid = ~~(lens / 2);
    const left = mergeSort(arr.slice(0,mid));
    const right = mergeSort(arr.slice(mid));

    return merge(left,right);
}

关于CSS will-change 属性你需要知道的事

不知道你有没有注意到,在基于Webkit的浏览器上执行某些CSS操作时页面会出现不流畅或者闪一下的情况,尤其是执行CSS动画的时候,那你之前可能已经听过“硬件加速(hardware acceleration)”这个专业术语了。

CPU&GPU&硬件加速(Hardware Acceleration)

简单来说硬件加速意味着浏览器会帮你把一部分对渲染页面来说较繁重的任务交给GPU(Graphics Processing Unit),而不是一股脑都交给CPU(Central Processing Unit )去处理,当这部分CSS操作得到硬件加速时,可以使页面渲染速度更快。

CPU在电脑主板上,就像是计算机的”大脑“,CPU几乎会做所有事,GPU位于计算机的显卡上,主要用做图形渲染,此外,GPU是专为执行图形渲染所需的复杂数学和几何计算而设计的,所以将一些操作移到GPU上去处理可以带来巨大的性能提升并且减轻了CPU的压力,在移动端尤为明显。

硬件加速(又名GPU加速)依赖于浏览器在渲染页面时使用的分层模型,当对页面上的元素执行某些操作(例如3D变换),对应元素会被提升到到自己的图层,独立于页面的其余部分的呈现并在后期合成(绘制到屏幕上),这样单独把元素隔离,可以使得当页面上只有这个元素发生变换(transform)的时候其余元素不需要重新渲染,从而带来速度提升的优点,值得一提的是,只有3D变换才有资格拥有自己的图层,2D变换则没有。

CSS animation,transform,transition这些属性并不会自动被硬件加速,而是由浏览器的渲染引擎去执行的,但是一些浏览器通过某些属性提供硬件加速,从而提升页面的渲染性能。例如CSS中的opacity属性是少数几个可以被浏览器认定为可被硬件加速的属性之一,因为GPU可以很容易的实现。一般来说,任何想要通过CSS transition或动画淡化不透明度的图层的行为,浏览器会把它丢给GPU去处理从而提高处理速度。所有的CSS属性中opacity是性能最好属性的之一,其他常见的硬件加速操作是CSS 3D变换

Hack方法来硬件加速:translateZ() 或者 translate3d()

使用translateZ()(或translate3d())这种hack方式(有时也称为null变换hack)来让浏览器对animationtransform行为使用硬件加速,通过向一个不会在三维空间中转换的元素添加简单的3D变换来实现硬件加速。例如通过给一个二维空间中动画添加简单的规则来硬件加速。

transform:translate3d(0,0,0)

硬件加速操作会创建所谓的合成层,合成层会被上传到GPU并由GPU合成,但是这种hack的方法去创建图层并不是万能的,图层创建可以加快页面加载速度,但会带来其他的成本:它会占用系统RAM和GPU上的内存(限于移动设备),并且很多时候都会带来不良影响(特别是在移动设备上),所以这种方法要合理的去使用,你必须要清楚的知道使用这种方式是不是真的可以提高页面性能,不能使这个操作反而成了影响页面性能的瓶颈。

除了这种创建图层的方法,CSS引入了一个新的属性,它允许我们提前告知浏览器可能会对元素进行哪些操作,让浏览器去优化并提前处理那些潜在的比较消耗性能的操作比如在动画开始之前,提前处理元素的动画行为。这个属性就是will-change

新属性will-change的荣光

will-change属性允许你提前通知浏览器你可能会对某个元素做什么类型的操作,以便于浏览器在需要的时候采取适当的优化方案。因此,避免了可能对页面的响应性产生负面影响的非必要成本,使元素可以更快地呈现。渲染并快速更新,从而获得更流畅的体验。

举个例子,当对元素使用CSS transform时,元素及其内容可能会提升为图层,如之前所言,之后会将他们合成(composited)(绘制在屏幕上),但是将一个元素提升到一个新图层是很消耗性能的,这可能会使transform动画的开始延迟明显的几分之一秒,从而引起明显的“闪烁”。

为了避免这种情况发生,我们可以提前告知浏览器,让浏览器可以提前做准备,那么当同样的操作发生时,因为元素的图层已准备就绪,然后就可以立刻执行转换动画,从而渲染元素并快速更新页面。

使用will-change可以提示浏览器将会发生的转化,语法非常简单

will-change:transform

也可以添加多个你期望改变的确切属性的值。如:

will-change:transform,opacity

确切指定你想改变属性可以让浏览器更好的决策如何以更优的办法处理这些变动,这明显是比使用hack手法让浏览器被迫创建可能无用的图层的更好且更有效的一种方法了

will-change 是否会给当前元素带来其他副作用

是,也不是,这取决于你给定的属性。如果该属性的任何非初始值将在元素上创建堆栈上下文,则在will-change中指定该属性将在元素上创建堆栈上下文。举个例子:将clip-path属性和opecity属性用于初始值以外的值时,都会在它们所应用的元素上创建堆栈上下文。因此,使用这些属性中的一个(或两个)作为will-change的值,即使在更改实际发生之前,也会在元素上创建堆叠上下文,这同样适用于将在元素上创建堆栈上下文的其他属性

同样,某些属性可能导致为固定位置的元素创建一个包含块。例如,一个转换后的元素为其所有定位子孙元素创建一个包含块,即使那些已被设置为position:fixed的元素。因此,如果某个属性会导致创建一个包含块,那么将其指定为will-change的值也将导致为固定位置元素生成包含块,所以如前所述,某些will-change属性的变化的导致创建新的合成层,但是,GPU不支持大多数浏览器中CPU所支持的亚像素抗锯齿功能,所以有时会导致内容模糊(尤其是文本)

另外will-change属性不会直接影响对应元素,它只是给浏览器打个预防针,让浏览器以更高效的渲染方式去呈现元素内容,除了在之前提到的一些情况下创建堆栈上下文和包含块外,它对对应元素没有直接影响。

使用will-change的注意事项

不要使用will-change去尝试优化一切操作,有时候相反会带来反作用,要更加聪明且合理的使用它,will-change也有一些无法直接检测到的副作用,毕竟这也只是一种和浏览器后台通话的方式而已,不能指望它为你做所有事情,因为使用此属性时,请牢记以下几点,以确保在最大程度上利用该属性,同时避免因滥用该属性而带来的危害。

  • 不要使用will-change声明对太多属性或元素的更改

    如同之前所提到的 直接让浏览器针对所有元素上的所有属性可能发生的更改进行优化的想法是很诱人的,所以可能会写出如下代码

    *,
    *::before,
    *::after {
    	will-change: all;
    }

    看上去好像很完美,但其实是有很大的问题的,首先all不是合法的will-change的值,其次这样笼统的规则根本就没什么用,这就好像在告诉浏览器这些所有的属性都可能变化且都需要要优化,那浏览器完全和没优化的版本处理没什么区别,因为没什么优先级也没有任何有用信息。并且如我们之前提到的hack方法,浏览器已经在自己做优化了,所以这样写没有任何意义,而且不可忽视的是因为浏览器要处理will-change相关的属性也会占用大量计算机资源,过度使用反而使得页面卡顿甚至崩溃

  • 给浏览器足够的时间来工作

    will-change属性顾名思义:只告知浏览器即发生的变化,所以这个阶段什么改变也没发生,我们使用will-change是以便于浏览器对我们声明的更改进行某些优化,浏览器需要时间去处理优化这些更改,为了在这些变化在真正发生时可以立即生效,所以在元素更改之前立即设置will-change几乎没有任何效果,并且可能比不设置更糟糕,举个例子:

    .element:hover {
    	will-change: transform;
    	transition: transform 2s;
    	transform: rotate(30deg) scale(1.5);
    }

    这相当告诉浏览器对已经发生的变化进行优化,这完全没有作用,而且也不符合will-change的定义,你应该找到一种方法,至少可以提前一点时间预测某种改变会发生,然后设置will-change的值,举个例子,当点击一个元素时发生变化,然后在该元素悬停时设置will-change属性,这样将为浏览器提供足够的时间来优化该更改,从悬停元素到用户实际单击元素之间的时间足以使浏览器进行优化了。

    .element {
    	/* style rules */
    	transition: transform 1s ease-out;
    }
    .element:hover {
    	will-change: transform;
    }
    .element:active {
    	transform: rotateY(180deg);
    }

    但是如果我们期望的是在鼠标悬停时变化要怎么做呢?正如我们所提到的,上面的代码也是无用的。在这种情况下,通常仍然可以找到某种方法来在动作发生之前对其进行预测,例如悬停在目标元素的祖先元素上可以提供浏览器足够的反应执行时间,因为将鼠标悬停在其祖先元素上并不一定总是表明该元素将与之交互,因此这个阶段,可以执行诸如设置will-change属性之类的操作

    .element {
    	transition: opacity .3s linear;
    }
    /* declare changes on the element when the mouse enters / hovers its ancestor */
    .ancestor:hover .element {
    	will-change: opacity;
    }
    /* apply change when element is hovered */
    .element:hover {
    	opacity: .5;
    }
  • 变更生效后移除掉will-change

    浏览器对即将发生的更改进行的优化通常成本很高,而且正如我们前面提到的,它会占用计算机的大部分资源,浏览器进行优化的通常行为是删除这些优化并尽快恢复到正常行为。但是,will-change会覆盖此行为,从而使优化保持的时间比浏览器原本的时间要长得多,正因为如此在使用完后要记得移除will-change来释放资源

    如果在样式中直接写死will-change,声明则无法直接移除,所以基本推荐使用JS来处理,通过脚本,可以在浏览器中声明你的更改,然后在更改完成后通过侦听更改完成的时间来移除will-change,举个例子:如我们之前提到的可以通过监听是否悬停在对应元素(或其祖先元素)上的mouseEnter事件中来设置will-change,如果你要对元素进行动画处理,则可以使用DOM事件animationEnd来侦听动画何时结束,然后在animationEnd触发后移除will-change

    // Rough generic example
    // Get the element that is going to be animated on click, for example
    var el = document.getElementById('element');
    
    // Set will-change when the element is hovered
    el.addEventListener('mouseenter', hintBrowser);
    el.addEventListener('animationEnd', removeHint);
    
    function hintBrowser() {
    	// The optimizable properties that are going to change
    	// in the animation's keyframes block
    	this.style.willChange = 'transform, opacity';
    }
    
    function removeHint() {
    	this.style.willChange = 'auto';
    }
  • 有选择性的在CSS中直接使用will-change

    如之前提到的will-change被用来向浏览器提示一个元素将在几毫秒内发生的更改,这是允许will-change直接写在css中的用例之一,尽管我们建议使用JavaScript来设置和取消will-change,但是在某些情况下,在css中进行设置(并保留)是更好的选择。

    一个例子:在少数可能被用户反复交互且应该以快速的方式响应用户的互动的元素上设置will-change,因为元素数量有限,这就意味着浏览器进行的优化不会被过度使用,因此不会造成太大的伤害,例如:通过在用户请求时将其滑出来转换边栏。如下规则就很合适:

    .sidebar {
    	will-change: transform;
    }

    另一个例子是在几乎不断变化的元素上使用will-change 比如一个元素响应用户的鼠标移动,并随着鼠标移动在屏幕上移动,这种情况下直接在css中声明will-change的值就可以。因为它准确地描述了元素将有规律/不断地变化,因此应保持优化状态

    .annoying-element-stuck-to-the-mouse-cursor {
    	will-change: left, top;
    }
  • will-change合法值

    will-change属性可取四种可能的值:auto, scroll-position,contents,<custom-ident>

    <custom-ident>值用于指定你希望更改的一个或多个属性的名称,多个属性值之间用逗号分隔,如下是合法的will-change属性名称的声明

    will-change: transform;
    will-change: opacity;
    will-change: top, left, bottom, right;

    <custom-ident>值不包括will-change, none, all, auto,scroll-position, contents,所以如同文章之前提到的will-change:all不是合法的属性值会被浏览器忽略,

    auto则表示没有特定的意图,意味着浏览器不会做任何特殊处理

    scoll-position 顾名思义,表示你期望将来随时可以更改元素的滚动位置,这个值很有用,因为在使用时,浏览器将准备并呈现超出可滚动元素的滚动窗口中可见内容的内容。浏览器通常仅在滚动窗口中渲染内容,而某些内容超过该窗口,平衡了因跳过渲染而节省的内存和时间,从而使滚动看起来顺滑,使用will-change:scroll-position,它可以进行进一步的渲染优化,以便可以平滑地完成更长和或更快的内容滚动。

    contents表示元素的内容可能会发生变化,浏览器通常会对元素做缓存,因为大部分情况下元素不会经常性发生变化,有时候仅仅只是位置的更改。这个值用来作为告诉浏览器减少或者避免对指定元素进行缓存的信号。因为如果元素的内容不断的变化,那么保留内容的缓存将毫无用处并且浪费时间,因为只要元素的内容发生更改,浏览器就会停止缓存并继续从头开始渲染该元素。
    如同我们之前提到的。如果在will-change中指定某些属性,这些属性将无效,因为浏览器不会对大多数属性的更改进行任何特殊的优化。

浏览器支持情况

原文链接: Everything You Need to Know About the CSS will-change Property
Introduction

常见的Web安全概念

XSS(Cross Site Script)跨站点脚本攻击

原理:往Web页面里插入恶意可执行网页脚本,当用户浏览该网页时,嵌入其中的Web代码被执行,从而达到盗取用户信息或侵犯隐私的目的
XSS类型:

  • 反射型XSS漏洞:通过给他人发送带有恶意脚本代码参数的URL 诱导点击触发,恶意代码被执行
    特征: 即时性不经过服务器存储,直接通过GET或POST请求就可完成一次攻击拿到用户隐私数据
    需要诱导点击触发
    盗取用户敏感保密信息
    防范:Web页面渲染的所有内容或渲染数据必须都来自于服务器端
    尽量不要从URL,document.referrer,document.forms这种DOM API获取数据直接渲染
    不要使用eval等可执行字符串方法
    前端渲染时对任何字段都要做escape转义编码

  • 存储型XSS露洞:通过向服务器提交数据将恶意代码存储于数据库持久保存当前页面获取数据后将其渲染
    特征:持久性,植入在数据库中,
    危害面广 盗取用户敏感私密信息
    防范:后端入库前将所有字段统一转义处理
    后端输出给前端数据统一转义处理
    前端渲染页面DOM时将任何字段都做转义处理

  • 基于字符串集的XSS:绕过转义处理的一种方式
    防范:指定

  • 基于Flash的跨站XSS
    防范:严格管理cookie的读写权限

  • 未经验证的跳转XSS
    防范:对跳转URL参数做白名单处理或者以某种规则过滤 对敏感信息的保护,对cookie使用来
    源做验证

CSRF:

  • 触发条件:用户已经登录A站点并记录了cookie
    cookie生效的情况下访问了诱导网站
    A站点未做任何CSRF防御
  • 防御:正确使用GET,POST请求和cookie
    非GET请求中增加token验证 为每个用户生成唯一的cookie token
    POST请求使用验证码,渲染表单时为每一个表单包含一个csrfToken提交表单时,后端做验证

SSRF

SSIT

SQL注入

命令行注入

DDos攻击

基于React搭配 React Router的微前端实践

什么是微前端

众所周知,微前端是一种架构方式,与后端的微服务可以说是一脉相承。其核心**就是"分而治之"。简单来说就是讲某单体应用解耦拆分为若干个子应用,这些子应用可以独立运行、开发、部署、和维护、且互不影响。且各个子应用可以选择不同的技术栈开发等等。目前在项目中有所尝试,下面内容就以实际业务为基础的微前端实践

需求背景

说之前先来分析一下为什么需要微前端?

背景分析: 项目中的子应用越来越多,其中部分功能越来越复杂,加之系统开发中碰到的问题越来越多

开发过程中的痛点:

  • 项目越来越大,目录结构越发不可控。
  • 组件之间耦合越来越深
  • 业务复杂度越来越高
  • 业务与业务之间相互调用 没有物理隔离
  • 定位排查问题困难
  • 开发、构建、部署耗时

预期结果

  • 公共组件抽离,统一维护,可高度复用,便于后续维护
  • 业务之间物理隔离 业务模块之间相互解耦,提高系统整体稳定性
  • 降低业务复杂度和项目规模
  • 子应用可以独立开发、测试、部署

技术方案

可以实现微前端的方案有很多,比较常见的一些微前端实现方案比如:iframe、Module Federation、动态路由等

iframe : 实现简单;可同时挂载多个应用;天然支持隔离,但是存在路由不同步,刷新页面状态丢失,通信困难只能通过postMessage等方式比较麻烦。

动态路由: 常见的实现方式比如single-spa 、qiankun等,通过统一维护所有应用的注册加载,挂载和卸载,针对不同技术栈的应用做聚合。但是通讯难,接入成本高。

Module Federation: 依赖webpack5,存在加载第三方依赖、版本控制等问题,会增加试错成本。

考虑到若选用上述方式,则会对项目本身带来较大影响,并且改造成本太高。 结合项目现状和改造成本,决定通过借鉴动态路由的**实现通过以React为基座的特定动态路由的方案。对于动态路由的方式来说,关键部分就是注册、加载、挂载与卸载,由于我们的技术栈都是React,而且react-router自身就具备对组件挂载和卸载的能力。利用这一点优势,将所有子应用作为一个component,将挂载卸载的工作交由react-router来完成,也不用每次切换路由都重新初始化整个子应用,用户体验和单页应用保持一致。

架构设计

todo

实现

基座:从配置文件中读取配置信息,在路由注册层注册信息。当路由匹配到子应用后,请求获取到子应用的打包文件,由react-router进行加载和卸载的控制
子应用: 导出umd资源,上传到服务器,更新配置文件中对应的子应用信息

整个执行流程如图所示

todo


配置子应用信息

在基座项目里创建子应用配置文件subApp.js

// container/scr/subApp.js
const app = [
  {
    name: 'subApp', // 子应用名称
    host: process.env.REACT_APP_SUBAPP_HOST, // 子应用的静态资源地址
    appName: 'App', // 导出的应用名 默认为'App'
  },
  ...
]

管理子应用

引入subApp.js配置信息,通过AppManager进行子应用管理

// container/src/index.js

function renderApp () {
  apps.forEach(AppManager.registerApp)
  ReactDOM.render(<App />,document.getElementById('root'));
}

在AppManager中维护子应用信息,及加载子应用的loadSubApp函数,loadSubApp函数获取到子应用的资源文件地址后,通过添加script标签的方式加载资源,并将返回的资源组价挂载到window上

// container/src/AppManager.js
class AppManager {
  subApps = {}
  subAppModules = {}
  registerApp = (subApp) => {
    this.subApps[subApp.name] = subApp
  }
  loadSubApp = (subAppInfo) => {
    const { name, host } = typeof subAppInfo === 'string'? this.subApps[subAppInfo] : subAppInfo
    if (!this.subAppModules[name]) {
      this.subAppModules[name] = new Promise((resolve, reject)=> {
        fetch(`${host}/${name}/asset-manifest.json`)
        .then(res => res.json())
        .then(manifest => {
          const script = document.createElement('script');
          script.src = `${host}${manifest.files['main.js']}`;
          const timeout = setTimeout(()=>{
            console.error(`MicroApp ${name} timeout`);
            reject(new Error(`MicroApp ${name} timeout`));
          }, 10000)
          script.onload = () => {
            clearTimeout(timeout)
            const app = window[name]
            console.log(`MicroApp ${name} loaded success`);
            resolve(app)
          }
          script.onerror = (e) => {
            clearTimeout(timeout);
            console.error(`MicroApp ${name} loaded error`, e);
            reject(e)
          }
          document.body.appendChild(script);
          // 加载css
          const link = document.createElement('link')
          link.rel = 'stylesheet'
          link.href = `${host}${manifest.files['main.css']}`
          document.head.appendChild(link)
        })
      })
    }
    return this.subAppModules[name]
  }
}

总结

todo

关于onInput() getComputedStyle()

onInput 属性实时监控< input> 或者<textarea>中的value值得变化状况 Object.target.value.length 可以获取文本框中的字符长度
onInput事件类似于 onchange 事件。不同之处在于 oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发。另外一点不同是 onchange 事件也可以作用于 < keygen> 和 < select> 元素

关于getComputedStyle() 与style直接获取元素的CSS属性值相比 style仅能获取内嵌在html代码中的css元素属性 而 getComputedStyle则都可以获取 但是是只读属性[针对IE的话要用currentStyle属性] 兼容一下的话:
(element.currentStyle? element.currentStyle : window.getComputedStyle(element, null)).height
getPropertyValue方法可以获取CSS样式申明对象上的属性值(直接属性名) 例如:window.getComputedStyle(element, null).getPropertyValue("float");

Vue学习笔记【含Nuxt.js】

vm = new Vue({ /*创建一个Vue实例 */})

概念 :当Vue实例被创建时,向Vue响应式系统(reactivity system)中加入其对象中所有能找到的属性,当这些属性发生变化时,视图将会更新匹配最新值【只有在实例被创建时存在的属性是响应式的】

指令(Directives) : 带有v- 前缀的特殊特性,作用是当表达式的值改变时,将其产生的连带影响响应式的作用于DOM

参数 : 一些指令能够接收一个"参数"

生命周期(lifecycle hooks)

beforCreate —— created —— beforeMount —— mounted —— beforeUpdate —— updated—— beforeDestroy—— destroyed

beforeCreate ——> 实例初始化后 数据观测和event/watcher事件配置之前被调用

created ——>实例创建完后立即调用,挂载阶段未开始

beforeMount ——> 挂载开始之前被调用,相关的render函数首次被调用【服务器端渲染时不被调用】

mounted ——>已挂载到实例上

beforeUpdate ——> 数据更新时调用 未更新前

updated ——> 组件已更新

activated ——> keep-alive组件激活时使用

deactivated——> keep-alive组件停用时调用

beforeDestory ——> 实例销毁之前,实例仍可用

destroyed——> 实例销毁后调用【子组件被销毁,事件监听器被移除,解除其他绑定】

errorCaptured ——> 当捕获到子孙组件错误时被调用,返回false阻止错误向上传播

模板语法

使用"{{ }}"语法 做文本插入

<span>Message: {{msg }}</span>

使用指令v-html插入原生html代码

<p>someting :<span v-html='rawHtml'></span></p>

Mustache语法不能作用在HTML特性上,需要使用v-bind指令

<div v-bind:id="bindId"></div>
<div v-bind:disable="bool"></div>

v-bind v-on 的缩写

<a v-bind:href="url">...</a>
<!--缩写-->
<a :href="url">...</a>

<a v-on:click="doSomething">...</a>
<!---缩写--->
<a @click="doSometing">...</a>

对象语法

通过 v-bind:class 一个对象,动态切换class,也可以与普通的class属性共存

<div v-bind:class="{active:isActive}"></div>
<div 
     class="static"
     v-bind:class="{active: isActive}"
     ></div>

数组语法

可以将一个数组传给 v-bind:class 应用一个class列表

<div v-bind:class="[activeClass, errorClass]"></div>
data:{
    activeClass: 'active',
    errorClass: 'text-danger'
}

也可以在其中使用三目运算符,也可以在其中使用对象语法

条件渲染

<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>

<template>元素上使用v-if条件渲染分组

v-else v-else-if必须紧跟在v-if 或者 v-else-if元素后面,否则不被识别

v-show根据条件展示元素的指令【原理是切换display的值,始终保留在DOM中】

<h1 v-show="ok">Hello</h1>

如果非常频繁的切换则使用v-show如果运行时条件很少改变则使用v-if

v-if v-for一起使用时,v-forv-if优先级更高

<ul id="example-1">
	<li v-for="item in items">
    	{{item.message}}
    </li>	
</ul>
var example1 = new Vue({
    el: '#example-1',
    data: {
        items:[
            {message: '1'},
            {message: '2'}
        ]
    }
})

v-for中第二个可选参数值为当前项的索引,也可以用v-for迭代一个对象属性

<div v-for="(value, key, index) in object">
	{{index}}.{{ key}}: {{value}}
</div>
<div v-for="item of items" :key="item.id">
<!--context--->
</div>

数组更新检测

变异方法【会修改原数组】 push pop unshift shift splice sort reverse

Vue无法监测到如下数组的变动

  • 利用索引直接设置一项 例如 vm.item[indexOfItem] = newValue
  • 修改数组长度 vm.items.length = newLength

解决直接设置值的问题可以利用如下方法

Vue.set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
vm.$set(vm.items, indexOfItem, newValue) 
//vm.$set是全局Vue.set方法的别名

解决修改数组长度的问题可以使用splcie

vm.items.splice(newLength)

当需要显示一个数组的过滤或者排序副本时,可以选择非变异方法的计算属性

<li v-for="n in evenNumbers">{{n}}</li>
data : {
    numbers: [1,2,3,4]
},
computed:{
    evenNumbers: function(){
        return this.numbers.filter(function(number){
            return number % 2 === 0
        })
    }
}

计算属性不适应的情况下(例如在嵌套v-for循环中)可以使用methods方法:

<li v-for="n in even(numbers)">{{n}}</li>
data: {
    numbers: [1,2,3,4]
},
methods: {
  even: function(numbers){
      return numbers.filter(function(number){
          return number % 2 === 0
      })
  }
}

对象的更新检测

Vue无法直接检测到对象属性的添加或删除,可以使用```Vue.set(object, key, value)``方法向对象添加响应式属性

var vm = new Vue({
    data: {
        userProfile: {
            name: 'Anika'
        }
    }
})

//添加一个新的age属性
Vue.set(vm.userProfile, 'age',27)
// vm.$set(vm.userProfile, 'age, 27')

当需要为已有对象赋新属性时,添加响应式属性

vm.userProfile = Object.assign({}, vm.userProfile, {
    age: 29,
    favoriteColor: 'red'
})

// don't do that
Object.assign(vm.userProfile,{
    age: 29,
    favoriteColor: 'red'
})

事件处理

v-on监听DOM事件,v-on也可以接收一个需要调用的方法名称。需要在内联语句处理器中访问原始的DOM事件,可以用特殊的变量$event传入methods

<div id="example-1">
    <button v-on:click="counter+=1">Add One</button>
    <button v-on:click="greet">Greet</button>
    <button v-on:click="say('hi')">Greet</button>
    <button v-on:click="warn('omg',$event)">Greet</button>
</div>
var example = new Vue({
    el: '#example-1',
    data:{
       counter: 0,
       name: 'Vue'
    },
    methods:{
        greet:functino(event){
        	console.log(this.name);
        	if(event){
    			console.log(event.target.tagName);
			}
    	},
        say:function(hi){
            console.log(hi)
        },
        warn:function(msg, event){
            if(event) event.preventDefault()
            console.log(msg);
        }
    }
})

事件修饰符

.stop .prevent .capture .self .once .passive
<!-- 阻止表单事件继续传播 -->
<a v-on:click.stop="doThis" ></a>

修饰符顺序执行 .passive不要和 .prevent一起使用后者会被忽略

按键修饰

<!--只有key是enter时调用-->
<input v-on:keyup.enter="submit" />

<!-- Alt+C -->
<input @keyup.alt.67="clear">

<!-- Ctrl+Click -->
<div @click.ctrl="doSomething">
    Do Something
</div>

表单输入绑定

v-model指令在<input /><textarea> <select>元素上创建双向数据绑定
v-model在内部使用不同的属性为不同的输入元素抛出不同的事件

<input v-model="message" />
<p>{{message}}</p>

组件

组件是可复用的Vue实例,类似于react中的自定义Component

Vue.component('button-counter,{
data: function(){
	return {
		count: 0,
	}
  },
template:'<button v-on:click="count++">Clicked{{count}}</button>'
})

组件中data必须是一个函数,每个实例可维护一份被返回对象的拷贝

组件通过prop向子组件传值

Vue.component('blog-post',{
    props:['title],
    template:'<h3>{{title}}</h3>'
})

<blog-post title="Something"></blog-post>

父组件可以通过v-on监听子组件实例的任意事件,同时子组件可以通过内建方法$emit方法并传入事件名称来触发一个事件

<blog-post
	...
	v-on:enlarge-text="postFontSize+=0.1"
></blog-post>


<button v-on:click="$emit('enlarge-text',0.1)">Enlarge Text</button>
<input v-model="searchText" />

相当于 ====>
<input 
  	v-bind:value="searchText"
    v-on:input="searchText = $event.target.value"
 />

prop使得父子组件之间形成了一个单向下行绑定

props的类型检查类似react的TypeProps

监听原生事件可以v-on 的 .native修饰符

<base-input v-on:focus.native="onFocus"></base-input>

有时候.native不会预期的被调用Vue提供了$listeners属性,包含作用在这个组件上的所有监听器

Solt

父模板里的内容实在父作用域中编译的,子模板是在子作用域中编译的

<a
   v-bind:href="url"
   class="nav-link">
    <solt></solt>
</a>
<navigation-link url='/profile'>
{{ url}}  <!-- undefined -->
</navigation-link>

slot有一个name属性,可以用来定义额外的插槽

<div>
    <header>
        <slot name="header"></slot>
    </header>
    <main>
        <slot></slot>
    </main>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>
<base-layout>
    <template v-solt:header>
        <h1></h1>
    </template>
    <p></p>
    <template v-slot:footer>
        <p></p>
    </template>
</base-layout>

通过bind可以将元素特性绑定,使父级作用域可访问

具名插槽的缩写将v-slot: 替换为 # 例如

v-slot:header ==>> #header

必须在有参数的情况下才能使用

keep-alive

缓存初次创建的组件实例

<keep-alive>
    <component v-bind:is="currentComponent"></component>
</keep-alive>

利用.$root 获取跟组件的数据

.$parent属性可以从子组件访问父组件实例

Computed VS Methods

Computed属性【对于复杂的逻辑 可以使用computed属性】

区别是computed是可以缓存的,只在相关依赖发生变化时才会重新求值而methods每次渲染时都会求值

watch【监听响应数据变化】:观察动作,监听props $emit或者组件自身的异步操作 无缓存性

Vue.nextTick

Vue数据修改后不会立即更新,Vue中的DOM更新是异步的,只要观察到数据变化Vue将开启一个任务队列并缓冲在同一事件循环中发生的所有数据变化,如果同一个watcher被多次触发,只会被推入到队列中一次,在下一次事件循环tick中,Vue刷新队列,并实行实际工作,在created钩子函数执行DOM相关操作时,将其放在Vue.nextTick中去执行

感觉有些类似于EvenLoop的事件循环机制

vm.msg = 'hello'

// DOM未更新
Vue.nextTick(function(){
    // DOM已更新
})

在同一事件循环中,当同步执行操作完成后再调用nextTick

同步执行数据全部更新完毕后DOM开始渲染,同一事件中存在多个nextTick则依次执行


Nuxt.js

目录结构

  • assets ----------------用于组织未编译的静态资源如less,sass,js

  • components ----------用于组织应用的Vue.js组件 Nuxt.js不会扩展该目录

  • layouts ---------------用于组织应用布局组件

  • middleware -----------存放中间件

  • pages -----------------组织应用的路由及视图,Nuxt.js读取该目录下的Vue文件,并生成路由配置

  • plugins ----------------组织需要在根vue.js应用实例化之前需要运行的Js插件

  • static ------------------用于存放静态文件,此类文件不会被Nuxt.js构建编译处理,服务器启动时该目录文件会映射到根路径 / 下

  • sotre ------------------组织应用的Vuex状态时树

  • nuxt.config.js ---------组织Nuxt.js应用的个性化设置覆盖默认配置

    Nuxt.js中定义带参数的动态路由需要创建对应的以下划线为前缀的文件或目录,可在动态路由中定义参数校验

    export default{ 
       validate({params}){ 
          Return /^\d+$/.test(params.id) // 必须为number类型 
       } 
    } 

asyncData

asyncData方法在组件每次加载之前被调用,第一次参数设定为当前页面的上下文对象,asyncData返回的数据融合组件data方法后将数据传给组件【初始化前被调用无法使用this引用组件实例】

    // 上下文例子
    export default{
        async asyncData({req, res}){
            if(process.server){
                return {host: req.headers.host}
            }
            return {}
        }
    }
    // 访问动态路由数据
    export default{
        async asyncData({params}){
            return {slug : params.slug}
        }
    }

assets

不需要通过Webpack处理的静态资源文件放在static文件目录

    <!--- 编译前 -->
    <template>
    	<img src="~/assets/xxx.png" />
    </template>
    // 编译后
    createElement('img', {attrs: {src: require('~/assets/xxx.png')}})

plugins

实例化Vue.js应用程序之前运行js插件【在Vue组件的生命周期中,只有beforeCreate和created这两个方法会在客户端和服务端被调用起亚生命周期函数仅在客户端被调用】

    // plugins/vue-notifications.js
    import Vue from 'vue'
    import VueNotifications from 'vue-notifications'
    
    Vue.use(VueNotfications)

    // nuxt.config.js
    module.exports = {
        plugins: ['~/plugins/vue-notifications']
    }

Modules

主要用于扩展可信功能并添加无限的集成

Vuex

使用Vuex管理state

普通方式

首先需要将状态导出为函数,将变量和操作作为文件对象导出

    //store/index.js
    export const state = ()=>({
        counter:0
    })
    
    export const mutations = {
      increment(state){
        state.counter++
      }
    }
    // 	store/todos.js
    export const state = ()=>({
      list : []
    })
    
    export const mutations = {
      add(state, text){
        state.list.push({
          text:text,
          done: false
        })
      },
      remove(state,todo){
        state.list.splice(state.list.indexOf(todo),1)
      },
      toggle(state, todo){
        todo.done = !todo.done
      }
    }
    // 生成后
    new Vuex.Store({
      state: ()=>({
        counter: 0
      }),
      mutations:{
        increment(state){
          state.counter++
        }
      },
      modules:{
        todos:{
          namespcaed: true,
          state:()=>({
            list: []
          }),
          mutations:{
            add(state, {text}){
              state.list.push({
                text,
                done: false,
              })
            },
            remove(state,{todo}){
              state.list.splice(state.list.indexOf(todo),1)
            },
            toggle(state,{todo}){
              todo.done = !todo.done
            }
          }
        }
      }
    })

模块文件

将模块文件分解为单独的文件:state.js actions.js,mitations.js, getter.js 【注意:拆分文件模块时,必须使用箭头函数功能使得this在此法可用】
插件【可将其他插件添加到store(模块模式下),将其放入store/index.js】

    import myPlugin from 'myPlug'
    
    export const plugins = [myPlugin]
    export const state = ()=>({
      counter: 0
    })
    export const mutations = {
      increment(state){
        state.counter++
      }
    }

fetch

fetch会在渲染页面前被调用作用是填充store数据 ,与asyncData方法类似不同的是它不会设置组件的数据

浏览器的缓存策略

说到缓存其实分很多种,比如CDN缓存,代理服务器缓存,数据库缓存,浏览器缓存等等。这里我们主要说浏览器的缓存。其中重点部分是HTTP的缓存策略。当一个请求发出后,浏览器接收到响应,需要根据响应头里的缓存策略来决定是否使用以及如何使用缓存。我们以Chrome为例,Chrome在很多抽象层面都实现了缓存,通常可以大致分为三类:

  • HTTP cache
  • Service Worker Caches
  • Blink cache

本文主要介绍HTTP缓存部分。关于HTTP缓存最为熟知和关键,通过网络发出的每个请求都严格遵循RFC的HTTP规范标准,其中直接决定HTTP缓存的几个规则是Cache-Control、ETag、Last-Modified、Expires

HTTP Cache

Cache-Control

Cache-Control的可取值有如下两种类型值,需要注意的一点是request请求头里带的值并不代表最终响应值。最终值仍需要服务端决定,请求头中的值只会作为一个参考,所以我们会重点关注响应头字段的内容。

Cache-Control : cache-request-directive | cache-response-directive

cache-request-directiveno-cache | no-store | max-age | max-stale | min-fresh | no-transform | only-if-cached

cache-response-directivepublic| private | no-cache | no-store | no-transform | must-revalidate | proxy-revalidate | max-age | s-maxage

Public vs Private

public表示可以任意缓存,可被作为共享缓存使用。即可以再多个用户之间共享。private则表示只针对特定用户做缓存并且不可作为共享缓存。MSDN中描述 Cache-Control 的默认值为private。

如图表示此响应只可作为私有缓存,而且每次请求都需要服务器验证其有效性。

Snipaste_2019-11-02_14-02-58-7e950c4f-4c8b-47a4-bfab-d9976747e7e1

must-revalidate vs proxy-revalidate vs no-cache vs no-store

must-revalidate表示缓存过期前浏览器、缓存服务器可以正常使用,一旦缓存过期则必须去源服务器验证响应数据的有效性。

proxy-revalidate类似于must-revalidate,区别是其仅针对共享缓存。

no-cache表示浏览器或缓存服务器可做缓存,但不管缓存是否过期,使用前必须经过源服务器验证其缓存的有效性。no-cache可以看做是must-revalidate的定制版,不同点是must-revalidate是在请求过期后直接从服务器获取响应,而no-cache则默认缓存是立即过期的。举个例子来说:如果某个响应缓存的时间是10秒有效,则在这个时间范围内must-revalidate会在10秒后使用缓存时被触发,而no-cache则是0秒触发must-revalidate。

no-store表示绝对🚫禁止缓存响应,每次必须向源服务器请求新的资源。

如图所以:此站点的这个请求通过添加no-cache no-store must-revalidate以及pragm:no-cache(用于兼容HTTP 1.0)来禁止缓存,每次请求都要发往源服务器。

Snipaste_2019-11-02_13-45-12-bd6c08d4-ad99-4b37-b020-098928e56309

再看一个例子,所有的Chrome extension采用的策略是no-cache,即缓存服务器在返回备份响应前需要向源服务器校验其有效性,如果可用则返回备份,否则要向源服务器发起请求。

Snipaste_2019-11-02_14-08-18-d53e287d-d974-43e5-8873-031353ac2e0e

max-age vs s-maxage

max-age用来表示表示缓存的有效时间。单位是秒(s)。超出这个时间段后被认为过期。那么在这个max-age时间段内再次发出的请求,浏览器会直接使用缓存,不会再向浏览器发出请求。另外如果max-age和Expires同时存在时,会覆盖掉Expires字段(限于HTTP 1.1及之后版本)。

s-maxage只用来表示共享缓存(比如CDN缓存)的有效时间,作用类似max-age单位同样是秒(s),即max-age适用于普通缓存,s-maxage适用于共享缓存。如果response header里同时存在max-age和Expires(后文有提到)则会被s-maxage覆盖,需要注意的是如果缓存是private类型 则s-maxage会被直接忽略。

如下的例子中此缓存的有效期是一分钟。

Snipaste_2019-11-02_13-56-50-ed4aef68-bfd3-4be1-aa13-cf99d54a91e9

no-transform

no-transform表示不得对资源进行任何变更,不能修改Content-Encoding,Content-Range和Content-Type的请求头。用到情况很少,不做过多说明。

Cache-Control除了上述这些属于规范的取值外还有一种可取的范围值类型 cache-extension ,它不属于文档规范的一部分,所以有兼容性问题。这类值中比较典型的代表如:immutable、stale-while-revalidate=、stale-if-error=等。


immutable vs stale-while-revalidate vs stale-if-error

immutable用来描述响应内容不会随时间变化。因为资源(如果未过期)在服务器上不变,所以即使在用户刷新页面时,也不需要再重新验证其有效性(例如,If-None-Match或If-Modified-Since)

stale-while-revalidate表示客户端在指定时间内允许在异步检查校验新的响应时,可接收已过期的资源响应。例如:Cache-Control : max-age=600, stale-while-revalidate=30 上述规则表示这条资源的有效期的600秒,若在异步状态下检查新的响应时过了有效期但依然可以最多保持30秒可用。如果验证未通过或者没有被触发,则在30秒后stale-while-revalidate规则不在有效,缓存真正过期。

state-if-error用来表示如果客户端获取最新资源失败在指定的时间内可接受已过期的资源响应。例如:Cache-Control: max-age=600, stale-if-error=1200它表示资源的有效期为600秒如果在过期后向服务器请求遇到错误(比如服务器返回500、502、503、504等)则额外增加1200秒有效期。如果在1800秒后去请求则缓存过期直接返回错误信息。

如图是一个简略版的Cache-Control字段的判断步骤

Expires

Expires用来指定缓存的过期时间,是一个绝对时间,由服务端生成。Expires字段告诉浏览器在响应过期前的时间内无需再次请求服务器,可以直接使用缓存值,除非强制刷新或者缓存被清空。同时还有个缺陷在于服务器时间和用户端时间可能存在不一致,所以HTTP 1.1加入Cache-Control来改进这个问题。Expires的时间其实就是请求时的时间➕max-age。上文我们提到如果存在Cache-Control字段并且值为max-age则Expires字段就会被覆盖,也就意味着Cache-Control的优先级要高于Expires。Expires字段一般还要搭配Last-Modified使用。

ETag

想象这么一个场景:一个请求的缓存资源有效期是120秒,过了这个时间点因为缓存已经过了有效期,所以浏览器不能使用。当一个新的请求发出后,浏览器需要重新获取整个响应资源,但是如果响应内容未发生变化的话,这样是很浪费资源的。ETag就是来解决这个问题的。

ETag是由实体内容生成的一段hash值,由服务端生成,于If-None-Match搭配使用,当客户端再次请求时通过If-None-Match带上ETag的值向发送给服务端,服务端通过对比ETag值是否相等来验证资源是否发生变化。如果未变化则返回304和一个空响应,从而更高效的利用缓存节省资源消耗。

Snipaste_2019-11-05_13-53-19-c0dfd14b-81b0-4070-a403-6ff8a2f3608e

如图所示:因为其资源未发生变化,所以服务器返回304和空响应。

Snipaste_2019-11-05_13-53-19-c0dfd14b-81b0-4070-a403-6ff8a2f3608e

Last-Modified

用来表示响应文件的最后修改时间,可以用来检查资源是否更新的一种方式。当客户端再次向服务端请求时,会向服务器发送带If-Modified-Since字段来判断资源在Last-Modified时间点后是否被修改过。如果没有被修改则返回304和空响应,反之重新向服务器获取最新资源。当然说到这里会发现Last-Modified和ETag字段作用相同,但是它们还是细微的差别的。HTTP1.1中ETag的出现主要是为了解决Last-Modified的一些不足。

  1. 某些文件可能是定期生成的但是内容没变,Last-Modified值却变了,就导致缓存失效。
  2. Last-Modified的时间描述只能精确到秒级别,如果响应资源的变化在秒以内,就无能为力了。
  3. 有些服务器无法准确判断页面的最后修改日期。

Last-Modified与If-Modified-Since配合使用,当响应头带上Last-Modified,那么下次再请求时会把这个值加到请求头的If-Modified-Since中,服务器通过对比这个值来判断资源是否发生变化,如果没有发生变化则返回304和空响应。

Pragma

用于兼容HTTP 1.0,因为HTTP 1.0不支持Cache-Control,当Cache-Control可用时会被忽略。主要用来做Cache-Control:no-cache的备胎。它只有一个可取值即:no-cache。使用的唯一场景就是向后兼容HTTP 1.0。

上面四种缓存规则的优先级为:Cache-Control > Expires > ETag > Last-Modified

小知识:浏览器中我们经常会看到Memory Cache 和Disk Cache那么来看下它们之间有什么区别?实际上Memory Cache是将资源存储在RAM里,所以访问速度会很快,但是当你关闭浏览器后缓存立马失效,而Disk Cache则是将缓存资源写在磁盘里,读取资源时从磁盘中加载读取,有效期更持久。

其他

浏览器的其他缓存策略比如HTML的Meta标签,Manifest,LocalStorage SessionStorage等

Manifest

Manifest已经被移除出标准规范所以不在推荐使用,建议使用Service Workers。

LocalStorage SessionStorage

关于LocalStorage和SessionStorage可以看之前总结的这篇文章Cookie, LocalStorage 与 SessionStorage

参考链接:

模拟实现一个call、apply、bind方法

模拟实现bind方法

if(!Function.prototype.bind){
  Function.prototype.bind = function(oThis){
    if(typeof this !== 'function'){
      throw new TypeError();
    }
    var aArgs = [].slice.call(arguments,1),
        self = this,
        fNOP = function(){},
        fBound  = function() {
          return self.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat([].slice.call(arguments)));
        };
    if(this.prototype){
      fNOP.prototype = this.prototype;
    }
    fBound = new fNOP();
    return fBound;
  };
}

模拟实现一个call与apply方法

思路

  • 将函数设为对象属性
  • 执行该函数
  • 删除该函数

call

Function.prototype.call = function(context){
  var context = cotext || window;
  context.fn = this;
  var args = [];
  for(var i = 1; i < arguments.length; i++){
    args.push('arguments['+i+']');
  }
  var result = eval('context.fn('+args+')');
  delete context.fn;
  return result;
} 

apply

Function.prototype.apply = function(context, arr){
  var context = context || window;
  contex.fn = this;
  var reslut = [];
  if(!arr){
    result = context.fn(arr);
  }else{
    var args = [];
    for(var i = 0; i < arr.length; i++){
      args.push('arguments['+i+']');
    }
    result = eval('context.fn('+arr')');
  }
  delete context.fn;
  return result;
}

三者区别

  • call和apply的区别是传入的参数不同,
  • apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组
  • call 方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组。
    call和apply与 bind 最大的区别是 call apply调用即执行了所对应的方法,bind的返回值是改变指向后的这个函数(未执行)

一些JavaScript奇技淫巧

使用~~向下取整 类似与Math.floor();

~~12.1  // 12
~~12.7 //12

数组去重

[...new Set(array)]

数组中的最大值

Math.max(...[1,2,3,4,5])    // 5

CSS中的定位

使用top-left-margin 方法 居中 和使用transform的translate方法居中的性能差异

浏览器渲染过程是先解析HTML文件 构建DOM树,创建一个或多个图层独立的绘制,将图作为纹理上传至GPU,复合多个图层来完成最终的图像,所以每个层的样式出现调整后,要重新计算样式,重新布局,使用top-left-margin这种方法只会创建一个图层,而使用translate则容器中的元素不会和自身放在一个图层,而是放在GPU单独的渲染层中,这样带来的好处有三点

  1. 该元素任何合成属性(Composite Property)的变化都不会影响原有布局,不会导致原有布局被重(流reflow),重绘(relayout)
  2. 该层将由GPU线程(Compositor Thread)负责渲染,节省CPU资源,不会阻塞主线程JS代码的执行
  3. 动画更为平滑,这是因为使用translate将可以以小于像素的单位(sub-pixel)来绘制,并在帧之间加入了blur(模糊)效果

缺点

额外的渲染层导致更多的线程间通信

渲染性能分析(七)之Input Handlers

对输入处理程序做防抖

处理用户输入也是潜在的可能会影响性能的因素,因为其可能会阻塞其他内容的加载并且导致不必要的布局(layout)工作

TL;DR

  • 避免时间过长运行处理输入程序,其会阻塞页面滚动;
  • 不要在处理输入的程序中修改样式;
  • 对输入处理程序做防抖,在下一帧的requestAnimationFrame回调中存储事件值和样式的更改

避免运行时间过长的处理程序

页面交互最快的情况是,当用户与页面交互时,页面的合成器线程接受用户的触摸输入并将内容四处移动。这个过程不需要与主线程通信,而是直接提交给GPU处理。所以不需要等待主线程对JS的处理、以及布局(layout)、绘制(paint)等操作完成。

Snipaste_2019-10-21_17-27-22-296a06f0-6a55-41a7-af01-ff773c8e1f7c

但是,如果附加了输入处理程序(如touchstart,touchmove,或者touchend)后,合成器线程必须等待该处理程序执行完毕,因为有可能调用了preventDefault()来阻止触摸滚动事件的发生。即使没有调用preventDefault(),合成器也必须等待其执行完毕,这样用户的滚动操作就被阻止就可能导致帧丢失从而引起卡顿。

Snipaste_2019-10-21_17-28-10-7182cacd-7819-4f15-8957-594510110f12

总而言之,你应该确保运行的所有输入处理程序都快速执行,并允许合成器执行其工作。

避免在输入处理程序中改变样式

输入处理程序被安排在requestAnimtionFrame回调之前运行。如果在这个处理程序中做样式上的修改,那么在requestAnimationFrame开始处有需要更改的样式处理,这会触发强制同步布局。

Snipaste_2019-10-21_17-41-01-f4a205b3-b672-4a2c-9ab0-e21e97d1d2d5

输入处理程序做防抖

上面两个问题的解决方案是相同的:你应该对下一个requestAnimationFrame的回调中做样式更改的情况做防抖处理。

function onScroll(evt){
    lastScrollY = window.scrollY;

    if(scheduleAnimationFrame)
        retun;
    scheduleAnimationFrame = true;
    requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll',onScroll);

这样做还有一个好处,就是保持输入处理程序的轻量,因为这样就不会阻塞比如滚动等其他操作。

ES6笔记

let

因为JavaScript中没有块级作用域这一概念 而使用let 则可以达到这一效果
let允许把变量作用域限制在块级域中 与var不同的是:var申明要么是全局的 要么是函数级的 而 无法是块级作用域

let vs var

let作用域 是 块级 var 作用域是函数

for(var i = 0;i<5;i++){
    console.log(i); // 0, 1, 2, 3, 4 
}
console.log(i)       // 5
for(let i = 0; i< 5;i++){
    console.log(i); // 0, 1, 2, 3, 4 
}
console.log(i)      // i is not defined

关于let 声明提升

  1. let 的「创建」过程被提升了,但是初始化没有提升。
  2. var 的「创建」和「初始化」都被提升了。
  3. function 的「创建」「初始化」和「赋值」都被提升了。

其他: let 声明会提升到块顶部 ,从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)
如果你在 TDZ 内使用该变量,JS 就会报错

块级作用域

很多语言中都有块级作用域 JavaScript中使用var 声明变量 以function来划分作用域 用大括号 "{}"
无法限制var的作用域 使用var声明的变量据欧变量提升(declaration hoisting)的效果
ES6中增加的let 在 {} ,if for 里声明 用法与var 相同 但是作用域限定在块级 let声明变量不存在变量提升

'use strict';
function f1(){
    var a = 1;
    let n = 2;
    if(true){
      var a = 20;
      let n = 10;
    }
    console.log(n);       //  2
    console.log(a);       //  20
}
f1();

const

const 用来声明和创建一个常量

  • 常量可以是全局或者局部的函数声明
  • 常量遵循与变量相同的作用域规则
  • 常量不能重新赋值 和 重复声明 所以可以在声明一个常量时 不进行初始化 这个常量将永远保持为undefined
  • 常量不能和他所在的作用域内的其他变量或者函数拥有相同的名称
  • const 也具有块级作用域属性
  • const不能变量提升(必须先声明后使用)
  • const指定变量所在地址 对该变量进行属性设置是可以的 如果完全不想让其变化可以冻结(Object.freeze())
    例子:
const num = 10;
num = 20;
console.log(num);     //10
const num = 10;
var num = 20;
console.log(num);    // 'num' has already been declared
const C = {};
C.a = 1;
console.log(C.a)  // 1
const D = Object.freeze({});
D.a = 2;  // Error
console.log(D.a)   // undefined

类声明和类表达式

类声明

类声明式定义类的一种方式 使用class关键字后跟一个类名就可以定义一个类

'use strict';
class Polygon{
  constructor(height,width){
    this.height = height;
    this.width = width;
  }
}

变量提升

类声明与函数声明不同的一点是 函数声明存在变量提升现象 而类声明不存在 也就是说 必须先声明类 才能使用 否则会抛出异常

var p = new Polygon();    // ReferenceError
class Polygon{}
"use strict"
class Person{
  constructor(name){
    this.name = name;
  }
  sayName(){
        console.log("My Name is "+this.name);
    }
}
var l= new Person("Lily");
l.sayName();   // "My Name is Lily"

类表达式

类表达式是另外一种方式 就像函数表达式一样 在类表达式中 类名是可有可无的 如果定义了类名 则该类名只有在类体中才可以访问到

"use strict";

// 匿名函数表达式
var Polygon = class {
    constructor(height,width){
        this.height = height;
        this.width = width;
    }
};

// 命名函数
var Polygon = class Polygon{
    constructor(height,width){
        this.height = height;
        this.width = width;
    }
};

构造函数

类成员包括类构造器和类方法(包括静态方法和实例方法)
class 根据constructor方法来创建和初始化对象
constructor方法是类的默认方法 通过new 命令生成对象实例时 自动调用该方法 一个类只能有一个constructor方法 如果没有显示定义就会被隐式添加

constructor(){}

constructor默认返回实例对象(即this)完全可以指定返回另一个对象

"use strict"

class Foo{
    constructor(){
        return Object.create(null);
    }
}

new Foo() instanceof Foo     // false

constructor 方法是一个特殊的类方法 它既不是静态方法也不是实例方法 它仅会在实例化一个类的时候被调用 一个类只能有一个名为constructor属性的方法 否则 会抛出SyntaxError异常

静态方法

static关键字定义两个类的静态方法 静态方法被称为无需实例化类也可以当类被实例化 静态方法通常用于为应用程序创建实用函数

"use strict";
class Point{
    constructor(x,y){
        this.x = x;
        this.y = y;
    }
    static distance(a,b){
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.sqrt(dx*dx+ddy*dy);
    }
}

const p1 = new Point(5,5);
const p2 = new Point(10,10);

console.log(Point.distance(p1,p2));

extend 关键字 创建子类

extends 关键字可以用来创建继承于某个类的子类

栗子:根据Animal 类创建一个Dog类

class Animal{
    constructor(name){
        this.name = name;
    }
    speak(){
        console.log(this.name+" makes a noise");
    }
}
class Dog extends Animal{
    speak(){
        console.log(this.name +" barks");
    }
}

var dog = new Dog("Mini");
dog.speak();       // "Mini barks"

Map

map对象是一个简单的键值 任何值(包括对象和原始值)都可以作为一个键或者一个值

var m  = new Map();
var o = {p:"Hello World"};
m.set(o,"content");
m.get(o)                 // "content"
console.log(m)      //  Map { { p: 'Hwll' } => 'content' }

上面代码使用set方法 将对象o作为m的一个键
Map也接受一个数组作为参数 该数组的成员是一个个表示键值对的数组

var map = new Map([["name","zhangsan"],["title","Author"]]);
map.size  // 2
map.get("name")   // "zhangsan"
map.get("title")     // "Author"

注意Map的键实际上是和内存地址绑定的 知道内存地址不一样就视为两个键

  • 如果使用对象作为键名 就不用担心自己的属性与原对象的属性名同名
  • 如果Map键是简单类型(数字,字符串 布尔)则 主要两个值严格相等 Map将其视为一个键 包括0 和-0
  • NaN不严格的等于自身 但是 Map将其视为同一个键
var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN);                 // "not a number"

var otherNaN = Number("foo");
myMap.get(otherNaN);        // "not a number"

两个NaN作为Map的键来说是没有区别的

实例的属性和操作方法

size属性返回Map结构的成员总数 即返回映射对象中的键/值对数目
**set(key,val)**属性设置key对应的键值 然后返回整个Map结构 如果key已有值 则覆盖原有值 否则就生成这个键值

var m = new Map();
m.set("edition",6);
m.set(272,"ozil");
m.set(undefined,"nah");

set方法返回的是Map本身 因此可以采用链式写法

  • get(key) 方法读取key对应的键值 找不到就返回 undefined
  • has(key) 方法返回一个布尔值 表示某个键是否存在于Map中
  • delete(key) 方法删除某个键 返回true 如果删除失败 返回false
  • clear() 清除所有成员 没有返回值
var m = new Map();
m.set("a",1);
m.set("b",2);
m.set("keys",3);
m.has("keys")           //  true
m.delete("a")            
console.log(m)         // Map(2) {"b" => 2, "keys" => 3}
m.clear()                   // {}

map 遍历方法

  • keys():返回键名的遍历器
  • values(): 返回键值得遍历器
  • entries(): 返回所有成员的遍历器
  • forEach(): 返回Map的所有成员
var myMap  = new Map();
myMap.set(0,"zero");
myMap.set(1,"one");
maMap.set(2,"two");

for(var key of myMap.keys()){
    console.log(key); 
}
//  0  1  2

for(var item of myMap.entries()){
    console.log(item);
}

//  [0, "zero"]  [1, "one"]  [2, "two"]

myMap.forEach(function(val,key){
    console.log(key+ "="+val);
},myMap)

//  0=zero  1=one 2=two

WeakMap

WeakMap结构与Map结构基本类似 唯一的区别是它只接受对象作为键名(null除外)不接受其他类型的值作为键名 而且键名所指向的对象 不计入垃圾回收机制

var map = new WeakMap();
map.set(1,2);
// TypeError: 1 is not an object 
map.set(Symbol(),2);
// TypeError:Invalid value used as weak map key

WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。
  典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
  WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。

数组扁平化

将多维数组转化为一维数组(类似_Underscore.js中的_flatten()函数)

var flatten = function(arr) {
    var res = [], idx = 0, val;
    for (var i =  0, length = arr && arr.length; i < length; i++) {
      val = arr[i];
      if (val && val.length >= 0 && Array.isArray(val)) {
        val= flatten(val);
        var j = 0, len = val.length;
        res.length += len;
        while (j < len) {
          res[idx++] = val[j++];
        }
      } else {
        res[idx++] = val;
      }
    }
    return res;
  }

flatten([1, [2], [3, [[4]]]])   //[1,2,3,4]

如果仅是字符串数组 则可用

arr = arr.join(",").split(",")
["1", ["2"], ["3", [["4"]]]].join(",").split(",")         //  ["1", "2", "3", "4"]

递归

function flatten(arr){
    var res = [];
    for(var i=0;i<arr.length;i++){
        if(Array.isArray(arr[i])){
            res = res.concat(flatten(arr[i]));
        }else{
            res.push(arr[i]);
        }
    }
    return res;
}

利用reduce

function flatten(arr){
    return arr.reduce(function(prev,item){
        return prev.concat(Array.isArray(item)?flatten(item):item);
    },[]);
}

限制并发

Promise实现一个 并发限制的方法 限制每次最多同时发送n个请求

async function pMap (iterable, mapper, concurrency) {
    return new Promise((resolve, reject_)=>{
        const result = []
        const iterator = iterable[Symbol.iterator]()

        let currentIndex = 0
        let resolvingCount = 0
        let isRejected = false
        let isIterableDone = false



        const reject = (reason) => {
            isRejected = true
            reject(reason)
        }


        const next = () => {
            if (isRejected) {
                return 
            }
            const nextItem = iterator.next()
            const index = currentIndex

            currentIndex++

            if (nextItem.done) {
                isIterableDone = true
                if (resolvingCount === 0) {
                    resolve(result)   
                }
                return
            }

            resolvingCount++

            (async()=>{
                try {
                    const element = nextItem.value
                    if (isRejected) {
                        return
                    }
                    const value = await mapper(element, index)
                    result[index] = value
                    resolvingCount--
                    next()
                } catch (error) {
                    reject(error)
                }
            })()
        }



        for(let index = 0; index < concurrency; index++) {
            try {
                next()
            } catch(error) {
                reject(error)
                break
            }
            if (isIterableDone || isRejected) {
                break
            }
        }

    })
}


const mapper = (time) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
          console.log(time)
          resolve(time)
      }, time);
    });
}


pMap([1000, 3000, 2000, 4000], mapper, 2)

Git常用操作

Git

git 删除远程分支:git push origin --delete{}
git查看远程分支:git branch -r
git 切换远程分支:git checkout [name]
git stash 保存当前修改 但不提交commit
git stash apply{stash@{n}} 回到某个stash状态

将某次提交应用到指定分支
git checkout {branch-name}
git cherry-pick {hash}

重命名分支名

  1. Rename your local branch. git branch -m new-name
  2. Delete the old-name remote branch and push the new-name local branch. git push origin :old-name new-name
  3. Reset the upstream branch for the new-name local branch. git push origin -u new-name

Git commit重命名

How to change your commit messages in Git

更多Tips

NULL和Undefined的区别

null表示"没有对象",即该处不应该有值
简单的用法如:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。

其他

 null === undefined // false
 null == undefined // true
 null === null // true

 null = 'value' // ReferenceError
 undefined = 'value' // 'value'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.