滴滴开源小程序框架 Mpx 新特性:局部运行时能力增强

Mpx 是滴滴开源的一款增强型跨端小程序框架,自 2018 年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在滴滴内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了滴滴内部全量小程序业务开发,成为了滴滴小程序开发的统一技术标准。

本文主要探讨MPX局部运行时能力增强的方案设计。如需深入了解滴滴开源项目MPX,请参阅相关文章:

滴滴开源小程序框架Mpx,致力于提高小程序开发体验

滴滴出行小程序体积优化实践

滴滴小程序框架Mpx发布2.0:可直接转换已有微信小程序

小程序框架Mpx的下一代脚手架升级之路|滴滴开源

滴滴小程序开发标准 Mpx 推出新版本,聚焦性能与包体积优化

目前在小程序社区当中存在两种小程序上层框架技术方向:

  1. 以 Mpx 为代表的重编译框架,源码采用小程序平台的dsl,编译后直接产出满足小程序规范的代码

  2. 以 Taro@3.x 为代表的重运行时框架,源码可以使用 Vue、React 作为上层dsl,在运行时当中注入框架代码,同时 polyfill 掉小程序在逻辑线程当中并不能访问的 Dom、Bom 等相关 api

由于技术选型和设计思路的差异,小程序框架的发展及开发者实际开发过程中面临的问题侧重点也有所不同。

对于小程序框架自身的迭代:

  • 重运行时框架已具备现代Web框架开发的效率和体验,主要解决Web标准与上层框架和小程序平台规范的差异性和兼容性问题

  • 重编译框架遵循小程序平台的开发规范,核心在于增强小程序能力,提高开发效率和体验,尽量与现代Web框架能力对齐

Mpx 正是在小程序能力增强的设计思路下,努力与现代 Web 框架的开发体验和能力拉齐,以全面提升用户的开发效率、体验及小程序性能。例如,近两年发布了两个 minor 版本,分别支持了 webpack5 持久化缓存来大幅提高编译构建速度以及 composition-api 特性,让用户能够使用当下最热门且备受好评的开发范式。

在实际业务开发过程中,如果想要使用 props 透传、默认插槽、事件透传等现代 Web 框架提供的功能,但由于小程序自身框架设计未支持这些特性,开发者需要编写大量冗余代码才能达到相同的效果。因此,为了尽可能与现代 Web 框架能力拉齐,我们需要在运行时层面进行能力增强,以提高用户的开发效率和体验。这就是接下来要介绍的 mpx 局部运行时方案设计。

方案设计

在整体的技术方案设计上还是按照小程序能力增强的思路进行,以最小的组件粒度来按需开启运行时渲染。

bee6f53b6272f5a3a20e6a254508ed7d.png

在具体实现上分为运行时和编译两部分内容。

在编译阶段主要完成:

  1. Render Function 增强

  2. 基础模板按需生成

  3. 胶水代码注入

在运行时阶段主要完成:

  1. propertiesToComputed

  2. vdom tree 生成&视图渲染

  3. 组件实例上下维护

  4. 事件代理&分发

aaff474fb8a97ac3b265f048bee7e769.png

核心模块设计

render 函数增强

Mpx 在视图层处理上严格遵守小程序开发规范:一个Mpx SFC文件经过编译后,其处理完成的template将被输出为符合小程序语法规范的wxml静态文件。

在性能优化方面,Mpx 引入了 render 函数。目前,render 函数的主要功能是收集响应式数据。在组件初次渲染阶段,调用 render 函数完成响应式数据的收集。当响应式数据发生变化时,重新触发 render 函数的执行,完成变更后数据的收集,然后进行数据 diff,最后通过 setData 完成页面UI视图的更新。render 函数的形式如下:

global.currentInject = {moduleId: "m3f28ff60",render: function () {this._c("message", this.message);({ tap: [["onViewTapBubble", 'b', "__mpx_event__"]] });this._i(this._c("navigatorList", this.navigatorList), function (item, index) {({ tap: [["jumpTo", item]] });item.name;});this._r();}
}

在运行时增强方案中,Mpx SFC 文件经过编译后,对于 template 处理最大的不同在于经过编译后是处理为一个执行后会生成 vdom tree 的 render 函数。

在组件初次渲染过程中,执行这个 render 函数,不仅完成了响应式数据的收集,还生成了 vdom tree 。这个 vdom tree 描述了 template 的内容,并将它注入到运行时的 render 函数中。因此,在页面的初次渲染阶段,setData 设置的数据是整个 vdom tree ,它描述了整个模板视图的内容。然后通过自定义容器组件 mpx-custom-element 的递归渲染来完成整个组件的渲染。

global.currentInject = {moduleId: 'm3f28ff60',render: function () {var vnode = this.__c('block', [this.__c('view',{class: this.__sc('root', undefined),mpxbindevents: {...},eventconfigs: {...}},[this._i(this._c('navigatorList', this.navigatorList),function (item, index) {return this.__c('view',{...},[...])})])])this._r(vnode)}
}

在组件初次渲染时,通过 setData 传递到渲染层的数据是全量的 vdom tree。当组件响应用户操作或接口请求导致组件实例上的响应式数据发生变化时,会触发 Render Function 的重执行,以生成一个新的 vdom tree。考虑到逻辑层与渲染层通信的成本和性能,这涉及到前后两次 vdom tree 的 diff。最终,生成具体的 vdom 路径数据更新内容,并通过 setData 传递到渲染层,完成视图的局部更新。

mpx-custom-element 容器组件

mpx-custom-element.mpx 是 mpx 内置的一个自定义组件,主要用于运行时渲染。其主要功能有两个:

  1. 作为基础容器组件承载模板渲染:mpx-custom-element.mpx 主要作为一个渲染容器,接收组件传递的需动态渲染的 composed tree 数据,并在其上下文中完成真实组件的渲染。需要注意的是,mpx-custom-element.mpx 本身也是一个基于模板动态渲染的组件,其最终编译输出的内容包含 wxml(视图层)/wxss(样式)/js(逻辑层)/json(配置)。

  2. 跳脱微信小程序模板递归渲染的限制:由于微信小程序的 template 能力不支持模板的自引用递归渲染,因此需要借助一层自定义组件容器来绕开这一限制,从而提供新的渲染上下文。

基础模板

所有模板动态化组件的视图都是基于 mpx-custom-element.mpx 提供的基础模板 mpx-custom-element.wxml 进行动态渲染的。在组件的编译阶段,会按需收集使用到的组件和属性,并将其输出到基础模板中,以确保基础模板的包体积最小化。

组件的 Render Function 执行后生成的 vdom tree 描述了组件的视图层结构和样式。最终,通过组件实例的 setData 方法将 vdom tree 传递到视图层,并利用这些基础模板进行动态渲染。

<template is="tmpl_0_container" wx:if="{{r && r.nodeType}}" data="{{ i: r }}"></template><template name="mpx_tmpl"><element r="{{r}}" wx:if="{{r}}"></element>
</template><template name="tmpl_0_block"><block wx:for="{{i.children}}" wx:key="index"><template is="tmpl_1_container" data="{{i:item}}" /></block>
</template><template name="tmpl_0_view"><view class="{{i.data.class}}" bind:tap="__invoke" data-eventconfigs="{{i.data.dataEventconfigs}}" style="{{i.data.style}}"  data-mpxuid="{{i.data.uid}}"><block wx:for="{{i.children}}" wx:key="index"><template is="tmpl_1_container" data="{{i:item}}" /></block></view>
</template>...

事件系统

小程序采用双线程架构(视图层+逻辑层),在其事件系统设计中:

  • 事件是视图层到逻辑层的通讯方式

  • 事件可以将用户的行为反馈到逻辑层进行处理

  • 事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数

同时,小程序的事件模型(事件冒泡、事件捕获)在视图层进行声明,而非在逻辑代码层通过 API 调用的形式实现,这也是小程序的事件系统和 Web 事件系统的最大差异点。

由于小程序没有真实的 DOM/BOM,其他运行时方案中的事件系统需要基于 vdom tree(vdom tree 上绑定了不同节点的事件处理类型&函数)按照浏览器的事件机制实现相关的事件流程(事件冒泡、事件捕获)。在整个事件系统处理的方案中,完全由运行时接管事件触发的生命周期。简单总结流程如下:

  • 由小程序的视图层接收事件的触发

  • 视图层与逻辑层通信,将用户行为反馈到逻辑层处理

  • 运行时代码模拟整个事件系统生命周期

<viewbindtap="__invoke"bindtouchstart="__invoke"bindtouchmove="__invoke"bindtouchend="__invoke"bindtouchcancel="__invoke"bindlongpress="__invoke"></view>

可以看到在基础模板上,我们只对普通的事件如 tap、touchstart 等进行了绑定(在模板上没有绑定其他特殊的事件类型),同时每个事件使用__invoke 代理方法统一响应微信小程序触发的事件。在其他运行时方案(以 Kbone 为例)中,运行时代码实现了一整套事件系统,在__invoke 代理方法内部根据实际的事件类型触发相应的事件,并根据 vdom tree 递归寻找事件触发的上层节点,从而模拟出事件冒泡的流程,事件捕获的流程同理。

然而,在 mpx 运行时能力增强方案中,整个方案设计的出发点是按组件粒度使用。如果事件系统也采用这种运行时模拟事件生命周期的方式,将无法很好地与微信小程序的事件机制贴合在一起:主要体现在事件的生命周期无法统一(一个是视图层的声明式,另一个是逻辑代码层的 API 形式)。因此,在 mpx 运行时能力增强方案中,针对事件系统的设计仍然遵循微信小程序的事件系统设计:视图层的声明式。我们不单独实现一套基于 vdom tree 的事件系统。

为了实现这一目标,在运行时组件的模板编译过程中,我们会收集那些使用了 catch、capture-bind、capture-catch 特殊事件生命周期的节点。这些节点最终会被注入到 mpx-custom-element.wxml 基础模板中,同时会根据节点的属性生成对应的 template 模板内容。这样,最终运行时组件的事件生命周期也与微信小程序保持一致。虽然使用了特殊事件生命周期的节点会被注入到基础模板中,导致 mpx-custom-element.wxml 基础模板的最终体积有所增加,但在我们实际使用的场景中,例如乘客主小程序里使用特殊场景事件的情况少之又少,所以这部分的基础模板体积增加是完全可接受的。

性能相关

对于运行时方案来说,小程序的渲染性能是需要特别关注的地方。相较于编译型框架,随着项目复杂度的提升,运行时方案的整体渲染性能表现可能会较差。

有几个比较典型的场景:

  • 页面初次渲染:对于 mpx 项目,最终页面的模板存在于视图层,渲染只需将模板中使用到的数据传递到视图层。而在运行时方案中,逻辑层需要将全量的 vdom tree 数据传递到渲染层驱动基础模板的渲染,因此整体的数据量会大得多。

  • 页面二次更新渲染:对于 mpx 项目,基于 Render Function 的数据 diff 最终只会收集发生变化的 key 以及 value。然而,对于运行时方案,例如使用 Vue 的 Taro 项目,如果响应式数据发生变化,重新触发 Render Function 执行及后续的 diff 操作,需要准确找到数据发生变更的 vdom 节点信息,因此传输的数据量相较于编译型框架而言也会更多。

在 mpx 运行时渲染中,同样会遇到这些性能问题,但在具体的实现过程中,我们会尽量将一些对性能有影响的点降到最低:

  • 首先,在运行时方案的整体策略上,mpx 运行时的能力是按需的,做到局部开启组件的运行时特性。因此,对于一个页面而言,可以部分走原有的编译方案,部分走运行时的方案。

  • 其次,相较于纯运行时方案,它们的基础模板基本要做到全量的输出,而在 mpx 局部方案中,我们是按需完成基础模板的收集和输出,同时对于模板节点的属性、事件绑定也能做到按需。

小程序基于运行时的能力拓展

对于运行时方案来说,它提供了一种新的逻辑层控制视图层渲染的方式。从某种程度上说,小程序的运行时渲染可以看作是对原生小程序能力的增强,这为小程序的能力拓展带来了更多的可能性。

例如,React 提出的 Server Components,通过调试官方提供的 demo,我们可以大致了解其工作流程。简单来说,前端通过接口(props数据)驱动后端 Server Components 的执行,生成描述前端页面结构的 ast json,然后这部分数据再驱动前端完成最终的视图渲染。

使用 Server Components 会带来以下好处:

  • Zero-Bundle-Size Components(在 server 端运行的组件及其纯 js 依赖最终并不会被下载到浏览器当中运行)

  • loading performance(Server Components 可以直接访问后端的服务or database,对于一些场景可以减少网络请求数量,同时把一些纯计算的工作可以放到 server)

那么,在小程序场景下,借助运行时的能力,我们是否可以尝试实现类似 Server Components 的功能呢?暂时抛开工程方面的问题,针对目前小程序的技术架构,可能会遇到以下的问题。

视图渲染

在小程序场景下,视图层渲染有两种方案:一种是原生小程序的渲染流程,另一种是运行时渲染流程。以 mpx 运行时能力增强为例,编译环节将 template 转换为可执行的 Render Function,运行后生成描述视图的 vdom tree,然后驱动视图层的基础模板进行渲染。在这个过程中,Render Function 的执行实际上是由 props/data 的变化驱动的,与平台和环境(例如不依赖小程序提供的API)无关。因此,Render Function 也可以在 server 端执行,生成描述视图的 vdom tree。

既然在 server 端可以实现与原本在小程序环境下相同的效果,那么小程序通过接口获取到这部分数据后,后续的流程就可以交给运行时方案接管,完成最终的渲染。

4830fb9ce9ed0d6420f4eb7d31ce9477.png

样式规则

对于 wxss 样式规则而言,需要注意的是:在小程序的技术架构中,开发者工具在编译构建阶段会将 wxss 和 wxml 提前处理为可执行的函数,这些代码最终在小程序运行时被注入到视图层执行。

然而,在 Server Components 中,样式规则不再是静态数据,而是由业务后端接口下发。这些数据最初存在于逻辑线程中。为了使逻辑线程中的样式规则能够在视图层生效,有且仅有一个方案就是在 Render Function 执行完生成 vdom tree 后将样式规则通过 inline 的方式合并到最终需要被渲染的 composed tree 当中。       

aa1a146c85a06b3ea65eec8e64e586c3.png

组件逻辑

对于一个组件来说,通常包括由外部传入的 props、自身的状态、生命周期、事件处理(用户交互)等方面。首先,我们可以明确的是,小程序组件的生命周期脱离了小程序平台是无法正常运行的,同时事件也需要在用户发生交互行为后才能触发。因此,对于组件本身而言,这部分内容无法像视图层渲染那样将js代码存放在远端并通过接口/cdn 的形式下发,而只能随着小程序的上线发布存在于逻辑线程中。这样,组件的生命周期和事件交互等方法可以与原有的执行策略保持一致,确保其正常工作。对于事件的处理,与运行时方案一致:通过事件代理的形式来分发真实的用户操作。

实际上,这在 server components demo 中也可以清晰地看到不同类型的组件的职责划分非常明确:

  • 对于 x.server 组件而言:由 props 驱动(props 来源可以是 client 的请求,也可能是服务端访问其他服务或 database),不涉及组件自身状态的维护、生命周期、用户事件交互,只做纯渲染工作;

  • 对于 x.client 组件而言:在浏览器中执行的代码与普通组件无异,可以进行任何操作。

server components 和 client components 混合渲染

在 RFC 中也提到了 server components 和 client components 相互引用的场景。其中,server components 最终在服务器端运行并生成 vdom tree,而 client components 则仅通过一个组件标识下发,其实例的创建在浏览器端执行。

在小程序运行时方案中,也存在类似的场景:部分组件采用运行时渲染,这些组件可能会引用原生的小程序组件,这意味着组件内部存在两种不同的组件渲染方式。对此,我们有相应的解决方案:将原生的小程序组件作为依赖注册到基础模板容器组件中,基础模板最终也需要生成原生组件。

结语

Mpx 作为一款兼具优秀开发体验和深度性能优化的增强型跨端小程序框架,我们一直致力于在此领域持续前进,同时也诚邀大家加入 Mpx 用户群,共同参与和交流。

40e6bada16a0aec9bd85a6f3dc4b898d.jpeg

1504e2dfb0e2c0bd9ebb941e49d311e9.gif

点阅读原文,跳转项目官网

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

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

相关文章

NVIDIA Isaac ROS - 入门教程(一)

系列文章目录 前言 欢迎访问 Isaac ROS&#xff0c;这是一套硬件加速、高性能、低延迟的 ROS 2 软件包&#xff0c;用于制造自主机器人&#xff0c;充分利用 Jetson 和其他英伟达平台的强大功能。 查看入门指南&#xff0c;开始使用 Isaac ROS。设置 Isaac ROS Buildfarm&…

如何让思维系统化,拥有「自己的想法」?

先思考一个问题&#xff1a; 如果外星人来访地球&#xff0c;你觉得他们更有可能是善意的&#xff0c;还是恶意的&#xff1f; 你的反应是什么&#xff0c;会如何切入思考&#xff1f; 一般人可能会这样回答&#xff1a; 当然是恶意的&#xff0c;因为要争夺资源啊&#xff0c;…

25考研政治备考计划

各位小伙伴大家好&#xff0c;今天给大家分享的是25考研政治复习备考计划。 政治没有基础阶段&#xff0c;直接就是强化&#xff0c;强化的内容也就是听课&#xff0c;刷题。 【时间安排】 *7-9月中 徐涛老师或腿姐强化课&#xff0c;推荐刷肖1000 *9月中-10月中 背腿姐的背…

ELK+Filebeat 部署实验

Filebeat是轻量级的开源日志文件数据搜集器。通常在需要采集数据的客户端安装 Filebeat&#xff0c;并指定目录与日志格式&#xff0c;Filebeat 就能快速收集数据&#xff0c;并发送给 logstash 进行解析&#xff0c;或是直接发给 Elasticsearch 存储&#xff0c;性能上相比运行…

MySQL介绍、安装和卸载

MySQL入门——网安基础 1. 数据库基本概念2. 数据库类型和常见的关系型数据库2.1 数据库类型2.2 常见的关系型数据库 3. MySQL介绍4. MySQL8的安装和卸载 1. 数据库基本概念 1. 数据 所谓数据&#xff08;Data&#xff09;是指对客观事物进行描述并可以鉴别的符号&#xff0c;…

万物简单AIoT 端云一体实战案例学习 之 空气质量检测系统

学物联网,来万物简单IoT物联网!! 下图是本案的3步导学,每个步骤中实现的功能请参考图中的说明。 1、简介 环境污染、空气污染是人类一直所关心并且讨论的永恒话题,人们对优质的环境和健康的身体非常向往。因此,如果有一种可以检测周围环境的空气质量的设备并且环境数据…

RocketMQ源码分析之消息重试(真相竟然是延时消息)

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 RocketMQ版本 5.1.0 普通消息 消息重试的的实现分并普通消息和顺序消息。两者的重试机制大同小异。我们这里先看看不同消息 这里是官网定义的消息重试次数以及…

docker 存储管理

文章目录 docker 存储管理容器存储方案docker 容器存储解决方案 docker 存储驱动基本概述存储驱动的选择原则主流的 docker 存储驱动docker 版本支持的存储驱动 overlay2 存储驱动OverlayFSoverlay2 存储驱动要求配置 docker 使用 overlay2 驱动 overlay2 存储驱动的工作机制Ov…

Linux的 .bashrc 有什么作用?

一、.bashrc 是什么? 有什么用&#xff1f; .bashrc是一个存储在你的home目录下的隐藏文件&#xff0c;它用来配置和自定义你的终端环境和行为。 每次你启动一个新的终端时&#xff0c;.bashrc文件就会被执行&#xff0c;加载你设置的环境变量&#xff0c;别名&#xff0c;函数…

林浩然矩阵江湖历险记

林浩然矩阵江湖历险记 Lin Haoran’s Matrix Adventures 在那充满神秘色彩的矩阵世界里&#xff0c;林浩然面对的挑战是驯服一个具有六个个性元素的23矩阵——“小三儿”。这个矩阵由两行三列组成&#xff0c;每一个元素都像是棋盘上的一枚棋子&#xff0c;它们紧密排列在一起&…

朴素贝叶斯分类算法

1.分类算法 分类算法是有监督学习的一个核心问题&#xff0c;他从数据中学习一个分类决策函数或分类模型&#xff0c;对新的输入进行预测&#xff0c;输出变量取有限个离散值。 &#x1f30d;分类算法的内容是要求给定特征&#xff0c;让我们得出类别。 那么如何由指定特征&…

【C++】入门基础

前言&#xff1a;C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加了许多有用的库&#xff0c;以及编程范式等。熟悉C语言之后&#xff0c;对C学习有一定的帮助&#xff0c;因此从今天开始们将进入&#xff23;的学习。 &#x1f496; 博主CSDN主页:…

如何阅读xml电子发票

xml电子发票是官方给出的电子存档的文件格式&#xff0c;本质是文本&#xff0c;所以文件很小&#xff0c;大量发票存储&#xff0c;能够更加凸显优势。 但是xml电子发票不方便阅读&#xff0c;因为里面是xml格式&#xff0c;对于财务人员来讲&#xff0c;看“代码”简直太难了…

Linux报 “no route to host” 异常 ping: sendmsg: No route to host

公司有台服务器迁移机房后跟另一台服务器相互ping不通&#xff0c;但是两台服务器都能上网能ping其他机器&#xff0c;其他机器都能ping通这两台服务器。检查两台服务器没有防火墙规则拦截&#xff0c;交换机上也没检查到acl过滤。 下图是迁移机房的服务器ping截图 下图是nfs服…

【云原生】认识docker容器操作命令

目录 一、容器操作命令 1、创建容器 2、删除容器以及停止容器运行 3、查看容器的运行状态 4、查看容器的详细信息 5、将容器的文件传输到宿主机以及将宿主机的文件传输到容器中 6、批量删除容器 7、进入容器 二、容器的迁移 1、先在容器中创建测试文件 2、将容器存储…

Scrum敏捷研发管理全流程/scrum管理工具

Leangoo领歌是一款永久免费的专业的敏捷开发管理工具&#xff0c;提供端到端敏捷研发管理解决方案&#xff0c;涵盖敏捷需求管理、任务协同、进展跟踪、统计度量等。 Leangoo领歌上手快、实施成本低&#xff0c;可帮助企业快速落地敏捷&#xff0c;提质增效、缩短周期、加速创新…

【详解】贪吃蛇游戏----上篇(介绍控制台和API等知识)

目录 知识点&#xff1a; Win32 API 宽字符的打印 控制台操作&#xff1a; &#xff08;1&#xff09;调整控制台大小 &#xff08;2&#xff09;控制台屏幕上的坐标COORD GetStdHandle GetConsoleCursorInfo CONSOLE_CURSOR_INFO SetConsoleCursorInfo SetConsoleC…

Cesium工具应用

文章目录 0.引言1.场景截图2.卷帘对比3.反选遮罩4.鹰眼视图5.指南针与比例尺6.坐标测量7.距离测量8.面积测量9.热力图10.视频投影11.日照分析12.淹没分析13.通视分析14.可视域分析15.缓冲区分析16.地形开挖17.要素聚合18.开启地下模式19.开启等高线20.坡度坡向21.填挖方量计算2…

排序(插入排序)

现在&#xff0c;我们学习了之前数据结构的部分内容&#xff0c;即将进入一个重要的领域&#xff1a;排序&#xff0c;这是一个看起来简单&#xff0c;但是想要理清其中逻辑并不简单的内容&#xff0c;让我们一起加油把&#xff01; 排序的概念及其运用 排序的概念 排序&…

解释性人工智能(XAI)—— AI 决策的透明之道

在当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为我们生活中不可或缺的一部分。AI 系统的决策和行为对我们的生活产生了深远的影响&#xff0c;从医疗保健到金融服务再到自动驾驶汽车。 然而&#xff0c;有时候 AI 的决策似乎像黑盒子一样难以理解&am…