浪里行舟

浪里行舟 查看完整档案

其它编辑HKU SPACE  |  前端 编辑前端工匠  |  前端 编辑 github.com/ljianshu/Blog 编辑
编辑

微信:frontJS,记得备注sf
公众号:前端工匠
文章首发地址:https://github.com/ljianshu/Blog(包括源代码和思维导图)

个人动态

浪里行舟 赞了文章 · 2020-08-12

Typescript 设计模式之工厂方法

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。此外,在日常生活中,我们也离不开大大小小的系统,这些系统是由不同的组件对象构成。

而作为一名 Web 软件开发工程师,在软件系统的设计与开发过程中,我们可以利用设计模式来提高代码的可重用性、可扩展性和可维护性。在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。

工厂模式可以分为三类:

  • 简单工厂模式(Simple Factory Pattern)
  • 工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)

本文阿宝哥将介绍简单工厂模式与工厂方法模式,而抽象工厂模式将在后续的文章中介绍,下面我们先来介绍简单工厂模式。

一、简单工厂模式

1.1 简单工厂模式简介

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

相信对于刚接触简单工厂模式的小伙伴来说,看到以上的描述可能会觉得有点抽象。这里为了让小伙伴更好地理解简单工厂模式,阿宝哥以用户买车为例,来介绍一下 BMW 工厂如何使用简单工厂模式来生产?。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥将介绍如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

1.2 简单工厂模式实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 创建 BMWFactory 工厂类
class BMWFactory {
  public static produceBMW(model: "730" | "840"): BMW {
    if (model === "730") {
      return new BMW730();
    } else {
      return new BMW840();
    }
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");

bmw730.run();
bmw840.run();

以上代码运行后的输出结果为:

BMW730 发动咯 
BMW840 发动咯 

通过观察以上的输出结果,我们可以知道我们的 BMWFactory 已经可以正常工作了。在 BMWFactory 类中,阿宝哥定义了一个 produceBMW() 方法,该方法会根据传入的模型参数来创建不同型号的车子。

看完简单工厂模式实战的示例,你是不是觉得简单工厂模式还是挺好理解的。那么什么场景下使用简单工厂模式呢?要回答这个问题我们需要来了解一下简单工厂的优缺点。

1.3 简单工厂模式优缺点

1.3.1 优点
  • 将创建实例与使用实例的任务分开,使用者不必关心对象是如何创建的,实现了系统的解耦;
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
1.3.2 缺点
  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,也有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

了解完简单工厂的优缺点,我们来看一下它的应用场景。

1.4 简单工厂模式应用场景

在满足以下条件下可以考虑使用简单工厂模式:

  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

介绍完简单工厂模式,接下来我们来介绍本文的主角 ”工厂方法模式“

二、工厂方法模式

2.1 工厂方法简介

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥来介绍如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

2.2 工厂方法实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 定义 BMWFactory 接口
interface BMWFactory {
  produceBMW(): BMW;
}
  1. 创建 BMW730Factory 类
class BMW730Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW730();
  }
}
  1. 创建 BMW840Factory 类
class BMW840Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW840();
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();

const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();

bmw730.run();
bmw840.run();

通过观察以上的输出结果,我们可以知道我们的 BMW730Factory 和 BMW840Factory 工厂已经可以正常工作了。相比前面的简单工厂模式,工厂方法模式通过创建不同的工厂来生产不同的产品。下面我们来看一下工厂方法有哪些优缺点。

2.3 工厂方法优缺点

2.3.1 优点
  • 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,更加符合 “开闭原则”。而简单工厂模式需要修改工厂类的判断逻辑。
  • 符合单一职责的原则,即每个具体工厂类只负责创建对应的产品。而简单工厂模式中的工厂类存在一定的逻辑判断。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
2.3.2 缺点
  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 一个具体工厂只能创建一种具体产品。

最后我们来简单介绍一下工厂方法的应用场景。

2.4 工厂方法应用场景

  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

三、参考资源

四、推荐阅读

查看原文

赞 18 收藏 9 评论 0

浪里行舟 赞了文章 · 2020-07-24

机器学习算法入门指南(全)

前言

机器学习 作为人工智能领域的核心组成,是计算机程序学习数据经验以优化自身算法,并产生相应的“智能化的”建议与决策的过程。

一个经典的机器学习的定义是:

A computer program is said to learn from experience E with respect to some class of tasks T and performance measure P,
if its performance at tasks in T, as measured by P, improves with experience E.

一、机器学习概论

机器学习是关于计算机基于数据分布构建出概率统计模型,并运用模型对数据进行分析与预测的方法。按照学习数据分布的方式的不同,主要可以分为监督学习和非监督学习

1.1 监督学习

从有标注的数据(x为变量特征空间, y为标签)中,通过选择的模型及确定的学习策略,再用合适算法计算后学习到最优模型,并用模型预测的过程。模型预测结果Y的取值有限的或者无限的,可分为分类模型或者回归模型

1.2 非监督学习:

从无标注的数据(x为变量特征空间),通过选择的模型及确定的学习策略,再用合适算法计算后学习到最优模型,并用模型发现数据的统计规律或者内在结构。按照应用场景,可以分为聚类,降维和关联分析等模型;

二、机器学习建模流程

2.1 明确业务问题

明确业务问题是机器学习的先决条件,这里需要抽象出现实业务问题的解决方案:需要学习什么样的数据作为输入,目标是得到什么样的模型做决策作为输出。

(如一个简单的新闻分类场景就是学习已有的新闻及其类别标签数据,得到一个分类模型,通过模型对每天新的新闻做类别预测,以归类到每个新闻频道。)

2.2 数据选择:收集及输入数据

数据决定了机器学习结果的上限,而算法只是尽可能逼近这个上限。
意味着数据的质量决定了模型的最终效果,在实际的工业应用中,算法通常占了很小的一部分,大部分工程师的工作都是在找数据、提炼数据、分析数据。数据选择需要关注的是:

① 数据的代表性:无代表性的数据可能会导致模型的过拟合,对训练数据之外的新数据无识别能力;

② 数据时间范围:监督学习的特征变量X及标签Y如与时间先后有关,则需要明确数据时间窗口,否则可能会导致数据泄漏,即存在和利用因果颠倒的特征变量的现象。(如预测明天会不会下雨,但是训练数据引入明天温湿度情况);

③ 数据业务范围:明确与任务相关的数据表范围,避免缺失代表性数据或引入大量无关数据作为噪音;

2.3 特征工程:数据预处理及特征提取

特征工程就是将原始数据加工转化为模型有用的特征,技术手段一般可分为:

数据预处理:特征表示,缺失值/异常值处理,数据离散化,数据标准化等;
特征提取:特征衍生,特征选择,特征降维等;

  • 特征表示

    数据需要转换为计算机能够处理的数值形式。如果数据是图片数据需要转换为RGB三维矩阵的表示。

图片
字符类的数据可以用多维数组表示,有Onehot独热编码表示、word2vetor分布式表示及bert动态编码等;
自然语言

  • 异常值处理

    收集的数据由于人为或者自然因素可能引入了异常值(噪音),这会对模型学习进行干扰。

    通常需要对人为引起的异常值进行处理,通过业务判断和技术手段(python、正则式匹配、pandas数据处理及matplotlib可视化等数据分析处理技术)筛选异常的信息,并结合业务情况删除或者替换数值。

  • 缺失值处理

    数据缺失的部分,通过结合业务进行填充数值、不做处理或者删除。
    根据缺失率情况及处理方式分为以下情况:

    ① 缺失率较高,并结合业务可以直接删除该特征变量。经验上可以新增一个bool类型的变量特征记录该字段的缺失情况,缺失记为1,非缺失记为0;

    ② 缺失率较低,结合业务可使用一些缺失值填充手段,如pandas的fillna方法、训练随机森林模型预测缺失值填充;

    ③ 不做处理:部分模型如随机森林、xgboost、lightgbm能够处理数据缺失的情况,不需要对缺失数据做任何的处理。

  • 数据离散化

    数据离散化能减小算法的时间和空间开销(不同算法情况不一),并可以使特征更有业务解释性。

    离散化是将连续的数据进行分段,使其变为一段段离散化的区间,分段的原则有等距离、等频率等方法。

  • 数据标准化

    数据各个特征变量的量纲差异很大,可以使用数据标准化消除不同分量量纲差异的影响,加速模型收敛的效率。常用的方法有:

    ① min-max 标准化:

    将数值范围缩放到(0,1),但没有改变数据分布。max为样本最大值,min为样本最小值。

    ② z-score 标准化:

    将数值范围缩放到0附近, 经过处理的数据符合标准正态分布。u是平均值,σ是标准差。

  • 特征衍生

基础特征对样本信息的表述有限,可通过特征衍生出新含义的特征进行补充。特征衍生是对现有基础特征的含义进行某种处理(组合/转换之类),常用方法如:

① 结合业务的理解做衍生,比如通过12个月工资可以加工出:平均月工资,薪资变化值,是否发工资 等等;

② 使用特征衍生工具:如feature tools等技术;

  • 特征选择

特征选择筛选出显著特征、摒弃非显著特征。特征选择方法一般分为三类:


① 过滤法:按照特征的发散性或者相关性指标对各个特征进行评分后选择,如方差验证、相关系数、IV值、卡方检验及信息增益等方法。

② 包装法:每次选择部分特征迭代训练模型,根据模型预测效果评分选择特征的去留。

③ 嵌入法:使用某些模型进行训练,得到各个特征的权值系数,根据权值系数从大到小来选择特征,如XGBOOST特征重要性选择特征。

  • 特征降维

如果特征选择后的特征数目仍太多,这种情形下经常会有数据样本稀疏、距离计算困难的问题(称为 “维数灾难”),可以通过特征降维解决。

常用的降维方法有:主成分分析法(PCA),
线性判别分析法(LDA)等。

2.4 模型训练

模型训练是选择模型学习数据分布的过程。这过程还需要依据训练结果调整算法的(超)参数,使得结果变得更加优良。

  • 2.4.1 数据集划分

    训练模型前,一般会把数据集分为训练集和测试集,并可再对训练集再细分为训练集和验证集,从而对模型的泛化能力进行评估。

    ① 训练集(training set):用于运行学习算法。

    ② 开发验证集(development set)用于调整参数,选择特征以及对算法其它优化。常用的验证方式有交叉验证Cross-validation,留一法等;

    ③ 测试集(test set)用于评估算法的性能,但不会据此改变学习算法或参数。

  • 2.4.2 模型选择

    常见的机器学习算法如下:

模型选择取决于数据情况和预测目标。可以训练多个模型,根据实际的效果选择表现较好的模型或者模型融合。

模型选择

  • 2.4.3 模型训练

    训练过程可以通过调参进行优化,调参的过程是一种基于数据集、模型和训练过程细节的实证过程。
    超参数优化需要基于对算法的原理的理解和经验,此外还有自动调参技术:网格搜索、随机搜索及贝叶斯优化等。

2.5 模型评估

模型评估的标准:模型学习的目的使学到的模型对新数据能有很好的预测能力(泛化能力)。现实中通常由训练误差及测试误差评估模型的训练数据学习程度及泛化能力。

  • 2.5.1 评估指标

    ① 评估分类模型:
    常用的评估标准有查准率P、查全率R、两者调和平均F1-score 等,并由混淆矩阵的统计相应的个数计算出数值:

    混淆矩阵

    查准率是指分类器分类正确的正样本(TP)的个数占该分类器所有预测为正样本个数(TP+FP)的比例;

    查全率是指分类器分类正确的正样本个数(TP)占所有的正样本个数(TP+FN)的比例。

    F1-score是查准率P、查全率R的调和平均:

    ② 评估回归模型:
    常用的评估指标有RMSE均方根误差 等。反馈的是预测数值与实际值的拟合情况。

    ③ 评估聚类模型:可分为两类方式,一类将聚类结果与某个“参考模型”的结果进行比较,称为“外部指标”(external index):如兰德指数,FM指数 等;
    另一类是直接考察聚类结果而不利用任何参考模型,称为“内部指标”(internal index):如紧凑度、分离度 等。

  • 2.5.2 模型评估及优化

    根据训练集及测试集的指标表现,分析原因并对模型进行优化,常用的方法有:

2.6 模型决策

决策是机器学习最终目的,对模型预测信息加以分析解释,并应用于实际的工作领域。

需要注意的是工程上是结果导向,模型在线上运行的效果直接决定模型的成败,不仅仅包括其准确程度、误差等情况,还包括其运行的速度(时间复杂度)、资源消耗程度(空间复杂度)、稳定性的综合考虑。

三、 参考文献

《机器学习》--周志华

《统计学习方法》--李航

Google machine-learning


关于作者

欢迎关注“算法进阶”公众号,这里定期推送机器学习、深度学习等技术好文。欢迎一起学习交流进步!

查看原文

赞 1 收藏 0 评论 0

浪里行舟 赞了文章 · 2020-07-24

程序员说模型过拟合的时候,说的是什么?

前言

机器学习中,模型的拟合效果意味着对新数据的预测能力的强弱(泛化能力)。而程序员评价模型拟合效果时,常说“过拟合”及“欠拟合”,那究竟什么是过/欠拟合呢?什么指标可以判断拟合效果?以及如何优化?

欠拟合&过拟合的概念

注:在机器学习或人工神经网络中,过拟合与欠拟合有时也被称为“过训练”和“欠训练”,本文不做术语差异上的专业区分。

欠拟合是指相较于数据而言,模型参数过少或者模型结构过于简单,以至于无法学习到数据中的规律。

过拟合是指模型只过分地匹配特定数据集,以至于对其他数据无良好地拟合及预测。其本质是模型从训练数据中学习到了统计噪声,由此分析影响因素有:

  1. 训练数据过于局部片面,模型学习到与真实数据不相符的噪音;
  2. 训练数据的噪音数据干扰过大,大到模型过分记住了噪音特征,反而忽略了真实的输入输出间的关系;
  3. 过于复杂的参数或结构模型(相较于数据而言),在可以“完美地”适应数据的同时,也学习更多的噪声;


如上图以虚线的区分效果来形象表示模型的拟合效果。Underfitting代表欠拟合模型,Overfitting代表过拟合模型,Good代表拟合良好的模型。

拟合效果的评估方式


现实中通常由训练误差及测试误差(泛化误差)评估模型的学习程度及泛化能力。

欠拟合时训练误差和测试误差在均较高,随着训练时间及模型复杂度的增加而下降。在到达一个拟合最优的临界点之后,训练误差下降,测试误差上升,这个时候就进入了过拟合区域。它们的误差情况差异如下表所示:

拟合效果的深入分析

对于拟合效果除了通过训练、测试的误差估计其泛化误差及判断拟合程度之外,我们往往还希望了解它为什么具有这样的泛化性能。统计学常用“偏差-方差分解”(bias-variance decomposition)来分析模型的泛化性能:其泛化误差为偏差、方差与噪声之和。

噪声(ε) 表达了在当前任务上任何学习算法所能达到的泛化误差的下界,即刻画了学习问题本身(客观存在)的难度。

偏差(Bias) 是指用所有可能的训练数据集训练出的所有模型的输出值与真实值之间的差异,刻画了模型的拟合能力。偏差较小即模型预测准确度越高,表示模型拟合程度越高。

方差(Variance) 是指不同的训练数据集训练出的模型对同预测样本输出值之间的差异,刻画了训练数据扰动所造成的影响。方差较大即模型预测值越不稳定,表示模型(过)拟合程度越高,受训练集扰动影响越大。

如下用靶心图形象表示不同方差及偏差下模型预测的差异:

偏差越小,模型预测值与目标值差异越小,预测值越准确;

方差越小,不同的训练数据集训练出的模型对同预测样本预测值差异越小,预测值越集中;

“偏差-方差分解” 说明,模型拟合过程的泛化性能是由学习算法的能力、数据的充分性以及学习任务本身的难度所共同决定的。

当模型欠拟合时:模型准确度不高(高偏差),受训练数据的扰动影响较小(低方差),其泛化误差大主要由高的偏差导致。

当模型过拟合时:模型准确度较高(低偏差),模型容易学习到训练数据扰动的噪音(高方差),其泛化误差大由高的方差导致。

拟合效果的优化方法

可结合交叉验证评估模型的表现,可较准确判断拟合程度。在优化欠/过拟合现象上,主要有如下方法:

模型欠拟合

  • 增加特征维度:如增加新业务层面特征,特征衍生来增大特征假设空间,以增加特征的表达能力;
  • 增加模型复杂度:如增加模型训练时间、结构复杂度,尝试复杂非线性模型等,以增加模型的学习能力;

模型过拟合

  • 增加数据: 如寻找更多训练数据样本,数据增强等,以减少对局部数据的依赖;
  • 特征选择:通过筛选掉冗余特征,减少冗余特征产生噪声干扰;
  • 降低模型复杂度

    1. 简化模型结构:如减少神经网络深度,决策树的数目等。
    2. L1/L2正则化:通过在代价函数加入正则项(权重整体的值)作为惩罚项,以限制模型学习的权重。

    (拓展:通过在神经网络的网络层引入随机的噪声,也有类似L2正则化的效果)
  1. 提前停止(Early stopping):通过迭代次数截断的方法,以限制模型学习的权重。

  • 结合多个模型

    1. 集成学习:如随机森林(bagging法)通过训练样本有放回抽样和随机特征选择训练多个模型,综合决策,可以减少对部分数据/模型的依赖,减少方差及误差;
    2. Dropout: 神经网络的前向传播过程中每次按一定的概率(比如50%)随机地“暂停”一部分神经元的作用。这类似于多种网络结构模型bagging取平均决策,且模型不会依赖某些局部的特征,从而有更好泛化性能。

关于作者

欢迎关注“算法进阶”公众号,这里定期推送机器学习、深度学习等技术好文。欢迎一起学习交流进步!

查看原文

赞 1 收藏 1 评论 1

浪里行舟 关注了用户 · 2020-07-23

马嘉伦 @majialun

Flutter资深开发者,有多个flutter开源库获得高分评价,码云最有价值开源项目 flutter-p2p-engine代码主要提供人,长期维护者。博客作者,最高阅读量50k+,文章曾被谷歌开发者官方公众号刊登并赠送礼品。

关注 803

浪里行舟 赞了文章 · 2020-07-21

实战技巧,Vue原来还可以这样写

两只黄鹂鸣翠柳,一堆bug上西天。

每天上班写着重复的代码,当一个cv仔,忙到八九点,工作效率低,感觉自己没有任何提升。如何能更快的完成手头的工作,今天小编整理了一些新的Vue使用技巧。你们先加班,我先下班陪女神去逛街了。

本文首发于公众号【前端有的玩】,关注我,我们一起玩前端,每天都有不一样的干货知识点等着你哦

hookEvent,原来可以这样监听组件生命周期

1. 内部监听生命周期函数

今天产品经理又给我甩过来一个需求,需要开发一个图表,拿到需求,瞄了一眼,然后我就去echarts官网复制示例代码了,复制完改了改差不多了,改完代码长这样


<template>
  <div class="echarts"></div>
</template>
<script>
  export default {
   mounted() {
     this.chart = echarts.init(this.$el)
      // 请求数据,赋值数据 等等一系列操作...
      // 监听窗口发生变化,resize组件
     window.addEventListener('resize',this.$_handleResizeChart)
  },
  updated() {
    // 干了一堆活
  },
  created() {
     // 干了一堆活
  },
  beforeDestroy() {
    // 组件销毁时,销毁监听事件
    window.removeEventListener('resize', this.$_handleResizeChart)
  },
  methods: {
    $_handleResizeChart() {
     this.chart.resize()
    },
  // 其他一堆方法
 }
}
</script>

功能写完开开心心的提测了,测试没啥问题,产品经理表示做的很棒。然而code review时候,技术大佬说了,这样有问题。

大佬:这样写不是很好,应该将监听resize事件与销毁resize事件放到一起,现在两段代码分开而且相隔几百行代码,可读性比较差
我:那我把两个生命周期钩子函数位置换一下,放到一起?
大佬:hook听过没?
我:Vue3.0才有啊,咋,咱要升级Vue?

然后技术大佬就不理我了,并向我扔过来一段代码

export default {
  mounted() {
    this.chart = echarts.init(this.$el)
    // 请求数据,赋值数据 等等一系列操作...
    // 监听窗口发生变化,resize组件
    window.addEventListener('resize', this.$_handleResizeChart)
    // 通过hook监听组件销毁钩子函数,并取消监听事件
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.$\_handleResizeChart)
    })
  },
  updated() {},
  created() {},
  methods: {
    $_handleResizeChart() {
      this.chart.resize()
    }
  }
}

看完代码,恍然大悟,大佬不愧是大佬,原来`Vue`还可以这样监听生命周期函数。

_在`Vue`组件中,可以用过`$on\`,\`$once`去监听所有的生命周期钩子函数,如监听组件的`updated`钩子函数可以写成 `this.$on('hook:updated', () => {})`_

2. 外部监听生命周期函数

今天同事在公司群里问,想在外部监听组件的生命周期函数,有没有办法啊?

为什么会有这样的需求呢,原来同事用了一个第三方组件,需要监听第三方组件数据的变化,但是组件又没有提供change事件,同事也没办法了,才想出来要去在外部监听组件的updated钩子函数。查看了一番资料,发现Vue支持在外部监听组件的生命周期钩子函数。

<template>
   <!--通过@hook:updated监听组件的updated生命钩子函数-->
   <!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
   <custom-select @hook:updated="$_handleSelectUpdated" />
</template>
<script>
  import CustomSelect from '../components/custom-select'
  export default {
     components: {
        CustomSelect
     },
   methods: {
     $_handleSelectUpdated() {
       console.log('custom-select组件的updated钩子函数被触发')
     }
   }
 }
</script>

小项目还用Vuex?用Vue.observable手写一个状态管理吧

在前端项目中,有许多数据需要在各个组件之间进行传递共享,这时候就需要有一个状态管理工具,一般情况下,我们都会使用Vuex,但对于小型项目来说,就像Vuex官网所说:“如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex”。这时候我们就可以使用Vue2.6提供的新API Vue.observable手动打造一个Vuex

1. 创建 store

import Vue from 'vue'
// 通过Vue.observable创建一个可响应的对象
export const store = Vue.observable({
  userInfo: {},
  roleIds: []
})
// 定义 mutations, 修改属性
export const mutations = {
   setUserInfo(userInfo) {
     store.userInfo = userInfo
   },
   setRoleIds(roleIds) {
     store.roleIds = roleIds
   }
}

2. 在组件中引用

<template>
   <div>
     {{ userInfo.name }}
   </div>
</template>
<script>
  import { store, mutations } from '../store'
  export default {
    computed: {
      userInfo() {
        return store.userInfo 
      }
   },
   created() {
     mutations.setUserInfo({
       name: '子君'
     })
   }
}
</script>

开发全局组件,你可能需要了解一下Vue.extend

Vue.extend是一个全局Api,平时我们在开发业务的时候很少会用到它,但有时候我们希望可以开发一些全局组件比如Loading,Notify,Message等组件时,这时候就可以使用Vue.extend

同学们在使用element-uiloading时,在代码中可能会这样写

// 显示loading
const loading = this.$loading()
// 关闭loading
loading.close()

这样写可能没什么特别的,但是如果你这样写

const loading = this.$loading()
const loading1 = this.$loading()
setTimeout(() => {
  loading.close()
}, 1000 * 3)

这时候你会发现,我调用了两次loading,但是只出现了一个,而且我只关闭了loading,但是loading1也被关闭了。这是怎么实现的呢?我们现在就是用Vue.extend + 单例模式去实现一个loading

1. 开发loading组件

<template>
  <transition name="custom-loading-fade">
    <!--loading蒙版-->
    <div v-show="visible" class="custom-loading-mask">
      <!--loading中间的图标-->
      <div class="custom-loading-spinner">
        <i class="custom-spinner-icon"></i>
        <!--loading上面显示的文字-->
        <p class="custom-loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>
<script>
export default {
  props: {
  // 是否显示loading
    visible: {
      type: Boolean,
      default: false
    },
    // loading上面的显示文字
    text: {
      type: String,
      default: ''
    }
  }
}
</script>

开发出来loading组件之后,如果需要直接使用,就要这样去用

<template>
  <div class="component-code">
    <!--其他一堆代码-->
    <custom-loading :visible="visible" text="加载中" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    }
  }
}
</script>

但这样使用并不能满足我们的需求

  1. 可以通过js直接调用方法来显示关闭
  2. loading可以将整个页面全部遮罩起来

2.通过Vue.extend将组件转换为全局组件

1. 改造loading组件,将组件的props改为data

export default {
  data() {
    return {
      text: '',
      visible: false
    }
  }
}

2. 通过Vue.extend改造组件

// loading/index.js
import Vue from 'vue'
import LoadingComponent from './loading.vue'

// 通过Vue.extend将组件包装成一个子类
const LoadingConstructor = Vue.extend(LoadingComponent)

let loading = undefined

LoadingConstructor.prototype.close = function() {
  // 如果loading 有引用,则去掉引用
  if (loading) {
    loading = undefined
  }
  // 先将组件隐藏
  this.visible = false
  // 延迟300毫秒,等待loading关闭动画执行完之后销毁组件
  setTimeout(() => {
    // 移除挂载的dom元素
    if (this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el)
    }
    // 调用组件的$destroy方法进行组件销毁
    this.$destroy()
  }, 300)
}

const Loading = (options = {}) => {
  // 如果组件已渲染,则返回即可
  if (loading) {
    return loading
  }
  // 要挂载的元素
  const parent = document.body
  // 组件属性
  const opts = {
    text: '',
    ...options
  }
  // 通过构造函数初始化组件 相当于 new Vue()
  const instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: opts
  })
  // 将loading元素挂在到parent上面
  parent.appendChild(instance.$el)
  // 显示loading
  Vue.nextTick(() => {
    instance.visible = true
  })
  // 将组件实例赋值给loading
  loading = instance
  return instance
}

export default Loading

3. 在页面使用loading

import Loading from './loading/index.js'
export default {
  created() {
    const loading = Loading({ text: '正在加载。。。' })
    // 三秒钟后关闭
    setTimeout(() => {
      loading.close()
    }, 3000)
  }
}

通过上面的改造,loading已经可以在全局使用了,如果需要像element-ui一样挂载到Vue.prototype上面,通过this.$loading调用,还需要改造一下

4. 将组件挂载到Vue.prototype上面

Vue.prototype.$loading = Loading
// 在export之前将Loading方法进行绑定
export default Loading

// 在组件内使用
this.$loading()

自定义指令,从底层解决问题

什么是指令?指令就是你女朋友指着你说,“那边搓衣板,跪下,这是命令!”。开玩笑啦,程序员哪里会有女朋友。

通过上一节我们开发了一个loading组件,开发完之后,其他开发在使用的时候又提出来了两个需求

  1. 可以将loading挂载到某一个元素上面,现在只能是全屏使用
  2. 可以使用指令在指定的元素上面挂载loading

有需求,咱就做,没话说

1.开发v-loading指令

import Vue from 'vue'
import LoadingComponent from './loading'
// 使用 Vue.extend构造组件子类
const LoadingContructor = Vue.extend(LoadingComponent)

// 定义一个名为loading的指令
Vue.directive('loading', {
  /**
   * 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
   * @param {*} el 指令要绑定的元素
   * @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
   */
  bind(el, binding) {
    const instance = new LoadingContructor({
      el: document.createElement('div'),
      data: {}
    })
    el.appendChild(instance.$el)
    el.instance = instance
    Vue.nextTick(() => {
      el.instance.visible = binding.value
    })
  },
  /**
   * 所在组件的 VNode 更新时调用
   * @param {*} el
   * @param {*} binding
   */
  update(el, binding) {
    // 通过对比值的变化判断loading是否显示
    if (binding.oldValue !== binding.value) {
      el.instance.visible = binding.value
    }
  },
  /**
   * 只调用一次,在 指令与元素解绑时调用
   * @param {*} el
   */
  unbind(el) {
    const mask = el.instance.$el
    if (mask.parentNode) {
      mask.parentNode.removeChild(mask)
    }
    el.instance.$destroy()
    el.instance = undefined
  }
})

2.在元素上面使用指令

<template>
  <div v-loading="visible"></div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    }
  },
  created() {
    this.visible = true
    fetch().then(() => {
      this.visible = false
    })
  }
}
</script>

3.项目中哪些场景可以自定义指令

  1. 为组件添加loading效果
  2. 按钮级别权限控制 v-permission
  3. 代码埋点,根据操作类型定义指令
  4. input输入框自动获取焦点
  5. 其他等等。。。

深度watchwatch立即触发回调,我可以监听到你的一举一动

在开发Vue项目时,我们会经常性的使用到watch去监听数据的变化,然后在变化之后做一系列操作。

1.基础用法

比如一个列表页,我们希望用户在搜索框输入搜索关键字的时候,可以自动触发搜索,此时除了监听搜索框的change事件之外,我们也可以通过watch监听搜索关键字的变化

<template>
  <!--此处示例使用了element-ui-->
  <div>
    <div>
      <span>搜索</span>
      <input v-model="searchValue" />
    </div>
    <!--列表,代码省略-->
  </div>
</template>
<script>
export default {
  data() {
    return {
      searchValue: ''
    }
  },
  watch: {
    // 在值发生变化之后,重新加载数据
    searchValue(newValue, oldValue) {
      // 判断搜索
      if (newValue !== oldValue) {
        this.$_loadData()
      }
    }
  },
  methods: {
    $_loadData() {
      // 重新加载数据,此处需要通过函数防抖
    }
  }
}
</script>

2.立即触发

通过上面的代码,现在已经可以在值发生变化的时候触发加载数据了,但是如果要在页面初始化时候加载数据,我们还需要在created或者mounted生命周期钩子里面再次调用$_loadData方法。不过,现在可以不用这样写了,通过配置watch的立即触发属性,就可以满足需求了

// 改造watch
export default {
  watch: {
    // 在值发生变化之后,重新加载数据
    searchValue: {
    // 通过handler来监听属性变化, 初次调用 newValue为""空字符串, oldValue为 undefined
      handler(newValue, oldValue) {
        if (newValue !== oldValue) {
          this.$_loadData()
        }
      },
      // 配置立即执行属性
      immediate: true
    }
  }
}

3.深度监听(我可以看到你内心的一举一动)

一个表单页面,需求希望用户在修改表单的任意一项之后,表单页面就需要变更为被修改状态。如果按照上例中watch的写法,那么我们就需要去监听表单每一个属性,太麻烦了,这时候就需要用到watch的深度监听deep

export default {
  data() {
    return {
      formData: {
        name: '',
        sex: '',
        age: 0,
        deptId: ''
      }
    }
  },
  watch: {
    // 在值发生变化之后,重新加载数据
    formData: {
      // 需要注意,因为对象引用的原因, newValue和oldValue的值一直相等
      handler(newValue, oldValue) {
        // 在这里标记页面编辑状态
      },
      // 通过指定deep属性为true, watch会监听对象里面每一个值的变化
      deep: true
    }
  }
}

随时监听,随时取消,了解一下$watch

有这样一个需求,有一个表单,在编辑的时候需要监听表单的变化,如果发生变化则保存按钮启用,否则保存按钮禁用。这时候对于新增表单来说,可以直接通过watch去监听表单数据(假设是formData),如上例所述,但对于编辑表单来说,表单需要回填数据,这时候会修改formData的值,会触发watch,无法准确的判断是否启用保存按钮。现在你就需要了解一下$watch

export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  },
  created() {
    this.$_loadData()
  },
  methods: {
    // 模拟异步请求数据
    $_loadData() {
      setTimeout(() => {
        // 先赋值
        this.formData = {
          name: '子君',
          age: 18
        }
        // 等表单数据回填之后,监听数据是否发生变化
        const unwatch = this.$watch(
          'formData',
          () => {
            console.log('数据发生了变化')
          },
          {
            deep: true
          }
        )
        // 模拟数据发生了变化
        setTimeout(() => {
          this.formData.name = '张三'
        }, 1000)
      }, 1000)
    }
  }
}

根据上例可以看到,我们可以在需要的时候通过this.$watch来监听数据变化。那么如何取消监听呢,上例中this.$watch返回了一个值unwatch,是一个函数,在需要取消的时候,执行 unwatch()即可取消

函数式组件,函数是组件?

什么是函数式组件?函数式组件就是函数是组件,感觉在玩文字游戏。使用过React的同学,应该不会对函数式组件感到陌生。函数式组件,我们可以理解为没有内部状态,没有生命周期钩子函数,没有this(不需要实例化的组件)。

在日常写bug的过程中,经常会开发一些纯展示性的业务组件,比如一些详情页面,列表界面等,它们有一个共同的特点是只需要将外部传入的数据进行展现,不需要有内部状态,不需要在生命周期钩子函数里面做处理,这时候你就可以考虑使用函数式组件。

1. 先来一个函数式组件的代码

export default {
  // 通过配置functional属性指定组件为函数式组件
  functional: true,
  // 组件接收的外部属性
  props: {
    avatar: {
      type: String
    }
  },
  /**
   * 渲染函数
   * @param {*} h
   * @param {*} context 函数式组件没有this, props, slots等都在context上面挂着
   */
  render(h, context) {
    const { props } = context
    if (props.avatar) {
      return <img data-original={props.avatar}></img>
    }
    return <img data-original="default-avatar.png"></img>
  }
}

在上例中,我们定义了一个头像组件,如果外部传入头像,则显示传入的头像,否则显示默认头像。上面的代码中大家看到有一个render函数,这个是Vue使用JSX的写法,关于JSX,小编将在后续文章中会出详细的使用教程。

2.为什么使用函数式组件

  1. 最主要最关键的原因是函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件结构比较简单,代码结构更清晰

3. 函数式组件与普通组件的区别

  1. 函数式组件需要在声明组件是指定functional
  2. 函数式组件不需要实例化,所以没有this,this通过render函数的第二个参数来代替
  3. 函数式组件没有生命周期钩子函数,不能使用计算属性,watch等等
  4. 函数式组件不能通过$emit对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都被解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

4.我不想用JSX,能用函数式组件吗?

Vue2.5之前,使用函数式组件只能通过JSX的方式,在之后,可以通过模板语法来生命函数式组件

<!--在template 上面添加 functional属性-->
<template functional>
  <img :data-original="props.avatar ? props.avatar : 'default-avatar.png'" />
</template>
<!--根据上一节第六条,可以省略声明props-->

结语:

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高

扫码关注,愿你遇到那个心疼你付出的人~

微信公众号宣传图.gif

查看原文

赞 47 收藏 31 评论 3

浪里行舟 关注了用户 · 2020-07-21

子君 @zijun_5f156624be160

微信公众号: 前端有得玩
微信账号:snowzijun
github仓库: https://github.com/snowzijun
寄语:不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。

关注 557

浪里行舟 关注了用户 · 2020-07-17

Linmi @linmi

Segmentfault Community Support | Notion Pro


一对一免费提供内容选题、写作指导 → 微信 linmib

关注 19

浪里行舟 赞了文章 · 2020-07-15

专业团队支持、全网流量扶持,思否编程技术讲师赋能计划启动 | 首期招募限 20 位

很多技术人员都有一个三尺讲台梦,希望分享自己曾踩过的坑帮助年轻人。

很多技术人员都在尝试沉淀技术内容,建立技术兴趣圈,互相促进交流,提升个人品牌。

当经济形势持续恶化,越来越多职场人开始探索知识变现。关注高薪副业,在业余时间增加收入。

996,项目不断,人少事儿多……也许你一直蠢蠢欲动却没迈出第一步。

其实你的忙碌,我们都懂!

首页banner.png

思否编程针对技术人才推出了思否编程技术讲师赋能计划,专为技术人员晋升讲师量身定制。首期仅限20位!?(立即申请

我们将提供授课技巧、流量扶持等多维度赋能,专业课程策划、编辑、运营团队为你服务,确保每一位技术人员快速完成讲师的华丽晋升。

心动了?一起探索更多可能!

?无需课程经验,只需具备:
  • 每周 2-3 小时的空闲时间
  • 工作经验 5 年+,擅长某一技术方向,具备突出项目经验者可适当放宽
  • 热爱分享,愿意帮助更多的开发者
  • 技术博主/图书作者/公众号主等,思否社区用户优先
⛽️享受多维度运营支持,具体包括:
  • 流量支持:公众号百万粉丝矩阵,站内千万级流量
  • 课程策划:为你量身定制课程选题,与你一起打磨课程大纲
  • 课程编辑:专业视频剪辑包装,资深编辑团队,与你一起优化教学材料
  • 课程包装:结合讲师和课程特点提供全方位课程包装,并进行配套宣传物料设计
  • 推广方案:专属售卖策略和讲师打造方案
  • 个人包装:SegmentFault 技术媒体访谈包装,有机会被推荐为顶级技术大会讲师、嘉宾评委,大大提升个人品牌影响力。(思否独有福利 (^o^)/
?短期内快速提升自已,获得丰厚收获:
  • 授课技能:帮助你快速成为具备授课技能的讲师,对工作中带新人也将受益匪浅。
  • 学习成长:与平台大咖讲师交流切磋,与学员分享知识的同时,不断自我总结成长。
  • 粉丝人气:快速建立自己粉丝圈,提升业界的影响力。
  • 课程收入:爆款课程将有不菲收入,每月也将有稳定的课程分成。
?急需技术方向:
  • 前端框架、性能优化等前端技术
  • Python、Java 等后端技术
  • 算法、数据挖掘、机器学习等人工智能方向
  • 技术选型、代码优化、网站架构、各类实战项目等
?优秀课程案例:

优秀案例.png

课程1:凯威教你学 Python: 系列课程
课程2:PHP 进阶之路
更多重磅课程,请访问:https://ke.segmentfault.com

从业者就业压力增大,在线培训成为多数人快速提升技能的有效途径之一。

思否编程依托 SegmentFault 思否 8 年行业积累和每月数千万精准流量,为开发者提供高质量的技术干货课程,解决工作中遇到的问题,快速成长。

课程将通过多样化的营销手段触达到更多用户,产生高额营收和广泛影响力。

6⃣️0⃣️天晋升技术讲师,你准备好了吗??(立即申请

更多咨询,请添加以下好友,等你呦?!

image

查看原文

赞 8 收藏 0 评论 0

浪里行舟 赞了文章 · 2020-07-14

手写一个Promise/A+,完美通过官方872个测试用例

Promise几乎是面试必考点,所以我们不能仅仅会用,还得知道他的底层原理,学习他原理的最好方法就是自己也实现一个Promise。所以本文会自己实现一个遵循Promise/A+规范的Promise。实现之后,我们还要用Promise/A+官方的测试工具来测试下我们的实现是否正确,这个工具总共有872个测试用例,全部通过才算是符合Promise/A+规范,下面是他们的链接:

Promise/A+规范: https://github.com/promises-aplus/promises-spec

Promise/A+测试工具: https://github.com/promises-aplus/promises-tests

本文的完整代码托管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

Promise用法

Promise的基本用法,网上有很多,我这里简单提一下,我还是用三个相互依赖的网络请求做例子,假如我们有三个网络请求,请求2必须依赖请求1的结果,请求3必须依赖请求2的结果,如果用回调的话会有三层,会陷入“回调地狱”,用Promise就清晰多了:

const request = require("request");

// 我们先用Promise包装下三个网络请求
// 请求成功时resolve这个Promise
const request1 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request1 success');
      }
    });
  });

  return promise;
}

const request2 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request2 success');
      }
    });
  });

  return promise;
}

const request3 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request3 success');
      }
    });
  });

  return promise;
}


// 先发起request1,等他resolve后再发起request2,
// 然后是request3
request1().then((data) => {
  console.log(data);
  return request2();
})
.then((data) => {
  console.log(data);
  return request3();
})
.then((data) => {
  console.log(data);
})

上面的例子里面,then是可以链式调用的,后面的then可以拿到前面resolve出来的数据,我们控制台可以看到三个success依次打出来:

image-20200324164123892

Promises/A+规范

通过上面的例子,其实我们已经知道了一个promise长什么样子,Promises/A+规范其实就是对这个长相进一步进行了规范。下面我会对这个规范进行一些讲解。

术语

  1. promise:是一个拥有 then 方法的对象或函数,其行为符合本规范
  2. thenable:是一个定义了 then 方法的对象或函数。这个主要是用来兼容一些老的Promise实现,只要一个Promise实现是thenable,也就是拥有then方法的,就可以跟Promises/A+兼容。
  3. value:指reslove出来的值,可以是任何合法的JS值(包括 undefined , thenable 和 promise等)
  4. exception:异常,在Promise里面用throw抛出来的值
  5. reason:拒绝原因,是reject里面传的参数,表示reject的原因

Promise状态

Promise总共有三个状态:

  1. pending: 一个promise在resolve或者reject前就处于这个状态。
  2. fulfilled: 一个promise被resolve后就处于fulfilled状态,这个状态不能再改变,而且必须拥有一个不可变的值(value)。
  3. rejected: 一个promise被reject后就处于rejected状态,这个状态也不能再改变,而且必须拥有一个不可变的拒绝原因(reason)。

注意这里的不可变指的是===,也就是说,如果value或者reason是对象,只要保证引用不变就行,规范没有强制要求里面的属性也不变。Promise状态其实很简单,画张图就是:

image-20200324173555225

then方法

一个promise必须拥有一个then方法来访问他的值或者拒绝原因。then方法有两个参数:

promise.then(onFulfilled, onRejected)

参数可选

onFulfilledonRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略
  • 如果 onRejected 不是函数,其必须被忽略

onFulfilled 特性

如果 onFulfilled 是函数:

  • promise 执行结束后其必须被调用,其第一个参数为 promise 的终值value
  • promise 执行结束前其不可被调用
  • 其调用次数不可超过一次

onRejected 特性

如果 onRejected 是函数:

  • promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的据因reason
  • promise 被拒绝执行前其不可被调用
  • 其调用次数不可超过一次

多次调用

then 方法可以被同一个 promise 调用多次

  • promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
  • promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调

返回

then 方法必须返回一个 promise 对象。

promise2 = promise1.then(onFulfilled, onRejected); 
  • 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行 Promise 解决过程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
  • 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
  • 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因

规范里面还有很大一部分是讲解Promise 解决过程的,光看规范,很空洞,前面这些规范已经可以指导我们开始写一个自己的Promise了,Promise 解决过程会在我们后面写到了再详细讲解。

自己写一个Promise

我们自己要写一个Promise,肯定需要知道有哪些工作需要做,我们先从Promise的使用来窥探下需要做啥:

  1. 新建Promise需要使用new关键字,那他肯定是作为面向对象的方式调用的,Promise是一个类。关于JS的面向对象更详细的解释可以看这篇文章。
  2. 我们new Promise(fn)的时候需要传一个函数进去,说明Promise的参数是一个函数
  3. 构造函数传进去的fn会收到resolvereject两个函数,用来表示Promise成功和失败,说明构造函数里面还需要resolvereject这两个函数,这两个函数的作用是改变Promise的状态。
  4. 根据规范,promise有pendingfulfilledrejected三个状态,初始状态为pending,调用resolve会将其改为fulfilled,调用reject会改为rejected
  5. promise实例对象建好后可以调用then方法,而且是可以链式调用then方法,说明then是一个实例方法。链式调用的实现这篇有详细解释,我这里不再赘述。简单的说就是then方法也必须返回一个带then方法的对象,可以是this或者新的promise实例。

构造函数

为了更好的兼容性,本文就不用ES6了。

// 先定义三个常量表示状态
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function MyPromise(fn) {
  this.status = PENDING;    // 初始状态为pending
  this.value = null;        // 初始化value
  this.reason = null;       // 初始化reason
}

resolvereject方法

根据规范,resolve方法是将状态改为fulfilled,reject是将状态改为rejected。

// 这两个方法直接写在构造函数里面
function MyPromise(fn) {
  // ...省略前面代码...
  
  // 存一下this,以便resolve和reject里面访问
  var that = this;
  // resolve方法参数是value
  function resolve(value) {
    if(that.status === PENDING) {
      that.status = FULFILLED;
      that.value = value;
    }
  }
  
  // reject方法参数是reason
  function reject(reason) {
    if(that.status === PENDING) {
      that.status = REJECTED;
      that.reason = reason;
    }
  }
}

调用构造函数参数

最后将resolvereject作为参数调用传进来的参数,记得加上try,如果捕获到错误就reject

function MyPromise(fn) {
  // ...省略前面代码...
  
  try {
    fn(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

then方法

根据我们前面的分析,then方法可以链式调用,所以他是实例方法,而且规范中的API是promise.then(onFulfilled, onRejected),我们先把架子搭出来:

MyPromise.prototype.then = function(onFulfilled, onRejected) {}

then方法里面应该干什么呢,其实规范也告诉我们了,先检查onFulfilledonRejected是不是函数,如果不是函数就忽略他们,所谓“忽略”并不是什么都不干,对于onFulfilled来说“忽略”就是将value原封不动的返回,对于onRejected来说就是返回reasononRejected因为是错误分支,我们返回reason应该throw一个Error:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // 如果onFulfilled不是函数,给一个默认函数,返回value
  var realOnFulfilled = onFulfilled;
  if(typeof realOnFulfilled !== 'function') {
    realOnFulfilled = function (value) {
      return value;
    }
  }

  // 如果onRejected不是函数,给一个默认函数,返回reason的Error
  var realOnRejected = onRejected;
  if(typeof realOnRejected !== 'function') {
    realOnRejected = function (reason) {
      throw reason;
    }
  }
}

参数检查完后就该干点真正的事情了,想想我们使用Promise的时候,如果promise操作成功了就会调用then里面的onFulfilled,如果他失败了,就会调用onRejected。对应我们的代码就应该检查下promise的status,如果是FULFILLED,就调用onFulfilled,如果是REJECTED,就调用onRejected:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // ...省略前面代码...

  if(this.status === FULFILLED) {
    onFulfilled(this.value)
  }

  if(this.status === REJECTED) {
    onRejected(this.reason);
  }
}

再想一下,我们新建一个promise的时候可能是直接这样用的:

new Promise(fn).then(onFulfilled, onRejected);

上面代码then是在实例对象一创建好就调用了,这时候fn里面的异步操作可能还没结束呢,也就是说他的status还是PENDING,这怎么办呢,这时候我们肯定不能立即调onFulfilled或者onRejected的,因为fn到底成功还是失败还不知道呢。那什么时候知道fn成功还是失败呢?答案是fn里面主动调resolve或者reject的时候。所以如果这时候status状态还是PENDING,我们应该将onFulfilledonRejected两个回调存起来,等到fn有了结论,resolve或者reject的时候再来调用对应的代码。因为后面then还有链式调用,会有多个onFulfilledonRejected,我这里用两个数组将他们存起来,等resolve或者reject的时候将数组里面的全部方法拿出来执行一遍

// 构造函数
function MyPromise(fn) {
  // ...省略其他代码...
  
  // 构造函数里面添加两个数组存储成功和失败的回调
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
  
  function resolve(value) {
    if(that.status === PENDING) {
      // ...省略其他代码...
      // resolve里面将所有成功的回调拿出来执行
      that.onFulfilledCallbacks.forEach(callback => {
        callback(that.value);
      });
    }
  }
  
  function reject(reason) {
    if(that.status === PENDING) {
      // ...省略其他代码...
      // resolve里面将所有失败的回调拿出来执行
      that.onRejectedCallbacks.forEach(callback => {
        callback(that.reason);
      });
    }
  }
}

// then方法
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // ...省略其他代码...

  // 如果还是PENDING状态,将回调保存下来
  if(this.status === PENDING) {
    this.onFulfilledCallbacks.push(realOnFulfilled);
    this.onRejectedCallbacks.push(realOnRejected);
  }
}

上面这种暂时将回调保存下来,等条件满足的时候再拿出来运行让我想起了一种模式:订阅发布模式。我们往回调数组里面push回调函数,其实就相当于往事件中心注册事件了,resolve就相当于发布了一个成功事件,所有注册了的事件,即onFulfilledCallbacks里面的所有方法都会拿出来执行,同理reject就相当于发布了一个失败事件。更多订阅发布模式的原理可以看这里

完成了一小步

到这里为止,其实我们已经可以实现异步调用了,只是then的返回值还没实现,还不能实现链式调用,我们先来玩一下:

var request = require("request");
var MyPromise = require('./MyPromise');

var promise1 = new MyPromise((resolve) => {
  request('https://www.baidu.com', function (error, response) {
    if (!error && response.statusCode == 200) {
      resolve('request1 success');
    }
  });
});

promise1.then(function(value) {
  console.log(value);
});

var promise2 = new MyPromise((resolve, reject) => {
  request('https://www.baidu.com', function (error, response) {
    if (!error && response.statusCode == 200) {
      reject('request2 failed');
    }
  });
});

promise2.then(function(value) {
  console.log(value);
}, function(reason) {
  console.log(reason);
});

上述代码输出如下图,符合我们的预期,说明到目前为止,我们的代码都没问题:

image-20200325172257655

then的返回值

根据规范then的返回值必须是一个promise,规范还定义了不同情况应该怎么处理,我们先来处理几种比较简单的情况:

  1. 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    // ... 省略其他代码 ...
  
  // 有了这个要求,在RESOLVED和REJECTED的时候就不能简单的运行onFulfilled和onRejected了。
  // 我们需要将他们用try...catch...包起来,如果有错就reject。
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        realOnFulfilled(that.value);
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }

  if(this.status === REJECTED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        realOnRejected(that.reason);
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
  
  // 如果还是PENDING状态,也不能直接保存回调方法了,需要包一层来捕获错误
  if(this.status === PENDING) {
    var promise2 = new MyPromise(function(resolve, reject) {
      that.onFulfilledCallbacks.push(function() {
        try {
          realOnFulfilled(that.value);
        } catch (error) {
          reject(error);
        }
      });
      that.onRejectedCallbacks.push(function() {
        try {
          realOnRejected(that.reason);
        } catch (error) {
          reject(error);
        }
      });
    });
  
    return promise2;
  }
}
  1. 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
// 我们就根据要求加个判断,注意else里面是正常执行流程,需要resolve
// 这是个例子,每个realOnFulfilled后面都要这样写
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if (typeof onFulfilled !== 'function') {
          resolve(that.value);
        } else {
          realOnFulfilled(that.value);
          resolve(that.value);
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
  1. 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因。这个要求其实在我们检测 onRejected 不是函数的时候已经做到了,因为我们默认给的onRejected里面会throw一个Error,所以代码肯定会走到catch里面去。但是我们为了更直观,代码还是跟规范一一对应吧。需要注意的是,如果promise1onRejected执行成功了,promise2应该被resolve。改造代码如下:
  if(this.status === REJECTED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if(typeof onRejected !== 'function') {
          reject(that.reason);
        } else {
          realOnRejected(that.reason);
          resolve();
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
  1. 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程[[Resolve]](promise2, x)。这条其实才是规范的第一条,因为他比较麻烦,所以我将它放到了最后。前面我们代码的实现,其实只要onRejected或者onFulfilled成功执行了,我们都要resolve promise2。多了这条,我们还需要对onRejected或者onFulfilled的返回值进行判断,如果有返回值就要进行 Promise 解决过程。我们专门写一个方法来进行Promise 解决过程。前面我们代码的实现,其实只要onRejected或者onFulfilled成功执行了,我们都要resolve promise2,这个过程我们也放到这个方法里面去吧,所以代码变为下面这样,其他地方类似:
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if (typeof onFulfilled !== 'function') {
          resolve(that.value);
        } else {
          var x = realOnFulfilled(that.value);
          resolvePromise(promise2, x, resolve, reject);   // 调用Promise 解决过程
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }

Promise 解决过程

现在我们该来实现resolvePromise方法了,规范中这一部分较长,我就直接把规范作为注释写在代码里面了。

function resolvePromise(promise, x, resolve, reject) {
  // 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
  // 这是为了防止死循环
  if (promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  if (x instanceof MyPromise) {
    // 如果 x 为 Promise ,则使 promise 接受 x 的状态
    // 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
    // 这个if跟下面判断then然后拿到执行其实重复了,可有可无
    x.then(function (y) {
      resolvePromise(promise, y, resolve, reject);
    }, reject);
  }
  // 如果 x 为对象或者函数
  else if (typeof x === 'object' || typeof x === 'function') {
    // 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
    if (x === null) {
      return resolve(x);
    }

    try {
      // 把 x.then 赋值给 then 
      var then = x.then;
    } catch (error) {
      // 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
      return reject(error);
    }

    // 如果 then 是函数
    if (typeof then === 'function') {
      var called = false;
      // 将 x 作为函数的作用域 this 调用之
      // 传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise
      // 名字重名了,我直接用匿名函数了
      try {
        then.call(
          x,
          // 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
          function (y) {
            // 如果 resolvePromise 和 rejectPromise 均被调用,
            // 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            // 实现这条需要前面加一个变量called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          function (r) {
            if (called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果调用 then 方法抛出了异常 e:
        // 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
        if (called) return;

        // 否则以 e 为据因拒绝 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函数,以 x 为参数执行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
}

onFulfilledonRejected 的执行时机

在规范中还有一条:onFulfilledonRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。这一条的意思是实践中要确保 onFulfilledonRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。所以在我们执行onFulfilledonRejected的时候都应该包到setTimeout里面去。

// 这块代码在then里面
if(this.status === FULFILLED) {
  var promise2 = new MyPromise(function(resolve, reject) {
    // 这里加setTimeout
    setTimeout(function() {
      try {
        if (typeof onFulfilled !== 'function') {
          resolve(that.value);
        } else {
          var x = realOnFulfilled(that.value);
          resolvePromise(promise2, x, resolve, reject);
        }
      } catch (error) {
        reject(error);
      }
    }, 0);
  });

  return promise2;
}

if(this.status === REJECTED) {
  var promise2 = new MyPromise(function(resolve, reject) {
    // 这里加setTimeout
    setTimeout(function() {
      try {
        if(typeof onRejected !== 'function') {
          reject(that.reason);
        } else {
          var x = realOnRejected(that.reason);
          resolvePromise(promise2, x, resolve, reject);
        }
      } catch (error) {
        reject(error);
      }
    }, 0);
  });

  return promise2;
}

if (this.status === PENDING) {
  var promise2 = new MyPromise(function (resolve, reject) {
    that.onFulfilledCallbacks.push(function () {
      // 这里加setTimeout
      setTimeout(function () {
        try {
          if (typeof onFulfilled !== 'function') {
            resolve(that.value);
          } else {
            var x = realOnFulfilled(that.value);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0);
    });
    that.onRejectedCallbacks.push(function () {
      // 这里加setTimeout
      setTimeout(function () {
        try {
          if (typeof onRejected !== 'function') {
            reject(that.reason);
          } else {
            var x = realOnRejected(that.reason);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0)
    });
  });

  return promise2;
}

测试我们的Promise

我们使用Promise/A+官方的测试工具promises-aplus-tests来对我们的MyPromise进行测试,要使用这个工具我们必须实现一个静态方法deferred,官方对这个方法的定义如下:

deferred: 返回一个包含{ promise, resolve, reject }的对象

promise 是一个处于pending状态的promise

resolve(value)value解决上面那个promise

reject(reason)reason拒绝上面那个promise

我们实现代码如下:

MyPromise.deferred = function() {
  var result = {};
  result.promise = new MyPromise(function(resolve, reject){
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}

然后用npm将promises-aplus-tests下载下来,再配置下package.json就可以跑测试了:

{
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  },
  "scripts": {
    "test": "promises-aplus-tests MyPromise"
  }
}

在跑测试的时候发现一个坑,在resolvePromise的时候,如果x是null,他的类型也是object,是应该直接用x来resolve的,之前的代码会走到catch然后reject,所以需要检测下null

// 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
if(x === null) {
  return resolve(x);
}

这个测试总共872用例,我们写的Promise完美通过了所有用例:

image-20200326214543894

其他Promise方法

在ES6的官方Promise还有很多API,比如:

Promise.resolve

Promise.reject

Promise.all

Promise.race

Promise.prototype.catch

Promise.prototype.finally

Promise.allSettled

虽然这些都不在Promise/A+里面,但是我们也来实现一下吧,加深理解。其实我们前面实现了Promise/A+再来实现这些已经是小菜一碟了,因为这些API全部是前面的封装而已。

Promise.resolve

将现有对象转为Promise对象,如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为fulfilled。

MyPromise.resolve = function(parameter) {
  if(parameter instanceof MyPromise) {
    return parameter;
  }

  return new MyPromise(function(resolve) {
    resolve(parameter);
  });
}

Promise.reject

返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

MyPromise.reject = function(reason) {
  return new MyPromise(function(resolve, reject) {
    reject(reason);
  });
}

Promise.all

该方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。当p1, p2, p3全部resolve,大的promise才resolve,有任何一个reject,大的promise都reject。

MyPromise.all = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var count = 0;
    var result = [];
    var length = promiseList.length;

    if(length === 0) {
      return resolve(result);
    }

    promiseList.forEach(function(promise, index) {
      MyPromise.resolve(promise).then(function(value){
        count++;
        result[index] = value;
        if(count === length) {
          resolve(result);
        }
      }, function(reason){
        reject(reason);
      });
    });
  });

  return resPromise;
}

Promise.race

用法:

const p = Promise.race([p1, p2, p3]);

该方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

MyPromise.race = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var length = promiseList.length;

    if(length === 0) {
      return resolve();
    } else {
      for(var i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then(function(value) {
          return resolve(value);
        }, function(reason) {
          return reject(reason);
        });
      }
    }
  });

  return resPromise;
}

Promise.prototype.catch

Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

MyPromise.prototype.catch = function(onRejected) {
  this.then(null, onRejected);
}

Promise.prototype.finally

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

MyPromise.prototype.finally = function(fn) {
  return this.then(function(value){
    return MyPromise.resolve(value).then(function(){
      return value;
    });
  }, function(error){
    return MyPromise.resolve(reason).then(function() {
      throw error
    });
  });
}

Promise.allSettled

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例的执行结果。

MyPromise.allSettled = function(promiseList) {
  return new MyPromise(function(resolve){
    var length = promiseList.length;
    var result = [];
    var count = 0;

    if(length === 0) {
      return resolve(result);
    } else {
      for(var i = 0; i < length; i++) {

        (function(i){
          var currentPromise = MyPromise.resolve(promiseList[i]);

          currentPromise.then(function(value){
            count++;
            result[i] = {
              status: 'fulfilled',
              value: value
            }
            if(count === length) {
              return resolve(result);
            }
          }, function(reason){
            count++;
            result[i] = {
              status: 'rejected',
              reason: reason
            }
            if(count === length) {
              return resolve(result);
            }
          });
        })(i)
      }
    }
  });
}

完整代码

完全版的代码较长,这里如果看不清楚的可以去我的GitHub上看:

https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

// 先定义三个常量表示状态
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function MyPromise(fn) {
  this.status = PENDING;    // 初始状态为pending
  this.value = null;        // 初始化value
  this.reason = null;       // 初始化reason

  // 构造函数里面添加两个数组存储成功和失败的回调
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];

  // 存一下this,以便resolve和reject里面访问
  var that = this;
  // resolve方法参数是value
  function resolve(value) {
    if (that.status === PENDING) {
      that.status = FULFILLED;
      that.value = value;

      // resolve里面将所有成功的回调拿出来执行
      that.onFulfilledCallbacks.forEach(callback => {
        callback(that.value);
      });
    }
  }

  // reject方法参数是reason
  function reject(reason) {
    if (that.status === PENDING) {
      that.status = REJECTED;
      that.reason = reason;

      // resolve里面将所有失败的回调拿出来执行
      that.onRejectedCallbacks.forEach(callback => {
        callback(that.reason);
      });
    }
  }

  try {
    fn(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

function resolvePromise(promise, x, resolve, reject) {
  // 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
  // 这是为了防止死循环
  if (promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  if (x instanceof MyPromise) {
    // 如果 x 为 Promise ,则使 promise 接受 x 的状态
    // 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
    // 这个if跟下面判断then然后拿到执行其实重复了,可有可无
    x.then(function (y) {
      resolvePromise(promise, y, resolve, reject);
    }, reject);
  }
  // 如果 x 为对象或者函数
  else if (typeof x === 'object' || typeof x === 'function') {
    // 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
    if (x === null) {
      return resolve(x);
    }

    try {
      // 把 x.then 赋值给 then 
      var then = x.then;
    } catch (error) {
      // 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
      return reject(error);
    }

    // 如果 then 是函数
    if (typeof then === 'function') {
      var called = false;
      // 将 x 作为函数的作用域 this 调用之
      // 传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise
      // 名字重名了,我直接用匿名函数了
      try {
        then.call(
          x,
          // 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
          function (y) {
            // 如果 resolvePromise 和 rejectPromise 均被调用,
            // 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            // 实现这条需要前面加一个变量called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          function (r) {
            if (called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果调用 then 方法抛出了异常 e:
        // 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
        if (called) return;

        // 否则以 e 为据因拒绝 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函数,以 x 为参数执行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  // 如果onFulfilled不是函数,给一个默认函数,返回value
  // 后面返回新promise的时候也做了onFulfilled的参数检查,这里可以删除,暂时保留是为了跟规范一一对应,看得更直观
  var realOnFulfilled = onFulfilled;
  if (typeof realOnFulfilled !== 'function') {
    realOnFulfilled = function (value) {
      return value;
    }
  }

  // 如果onRejected不是函数,给一个默认函数,返回reason的Error
  // 后面返回新promise的时候也做了onRejected的参数检查,这里可以删除,暂时保留是为了跟规范一一对应,看得更直观
  var realOnRejected = onRejected;
  if (typeof realOnRejected !== 'function') {
    realOnRejected = function (reason) {
      throw reason;
    }
  }

  var that = this;   // 保存一下this

  if (this.status === FULFILLED) {
    var promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        try {
          if (typeof onFulfilled !== 'function') {
            resolve(that.value);
          } else {
            var x = realOnFulfilled(that.value);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0);
    });

    return promise2;
  }

  if (this.status === REJECTED) {
    var promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        try {
          if (typeof onRejected !== 'function') {
            reject(that.reason);
          } else {
            var x = realOnRejected(that.reason);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0);
    });

    return promise2;
  }

  // 如果还是PENDING状态,将回调保存下来
  if (this.status === PENDING) {
    var promise2 = new MyPromise(function (resolve, reject) {
      that.onFulfilledCallbacks.push(function () {
        setTimeout(function () {
          try {
            if (typeof onFulfilled !== 'function') {
              resolve(that.value);
            } else {
              var x = realOnFulfilled(that.value);
              resolvePromise(promise2, x, resolve, reject);
            }
          } catch (error) {
            reject(error);
          }
        }, 0);
      });
      that.onRejectedCallbacks.push(function () {
        setTimeout(function () {
          try {
            if (typeof onRejected !== 'function') {
              reject(that.reason);
            } else {
              var x = realOnRejected(that.reason);
              resolvePromise(promise2, x, resolve, reject);
            }
          } catch (error) {
            reject(error);
          }
        }, 0)
      });
    });

    return promise2;
  }
}

MyPromise.deferred = function () {
  var result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}

MyPromise.resolve = function (parameter) {
  if (parameter instanceof MyPromise) {
    return parameter;
  }

  return new MyPromise(function (resolve) {
    resolve(parameter);
  });
}

MyPromise.reject = function (reason) {
  return new MyPromise(function (resolve, reject) {
    reject(reason);
  });
}

MyPromise.all = function (promiseList) {
  var resPromise = new MyPromise(function (resolve, reject) {
    var count = 0;
    var result = [];
    var length = promiseList.length;

    if (length === 0) {
      return resolve(result);
    }

    promiseList.forEach(function (promise, index) {
      MyPromise.resolve(promise).then(function (value) {
        count++;
        result[index] = value;
        if (count === length) {
          resolve(result);
        }
      }, function (reason) {
        reject(reason);
      });
    });
  });

  return resPromise;
}

MyPromise.race = function (promiseList) {
  var resPromise = new MyPromise(function (resolve, reject) {
    var length = promiseList.length;

    if (length === 0) {
      return resolve();
    } else {
      for (var i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then(function (value) {
          return resolve(value);
        }, function (reason) {
          return reject(reason);
        });
      }
    }
  });

  return resPromise;
}

MyPromise.prototype.catch = function (onRejected) {
  this.then(null, onRejected);
}

MyPromise.prototype.finally = function (fn) {
  return this.then(function (value) {
    return MyPromise.resolve(fn()).then(function () {
      return value;
    });
  }, function (error) {
    return MyPromise.resolve(fn()).then(function () {
      throw error
    });
  });
}

MyPromise.allSettled = function (promiseList) {
  return new MyPromise(function (resolve) {
    var length = promiseList.length;
    var result = [];
    var count = 0;

    if (length === 0) {
      return resolve(result);
    } else {
      for (var i = 0; i < length; i++) {

        (function (i) {
          var currentPromise = MyPromise.resolve(promiseList[i]);

          currentPromise.then(function (value) {
            count++;
            result[i] = {
              status: 'fulfilled',
              value: value
            }
            if (count === length) {
              return resolve(result);
            }
          }, function (reason) {
            count++;
            result[i] = {
              status: 'rejected',
              reason: reason
            }
            if (count === length) {
              return resolve(result);
            }
          });
        })(i)
      }
    }
  });
}

module.exports = MyPromise;

总结

至此,我们的Promise就简单实现了,只是我们不是原生代码,不能做成微任务,如果一定要做成微任务的话,只能用其他微任务API模拟,比如MutaionObserver或者process.nextTick。下面再回顾下几个要点:

  1. Promise其实是一个发布订阅模式
  2. then方法对于还在pending的任务,其实是将回调函数onFilfilledonRejected塞入了两个数组
  3. Promise构造函数里面的resolve方法会将数组onFilfilledCallbacks里面的方法全部拿出来执行,这里面是之前then方法塞进去的成功回调
  4. 同理,Promise构造函数里面的reject方法会将数组onRejectedCallbacks里面的方法全部拿出来执行,这里面是之前then方法塞进去的失败回调
  5. then方法会返回一个新的Promise以便执行链式调用
  6. catchfinally这些实例方法都必须返回一个新的Promise实例以便实现链式调用

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

赞 50 收藏 36 评论 2

浪里行舟 关注了用户 · 2020-07-14

蒋鹏飞 @jiangpengfei_5ecce944a3d8a

前端工程师,底层技术人。
思否2020年度“Top Writer”!
掘金“优秀作者”!
开源中国2020年度“优秀源创作者”!
分享各种大前端进阶知识!
关注公众号【进击的大前端】第一时间获取高质量原创。
更多文章和示例源码请看:https://github.com/dennis-jia...

关注 1860

认证与成就

  • 获得 6168 次点赞
  • 获得 40 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 37 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2018-06-05
个人主页被 27.7k 人浏览