【面试题】面试官:为什么Promise中的错误不能被try/catch?

news2024/11/15 21:46:08

大厂面试题分享 面试题库

前端面试题库 (面试必备)   推荐:★★★★★

地址:前端面试题库

前言

之前我写过一篇文章,讨论了为什么async await中的错误可以被try catch,而setTimeout等api不能,有小伙伴提出之前面试被面试官问过为什么Promise的错误不能try catch,为什么要这么设计。好吧,虽然Promise这个话题大家都聊烂了,今天我们再来展开聊聊🤭。

什么是Promise

Promise是一个用来代表异步操作结果的对象,我们可以通过观察者模式观察异步操作的结果。在其它语言里面,我们多多少少接触过futuredeferred这些概念,Promise其实就是Javascript的类似实现。 根据MDN定义:

Promise is in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

一个fulfilled Promise有一个fulfillment值,而rejected Promise则有一个rejection reason

为什么要引入Promise?

异步处理在我们日常开发中是很常见的场景,在Promise出现之前,我们都是通过回调来处理异步代码的结果,但是出现了一些问题:

  • 回调地狱,在有多个异步逻辑存在依赖关系时,我们只能在回调里嵌套,这些深度嵌套的代码让代码难以阅读和维护,业界称之为回调地狱
  • 回调也没用标准的方式来处理错误,大家都凭自己的喜好来处理错误,可能我们使用的库跟api都定义了一套处理错误的方式,那我们把多个库一起搭配使用时,就需要花额外的精力去把他们处理皮实
  • 有时候我们需要对一个已经完成的逻辑注册回调。这也没有统一的标准,对于大部分代码,我们根本就不能对这些已经执行完的代码注册回调,有些会同步执行回调,有些会异步执行回调,我们根本不可能记住所有api的机制,要么每次使用时我们都要研究这个api的实现机制,要么我们可能就在写bug
  • 而且,如果我们想对一个异步逻辑注册多个回调,这也要看api提供方支不支持
  • 最重要的,如果有统一的方式来处理错误跟正确结果的话,我们就有可能实现一套通用的逻辑来简化代码复杂度,这种自己发挥的情况就很难

是的,Promise的出现就是为了解决这所有的问题。

怎么创建Promise

Promise构造函数

Promise有一个构造函数,接收一个函数作为参数,这个传入构造函数里的函数被称作executor。 Promise的构造函数会同步地调用executorexecutor又接收resolve函数跟reject函数作为参数,然后我们就可以通过这两个函数俩决定当前Promise的状态(resolve进入fulfilled或者reject进入rejected)。

我们在resolve Promise时,可以直接给它一个值,或者给它另外一个Promise,这样最终是fulfilled还是rejected将取决于我们给它的这个Promise最后的状态。

假如我们现在有一个promise a

  • 如果我们在promise a里面调用resolve,传入了另一个promise bpromise a的状态将取决于promise b的执行结果
  • 如果我们直接传给resolve一个普通的值,则promise a带着这个值进入fulfilled状态
  • 如果我们调用reject,则promise a带着我们传给reject的值进入rejected状态

Promise在一开始都是pending状态,之后执行完逻辑之后变成settled(fulfilled或者rejected)settled不能变成pendingfulfilled不能变成rejectedrejected也不能变成fulfilled。总之一旦变成settled状态,之后就不会再变了。

我们也不能直接拿到Promise的状态,只能通过注册handler的方式,Promise会在恰当的时机调用这些handlerJavaScript Promise可以注册三种handler

  • then 当Promise进入fulfilled状态时会调用此函数
  • catch 当Promise进入rejected状态时会调用此函数
  • finallyPromnise进入settled状态时会调用此函数(无论fulfilled还是rejected

这三个handler函数都会返回一个新的Promise,这个新的Promise跟前面的Promise关联在一起,他的状态取决于前面Promise状态以及当前handler的执行情况。

我们先来看一段代码直观感受下:

function maybeNum() {
  // create a promise
  return new Promise((resolve, reject)=>{
    console.info('Promise Start')
    setTimeout(()=>{
      try{
        const num=Math.random();
        const isLessThanHalf=num<=0.5;
        if(isLessThanHalf){
          resolve(num)
        }else{
          throw new Error('num is grater than 0.5')
        }
      }catch (e) {
        reject(e)
      }
    },100)
    console.info('Promise End')
  })
}

maybeNum().then(value => {
  console.info('fulfilled',value)
}).catch(error=>{
  console.error('rejected',error)
}).finally(()=>{
  console.info('finally')
})
console.info('End')
复制代码

maybeNum函数返回了一个PromisePromise里面我们调用了setTimeout做了一些异步操作,以及一些console打印。

出现的结果类似这样:

Promise Start
Promise End
End
fulfilled 0.438256424793777
finally
复制代码

或者这样:

Promise Start
Promise End
End
rejected Error: num is grater than 0.5 ...
finally
复制代码

我们可以发现,除了setTimeout里的部分,其它都是同步按顺序执行的,所以Promise本身并没有做什么骚操作,它只是提供了一种观察异步逻辑的途径,而不是让我们的逻辑变成异步,比如在这里我们自己实现异步逻辑时还是要通过调用setTimeout

此外,我们还可以通过Promise.resolvePromise.reject来创建Promise

Promise.resolve

Promise.resolve(x)等价于

x instanceof Promise?x:new Promise(resolve=>resolve(x))
复制代码

如果我们传给它的参数是一个Promise,(而不是thenable,关于什么是thenable我们稍后会讲)它会立即返回这个Promise,否则它会创建一个新的Promiseresolve的结果为我们传给它的参数,如果参数是一个thenable,那会视这个thenable的情况而定,否则直接带着这个值进入fulfilled状态。

这样我们就可以很轻松地把一个thenable转换为一个原生的Promise,而且更加方便的是如果有时候我们不确定我们接收到的对象是不是Promise,用它包裹一下就好了,这样我们拿到的肯定是一个Promise

Promise.reject

Promise.reject等价于

new Promise((resolve,reject)=>reject(x))
复制代码

也就是说,不管我们给它什么,它直接用它reject,哪怕我们给的是一个Promise

Thenable

JavaScript Promise的标准来自Promise/A+,,所以JavaScriptPromise符合Promise/A+标准,但是也增加了一些自己的特性,比如catchfinally。(Promise/A+只定义了then

Promise/A+里面有个thenable的概念,跟Promise有一丢丢区别:

  • A “promise” is an object or function with a then method whose behavior conforms to [the Promises/A+ specification].
  • A “thenable” is an object or function that defines a then method.

所以Promisethenable,但是thenable不一定是Promise。之所以提到这个,是因为互操作性。Promise/A+是标准,有不少实现,我们刚刚说过,我们在resolve一个Promise时,有两种可能性,Promise实现需要知道我们给它的值是一个可以直接用的值还是thenable。如果是一个带有thenable方法的对象,就会调用它的thenable方法来resolve给当前Promise。这听起来很挫,万一我们恰好有个对象,它就带thenable方法,但是又跟Promise没啥关系呢? 这已经是目前最好的方案了,在Promise被添加进JavaScript之前,就已经存在很多Promise实现了,通过这种方式可以让多个Promise实现互相兼容,否则的话,所有的Promise实现都需要搞个flag来表示它的PromisePromise

再具体谈谈使用Promise

刚刚的例子里,我们已经粗略了解了一下Promise的创建使用,我们通过then``catch``finally来“hook”进Promisefulfillmentrejectioncompletion阶段。大部分情况下,我们还是使用其它api返回的Promise,比如fetch的返回结果,只有我们自己提供api时或者封装一些老的api时(比如包装xhr),我们才会自己创建一个Promise。所以我们现在来进一步了解一下Promise的使用。

then

then的使用很简单,

const p2=p1.then(result=>doSomethingWith(result))
复制代码

我们注册了一个fulfillment handler,并且返回了一个新的Promise(p2)p2fulfilled还是rejected将取决于p1的状态以及doSomethingWith的执行结果。如果p1变成了rejected,我们注册的handler不会被调用,p2直接变成rejectedrejection reason就是p1rejection reason。如果p1fulfilled,那我们注册的handler就会被调用了。根据handler的执行情况,有这几种可能:

  • doSomethingWith返回一个thenablep2将会被resolve到这个thenable(取决于这个thenable的执行情况,决定p2fulfilled还是rejected
  • 如果返回了其它值,p2直接带着那个值进入fulfilled状态
  • 如果doSomethingWith中途出现throwp2进入rejected状态

这词儿怎么看着这么眼熟?没错我们刚刚介绍resolvereject时就是这么说的,这些是一样的行为,在我们的handlerthrow跟调用reject一个效果,returnresolve一个效果。

而且我们知道了我们可以在then/catch/finally里面返回Promiseresolve它们创建的Promise,那我们就可以串联一些依赖其它异步操作结果且返回Promise的api了。像这样:

p1.then(result=>secondOperation(result))
  .then(result=>thirdOperation(result))
  .then(result=>fourthOperation(result))
  .then(result=>fifthOperation(result))
  .catch(error=>console.error(error))
复制代码

其中任何一步出了差错都会调用catch

如果这些代码都改成回调的方式,就会形成回调地狱,每一步都要判断错误,一层一层嵌套,大大增加了代码的复杂度,而Promise的机制能够让代码扁平化,相比之下更容易理解。

catch

catch的作用我们刚刚也讨论过了,它会注册一个函数在Promise进入rejected状态时调用,除了这个,其他行为可以说跟then一模一样。

const p2=p1.catch(error=>doSomethingWith(error))
复制代码

这里我们在p1上注册了一个rejection handler,并返回了一个新的Promise p2p2的状态将取决于p1跟我们在这个catch里面做的操作。如果p1fulfilled,这边的handler不会被调用,p2就直接带着p1fulfillment value进入fulfilled状态,如果p1进入rejected状态了,这个handler就会被调用。取决于我们的handler做了什么:

  • doSomethingWith返回一个thenablep2将会被resolve到这个thenable
  • 如果返回了其它值,p2直接带着那个值进入fulfilled状态
  • 如果doSomethingWith中途出现throwp2进入rejected状态

没错,这个行为跟我们之前讲的then的行为一模一样,有了这种一致性的保障,我们就不需要针对不同的机制记不同的规则了。

这边尤其需要注意的是,如果我们从catch handler里面返回了一个non-thenable,这个Promise就会带着这个值进入fulfilled状态。这将p1rejection转换成了p2fulfillment,这有点类似于try/catch机制里的catch,可以阻止错误继续向外传播。

这是有一个小问题的,如果我们把catch handler放在错误的地方:

someOperation()
    .catch(error => {
        reportError(error);
    })
    .then(result => {
        console.log(result.someProperty);
    });
复制代码

这种情况如果someOperation失败了,reportError会报告错误,但是catch handler里什么都没返回,默认就返回了undefined,这会导致后面的then里面因为返回了undefinedsomeProperty而报错。

Uncaught (in promise) TypeError: Cannot read property 'someProperty' of undefined
复制代码

由于这时候的错误没有catch来处理,JavaScript引擎会报一个Unhandled rejection。 所以如果我们确实需要在链式调用的中间插入catch handler的话,我们一定要确保整个链路都有恰当的处理。

finally

我们已经知道,finally方法有点像try/catch/finally里面的finally块,finally handler到最后一定会被调用,不管当前Promisefulfilled还是rejected。它也会返回一个新的Promise,然后它的状态也是根据之前的Promise以及handler的执行结果决定的。不过finally handler能做的事相比而言更有限。

function doStuff() {
    loading.show();
    return getSomething()
        .then(result => render(result.stuff))
        .finally(() => loading.hide());
}
复制代码

我们可以在做某件耗时操作时展示一个加载中的组件,然后在最后结束时把它隐藏。我在这里没有去处理finally handler可能出现的错误,这样我代码的调用方既可以处理结果也可以处理错误,而我可以保证我打开的一些副作用被正确销毁(比如这里的隐藏loading)。

细心的同学可以发现,Promise的三种handler有点类似于传统的try/catch/finally:

try{
  // xxx
}catch (e) {
  // xxx
}finally {
  
}
复制代码

正常情况下,finally handler不会影响它之前的Promise传过来的结果,就像try/catch/finally里面的finally一样。除了返回的rejectedthenable,其他的值都会被忽略。也就是说,如果finally里面产生了异常,或者返回的thenable进入rejected状态了,它会改变返回的Promise的结果。所以它即使返回了一个新的值,最后调用方拿到的也是它之前的Promise返回的值,但是它可以把fulfillment变成rejection,也可以延迟fulfillment(毕竟返回一个thenable的话,要等它执行完才行)。

简单来说就是,它就像finally块一样,不能包含return,它可以抛出异常,但是不能返回新的值。

function returnWithDelay(value, delay = 10) {
    return new Promise(resolve => setTimeout(resolve, delay, value));
}
 
// The function doing the work
function work() {
    return returnWithDelay("original value")
        .finally(() => {
            return "value from finally";
        });
}
 
work()
    .then(value => {
        console.log("value = " + value); // "value = original value"
    });
复制代码

这边我们可以看到最后返回的值并不是finally里面返回的值,主要有两方面:

  • finally主要用来做一些清理操作,如果需要返回值应该使用then
  • 没有return的函数、只有return的函数、以及return undefined的函数,从语法上来说都是返回undefined的函数,Promise机制无法区分这个undefined要不要替换最终返回的值

then其实有两个参数

我们目前为止看到的then都是接受一个handler,其实它可以接收两个参数,一个用于fulfillment,一个用于rejection。而且Promise.catch等价于Promise.then(undefined,rejectionHadler)

p1.then(result=>{
  
},error=>{
  
})
复制代码

这个跟

p1.then(result=>{

}).catch(error=>{

})
复制代码

可不等价,前者两个handler都注册在同一个Promise上,而后者catch注册在then返回的Promnise上,这意味着如果前者里只有p1出错了才会被处理,而后者p1出错,以及then返回的Promise出错都能被处理。

解答开头的问题

现在我们知道要提供Promise给外部使用,Promise设计成在外面是没有办法获取resolve函数的,也就改变不了一个已有Promise的状态,我们只能基于已有Promise去生成新的Promise。如果允许异常向外抛出,那我们该怎么恢复后续Promise的执行?比如Promise a出现异常了,异常向外抛出,外面是没办法改变Promise a的数据的。设计成在Promise里面发生任何错误时,都让当前Promise进入rejected状态,然后调用之后的catch handlercatch handler有能力返回新的Promise,提供fallback方案,可以大大简化这其中的复杂度。

工具方法

Promise还提供了一些工具方法,我们可以使用它们来同时处理多个Promise,例如Promise.allPromise.racePromise.allsettledPromise.any,今天我就不一一介绍了,大家感兴趣的可以自行了解一下。

写在结尾

Promise的出现,让我们:

  1. Promise提供了标准的方式来处理结果
  2. Promisethen返回新的Promise,可以多个串联,达到注册多个回调的效果
  3. 对于已经完成的异步操作,我们后来注册的then也能被调用
  4. 我们只能通过executor函数提供的两个函数来改变Promise的状态,没有其他办法可以resolve或者reject Promise,而且这两个方法也不存在于Promise本身,所以我们可以把我们的Promise对象给其他人去使用,比如我们提供给外部一个api,以Promise返回,可以放心地让外部通过Promise来观察最终的结果,他们也没办法来改变Promise的状态。
  5. 可以实现统一的同时处理多个Promise的逻辑

而且,我在本文开头提到过,回调地狱有两个问题是:

  • 向已经完成的操作添加回调并没有统一的标准
  • 很难向某个操作添加多个回调

这些都被Promise的标准解决了,标准确保了两件事:

  • handler一定会被调用
  • 调用是异步的

也就是说,如果我们获取到了其它api提供的Promise,有了类似如下的代码:

console.log('before')
p1.then(()=>{
  console.log('in')
})
console.log('after')
复制代码

标准确保了,执行结果是before,然后是after,最后是(在p1变成fulfilled状态或者已经变成fulfilled状态时)in。如果Promise在经过一段时间之后才变成fulfilled,这个handler也会被往后调度。如果Promise已经变成fulfilled了,那fulfillment handler会被立即调度(不是立即执行),调度指的是被加入微任务队列,确保这些handler被异步调用大概是Promise唯一让同步代码被异步调用的情形了。

Promise推出也好多年了,我们日常开发中已经离不开它了,即使是async await背地里还是在跟它打交道,希望本文带给大家对Promise更全面的认识,当然了,关于Promise还有一些最佳实践跟反模式,由于篇幅的原因下次再见啦,Happy coding~

 

大厂面试题分享 面试题库

前端面试题库 (面试必备)   推荐:★★★★★

地址:前端面试题库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129430.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【pandas】教程:7-调整表格数据的布局

Pandas 调整表格的布局 本节使用的数据为 data/titanic.csv&#xff0c;链接为 pandas案例和教程所使用的数据-机器学习文档类资源-CSDN文库 导入数据 import pandas as pd titanic pd.read_csv("data/titanic.csv") titanic.head()PassengerId Survived Pclas…

传感器工作原理以及传感器种类详解

随着物联网时代的到来&#xff0c;现代信息技术快速发展&#xff0c;其中包含了计算机技术、通信技术和传感器技术等&#xff0c;计算机相当于人类的大脑&#xff0c;通信技术类似人体的神经&#xff0c;而传感器就等同于人的感觉器官。从广义上说&#xff0c;传感器就是一种能…

存储控制器

存储控制器是按照一定的时序规则对存储器的访问进行必要控制的设备&#xff0c;包括地址信号、数据信号以及各种命令信号的控制&#xff0c;使主设备(访问存储器的设备)能够根据自己的要求使用存储器上的存储资源。 存储控制器的作用主要就是进行接口的转换&#xff0c;将主设…

通信原理 | 一些常用的概念记录

这篇文章只是记录平时了解到的一些概念,并没有针对性,比较杂乱,纯粹就是当做笔记本用的,各位看官请在茶余饭后的休闲时间阅读最为合适了解到新的概念的话,会随时更新世界四大导航系统 世界上有四大卫星导航系统,它们分别是美国的GPS、俄罗斯的格洛纳斯卫星导航系统、欧盟…

尚医通-SpringBoot整合MongoDB(十七)

目录&#xff1a; &#xff08;1&#xff09;MongDB-SpringBoot整合-MongoTemplate操作 &#xff08;2&#xff09;MongoTemplate操作2 &#xff08;3&#xff09;MongoTemplate操作3 &#xff08;4&#xff09;MongoDB-SpringBoot整合-MongoRepository操作 &#xff08;1&a…

FIIL、南卡、漫步者蓝牙耳机怎么选?国产半入耳蓝牙耳机推荐

随着 TWS耳机市场的发展&#xff0c;越来越多的手机厂商&#xff0c;新晋的品牌&#xff0c;甚至是老牌的音频品牌都加入到了 TWS耳机的行列中&#xff0c;让消费者的选择范围变得更大。当前热销的南卡小音舱、漫步者Lolli3、FIIL CC2蓝牙耳机都是目前受消费者欢迎的&#xff0…

pyqt 显示图片的若干方法

date: 2022-11-30 14:23 status: public title: ‘pyqt 显示图片的若干方法’ 单张图片 使用lable 显示图片 特点是最简单&#xff0c;但功能也最少。 #!/usr/bin/env python # -*- coding: utf-8 -*- import sysfrom PyQt5.QtGui import QPixmap from PyQt5.QtWidgets impor…

JAVAGUI编程初识之AWT

文章目录前言一 GUI编程简介二 AWT简介2.1 组件(Component)和容器(Container)2.2 Frame2.2.1 演示1-创建一个窗口2.2.2 演示2-多个窗口的创建2.3 Panel2.3.1 演示-Panel使用三 布局管理3.1 布局管理器之FlowLayout3.1.1 FlowLayout简介3.1.2 演示-FlowLayout使用3.2 布局管理器…

电子签章结构以及规范讲解

前言&#xff1a; 安全电子签章是通过采用PKI公钥密码技术&#xff0c;将数字图像处理技术与电子签名技术进行结合&#xff0c;以电子形式对加盖印章图像数据的电子文档进行数字签名&#xff0c;以确保文档来源的真实性以及文档的完整性&#xff0c;防止对文档未经授权的篡改&…

算法训练 —— 链表(1)

目录 1. LeetCode203.移除链表元素 2. LeetCode21.合并两个有序链表 3. LeetCode206.翻转链表 4. LeetCode707.设计链表 1. LeetCode203.移除链表元素 移除链表元素 题解&#xff1a;通过两个指针来控制&#xff0c;cur和prev&#xff1b;cur指针去找val&#xff0c;prev…

冰冰学习笔记:位图与布隆过滤器

欢迎各位大佬光临本文章&#xff01;&#xff01;&#xff01; 还请各位大佬提出宝贵的意见&#xff0c;如发现文章错误请联系冰冰&#xff0c;冰冰一定会虚心接受&#xff0c;及时改正。 本系列文章为冰冰学习编程的学习笔记&#xff0c;如果对您也有帮助&#xff0c;还请各位…

MySQL复制技术方案——半同步复制配置

Google为MySQL和InnoDB设计了一个大规模补丁集以量身打造服务器和存储引擎。其中一个修补程序可用于MySQL5.0版本&#xff0c;是半同步的复制补丁。MySQL已经打上了该补丁并在MySQL5.5中发布了。 半同步复制的理念是在允许更改操作继续执行前&#xff0c;确保更改操作至少被写…

34、基于STM32的电子时钟设计(DS1302)时钟、秒表、倒计时(Proteus仿真+程序)

编号&#xff1a;34 基于STM32的电子时钟设计&#xff08;DS1302&#xff09;时钟、秒表、倒计时 功能描述&#xff1a; 本系统由STM32F103系统LCD1602液晶显示按键模块DS1302时钟模块声光报警模块组成。 1、使用LCD1602显示当前日期、时间、星期 2、具有闹钟、倒计时、计时功…

【Java寒假打卡】Java基础-抽象类

【Java寒假打卡】Java基础-抽象类一、概述二、抽象类注意事项三、模板设计模式四、final关键字五、代码块一、概述 抽象方法&#xff1a;将共性的方法抽取到父类之后&#xff0c;发现该方法的实现逻辑无法在父类中给出具体明确&#xff0c;该方法就可定义为抽象方法抽象类&…

【C++初阶8-vector】熟悉的ta

前言 本期看看这位熟悉又陌生的朋友——vector。 博主水平有限&#xff0c;不足之处望请斧正&#xff01; 是什么 vecotr是序列容器&#xff0c;可变大小的数组。 *vector有矢量、向量的意思&#xff0c;用其命名可能想强调“序列”这个概念。 class template std::vecto…

独占指针 std::unique_ptr

学习智能指针之前需要知道的&#xff1a; 智能指针是原始指针的封装&#xff0c;在头文件<memory>中&#xff0c;优点就是自动分配内存&#xff0c;不用担心潜在的内存泄漏。不是所有的指针都可以封装成智能指针&#xff0c;很多时候原始指针更方便。各指针中&#xff0…

Webpack中常见的Loader?解决了什么问题?

一、是什么 loader 用于对模块的源代码进行转换&#xff0c;在 import 或"加载"模块时预处理文件 webpack做的事情&#xff0c;仅仅是分析出各种模块的依赖关系&#xff0c;然后形成资源列表&#xff0c;最终打包生成到指定的文件中。如下图所示&#xff1a; 在web…

【网络安全】——web渗透的前缀知识

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门 创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座…

ArcGIS基础实验操作100例--实验18合并表格

本实验专栏来自于汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验18 合并表格 目录 一、实验背景 二、实验数据 三、实验步骤 方法一&#xff1a;导出…

whisper

Robust Speech Recognition via Large-Scale Weak Supervision 介绍 大规模弱监督的训练。先前的方法都是通过大量的无监督学习训练&#xff08;无监督的数据容易收集&#xff0c;所以通过大量无监督的学习可以训练出一个质量较好的encoder&#xff09;。但是用的时候还需要找…