叫我欧文就好

叫我欧文就好 查看完整档案

广州编辑  |  填写毕业院校广州凡岛网络有限公司  |  web前端开发 编辑 zbw-zbw.github.io/kyriewen-blog/ 编辑
编辑

记录一个前端菜鸟的进阶之路...

个人动态

叫我欧文就好 发布了文章 · 4月10日

深入理解JavaScript中的类型

一、类型的定义

大多数人认为,像 JavaScript 这样的动态语言是没有类型(type)的。

也有人认为,JavaScript 中的“类型”应该称为“标签”(tag)或者“子类型”(subtype)。

定义:类型是值的内部特征,它定义了值的行为,目的是为了区分其他值。

怎么理解?变量是没有类型的,值才具有类型,类型描述了值的行为特征;换句话说,当某个值发生改变,类型也随之改变(如 a = 1 --> a = '1')。

二、内置类型(七种)

JavaScript中有七种内置类型:

  • 空值(null)
  • 未定义(undefined)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 布尔值(boolean)
  • 符号(symbol,ES6中新增
除了对象之外,其他统称为‘基本类型’。

三、检测值的类型(typeof)

我们可以使用typeof运算符来查看值的类型,注意:它的返回值是字符串(string)

typeof undefined // 'undefined'
typeof 11 // 'number'
typeof 'kyrie' // 'string'
typeof { name: 'kyrie' } // 'object'
typeof true // 'boolean'=
typeof null // 'object' 远古bug(二进制问题)
// ES6新增类型
typeof Symbol('11') // 'symbol'

你可能注意到少了null类型,而且typeof null 竟然返回的'object'类型,这是个远古bug,存在将近20年,但这个bug应该不会修复了,一修复可能牵扯到的web系统都崩了...

提示:文末有关于这个bug的详细解释。

我们可以使用&&来检测null值的类型

var a = null
if (!a && typeof null === 'object') // true
  • object的子类型(函数)
typeof function demo() {...} // 'object'

函数实际是object的一个“子类型”

函数是“可调用的对象”,他也可以拥有属性

function demo(a, b) {...}
demo.length // 2

函数(对象)的length属性是形参的个数

  • object的子类型(数组)
var arr = [1, 2, 3]
typeof arr // 'object'
arr.length // 3

数组也是object的一个“子类型”

数组(对象)的length属性是元素的个数

四、undefined和undeclared

变量在定义了(但没有值)时为undefined

var a;
typeof a // 'undefined'

那undeclared又是什么?

undeclared(未声明),大多数人认为它和 undefined(未定义值) 是同一个东西,但其实不是。

区别:已经在作用域中声明了但还没有赋值的变量,是undefined的;还没有在作用域中声明过的变量,是undeclared的。
var a;
a // undefined
b // b is not defined (undeclaced)

这里,浏览器的处理很让人误解, “b is not defined”很容易让人误以为 b 是 undefined(未定义值)的,但其实b是undeclaced(未声明)的,如果浏览器报错为:“b is not found” 或者 “b is not declared” 或许更准确一点。

更气人的是,typeof对这类处理让人绝望。

var a;
typeof a //'undefined'
typeof b //'undefined'返回 undeclared会好很多

对于undeclared或者undefined的变量,typeof的返回值都是'undefined';但请注意,这里typeof b没有报错(直接访问b会报错),这是typeof一种特殊安全防范机制

五、typeof的安全防范机制

可以用来检测某个全局变量是否存在

// 报错 debug是未声明(undeclared)的
if (debug) {
  console.log('Debugging is starting!!!')
}

// 安全 typeof的安全防范机制
if (typeof debug !== 'undefined') {
  console.log('Debugging is starting!!!')
}

某些时候也可以不用typeof的安全防范机制

if (window.debug) {
  console.log('Debugging is starting!!!')
}

上面的实现方式只适用于代码运行在浏览器环境(拥有window对象),但其他环境不行(如Node.js/服务端...),因为全局对象并不是window。

总结

  1. JavaScript中有七种内置类型:null、undefined、boolean、number、string、object、symbol,可以使用typeof运算符来判断某个值的类型。
  2. 变量是没有类型的,只有值有类型,类型定义(描述)了值的行为特征。
  3. undefined和undeclared不是同一个东西。undefined是值的类型之一,undeclared表示变量还未声明。
  4. 直接访问undeclared变量是会报错的(ReferenceError: xxx is not defined),而且typeof对undefined和undeclared变量都返回'undefined'。
  5. 可以通过typeof的安全防范机制(阻止报错)来检查undeclared变量
附:关于typeof null === 'object'的解释

在第一版的 JavaScript 中,变量的值被设计保存在一个 32 位的内存单元中。该单元包含一个 1 或 3 位的类型标志,和实际数据的值。类型标志存储在单元的最后。包括以下5种情况:

  • 000:object - 数据为对象的引用
  • 1:int - 数据为31位的有符号整型
  • 010:double - 数据为一个浮点数的引用
  • 100:string - 数据为一个字符串的引用
  • 110:boolean - 数据为布尔类型的值

除此之外,还有两种特殊情况:

  • undefined 负的 2 的 30 次方(超出当时整型取值范围的一个数)
  • null 空指针(全是0)

正是因为:null 的存储单元最后三位和 object 一样是 000(罪魁祸首)

所以typeof null === 'object' // true

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 4月7日

彻底搞懂JavaScript中的作用域和闭包

一、作用域

  • 作用域是什么

几乎所有的编程语言都有一个基本功能,就是能够存储变量的值,并且能在之后对这个值进行访问和修改。

那这些变量存储在哪里?怎么找到它?因为只有找到它才能对它进行访问和修改。

简单来说,作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)。


那么问题来了,究竟在哪里设置这些作用域的规则呢?怎样设置?

首先,我们要知道,一段代码在执行之前会经历三个步骤,统称为“编译”。

  1. 分词/词法分析

这个过程会将字符串分解成有意义的代码块,这些代码块称为词法单元

var a = 1;
// 这段代码会被分解为五个词法单元:
var 、 a 、 = 、 1 、 ;
  1. 解析/语法分析

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

  1. 代码生成

这个过程是将AST转换为可执行的代码

简单来说,用某种方法可以将
var a = 2; 
的抽象语法树(AST)转化为一组机器指令,
指令用来创建一个叫作a的变量,并将一个值2存在a中

在这个过程中,有3个重要的角色:

  1. 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
  2. 编译器:负责语法分析及代码生成
  3. 作用域(今天的主角):负责收集并维护由所有声明的变量(标识符)注册的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些变量的访问权限。

所以,看似简单的一段代码 var a = 1; 编译器是怎么处理的呢?

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

那么,引擎是如何查找变量的?

引擎会为变量 a 进行LHS查询(左侧)。另外一个叫RHS查询(右侧)

简单来说,LHS查询就是试图找到变量的容器本身(比如a);而RHS查询就是查询某个变量的值(比如1)

总结:作用域就是根据名称查找变量的一套规则


  • 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

function add(a) {
  console.log(a + b)
}

var b = 2;

add(1) // 3

在add()内部对b进行RHS查询,发现查询不到,但可以在上一级作用域(这里是全局作用域)中查询到。

怎么区分LHS和RHS查询?思考以下代码

function add(a) {
  // 对b进行RHS查询 无法找到(未声明)
  console.log(a + b) // 对变量b来说,取值操作
  b = a // 对变量b来说,赋值操作
}

add(1) // ReferenceError: b is not defined
function add(a) {
  // 对b进行LHS查询,无法找到,会自动创建一个全局变量window.b(非严格模式)
  b = a  // 对变量b来说,赋值操作
  console.log(a + b)// 对变量b来说,取值操作
}

add(1) // 2

总结:如果查找变量的目的是赋值,则进行LHS查询;如果是取值,则进行RHS查询


  • 词法作用域

作用域有两种主要的工作模型。第一种最为普遍,也是重点,叫作词法作用域,另一种叫作动态作用域(几乎不用)

简单来说,词法作用域就是定义在词法阶段的作用域(通俗易懂的说,就是在写代码时变量或者函数声明的位置)。

function foo(a) {
  var b = a * 2
  
  function bar(c) {
    console.log(a, b, c)
  }
  
  bar(b * 3)
}

foo(2) // 2, 4, 12
  1. 全局作用域中有1个变量:foo
  2. foo作用域中有3个变量:a、b、bar
  3. bar作用域中有1个变量:c

变量查找的过程:首先从最内部的作用域(即bar函数)的作用域开始查找,引擎无法找到变量a,因此会到上一级作用域(foo函数)中继续查找,在这里找到了变量a,因此引擎使用了这个引用。变量b同理,对于变量c来说,引擎在bar函数中的作用域就找到了它。

注意:作用域查找会在找到第一个匹配的变量(标识符)时停止查找


  • 函数作用域

简单来说,函数作用域是指,属于这个函数的全部变量都可以在这个函数范围内使用及复用(复用:即在嵌套的其他作用域中也可以使用)。

var a = 1

// 定义一个函数包裹代码块,形成函数作用域
function foo() {
  var a = 2
  console.log(a) // 2
}

foo()
console.log(a) // 1

你会觉得,如果我要使用函数作用域,那么我必须定义一个foo函数,这让全局作用域多了个函数,污染了全局作用域,且必须执行一次该函数才能运行其中的代码块。

那有没有一种办法,可以让我不污染全局作用域(即不定义新的具名函数),且函数可以自动执行呢?

你一定想到了,IIFE(立即执行函数)

var a = 1;
(function foo() {
  var a = 2
  console.log(a) // 2
})()
console.log(a) // 1

这种写法,实际上不是一个函数声明,而是一个函数表达式。要区分这两者,最简单的方法就是看function关键字是否出现在第一个位置(第一个词),如果是,那么是函数声明,否则是一个函数表达式。

  • 块作用域

尽管你可能没写过块作用域的代码,但你一定对下面的代码块很熟悉:

for(var i = 0; i < 5; i++) {
  console.log(i)
}

我们在for循环的头部定义了变量i,是因为想在for循环内部的上下文中使用i,而忽略了最重要的一点:i会被绑定在外部作用域(即全局作用域中)。

ES6改变了这种情况,引入let关键字,提供另一种声明变量的方式。

{
  let a = 2;
  console.log(a) // 2
}
console.log(a) // ReferenceError: a is not defined

讨论一下之前的for循环

for(let i = 0; i < 5; i++) {
  console.log(i)
}
console.log(i) // ReferenceError: i is not defined

这里,for循环头部的i绑定在循环内部,其实它在每一次循环中,对i进行了重新赋值。

{
  let j;
  for(let j = 0; j < 5; j++) {
    let i = j // 每次循环重新赋值
    console.log(i)
  }
  j++
}
console.log(i) // ReferenceError: i is not defined

小知识:其实在ES6之前,使用try/catch结构(在catch分句中)也有块作用域


  • 提升

先有鸡(声明)还是先有蛋(赋值)?

简单来说,一个作用域中,包括变量和函数在内的所有声明都会在任何代码被执行前首先被 “移动” 到作用域的最顶端,这个过程就叫作提升。

a = 2
var a
console.log(a) // 2

// 引擎解析:
var a
a = 2
console.log(a) // 2
console.log(a) // undefined
var a = 2

//引擎解析:
var a
console.log(a) // undefined
a = 2

可以发现,当JavaScript看到 var a = 2; 时,会分成两个阶段,编译阶段执行阶段

编译阶段:定义声明,var a

执行阶段: 赋值声明,a = 2

结论:先有蛋(声明),后有鸡(赋值)。

  • 函数优先

函数和变量都会提升,但函数会首先被提升,然后是变量。

foo() // 2

var foo = 1

function foo() {
  console.log(2)
}

foo = function() {
  console.log(3)
}

// 引擎解析:
function foo() {...}
foo()
foo = function() {...}

多个同名函数,后面的会覆盖前面的函数

foo() // 3

var foo = 1

function foo() {
  console.log(2)
}

function foo() {
  console.log(3)
}

提升不受条件判断控制

foo() // 2

if (true) {
  function foo() {
    console.log(1)
  }
} else {
  function foo() {
    console.log(2)
  }
}

注意:尽量避免普通的var声明和函数声明混合在一起使用。

二、闭包

  • 定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

function foo() {
  var a = 2
  
  function bar() {
    console.log(a)
  }
  
  return bar
}

var baz = foo()
baz() // 2 快看啊,这就是闭包!!!

函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()本身当作一个值类型进行传递。

正常情况下,当foo()执行后,foo()内部的作用域都会被销毁(引擎的垃圾回收机制),而闭包的“神奇”之处就是可以阻止这件事请的发生。事实上foo()内部的作用域依然存在,不然bar()里面无法访问到foo()作用域内的变量a

foo()执行后,bar()依然持有该作用域的引用,而这个引用就叫作闭包

总结:无论何时何地,如果将函数当作值类型进行传递,你就会看到闭包在这些函数中的应用(定时器,ajax请求,事件监听器...)。

我相信你懂了!

回顾一下之前提到的for循环

for(var i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

期望:每秒依次打印1、2、3、4、5...9

结果:每秒打印的都是10

稍稍改进一下代码(利用IIFE)

for(var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function timer() {
        console.log(i)
    }, i * 1000)
  })(i)
}

问题解决!对了,我们差点忘了let关键字

for(var i = 0; i < 10; i++) {
  let j = i // 闭包的块作用域
  setTimeout(function timer() {
    console.log(j)
  }, j * 1000)
}

还记得吗?之前有提到,for循环头部的let声明在每次迭代都会重新声明赋值,而且每个迭代都会使用上一个迭代结束的值来进行这次值的初始化。

最终版:

for(let i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

好了,现在你肯定懂了!

总结:当函数可以记住并访问所在的词法作用域,即使函数是在当前的词法作用域之外执行,就产生了闭包

如果你坚持看到了这里,我替你感到高兴,因为你已经掌握了JavaScript中的作用域和闭包,这些知识都是进阶必备的,如果有不理解的,花时间多看几遍,相信你一定可以掌握其中的精髓。

都到这儿了!

点个关注再走呗!!

查看原文

赞 6 收藏 3 评论 0

叫我欧文就好 发布了文章 · 4月6日

彻底搞懂JavaScript中的this关键字

本文对JavaScript中的this关键字进行全方位的解析,看完本篇文章,希望读者们能够完全理解this的绑定问题。

开篇:对于那些没有投入时间去学习this机制的JavaScript开发者来说,this的绑定是一件令人困惑的事。(包括曾经的自己)。

误区:学习this的第一步是明白this既不指向函数本身也不指向函数的词法作用域,你是否被类似这样的解释所误导?但其实这种说法都是错误的。

概括:this实际是在函数被调用时发生的绑定,它所指向的位置完全取决于函数被调用的位置。

一、调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

所以说,寻找调用位置就是寻找“函数被调用的位置”,这里最重要的点是要分析调用栈(存放当前正在执行的函数的位置)

什么是调用栈和调用位置?

关系:调用位置就在当前正在执行的函数(调用栈)的前一个位置

function func1() {
  // 当前调用栈:func1
  // 当前调用位置是全局作用域(调用栈的前一个位置)
  console.log('func1')
  func2() // 这里是:func2的调用位置
}
function func2() {
  // 当前调用栈:func1 -> func2
  // 当前调用位置是在func1(调用栈的前一个位置)
  console.log('func2')
  func3() // 这里是:func3的调用位置
}
function func3() {
  // 当前调用栈:func1 -> func2 -> func3
  // 当前调用位置是在func2(调用栈的前一个位置)
  console.log('func3')
}
func1() // 这里是:func1的调用位置

关注点:我们是如何从调用栈中分析出真正的调用位置的,因为这决定了this的绑定

二、绑定规则

  • 默认绑定

最常用的函数调用类型:独立函数调用

function getName() {
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'kyrie'

当调用getName()时,this.name拿到了全局对象的name。因为getName()是直接调用的,不带任何修饰符,使用的是默认绑定,因此this指向全局对象(非严格模式)。

如果使用严格模式('strict mode')呢?

function getName() {
  'use strict';
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'TypeError: this is undefined'

那么全局对象无法使用默认绑定,因此this会绑定到undefined

  • 隐式绑定

调用位置是否有上下文对象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
person.getName() // 'kyrie'

当getName()被调用时,它的落脚点指向person对象,当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象,因此调用getName()时this被绑定到person,因此this.name跟person.name是一样的

常见问题:隐式丢失?

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
var getName2 = person.getName() // 函数别名
var name = 'wen' // name是全局对象的属性
getName2() // 'wen' 这里拿到的是全局对象的name

解释:虽然getName2是person.getName的一个函数引用,但它引用的getName函数的本身,因此getName2()调用时不带任何修饰符,使用的是默认绑定,因此this绑定了全局对象

  • 显式绑定

使用call() / apply() / bind() 指定this的绑定对象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie'
}
getName.call(person) // 'kyrie'
getName.apply(person) // 'kyrie'

通过getName.call()/ getName.apply() 调用强制把它的this绑定到person上。

  • new绑定

所有函数都可以用new来调用,这种函数调用称为构造函数调用

重点:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”

使用new来调用函数,或者说发生构造函数调用时,会自动执行以下的四步操作:
  1. 创建(或者构造)一个新的对象
  2. 这个新对象会被执行[[原型]]连接(暂时忽略,属于原型内容,后面再介绍它)
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,则new表达式中的函数会自动返回这个新的对象
function setName(name) {
  this.name = name
}
var person = new setName('kyrie')
console.log(person.name) // 'kyrie'

使用new调用setName()时,会创建一个新对象并把这个新对象绑定到setName()调用的this上,并把这个对象返回

三、优先级

毫无疑问,默认绑定的优先级是四条规则中最低的,所以暂不考虑它。

  1. 隐式绑定和显式绑定哪个优先级高?
function getName() {
  console.log(this.name)
}
var p1 = {
  name: 'kyrie',
  getName: getName
}
var p2 = {
  name: 'wen',
  getName: getName
}
p1.getName() // 'kyrie'
p2.getName() // 'wen'
p1.getName.call(p2) // 'wen'
p2.getName.call(p1) // 'kyrie'

结果,显式绑定的优先级比隐式绑定高。

  1. 隐式绑定和new绑定哪个优先级高?
function setName(name) {
  this.name = name
}
var p1 = {
  setName: setName
}
var p2 = {}
p1.setName('kyrie')
console.log(p1.name) // 'kyrie'
p1.setName.call(p2, 'wen')
console.log(p2.name) // 'wen'
var p3 = new p1.setName('zbw')
console.log(p1.name) // 'kyrie'
console.log(p3.name) // 'zbw'

结果,new绑定的优先级比隐式绑定高

  1. 显式绑定和new绑定的哪个优先级高?
function setName(name) {
  this.name = name
}
var p1 = {}
// bind会返回一个新的函数
var setP1Name = setName.bind(p1)
setP1Name('kyrie')
console.log(p1.name) // 'kyrie'
var p2 = new setP1Name('wen')
console.log(p1.name) // 'kyrie'
console.log(p2.name) // 'wen'

结果,new绑定的优先级比显示绑定高

综上,优先级的正确排序:

从高到低: new > 显示 > 隐式 > 默认

  • 判断this的指向

现在我们可以根据优先级来判断函数在某个位置调用this的指向。

  1. 函数是否通过new来调用(new绑定)?如果是,则this指向新创建的对象
var p1 = new Person()
  1. 函数是否通过call/apply/bind调用(显式绑定)?如果是,则this指向第一个参数
var p1 = setName.call(p2)
  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是,则this指向该上下文对象
var p2 = p1.setName()
  1. 如果以上三个条件都不满足,则使用默认绑定。如果是在严格模式中,this指向undefined,否则指向全局对象。
var p1 = setName()

四、箭头函数的this

以上上提到判断this指向的四条规则包含所有正常的函数,除了ES6中的箭头函数

概括:箭头函数不像普通函数那样使用function关键字定义,而是用 “胖箭头” => 定义 。而且箭头函数并不适用以上的四条规则,它的this绑定完全是根据 外层作用域(函数或者全局) 来决定的。

function getName() {
  // 箭头函数的this指向外层作用域
  return (name) => {
    console.log(this.name)
  }
}
var p1 = {
  name: 'kyrie'
}
var p2 = {
  name: 'wen'
}
var func = getName.call(p1)
func.call(p2) // 'kyrie'

getName()内部创建的箭头函数会捕获调用时外层作用域(getName)的this,由于getName的this通过显示绑定到p1上,所以getName里创建的箭头函数也会指向p1,最重要的一点:箭头函数的this无法被修改(即使是优先级最高的new绑定也不行

总结

要判断一个运行中的函数的this绑定,需要找到该函数的调用位置(结合调用栈),接着根据优先级得出的四条规则来判断this的绑定对象。

  1. 函数由new调用?绑定到新创建的对象
  2. 由call/apply/bind调用?绑定到指定对象
  3. 由上下文对象调用?绑定到上下文对象
  4. 默认:严格模式下绑定到undefined,否则绑定到全局对象

ES6的箭头函数不适用以上四条规则,而是根据当前的词法作用域来决定this绑定,也就是说,箭头函数会继承外层函数调用的this绑定(无论绑定到什么),而且箭头函数的this绑定无法被修改

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 赞了回答 · 4月5日

解决怎么理解for循环中用let声明的迭代变量每次是新的变量?

按你的理解,还有点不对,虽然跨块,但你变的仍然是 i 本身,我认为应该是这样(仍然按你的思路)

{
    let i = 0;
    {
        let _i = i;
        a[_i] = function() {
            console.log(_i);
        };
    }
    i++;
}

再来给你看个神奇的示例,可以证明你的理解基本正确(虽然给的代码错了,但是我理解了你的意思)

for (var i = 0; i < 3; console.log("in for expression", i), i++) {
    let i;
    console.log("in for block", i);
}

clipboard.png

关注 17 回答 7

叫我欧文就好 赞了文章 · 4月3日

30分钟,让你彻底明白Promise原理

原文链接

前言

前一阵子记录了promise的一些常规用法,这篇文章再深入一个层次,来分析分析promise的这种规则机制是如何实现的。ps:本文适合已经对promise的用法有所了解的人阅读,如果对其用法还不是太了解,可以移步我的上一篇博文

本文的promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译

引子

为了让大家更容易理解,我们从一个场景开始讲解,让大家一步一步跟着思路思考,相信你一定会更容易看懂。

考虑下面一种获取用户id的请求处理

//例1
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(results) {
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
})

getUserId方法返回一个promise,可以通过它的then方法注册(注意注册这个词)在promise异步操作成功时执行的回调。这种执行方式,使得异步调用变得十分顺手。

原理剖析

那么类似这种功能的Promise怎么实现呢?其实按照上面一句话,实现一个最基础的雏形还是很easy的。

极简promise雏形

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

上述代码很简单,大致的逻辑是这样的:

  1. 调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

可以结合例1中的代码来看,首先new Promise时,传给promise的函数发送异步请求,接着调用promise对象的then属性,注册请求成功的回调函数,然后当异步请求发送成功时,调用resolve(results.id)方法, 该方法执行then方法注册的回调数组。

相信仔细的人应该可以看出来,then方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让then方法支持链式调用,其实也是很简单的:

this.then = function (onFulfilled) {
    callbacks.push(onFulfilled);
    return this;
};

see?只要简单一句话就可以实现类似下面的链式调用:

// 例2
getUserId().then(function (id) {
    // 一些处理
}).then(function (id) {
    // 一些处理
});

加入延时机制

细心的同学应该发现,上述代码可能还存在一个问题:如果在then方法注册回调之前,resolve函数就执行了,怎么办?比如promise内部的函数是同步函数:

// 例3
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // 一些处理
});

这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve执行之前,then方法已经注册完所有的回调。我们可以这样改造下resolve函数:

function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)
} 

上述代码的思路也很简单,就是通过setTimeout机制,将resolve中执行回调的逻辑放置到JS任务队列末尾,以保证在resolve执行时,then方法的回调函数已经注册完成.

但是,这样好像还存在一个问题,可以细想一下:如果Promise异步操作已经成功,这时,在异步操作成功之前注册的回调都会执行,但是在Promise异步操作成功这之后调用的then注册的回调就再也不会执行了,这显然不是我们想要的。

加入状态

恩,为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的pendingfulfilledrejected

Promises/A+规范中的2.1Promise States中明确规定了,pending可以转化为fulfilledrejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilledrejected状态只能由pending转化而来,两者之间不能互相转换。一图胜千言:

alt promise state

改进后的代码是这样的:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };

    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }

    fn(resolve);
}

上述代码的思路是这样的:resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行。

这里没有任何地方将state设为rejected,为了让大家聚焦在核心代码上,这个问题后面会有一小节专门加入。

链式Promise

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise,该如何解决?比如下面的例4

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}

这种场景相信用过promise的人都知道会有很多,那么类似这种就是所谓的链式Promise

链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise(后邻promise)。那么我们如何衔接当前promise和后邻promise呢?(这是这里的难点)。

其实也不是辣么难,只要在then方法里面return一个promise就好啦。Promises/A+规范中的2.2.7就是这么说哒(微笑脸)~

下面来看看这段暗藏玄机的then方法和resolve方法改造代码:


function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //如果then中没有传递任何东西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }

        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    
    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve);
}

我们结合例4的代码,分析下上面的代码逻辑,为了方便阅读,我把例4的代码贴在这里:

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}
  1. then方法中,创建并返回了新的Promise实例,这是串行Promise的基础,并且支持链式调用。
  2. handle方法是promise内部的方法。then方法传入的形参onFulfilled以及创建新Promise实例时传入的resolve均被push到当前promisecallbacks队列中,这是衔接当前promise和后邻promise的关键所在(这里一定要好好的分析下handle的作用)。
  3. getUserId生成的promise(简称getUserId promise)异步操作成功,执行其内部方法resolve,传入的参数正是异步操作的结果id
  4. 调用handle方法处理callbacks队列中的回调:getUserJobById方法,生成新的promisegetUserJobById promise
  5. 执行之前由getUserId promisethen方法生成的新promise(称为bridge promise)的resolve方法,传入参数为getUserJobById promise。这种情况下,会将该resolve方法传入getUserJobById promisethen方法中,并直接返回。
  6. getUserJobById promise异步操作成功时,执行其callbacks中的回调:getUserId bridge promise中的resolve方法
  7. 最后执行getUserId bridge promise的后邻promisecallbacks中的回调。

更直白的可以看下面的图,一图胜千言(都是根据自己的理解画出来的,如有不对欢迎指正):

alt promise analysis

失败处理

在异步操作失败时,标记其状态为rejected,并执行注册的失败回调:

//例5
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(error, results) {
            if (error) {
                reject(error);
            }
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
}, function(error) {
    console.log(error)
})

有了之前处理fulfilled状态的经验,支持错误处理变得很容易,只需要在注册回调、处理状态变更上都要加入新的逻辑:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function (resolve, reject) {
            handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }

        var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
            ret;
        if (cb === null) {
            cb = state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(value);
            return;
        }
        ret = cb(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve, reject);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        execute();
    }

    function reject(reason) {
        state = 'rejected';
        value = reason;
        execute();
    }

    function execute() {
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve, reject);
}

上述代码增加了新的reject方法,供异步操作失败时调用,同时抽出了resolvereject共用的部分,形成execute方法。

错误冒泡是上述代码已经支持,且非常实用的一个特性。在handle中发现没有指定异步操作失败的回调时,会直接将bridge promise(then函数返回的promise,后同)设为rejected状态,如此达成执行后续失败回调的效果。这有利于简化串行Promise的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:

//例6
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 处理job
    }, function (error) {
        // getUserId或者getUerJobById时出现的错误
        console.log(error);
    });

异常处理

细心的同学会想到:如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用try-catch捕获错误,并将bridge promise设为rejected状态。handle方法改造如下:

function handle(callback) {
    if (state === 'pending') {
        callbacks.push(callback);
        return;
    }

    var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
        ret;
    if (cb === null) {
        cb = state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(value);
        return;
    }
    try {
        ret = cb(value);
        callback.resolve(ret);
    } catch (e) {
        callback.reject(e);
    } 
}

如果在异步操作中,多次执行resolve或者reject会重复处理后续回调,可以通过内置一个标志位解决。

总结

刚开始看promise源码的时候总不能很好的理解then和resolve函数的运行机理,但是如果你静下心来,反过来根据执行promise时的逻辑来推演,就不难理解了。这里一定要注意的点是:promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的,理清了这层,再来分析源码会省力的多。

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

  1. 通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。
  2. 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

参考文献

深入理解 Promise
JavaScript Promises ... In Wicked Detail

查看原文

赞 185 收藏 250 评论 38

叫我欧文就好 发布了文章 · 4月2日

20个常用的CSS知识点

1. 如何隐藏滚动条

// chrome 和Safari
*::-webkit-scrollbar { width: 0 !important }
// IE 10+
* { -ms-overflow-style: none; }
// Firefox
* { overflow: -moz-scrollbars-none; }

2. 修改滚动条样式

*::-webkit-scrollbar {
  /*定义纵向滚动条宽度*/
  width: 12px!important;
  /*定义横向滚动条高度*/
  height: 12px!important; 
}
*::-webkit-scrollbar-thumb {
  /*滚动条内部滑块*/
  border-radius: 16px;
  background-color:#c1c1c1;
  transition: background-color 0.3s;
  &:hover {
    /*鼠标悬停滚动条内部滑块*/
    background: #bbb;
  }
 }
*::-webkit-scrollbar-track {
  /*滚动条内部轨道*/
  background: #f1f1f1;
}

3. 修改input框placeholder的颜色

input::input-placeholder{
    color:red;
}

4. 按钮不可点击的样式

cursor: not-allowed

5. CSS鼠标指针事件:阻止任何JS事件

.disabled { pointer-events: none; }

6. 文字超出强制n行 超出部分用省略号代替

div {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: n; // 行数
  -webkit-box-orient: vertical;
}

7. 修改字体间距

letter-spacing: 8px

8. 谷歌浏览器控制台提示/deep/将要被移除

<style scoped lang="less">
// 采用的less的转义和变量插值
@deep: ~'>>>';
.select {
     @{deep} .ivu-card-body {
        width: 100%;
      }
    }
</style>

9. animate动画停在某个关键帧

animation-fill-mode: forwards;

10. 盒子阴影

box-shadow: 0 2px 2px rgba(10,16,20,.24),0 0 2px rgba(10,16,20,.12);
transition: box-shadow .5s;

11.使图片覆盖它的整个容器

img {
  object-fit: cover;
}

12. 表格中td的内容自动换行

<table style="word-break:break-all; word-wrap:break-all;">

13. 浏览器打印功能 图片失效

body {
    -webkit-print-color-adjust: exact;
}

14. 背景图像完美适配视口

body {
  background-image: url('xxx');
  background-repeat: no-repeat;
  background-position: center;
  background-attachment: fixed;
  background-size: cover;
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
}

15. 如何使用多个背景图片

body {
  background-image: url('xxx'), url('xxx');
  background-position: center, top;
  background-repeat: repeat, no-repeat;
  background-size: contain, cover;
}

16. 如何给背景图叠加渐变

body {
  background-image: 
    linear-gradient(
      4deg, 
      rgba(38,8,31,0.75) 30%, 
      rgba(213,49,127,0.3) 45%, 
      rgba(232,120,12,0.3) 100%),
      url("xxx");
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: center
}

17. 如何将背景图设为文本颜色

<body>
    <h1>hello!!!</h1>
</body>

body {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  width: 100%;
  text-align: center;
  min-height: 100vh;
  font-size: 120px;
}

h1 {
   background-image: url("xxx");
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

18. 如何获取和设置盒子的宽高

//第一种
dom.style.width/height //只能获取内联样式的元素宽高
//第二种
dom.currentStyle.width/height //只有IE浏览器支持
//第三种
dom.getComputedStyle(Dom).width/height //只有浏览器渲染后才能获取 兼容好
//第四种
dom.getBoundingClientRect().width/height //计算一个元素的绝对位置(相对于视窗左上角) 能拿到元素的left、right、width、height

19. 如何让图片垂直居中

img {
  vertical-align: middle;
  margin-top: -xpx;
}

20. 消除图片自带的间距

img {
  display: block;
}

// 或者 父盒子

div {
  font-size: 0;
}
查看原文

赞 31 收藏 22 评论 2

叫我欧文就好 发布了文章 · 3月30日

你一定踩过的微信H5的坑

1. IOS的input光标高度问题

问题:IOS的input框高度会默认占满父盒子的高度,导致光标也会撑满整个input框
解决:父盒子使用上下padding撑开,不使用行高(line-height)居中

2. IOS微信h5页面上下滑动时卡顿,页面缺失

问题:如果页面高度超出了一屏,滑动页面就会出现卡顿,有时会有页面显示不全的情况
分析:苹果微信浏览器内核使用自带的safari,需要overflow-scrolling开启回弹效果
解决:*{ -webkit-overflow-scrolling: touch }
注意:如果页面中有绝对定位的节点,该节点的显示会错乱

3.IOS键盘唤起,键盘收起以后页面不归位

问题: 输入内容时,键盘弹出,页面内容整体上移,但是键盘收起,页面内容无法归位
分析:input聚焦时,弹出的软键盘占位,失去焦点时软键盘消失,但还是占位的,导致input框不能再次输入
解决: 监听input的blur方法,失去焦点时,用js滚动页面

4. Android弹出的软键盘遮盖文本框

问题:安卓微信h5页面太长时弹出软键盘后会挡住input输入框
解决:给input和textarea标签添加focus事件,聚焦时延时滚动

5. IOS去除input默认阴影失效

问题:一般去除input阴影我们可以使用outline: none; 也可以使用border: 0;但是我们有时候需要border的情况下,只能使用outline: none; 安卓可以生效,IOS发现阴影还在?
解决:使用-webkit-appearance: none; 即可去除input默认阴影
查看原文

赞 5 收藏 5 评论 0

叫我欧文就好 发布了文章 · 3月23日

JS常用的15个操作数组的方法

JS中那些常用的数组方法:

数组的增删改查:

**push(插入的元素<可传多个>)
作用:往数组的尾部插入元素**

let arr = [1, 2, 3]
arr.push(4, 5)
let res = arr.push(4, 5) //返回值为插入元素后数组的长度(res:5)
console.log(arr) // [1, 2, 3, 4, 5]

**unshift(插入的元素<可传多个>)
作用:往数组的头部插入元素**

let arr = [1, 2, 3]
arr.unshift(-2, -1, 0)
let res = arr.unshift(-2, -1, 0) //返回值为插入元素后数组的长度(res:6)
console.log(arr) // [-2, -1, 0, 1, 2, 3]

**pop()
作用:删除数组的最后一个元素**

let arr = [1, 2, 3]
arr.pop()
let res = arr.pop() // 返回值为删除的元素(res: [3])
console.log(arr) // [1, 2]

**shift()
作用:删除数组的第一个元素**

let arr = [1, 2, 3]
arr.shift()
let res = arr.shift() // 返回值为删除的元素(res: [1])
console.log(arr) // [2, 3]

**slice(开始元素的下标,结束元素的下标)
作用:截取数组特定元素(不改变原数组,可用作浅拷贝)**

// 浅拷贝
let arr = [1, 2, 3]
let newArr = arr.slice() // 返回值为截取后的新数组
console.log(newArr) // [1, 2, 3] 

//截取元素
let arr = [1, 2, 3]
let newArr = arr.slice(0, 2) // 从第下标为0的元素开始截取,到元素下标为2的元素(注:但不包括元素下标为2的元素)
console.log(newArr) // [1, 2]
console.log(arr) // [1, 2, 3] // 不改变原数组

**splice(开始元素的下标,删除的元素个数,添加的元素...)
作用:可新增元素或者删除元素**

// 新增元素
let arr = [1, 3, 5]
arr.splice(1, 0, 2) // 在元素下标为1的元素后面新增一个元素2
let res = arr.splice(1, 0, 2) // 由于此处为新增元素,返回值为空数组[](res: []) 
console.log(arr) // [1, 2, 3, 5]

//删除元素
let arr = [1, 2, 3]
arr.splice(1, 1) // 在元素下标为1的元素后面删除一个元素
let res = arr.splice(1, 1) // 返回值为删除的元素(res: [2])
console.log(arr) // [1, 3]

**join(传入分割每个元素的标志)
作用:数组转换为字符串**

let arr = ['zbw', 'cyl']
let res = arr.join(',') 
console.log(res) // 'zbw, cyl'

let res = arr.join('-')
console.log(res) // 'zbw-cyl'

**concat()
作用:连接多个数组或值**

let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]

let res = arr1.concat(arr2)
console.log(res) // [1, 2, 3, 4, 5, 6]

let res = arr1.concat(4, arr2, 7, 8)
console.log(res) // [1, 2, 3, 4, 4, 5, 6, 7, 8]

操作数组的高阶函数

高阶函数:把函数作为参数传入或把函数作为返回值的函数为高阶函数

**map()
作用:操作数组,返回经过处理后的数组**

let arr = [1, 2, 3, 4]
let res = arr.map(item => item * 2)
console.log(res) // [2, 4, 6, 8]

**filter()
作用:过滤数组,返回操作表达式为true的数组元素**

let arr = [1, 2, 3, 4]
let res = arr.filter(item => item >= 2)
console.log(res) // [2, 3, 4]

**reduce(pre<初始值>, cur<当前项>, arr<数组本身>, <初始值>)
作用:求和,数组转对象**

// 求和
let arr = [1, 2, 3, 4]
let res = arr.reduce((pre, cur) => {
  return pre += cur
}, 0) // 这里是求和 所以初始值为0
console.log(res) // 10

// 数组转对象
let arr = [
  { id: 1, name: 'zbw', age: 18 },
  { id: 2, name: 'cyl', age: 18 },
]

let obj = arr.reduce((pre, cur) => {
   pre[cur.id] = cur.name
   return pre
}, {}) // 这里是数组转对象 所以初始值为空对象{}

console.log(obj) // {1: 'zbw', 2: 'cyl'} 

**forEach()
作用:跟map()作用类似,区别在于forEach()会改变原数组,且没有返回值**

let arr = [
  { name: 'zbw', age: 22 },
  { name: 'cyl', age: 18 },
]

arr.forEach(item => item.age = 20)
console.log(arr) // [{ name: 'zbw', age: 20 },{ name: 'cyl', age:20 }]

**some()
作用:判断数组中是否存在符合条件表达式的条件,如果有一项符合返回true,否则返回false**

let arr = [1, 2, 3, 4]
let bool = arr.some(item => item > 2)
console.log(bool) // true 因为存在大于2的元素

let bool = arr.some(item => item > 5)
console.log(bool) // false 因为不存在大于5的元素

**every()
作用:判断数组中是否全部元素都符合表达式的条件,全部都符合返回true,否则返回false**

let arr = [1, 2, 3, 4]
let bool = arr.every(item => item > 0)
console.log(bool) // true 因为所有元素都大于0

let bool = arr.every(item => item > 1)
console.log(bool) // false 因为存在不大于1的元素

**find()
作用:找到数组中符合条件的第一个元素**

let arr = [{ id: 1, name: 'zbw', age: 18 }, { id: 2, name: 'cyl', age: 18 }]

let res = arr.find(item => item.age === 18)
console.log(res) // { id: 1, name: 'zbw', age: 18 }

let res = arr.find(item => item.id === 2)
console.log(res) // { id: 2, name: 'cyl', age: 18 }
查看原文

赞 19 收藏 13 评论 2

叫我欧文就好 发布了文章 · 3月22日

useEffect进阶指南(下)

React的内部更新机制?

`<h1 class="name">我的名字是:Kyrie</h1>
<h1 class="name">我的名字是:IRVING</h1>
`

React也跟vue有着Diff算法,它每次render只会更新与上一次渲染不同的节点和内容,所以上面的例子,React只需要这样做:

h1.innerText = Kyrie -> h1.innerText = IRVING

那useEffect呢?

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  // 当我们把count作为依赖传入时,count改变就会重新执行
  useEffect(() => {
    console.log('count的值为:', count)
  }, [count])
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

所以useEffect的更新机制完成依靠我们传入的依赖,只要在useEffect里使用到的状态值都必须在依赖中声明,让React内部进行依赖更新。

所以,当依赖的状态变得多起来的时候,难免会让我们在性能方面有所担心。

useEffect传入依赖的正确方式?

现在有个需求:书写一个函数,每秒自动让count+1

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [])
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

上面的例子,我们把[]作为useEffect的依赖,就是说effect只会在组件挂载后执行一次。定时器只需要设定一个,每秒setInterval后帮我们把count + 1对吧,这不就能实现需求了吗

看起来貌似没什么问题,但运行起来发现count只自增了一次就停住了,也就是useEffect只执行了一次,what???

原因很简单,我们把[]作为依赖,React内部就会认为effect函数内没有依赖任何值,所以当useEffect第一次执行时,setCount(0 + 1) 此时count = 1,然后1s后,setCount(1 + 1),count = 2? 不,React内部会自动忽略第二次及以后的每次更新,因为我们把[]作为依赖,就代表了effect只会执行一次,第二次开始,count一直都是1,这就导致了count变为1后就不增加了。

那好,我们把count传入作为依赖

`useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [count])
`

运行之后发现,问题确实解决了,count成功的每秒自动自增,但这其实不是最好的解决方案,因为我们知道每当count值改变,就会触发render,每次都会生成一个新的useEffect,然后执行,重新生成一个定时器,虽然目的达到了,但这显然不是最优解,我们最好能避免每次都生成一个新的定时器,因为这样我们的定时器将毫无意义...

`useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [])
`

可以看到,我们把count从依赖中取出,然后在setCount(count => count + 1),这样做得目的是为了告诉React,我们只需要count+1,并不需要读取它的值,因为React内部肯定是知道当前count的值,这样effect内部就不依赖count了,useEffect只需执行一次即可,这也是setState的函数写法,函数的参数就是最新的状态值,如果不太了解这种写法的朋友可以去查一下资料,这里就不再过多阐述了...

useEffect配合useReducer使用?

这就是useEffect究极使用技巧,用法及其广泛...还是使用上面的例子,让我们用useReducer改写一下

`function Demo() {
  const initState = { count: 0 }
  const [state, dispatch] = useReducer(reducer, initState)
  
  function reducer(state, action) {
    switch(action.type) {
      case 'increment' :
        return {count: state.count + 1}
      default: 
        throw new Error('type不存在...')
    } 
  }
  
  useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      dispatch({type: 'increment'})
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [dispatch]) // 这里我们依赖了dispatch 其实可以省略
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

大家可能会问我,这种写法的好处是什么?其实,我们利用useReducer通过action来描述行为,实现状态和行为的分离,在多个依赖的时候这种写法的优势就能很好的体现出来。

还有最后一个疑点:为什么dispatch可以省略?其实上面的例子,即使我们把dispatch从依赖中取出,也能正常运行,effect也只会执行一次。这么神奇?effect是怎么知道我们的行为是什么?其实,React内部会帮我们记住dispatch的各种行为(action),且能拿到最新的count,这一系列操作是React内部发生的,并不需要放在effect内。

useMemo和useCallback(性能优化)?

useCallback(缓存函数)

`const memoizedSetCount = useCallback(
  () => {
    setCount(count + 1)
  },
  [count],
);
`

把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的缓存版本,该回调函数仅在某个依赖项改变时才会更新。

useMemo(缓存值:类似于Vue的计算属性)

`const memoizedCount = useMemo(() => {
  const doubleCount = count * 2
}, [count]);
`

把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算缓存值。这种优化有助于避免在每次渲染时都进行高开销的计算。

到这里,useEffect的进阶指南就介绍完了,要更好的运用React hooks 还得日常开发中的实战和积累,相信在往后的开发中,大家能更合理的使用hooks,也可以封装属于个人的hooks...大家加油,一起学习。
查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 3月21日

useEffect进阶指南(上)

  1. 每次渲染都有独立的状态(State)

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

每当用户点击一次按钮 都会重新触发render函数,每次render拿到的都是独立的状态

因为我们生命count的值时使用const,所以每次渲染拿到的count值是一个独立的常量。

  1. 每次渲染都有不同且独立的函数(Effect函数)

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  // 假设在1s内多次点击按钮 这里打印的count值是什么?
  useEffect(() => {
    setTimeout(() => {
      console.log(count) // 这里打印的会是当前这一次的count值,并不是最新的count值
    }, 1000)
  })
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

每次count值改变,都会触发render,组件重新渲染,所以每次都会生成对应的useEffect函数

而且我们发现每次打印count的值拿到的都是当前轮次的count值(并不是最新的count)

  1. useEffect到底是怎样拿到最新的状态值的?

我们知道每次渲染都会触发render,每次更新就会生成一个新的Effect函数,并且每一个Effect函数里面都有独立的State,且只能访问自己本次更新的State。

所以用上面的例子,得出的结论就是:count值其实不是在同一个Effect函数里面发生改变,而是每一次的组件更新,都会生成一个维护着本次更新的Effect函数,在这个最新的Effect函数里就可以访问到最新的count值。

  1. useEffect返回的函数是如何进行清理工作的?

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
 
  useEffect(() => {
    let timer = setTimeout(() => {
      console.log(count)
    }, 1000)
    
    // 清理工作
    return () => {
      clearTimeout(timer)
    }
  })
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

假设用户点击了两次次按钮 当第一次点击的时候 count + 1 = 1,然后执行clearTimout清除本次的定时器? 接着继续count + 1 = 2 然后执行clearTimeout清除本次的定时器?

正确的顺序应该是:当第一次点击 count + 1 = 1,然后clearTimeout会被延迟执行,等到第二次点击的时候 count + 1 = 2 再执行上一次的clearTimeout 然后以此类推...问题来了 不是说effect函数只能访问本次的State吗?那它怎么拿到上一次的clearTimeout并执行的?

其实很简单,就是React会帮你记住每次effect函数的State(包括清除函数),它确实是只能读取本次更新的State,只不过是延迟执行了(把清除函数的执行时机放在DOM渲染完成后,在下一次render触发之前)

剩下的内容下期见吧,我累了,现在要去吃麦当劳补充一下能量...

下期预告:useEffect的第二个参数详细解析及使用,在开发中合理使用依赖(避免死循环、性能优化...)

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 61 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-26
个人主页被 905 人浏览