Fork me on GitHub

JavaScript--有哪些情况会产生闭包?

说起闭包,总是让人觉得陌生又熟悉,听起来似乎并不是那么难,却又好像从来没有知道哪些地方会永=用到闭包。我们来归纳一下,有什么情况下会是闭包。

闭包的概念

在你不知道的Javascript上卷一书中,闭包的定义是这样的:当函数可以记住并访问所在的词法作用域,即使的函数是当前词法作用域之外执行,这时就产生了闭包。怎么理解这句话呢?就是说当函数内部的东西,能在函数外面执行的时候,这就产生了闭包。是不是很简单?那么哪些情况会产生闭包呢?

产生闭包的情况

(一)回调闭包

1
2
3
4
5
function wait (message) {
setTimeout(function timer () {
console.log(message)
}, 1000)
}

我们可以看到setTimeout定时器中有个函数叫timer,这个函数就是一个回调函数,我们可以看到,timer具有对wait函数作用域的全覆盖。也就是说,timer 具有涵盖 wait(..) 作用域的闭包,因此还保有对变量 message 的引用。当wait被执行1000毫秒之后,它的内部作用域并不会消失,timer 函数依然保有 wait(..)作用域的闭包。所以说,本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

(二)自调函数(IIFE模式)

1
2
3
4
var a = 2
(function IIFE () {
console.log(a) // 2
})()

我们再来看下闭包的定义,函数可以记住并且访问当前词法作用域使得函数是在当前词法作用域之外执行,这时会产生闭包。所以,IIFE模式看起来并不闭包的一种,因为函数并不是在他定义的作用域之外执行,变量a是普通的词法作用域查找得到的,并不是通过闭包找到的。但是需要注意的是,它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用。

(三)循环闭包

循环闭包,最常见的就是for循环。看下面的代码(我们期待最后输出的结果是1,2,3,4,5):

1
2
3
4
5
for (var i = 1; i <= 5;i++) {
setTimeout(function(){
console.log(i) // 6(5次)
}, i*1000)
}

先来解释一下,6是怎么来的。我们可以看到循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
我们来看IIFE模式:

1
2
3
4
5
for(var i = 1;i < 5;i++) {
(setTimeout(function () {
console.log(i) // 6 (5次)
}, 1000))()
}

这样不行。但是为什么呢?我们现在显然拥有更多的词法作用域了。的确每个延迟函数都会将IIFE 在每次迭代中创建的作用域封闭起来。如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的 IIFE 只是一 个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。

1
2
3
4
5
6
7
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}

ok,这样就完美了。这些 IIFE 也不过就是函数,因此我们可以将 i 传递进去,在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

(四)块作用域闭包

是的,你想的没错,就是let.let将一个块转换成一个可以被关闭的作用域看代码:

1
2
3
4
5
6
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域!
setTimeout(function timer() {
console.log( j );
}, j*1000 );
}

这还不够完美。for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

1
2
3
4
5
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

这样就完美多了。

(五)模块闭包

不知道你有没有想过,模块的实现竟然能和闭包联系在一起吗?但是是肯定的。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myModule () {
var name = '小明'
var age = 23
function getName () {
console.log(name)
}
function getAge () {
console.log(age)
}
return {
getName,
getAge
}
}
let foo = myModule()
foo.getName() // 小明
foo.getAge() // 23

这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露.

首先,myModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行 外部函数,内部作用域和闭包都无法被创建。
其次,myModule() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐 藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。
getName() 和 getAge() 函数具有涵盖模块实例内部作用域的闭包(通过调用 myModule() 实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作 用域外部时,我们已经创造了可以观察和实践闭包的条件。
模块模式需要具备两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。

(六) ES6模块闭包

ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立 的模块来处理。每个模块都可以导入其他模块或特定的 API 成员,同样也可以导出自己的 API 成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello;

foo.js
// 仅从 "bar" 模块导入 hello() import hello from "bar";
var hungry = "hippo";
function awesome() { console.log(
hello( hungry ).toUpperCase() );
}
export awesome;

baz.js
// 导入完整的 "foo" 和 "bar" 模块
module foo from "foo"; module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce:
rhino foo.awesome(); // LET ME INTRODUCE: HIPPO

import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量 上(在我们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。

尽管在这么多的场景中会出现闭包,所以我们一定要多多注意。

使用闭包注意点

  • 1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  • 2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

JavaScript--词法作用域是什么?

我们知道,作用域是用来查找变量的一套规则,JavaScript引擎在执行代码的时候,会经历三个阶段:词法分析,语法分析,生成代码这三个阶段,我们所说的词法作用域就是发生在词法分析阶段。

简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

全局变量会自动成为全局对象(比如浏览器中的 window 对象)的属性,因此 可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引 用来对其进行访问。

1
window.a

通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量 如果被遮蔽了,无论如何都无法被访问到。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处 的位置决定。词法作用域查找只会查找一级标识符.

这里比较坑的地方就是欺骗词法。那么什么是欺骗词法呢?如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢,这就是所谓的欺骗词法。通常会有俩中机制来实现,但是,欺骗词法作用域会导致性能下降

eval()函数

JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。根据这个原理来理解 eval(..),它是如何通过代码欺骗和假装成书写时(也就是词法期) 代码就在那,来实现修改词法作用域环境的,这个原理就变得清晰易懂了。在执行 eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插 入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

1
2
3
4
5
6
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

默认情况下,如果 eval(..) 中所执行的代码包含有一个或多个声明(无论是变量还是函数),就会对 eval(..) 所处的词法作用域进行修改。技术上,通过一些技巧(已经超出我 们的讨论范围)可以间接调用 eval(..) 来使其运行在全局作用域中,并对全局作用域进行 修改。但无论何种情况,eval(..) 都可以在运行期修改书写期的词法作用域。
在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。

1
2
3
4
function foo(str) { "use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined }
foo( "var a = 2" );
with

JavaScript 中另一个难以掌握(并且现在也不推荐使用)的用来欺骗词法作用域的功能是 with 关键字。with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。看下面的俩段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
// 但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作 用域中。
eval(..) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。

另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响(限 制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。

性能

eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。这对性能造成了很大的影响,JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符。

但如果引擎在代码中发现了 eval(..) 或 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。

最悲观的情况是如果出现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简 单的做法就是完全不做任何优化。如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。无论引擎多聪 明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代 码会运行得更慢这个事实。

JavaScript--作用域是什么?

Javascript作用域问题,看似简单,想要说明白却着实不简单。那么作用域到底是什么呢?且听我慢慢道来:

想要理解作用域的问题,要先了解一下编译的原理是怎么样的?
我们都知道Javascript通常来说是一门动态的,解释执行的语言,但是实际上来说他是一门编译语言,所以,和传统的语言的编译基本类似,只是在某些阶段比较复杂而已。大致需要经历三个阶段:

分词/词法分析

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块就是被称为是词法单元。例如:var a = 2;会被分解为 var ,a,=,2,;,空格是否会被当作词法单元,取决于空格的意义。

解析/语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

代码生成

这个过程是将AST树转化为真正代码的过程。具体来说,就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且 通常马上就会执行它。

那么是谁做了承接以上三个步骤的共能了呢?下面来介绍几个概念:

  • 引擎:负责从头到尾整个javascript的编译和执行过程。
  • 编译器:负责语法分析以及代码生成。
  • 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

我们来看var a = 2;这样的一个例子:
首先,编译器会将这段代码解析成词法单元;
然后,将词法单元解析生成一颗结构树;
最后,就是代码生成:

  • 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
  • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。

LHS查询和RHS查询

引擎在执行时,就会执行查找,所以,就会执行LHS查询和RHS查询,就是赋值操作的左侧和右侧。当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。

为什么区分 LHS 和 RHS 是一件重要的事情?
因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行 为是不一样的。
考虑如下代码:

1
2
3
4
5
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );

第一次对 b 进行 RHS 查询时是无法找到该变量的。也就是说,这是一个“未声明”的变 量,因为在任何相关的作用域中都无法找到它。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。

“不,这个变量之前并不存在,但是我很热心地帮你创建了一个。”

ES5 中引入了“严格模式”。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上 有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在 严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询 失败时类似的 ReferenceError 异常。

接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。

ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

嵌套作用域和作用域链

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都 会停止。

CSS--grid布局是什么

概览

grid布局是css中最强大的布局之一,他是将网页分割成网状结构,即一个个网格项目,现在已经内置到浏览器。grid布局和flex布局有几分相似,flex是轴线布局。只能针对项目的轴线布局,可以看作是一维布局,Grid布局则是将容器划分成”行”和”列”,产生单元格,然后指定”项目”所在的单元格,可以看作是二维布局。Grid 布局远比Flex 布局强大。下面的布局是grid布局的拿手好戏:
grid布局

基本概念

在了解grid布局之前,先说一些基本概念:

  • 容器和项目
    容器:采用网格布局的区域,被叫做“容器”;
    项目:容器内部采用网格定位的子元素(必须是子元素),称为”项目”。Grid 布局只对项目生效。

  • 行,列和单元格
    行:容器中水平方向的区域称为行;
    列:容器中垂直方向的区域称为列;
    单元格:行和列相交就是单元格,一般情况下单元格=m行*n列。
    grid布局

  • 网格
    划分网格的线,称为”网格线”(grid line)。水平网格线划分出行,垂直网格线划分出列。
    正常情况下,n行有n + 1根水平网格线,m列有m + 1根垂直网格线,比如三行就有四根水平网格线。
    grid布局
    grid布局的属性分为俩种,一种是容器属性,一种是项目属性。

    容器属性

    display属性

    display: grid; // 指定容器采用grid布局
    display: inline-grid; // 指定容器采用内成行内grid布局。

    1
    2
    3
    .container {
    display: grid;
    }

    grid布局

    1
    2
    3
    .container {
    display: inline-grid;
    }

    grid布局
    注意,设为网格布局以后,容器子元素(项目)的float、display: inline-block、display: table-cell、vertical-align和column-*等设置都将失效。

grid-template-columns 属性,grid-template-rows 属性

从字面意思就可以看出是和列和行有关的属性,没错,grid-template-columns用来指定容器中列的宽度,grid-template-rows用来指定容器中行的高度。

1
2
3
4
5
.container {
display: grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}

除了使用觉得单位,也可以使用相对单位,百分比等。
grid布局

  • repeat()
    有时候列或者行比较多的时候,重复写比较麻烦,所以提供了repeat方法:

    1
    2
    3
    4
    5
    .container {
    display: grid;
    grid-template-columns: repeat(3, 100px);
    grid-template-rows: repeat(3, 100px);
    }

    repeat()接受两个参数,第一个参数是重复的次数(上例是3),第二个参数是所要重复的值。repeat也可以重复某种模式。

    1
    2
    3
    4
    5
    .container {
    display: grid;
    grid-template-columns: repeat(2, 100rpx, 80rpx, 30rpx);
    grid-template-rows: 100px 100px 100px;
    }

    定义了六列,第一列和第四列宽100px,以此类推。

  • auto-fill 关键字
    有时,单元格的大小是固定的,但是容器的大小不确定。如果希望每一行(或每一列)容纳尽可能多的单元格,这时可以使用auto-fill关键字表示自动填充。

    1
    2
    3
    4
    .container {
    display: grid;
    grid-template-columns: repeat(auto-fill, 100px);
    }

    grid布局

  • fr 关键字
    为了方便表示比例关系,网格布局提供了fr关键字(fraction 的缩写,意为”片段”)。如果两列的宽度分别为1fr和2fr,就表示后者是前者的两倍。

    1
    2
    3
    4
    .container {
    display: grid;
    grid-template-columns: 1fr 1fr;
    }

    fr可以与绝对长度的单位结合使用,这时会非常方便。

    1
    2
    3
    4
    .container {
    display: grid;
    grid-template-columns: 150px 1fr 2fr;
    }

    第一列的宽度为150像素,第二列的宽度是第三列的一半。
    grid布局

  • minmax()
    minmax()函数产生一个长度范围,表示长度就在这个范围之中。它接受两个参数,分别为最小值和最大值。

    1
    2
    3
    4
    .container {
    display: grid;
    grid-template-columns: 1fr 1fr minmax(100px, 1fr);
    }

    minmax(100px, 1fr)表示列宽不小于100px,不大于1fr。

  • auto 关键字

    1
    2
    3
    4
    .container {
    display: grid;
    grid-template-columns: 100px auto 100px;
    }

    第二列的宽度,基本上等于该列单元格的最大宽度,除非单元格内容设置了min-width,且这个值大于最大宽度。

  • 网格线的名称
    grid-template-columns属性和grid-template-rows属性里面,还可以使用方括号,指定每一根网格线的名字,方便以后的引用。

    1
    2
    3
    4
    5
    .container {
    display: grid;
    grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4];
    grid-template-rows: [r1] 100px [r2] 100px [r3] auto [r4];
    }
  • 布局实例
    grid-template-columns属性对于网页布局非常有用。两栏式布局只需要一行代码。

    1
    2
    3
    4
    .wrapper {
    display: grid;
    grid-template-columns: 70% 30%;
    }

    将左边栏设为70%,右边栏设为30%。
    栅格布局

    1
    2
    3
    .container {
    grid-template-columns: repeat(12, 1fr);
    }

    grid-row-gap 属性,grid-column-gap 属性,grid-gap 属性

    gap意思为间距的意思,所以见字得意,grid-row-gap, grid-column-gap,grid-gap都是设置间距的意思,分别是行间距,列间距,以及二者的简写。根据最新标准,上面三个属性名的grid-前缀已经删除,grid-column-gap和grid-row-gap写成column-gap和row-gap,grid-gap写成gap。
    grid布局

    grid-template-areas 属性

    网格布局允许指定”区域”(area),一个区域由单个或多个单元格组成。grid-template-areas属性用于定义区域。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    .container {
    display: grid;
    grid-template-columns: 100px 100px 100px;
    grid-template-rows: 100px 100px 100px;
    grid-template-areas: 'a b c'
    'd e f'
    'g h i';
    // grid-template-areas: 'a a a'
    'b b b'
    'c c c';
    // grid-template-areas: "header header header"
    "main main sidebar"
    "footer footer footer";
    // grid-template-areas: 'a . c'
    'd . f'
    'g . i';
    }

    中间一列点表示没有用到该单元格,或者该单元格不属于任何区域。
    注意,区域的命名会影响到网格线。每个区域的起始网格线,会自动命名为区域名-start,终止网格线自动命名为区域名-end。比如,区域名为header,则起始位置的水平网格线和垂直网格线叫做header-start,终止位置的水平网格线和垂直网格线叫做header-end。

grid-auto-flow 属性

这个属性决定了容器中的项目的排列方式,默认情况下的值是row.既是按照行排列,先填满第一行,再第二行。

  • row
    grid布局
  • column
    grid布局
  • row dense:先行后列,并且尽可能紧密填满,尽量不出现空格。
    grid布局
  • column dense:先列后行,并且尽量填满空格。
    grid布局

    justify-items 属性,align-items 属性,place-items 属性

    justify-items属性设置单元格内容的水平位置(左中右),align-items属性设置单元格内容的垂直位置(上中下)。
    1
    2
    3
    4
    .container {
    justify-items: start | end | center | stretch;
    align-items: start | end | center | stretch;
    }
  • start:对齐单元格的起始边缘。
  • end:对齐单元格的结束边缘。
  • center:单元格内部居中。
  • stretch:拉伸,占满单元格的整个宽度(默认值)。
    1
    2
    3
    .container {
    justify-items: start;
    }
    grid布局
    1
    2
    3
    .container {
    align-items: start;
    }
    grid布局
    place-items属性是align-items属性和justify-items属性的合并简写形式。
    1
    2
    3
    .container {
    place-items: start start;
    }

    justify-content 属性,align-content 属性,place-content 属性

    justify-content属性是整个内容区域在容器里面的水平位置(左中右),align-content属性是整个内容区域的垂直位置(上中下)。
    1
    2
    3
    4
    .container {
    justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
    align-content: start | end | center | stretch | space-around | space-between | space-evenly;
    }
  • start - 对齐容器的起始边框。
    grid布局
  • end - 对齐容器的结束边框。
    grid布局
  • center - 容器内部居中。
    grid布局
  • stretch - 项目大小没有指定时,拉伸占据整个网格容器.
    grid布局
  • space-around - 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与容器边框的间隔大一倍。
    grid布局
  • space-between - 项目与项目的间隔相等,项目与容器边框之间没有间隔。
    grid布局
  • space-evenly - 项目与项目的间隔相等,项目与容器边框之间也是同样长度的间隔。
    grid布局

place-content属性是align-content属性和justify-content属性的合并简写形式.

1
place-content: <align-content> <justify-content>

grid-auto-columns 属性,grid-auto-rows 属性

grid-auto-columns属性和grid-auto-rows属性用来设置,浏览器自动创建的多余网格的列宽和行高。它们的写法与grid-template-columns和grid-template-rows完全相同。如果不指定这两个属性,浏览器完全根据单元格内容的大小,决定新增网格的列宽和行高。

1
2
3
4
5
6
.container {
display: grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
grid-auto-rows: 50px;
}

grid布局

grid-template 属性,grid 属性

grid-template属性是grid-template-columns、grid-template-rows和grid-template-areas这三个属性的合并简写形式。

grid属性是grid-template-rows、grid-template-columns、grid-template-areas、 grid-auto-rows、grid-auto-columns、grid-auto-flow这六个属性的合并简写形式。

项目属性

grid-column-start 属性,grid-column-end 属性,grid-row-start 属性,grid-row-end 属性

项目的位置是可以指定的,具体方法就是指定项目的四个边框,分别定位在哪根网格线。

  • grid-column-start属性:左边框所在的垂直网格线
  • grid-column-end属性:右边框所在的垂直网格线
  • grid-row-start属性:上边框所在的水平网格线
  • grid-row-end属性:下边框所在的水平网格线
    1
    2
    3
    4
    .item-1 {
    grid-column-start: 2;
    grid-column-end: 4;
    }
    1号项目的左边框是第二根垂直网格线,右边框是第四根垂直网格线。
    grid布局
    只指定了1号项目的左右边框,没有指定上下边框,所以会采用默认位置,即上边框是第一根水平网格线,下边框是第二根水平网格线。

除了1号项目以外,其他项目都没有指定位置,由浏览器自动布局,这时它们的位置由容器的grid-auto-flow属性决定,这个属性的默认值是row,因此会”先行后列”进行排列。读者可以把这个属性的值分别改成column、row dense和column dense,看看其他项目的位置发生了怎样的变化。

1
2
3
4
5
6
.item-1 {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 2;
grid-row-end: 4;
}

grid布局
这四个属性的值,除了指定为第几个网格线,还可以指定为网格线的名字。

1
2
3
4
.item-1 {
grid-column-start: header-start;
grid-column-end: header-end;
}

这四个属性的值还可以使用span关键字,表示”跨越”,即左右边框(上下边框)之间跨越多少个网格。

1
2
3
.item-1 {
grid-column-start: span 2;
}

grid布局
使用这四个属性,如果产生了项目的重叠,则使用z-index属性指定项目的重叠顺序

grid-column 属性,grid-row 属性

grid-column属性是grid-column-start和grid-column-end的合并简写形式,grid-row属性是grid-row-start属性和grid-row-end的合并简写形式。

1
2
3
4
5
6
7
8
9
10
11
.item-1 {
grid-column: 1 / 3;
grid-row: 1 / 2;
}
/* 等同于 */
.item-1 {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 1;
grid-row-end: 2;
}
1
2
3
4
5
6
7
8
9
10
11
.item-1 {
background: #b03532;
grid-column: 1 / 3;
grid-row: 1 / 3;
}
/* 等同于 */
.item-1 {
background: #b03532;
grid-column: 1 / span 2;
grid-row: 1 / span 2;
}

grid布局
斜杠以及后面的部分可以省略,默认跨越一个网格。

1
2
3
4
.item-1 {
grid-column: 1;
grid-row: 1;
}

grid-area 属性

grid-area属性指定项目放在哪一个区域.

1
2
3
.item-1 {
grid-area: e;
}

grid
grid-area属性还可用作grid-row-start、grid-column-start、grid-row-end、grid-column-end的合并简写形式,直接指定项目的位置。

1
2
3
.item {
grid-area: <row-start> / <column-start> / <row-end> / <column-end>;
}

justify-self 属性,align-self 属性,place-self 属性

justify-self属性设置单元格内容的水平位置(左中右),跟justify-items属性的用法完全一致,但只作用于单个项目。

align-self属性设置单元格内容的垂直位置(上中下),跟align-items属性的用法完全一致,也是只作用于单个项目。

1
2
3
4
.item {
justify-self: start | end | center | stretch;
align-self: start | end | center | stretch;
}
  • start:对齐单元格的起始边缘。
  • end:对齐单元格的结束边缘。
  • center:单元格内部居中。
  • stretch:拉伸,占满单元格的整个宽度(默认值)。
    1
    2
    3
    .item-1  {
    justify-self: start;
    }
    grid布局
    place-self属性是align-self属性和justify-self属性的合并简写形式。
    1
    place-self: <align-self> <justify-self>;

CSS-flex布局是什么

说起flex布局,应该是我们比较熟悉的一种,但是有很多不常用的属性,我们还是需要总结一下:

什么是flex布局

flex布局就是flexible box,就是弹性盒子的意思,也就是为盒子提供最大的灵活性。为任意的盒子指定flex布局:

  • 任意元素
    1
    2
    3
    .container {
    display: flex;
    }
  • 行内元素
    1
    2
    3
    .container {
    display: inline-flex;
    }
    注意,设为 Flex 布局以后,子元素的float、clear和vertical-align属性将失效。

    基本概念

    采用flex布局的元素,称为容器,而容器中的子元素则称为项目。
    flex布局
    容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。

    容器属性

  • flex-direction
  • flex-wrap
  • flex-flow
  • justify-content
  • align-items
  • align-content

flex-direction

flex-direction决定了主轴的方向,也就是项目排列的方向。

1
2
3
.box {
flex-direction: column-reverse | column| row | row-reverse;
}
  • column-reverse沿着纵轴的负正方向排列;

  • column沿着纵轴方向负排列;

  • row (默认值)沿着横轴正方向排列;

  • row-reverse 沿着横轴的负方向排列;
    flex布局

    flex-wrap

    flex-wrap表示容器中的项目是否换行。

    1
    2
    3
    .box{
    flex-wrap: nowrap | wrap | wrap-reverse;
    }

    flex布局

  • nowrap 不换行
    flex布局

  • wrap允许换行,第一行在上方。
    flex布局

  • wrap-reverse 换行,第一行在下方。
    flex布局

flex-flow

flex-flow是flex-direction 和flex-wrap的简写, 默认值为row和nowrap

1
2
3
.box {
flex-flow: <flex-direction> || <flex-wrap>;
}

justify-content

justify-content属性表示沿主轴排列的对奇方式:

1
2
3
.box {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
}
  • center剧中对其,
  • flex-start 左对齐
  • flex-end右对齐
  • space-between俩端对齐
  • space-around每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
  • space-evenly 每个项目两侧的间隔以及同项目流两侧的间隔相等。
    flex布局

align-items

align-items是表示其在交叉轴的对齐方式。

1
2
3
.box {
align-items: flex-start | flex-end | center | baseline | stretch;
}
  • flex-start:交叉轴的起点对齐。
  • flex-end:交叉轴的终点对齐。
  • center:交叉轴的中点对齐。
  • baseline: 项目的第一行文字的基线对齐。
  • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
    flex布局

    align-content

    align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
    1
    2
    3
    .box {
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
    }
  • flex-start:与交叉轴的起点对齐。
  • flex-end:与交叉轴的终点对齐。
  • center:与交叉轴的中点对齐。
  • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
  • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
  • stretch(默认值):轴线占满整个交叉轴。

项目属性

  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • flex
  • align-self

    order

    order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
    1
    2
    3
    .item {
    order: <integer>;
    }
    flex布局

    flex-grow

    flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
    1
    2
    3
    .item {
    flex-grow: <number>; /* default 0 */
    }
    flex布局
    如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

    flex-shrink

    flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小.
    1
    2
    3
    .item {
    flex-shrink: <number>; /* default 1 */
    }
    如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。负值对该属性无效。

    flex-basis

    flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
    1
    2
    3
    .item {
    flex-basis: <length> | auto; /* default auto */
    }
    它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。

    flex

    flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。
    1
    2
    3
    .item {
    flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
    }
    该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。

    align-self

    align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。
    1
    2
    3
    .item {
    align-self: auto | flex-start | flex-end | center | baseline | stretch;
    }
    flex布局
    该属性可能取6个值,除了auto,其他都与align-items属性完全一致。
    (That’s all!)

微信系列--微信小程序的性能优化分包管理

小程序开发,累代码,打页面容易,但是当小程序越来越大的时候,性能是一个非常严峻的问题。所以,我们来讲讲小程序的性能优化–分包管理。

微信小程序的加载顺序

小程序加载顺序
小程序的加载流程主要是分三个步骤:

  • 资源准备,小程序在准备下载资源包;
  • 业务代码注入和渲染,就是说小程序开始将业务代码注入到视图层和逻辑层,然后开始渲染页面;
  • 异步数据请求,就是当进入首页如果有数据请求,那么现在开始异步数据加载。

那么,我们优化应该重点从这三步中开始。

代码包大小的优化

代码包的大小直接影响到小程序的启动速度,代码包体积越大,下载资源的速度就越慢,代码注入的时间,所以控制代码包的大小就会非常的有必要。
(1)控制包的大小

  • 开发完成之后,通过开发者工具上传代码,可以打开代码自动压缩的配置,这样可以将包的体积变小,另外也可以通过webpack,gulp等第三方工具压缩代码;
  • 将没有用的代码,文件等及时删除,清理,也可以减少包的体积;
  • 要尽量将图片等资源文件放到CDN上,因为小程序对于资源文件的压缩非常有限。

(2)分包加载
若果不做分包加载,那么当我们第一次打开小程序的时候,就要将小程序的所有包都下载下来,这时候,那些暂时用不到的包就会导致包下载耗时,所以,我们可以将资源做分包处理,例如将tab页面的包作为主包先下载下来,其他包作为分包,按需加载,这样打开小程序的就能稍微快一点。这样做的优势有:

  • 承载更多功能:小程序单个代码包的体积上限为 2M,使用分包可以提升小程序代码包总体积上限,承载更多的功能与服务。
  • 降低代码包下载耗时:使用分包后可以显著启动时需要下载的代码包大小,在不影响功能正常使用的前提下明显提升启动耗时。
  • 降低开发者代码注入耗时:小程序启动时会一次性注入全部的开发者代码,使用分包后可以降低注入的代码量,从而降低注入耗时。
  • 降低页面渲染耗时
    但是,分包加载也有局限性,那就是用户首次打开分包的页面的时候,需要先进行代码包的下载和注入,这样就会出现一定的延时。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //  app.json中配置
    {
    "pages":[
    "pages/index",
    "pages/logs"
    ],
    "subpackages": [
    {
    "root": "packageA",
    "pages": [
    "pages/cat",
    "pages/dog"
    ]
    }, {
    "root": "packageB",
    "name": "pack2",
    "pages": [
    "pages/apple",
    "pages/banana"
    ]
    }
    ]
    }

    分包主要是将分包的页面放到subpackages配置项中。

(3)分包预加载
因为分包加载的局限性。所以可以预加载小程序的分包。也就是说,在主包加载完成之后,静默开启分包的加载和注入。这是个无感的过程。分包预加载需要注意的是:同一个分包中的页面享有共同的预下载大小限额2M,限额会在工具中打包时校验,因此不能把所有的分包页面都配置到分包预加载的配置中,只配置主包页面会跳转的页面即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//  app.js中配置
{
"pages": ["pages/index"],
"subpackages": [
{
"root": "important",
"pages": ["index"],
},
{
"root": "sub1",
"pages": ["index"],
},
{
"name": "hello",
"root": "path/to",
"pages": ["index"]
},
{
"root": "sub3",
"pages": ["index"]
},
{
"root": "indep",
"pages": ["index"],
"independent": true
}
],
"preloadRule": {
"pages/index": {
"network": "all",
"packages": ["important"]
},
"sub1/index": {
"packages": ["hello", "sub3"]
},
"sub3/index": {
"packages": ["path/to"]
},
"indep/index": {
"packages": ["__APP__"]
}
}
}
}

分包预加载主要是preloadRule中,key 是页面路径,value 是进入此页面的预下载配置。

(4)独立分包
从分包页面启动时,必须依赖于主包的下载和注入,启动所以就会收到主包大小的限制,因此,我们就需要独立分包,这样在启动页面的时候,就可以不依赖于主包,减少了主包的下载和注入时间,通常会将广告,活动等具有独立逻辑的代码做独立分包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//  app.json中配置
{
"pages": [
"pages/index",
"pages/logs"
],
"subpackages": [
{
"root": "moduleA",
"pages": [
"pages/rabbit",
"pages/squirrel"
]
}, {
"root": "moduleB",
"pages": [
"pages/pear",
"pages/pineapple"
],
"independent": true
}
]
}

独立分包通过在app.json的subpackages字段中对应的分包配置项中定义independent字段声明对应分包为独立分包。

代码注入的优化

(1)减少启动过程中的同步调用
在小程序的启动过程中,会依次调用App.onLaunch, App.onShow, Page.onLoad, Page.onShow生命周期函数。应避免执行复杂的计算逻辑或过度使用Sync结尾的同步API。对于 getSystemInfo, getSystemInfoSync 的结果应进行缓存,避免重复调用。

(2)使用依赖注入
通常情况下,在小程序启动时,启动页面所在分包和主包(独立分包除外)的所有JS代码会全部合并注入,包括其他未访问的页面以及未用到自定义组件,造成很多没有使用的代码注入到小程序运行环境中,影响注入耗时和内存占用。
自基础库版本 2.11.1 起,小程序支持仅注入当前页面需要的自定义组件和当前页面代码,以降低小程序的启动时间和运行时内存。开发者可以在 app.json配置:

1
2
3
4
5
//  app.json配置
{
"lazyCodeLoading": "requiredComponents"
}

注意:添加这项配置后,未使用到的代码文件将不被执行。

页面渲染优化

(1)提高首屏渲染速度
大部分小程序在渲染首页时,需要依赖服务端的接口数据,小程序为开发者提供了提前发起数据请求的能力:

  • 数据预拉取:能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度。
  • 周期性更新:在用户未打开小程序的情况下,也能从服务器提前拉取数据,当用户打开小程序时可以更快地渲染页面,减少用户等待时间。

(2)骨架屏
页面数据未准备好之前,避免长时间白屏,可以使用骨架屏来显示页面结构。提升用户等待的意愿。

(3)缓存数据
小程序提供了wx.setStorage、wx.getStorage等读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。如果开发者基于某些原因无法采用数据预拉取与周期性更新,我们推荐优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新。

(4)精简首屏数据
我们推荐开发者延迟请求非关键渲染数据,与视图层渲染无关的数据尽量不要放在 data 中,加快页面渲染完成时间。

运行时性能优化

setData

小程序中setData的调用是最为频繁的。也是最容易引起性能问题的接口。

setData的工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

setData的错误使用
  1. 频繁调用setData
  • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
  • 渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
  1. 每次 setData 都传递大量新数据
    由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程.

  2. 后台态页面进行 setData
    当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

图片资源

目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收。除了内存问题外,大图片也会造成页面切换的卡顿。我们分析过的案例中,有一部分小程序会在页面中引用大图片,在页面后退切换中会出现掉帧卡顿的情况。当前我们建议开发者尽量减少使用大图片资源。

函数柯理化是什么,手动实现一个柯理化函数

函数柯理化

  • 原理:函数柯理化的本质就是将函数接受的多个参数单一化,并且返回接受余下的参数且返回结果的新函数的一种技术。

简单版求和

1
2
3
4
5
6
7
8
function add (a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
console.log(add(1)(2)(3)) // 6

函数柯理化

  • 参数长度固定
1
2
3
4
5
6
7
8
9
10
const curry = (fn) =>
(judge = (...args) =>
args.length === fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg));
const add = (a, b, c) => a + b + c;
const curryAdd = curry(add);
console.log(curryAdd(1)(2)(3)); // 6
console.log(curryAdd(1, 2)(3)); // 6
console.log(curryAdd(1)(2, 3)); // 6
  • 参数长度不固定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function add (...args) {
args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function temp (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return temp
} else {
let val = fn.apply(this, args)
args = []
return val
}
}
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)) // 6

that’s all!

函数截流是什么,手动实现一个截流函数

防抖截流

  • 原理:规定在单位时间内只能触发一次的函数。如果这个单位时间内触发多次函数,但是只执行一次。
  • 应用场景:
    • 拖拽场景:固定时间内只执行一次,防止高频触发位置变动;
    • 缩放场景:监控浏览器resize;

时间戳实现方式

  • 用当前执行的时间戳减去上一次执行时候的时间戳,如果大于设置的等待时间,就执行,反之,就不执行。
1
2
3
4
5
6
7
8
9
10
11
12
let throttle = (fun, wait) => {
let context, args, previous = 0
return () {
let now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fun.apply(context, args)
previous = now
}
}
}

使用定时器方式

  • 可以通过出发定时器来实现,如果定时器存在就不执行,反之,就执行。
1
2
3
4
5
6
7
8
9
10
11
let throttle = (fun, wait) {
let timer
return function () {
let context = this
let args = arguments
timer = setTimeout(function () {
timer = null
fun.apply(context, args)
}, wait)
}
}

that’s all!

函数防抖是什么,手动实现一个防抖函数

防抖函数

  • 原理:在事件出发n秒之后再执行回调,如果在这n秒内再次触发,则重新开始计时。
  • 应用场景:
    • 按钮提交场景:防止多次点击按钮提交。只执行最后一次;
    • 搜索框连续输入场景:防止连续发送请求,只发送最后一次。

简易版:

1
2
3
4
5
6
7
8
9
10
11
let debounce = (fun, wait) => {
let timer
return function () {
const context = this
const args = arguments
clearTimeout(timer)
timeout = setTimeout(() => {
fun.apply(context, args)
}, wait)
}
}

立即执行

  • 立即触发,然后等到停止出发n秒后再次重新触发执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let debounce = (fun, wait, immediate) => {
let timer
return function () {
const context = this
const args = arguments
if (timer) clearTimeout(timer)
if (immediate) {
const callNow = !timer
timer = setTimeout(function () {
timer = null
}, wait)
if (callNow) {
fun.apply(context, args)
}
} else {
timer = setTimeout(function () {
fun.apply(context, args)
}, wait)
}
}
}

有返回值

  • 某些情况下,fun可能会有返回值,所以需要返回函数的结果,但是当immediate为false的时候,因为使用了setTimeout,我们将fun.apply(context, args)的返回值赋值给变量,然后再return的时候,值将会一直undefined,所以只有immediate值为true的时候返回函数的执行结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let debounce = (fun, wait, immediate) => {
let timer, result
return function () {
const context = this
const args = arguments
if (timer) clearTimeout(timer)
if (immediate) {
const callNow = !timer
timer = setTimeout(function () {
timer = null
}, wait)
if (callNow) {
result = fun.apply(context, args)
}
} else {
timer = setTimeout(function () {
fun.apply(context, args)
}, wait)
}
return result
}
}

that’s all!

JavaScript--GET请求和POST请求的区别到底是什么?

GET请求和POST请求是比较常见的俩种请求,基本都能说出一二,下面是来自w3c的标准答案:

  • GET请求的参数是在url中,而POST请求的参数是request body中;
  • GET请求会主动被浏览器缓存,而POST请求不会,除非手动设置;
  • GET请求的参数会被缓存在浏览器的历史记录中,而PSOT请求不会;
  • GET请求会比POST请求更加安全,因为GET请求的参数是在URL中裸奔;
  • GET请求在浏览器回退的时候是无害的,而POST请求会再次发起请求;
  • GET请求的参数的长度会有限制,而POST请求没有;
  • GET请求对参数的数据类型,GET只接受ASCII字符,而POST没有限制;
  • GET请求产生的URL地址可以被Bookmark,而POST请求不可以。

能答出这些答案的说明你很牛逼,但是离相当牛逼还差点对本质上区别的理解,且听我给你慢慢道来:

本质上来说,GET请求和POST请求其实没什么区别。get请求和post请求是什么,他们是HTTP协议中的俩种请求方法,那么HTTP又是什么呢?HTTP是基于TCP/IP的数据在万维网中传输的协议。因此,HTTP的底层就是TCP/IP,SO,GET请求和POST请求的底层也是TCP/IP,是不是很神奇?所以,GET/POST请求就是TCP链接,他们做的事情其实是一样的,把GET请求的参数放到request body中,把POST请求的参数放到URL上在技术层面将其实是可行的。那么为什么还有上面的那些区别呢?

其实在万维网的世界中,TCP就像是大货车,是用来运送数据的,这其实很安全,并不会出现丢包少包的现象。但有个很致命的问题就是,公路上跑的大货车都完全一样。根本无法区分。所谓无规矩不成方圆。所以,HTTP就应用而生。HTTP给大货车贴上了不同的标签。是GET,POST,PUT或者是DELETE等等?并且规定了所携带的货物(参数)放在那里?比如,GET请求要放到URL中,而POST请求要放到request body中。所以,这就是上面所说的GET请求的参数在URL,而POST请求参数在request body中的原因。

可是上面讲数据量的大小也不一样,这又是为什么呢?这是因为还有个运输公司的角色(浏览器)存在着。不同浏览器运输数据的成本也不相同,因此他们在浏览器和服务器数据传输上做了相应的限制。业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。

所以,GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

但是,但是,但是,但是,还没有完。。。。。

GET和POST还有一个重大区别:GET产生一个TCP数据包;POST产生两个TCP数据包

  • 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

这样来说,似乎GET请求会比POST请求更高效,但其实在网络环境比较好的情况下,几乎可以忽略他们的差别。还有一点就是其实并不是所有的浏览器的PSOT请求都是发送俩次,切记fireFox是个例外。

这才是GET请求和POST请求区别的完整版。。。

手动实现一个Promise

手写一个Promise是面试中常考的重点,所以想要手写一个Promise,我们必须先知道Promise是什么,有那些特点。在其原型上有那些方法,需要哪些参数等等相关信息。。。。下面我们就先来总结一下:

Promise 是一个对象,代表了未来将要发生的事件,用来传递异步操作的消息。

Promise

  • Promise有三种状态:pending(进行中)、fullfilled(成功)、rejected(失败);
  • Promise对象接受一个回掉函数作为参数,该回掉函数接受俩个参数,resolve(成功回调)和reject(失败回调),resolve的参数除了正常值外,还可以是一个Promise对象的实例,reject的参数通常则是一个Error的实例;
  • then方法返回一个Promise实例,并接受俩个参数,即resolve和reject函数;
  • catch方法返回一个新的Promise实例;
  • finally方法不管是Promise是处于fullfilled状态还是rejected状态,都会执行,而且不接受任何参数;
  • Promise.all()方法是将多个Promise对象包装成一个新的Promise对象。该方法的参数可以不是一个数组,但是必须是具有iterator接口的数据类型,其返回值是每个每个Promise的实例。其中只要有一个是rejected状态,整个Promise.all()就会进入到catch中,而要想正常返回值,就必须是所有的Promise都是fullfilled状态。
  • Promise.race()是和Promise.all()相类似,接受同样的参数,但是不同的是,Promise.race()中,只要有一个实例正常返回,该实例就会返回到Promise.race()中;
  • Promise.resolve()方法是将现有参数转化为Promise对象。如果该方法的参数是一个Promise,那么将不会做任何处理,如果参数是个thenable,那么Promise.resolve()会将该对象转化为Promise对象,并且立即执行then方法。如果参数是一个原始值,或者是一个不具备then方法的对象,则Promise.resolve()方法会返回一个新的Promise,状态为fullfilled,其参数作为then方法中resolve的参数。如果Promise.resolve()不带任何参数,会直接返回一个fullfilled状态的Promise对象。是本轮‘事件循环’的结束时执行。而不是下一轮‘事件循环’的开始;
  • Promise.reject()同样是返回一个Promise对象。状态为rejected,无聊参数如何,都将会作为reject的参数返回。

Promise的优点

  1. 统一的异步API:逐步统一各种不同浏览器中异步API,以及不兼容的模式和手法;
  2. 与事件相比:Promise更适合处理一次性的结果,在结果计算出来之前,或者之后,注册回调函数都是可以的,都是可以拿到正确的值。但是Promise不能处理多次触发的事件,链式调用是Promise的另一个优点,而事件却不能链式调用;
  3. 与回调相比:解决了回调地狱的问题。将异步操作以同步的方式表达出来。
  4. Promise的额外好处是更好的错误处理,写起来更轻松,可以重用一些同步工具等。

Promise的缺点

  1. Promise一旦创建,就会立即执行,并且无法终止;
  2. Promise内部抛出的错误,不会暴露到Promise的外部,只有通过错误的回调来检测错误;
  3. 当Promise的状态是pending状态的时候,并不能明确Promise进行到什么阶段;
  4. 当Promise开始执行到回调的时候,实际Promise部分已经执行完成,但是其中抛出的错误对上下文并不友好。

Promise代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function _Promise (fn) {
const _this = this
_this.state = 'pending' // 保存Promise的状态
_this.value = '' // 保存Promise的值
_this.reason = '' // 保存Promise的reason
_this.onFullfiledCb = [] // 存储then方法中注册的成功回调函数参数
_this.onRejectedCb = [] // 存储then方法中注册的失败回调函数参数
function resolve (value) {
if (_this.state === 'pending') {
_this.state = 'fullfilled'
_this.value = value
setTimeout(function() {
_this.onFullfiledCb.map(item => item(_this.value))
}, 0)
}
}
function reject (reason) {
if (_this.state === 'pending') {
_this.state = 'rejected'
_this.reason = reason
setTimeout(function () {
_this.onRejectedCb.map(item => item(this.reason))
}, 0)
}
}
try {
fn(resolve, reject)
} catch(err) {
reject(err)
}
}
_Promise.prototype.then = function (onFullfiled, onRejected) {
const _self = this
switch(this.state) {
case 'fullfilled':
onFullfiled(self.value)
break
case 'rejected':
onRejected(self.value)
break
default:
onRejected(self.value)

}
}

// 测试
let p = new _Promise((resolve, reject) => {
resolve('我的世界')
})
p.then(res => console.log(res)) // 我的世界

手动实现Promise.then()的链式调用

Promise.then()方法的链式调用的使用,自不必多说,我们再熟悉不过,那么他是如何实现的呢?话不多说直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const PENDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECTED = 'REJECTED'

class _Promise {
constructor(executor) {
//立即调用函数
executor(this.resolve, this.reject)
}
//初始化状态
status = PENDING
//初始化成功之后的值
success = undefined
//初始化失败之后的值
error = undefined
//用于保存成功回调,成功回调的默认值需要改成数组,因为只有数组可以存储多个回调函数
successCallback = []
//用于保存失败回调,失败回调的默认值需要改成数组,因为只有数组可以存储多个回调函数
failCallback = []
resolve = (success) => {
//如果状态不是等待 则阻止程序执行
if (this.state !== PENDING) return
this.state = FULLFILLED
//保存成功之后的值
this.success = success
//如果有这个回调,那么要执行这个回调,并且把成功的值传递进去
//this.successCallback && this.successCallback(this.success);
//现在呢数组中存储了多个回调函数,所以遍历数组的每个函数并让其执行,上面的代码已经不符合要求
//重新编写逻辑
//这里使用while语句用successCallback.length作为循环条件,如果数组中有值,拿到数组中的第一个回调函数传值并执行
while (this.successCallback.length) this.successCallback.shift()(this.success);
}
reject = (rejected) => {
//如果状态不是等待 则阻止程序执行
if (this.state !== PENDING) return
//把promise状态改为失败
this.state = REJECTED
//保存失败之后的值
this.error = rejected
//如果有这个回调,那么要执行这个回调,并且把失败的原因传递进去
//this.failCallback && this.failCallback(this.error);
//现在呢数组中存储了多个回调函数,所以遍历数组的每个函数并让其执行,上面的代码已经不符合要求
//重新编写逻辑
//这里使用while语句用failCallback.length作为循环条件,拿到数组中的第一个回调函数传值并执行
while (this.failCallback.length) this.failCallback.shift()(this.error);
}
then(successCallback, failCallback) {
//要实现then方法的链式调用必须创建一promise对象
//新建一个promise对象
return new _Promise((resolve, reject) => {
//逻辑判断如果当前状态为成功 则执行成功的回调并且把保存成功的值传递进去
if (this.status === FULFILLED) {
//保存上一个函数的返回值
let x = successCallback(this.success)
//并且把返回值传递给下一个then方法
resolve(x);
//逻辑判断如果当前状态为成功 则执行失败的回调并且把失败的原因传递进去
} else if (this.status === REJECTED) {
failCallback(this.error)
} else {
//当前状态为等待,也就是promise状态为pending,
//如果是等待的话那应该调用成功回调还是失败回调呢
//那当然是两个回调都无法调用,应为不知道到底是成功了还是还是失败了
//在这种情况下应该将成功回调和失败回调进行保存
//保存成功回调函数
//在这里有一个问题 this.successCallback一次只能存储一个函数这样的不符合要求
//所以在上面定义successCallback的时候将其定义为数组,这样就可以存储多个回调 ,将回调push进去
this.successCallback.push(successCallback);
//保存失败回调函数
this.failCallback.push(failCallback);
}
})
}
}

测试

1
2
3
4
5
6
7
8
9
let promise = _Promise((resolve, reject) => {
resolve('success');
})
promise.then(res => {
console.log(res); // success
return 1000
}).then(res => {
console.log(res); // 1000
})

完美实现Promise.then()的链式调用。

  • Copyrights © 2015-2022 Lee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信