用一个RecyclerView实现二级评论

news2024/12/27 10:56:58

先上个效果图(没有UI,将就看吧),写代码的整个过程花了4个小时左右,相比当初自己开发需求已经快了很多了哈。

给产品估个两天时间,摸一天半的鱼不过分吧(手动斜眼)

需求拆分

这种大家常用的评论功能其实也就没啥好拆分的了,简单列一下:

  • 默认展示一级评论和二级评论中的热评,可以上拉加载更多。
  • 二级评论超过两条时,可以点击展开加载更多二级评论,展开后可以点击收起折叠到初始状态。
  • 回复评论后插入到该评论的下方。

技术选型

前面我在给掘友的评论中,也提到了技术选型的要点:

单RecyclerView + 多ItemType + ListAdapter

这是基本的UI框架。

为啥要只用一个RecyclerView?最重要的原因,就是在RecyclerView中嵌套同方向RecyclerView,会有性能问题和滑动冲突。其次,当下声明式UI正是各方大佬推崇的最佳开发实践之一,虽然我们没有使用声明式UI基础上开发的Compose/Flutter技术,但其构建思想仍然对我们的开发具有一定的指导意义。我猜测,androidx.recyclerview.widget.ListAdapter可能也是响应声明式UI号召的一个针对RecyclerView的解决方案吧。

数据源的转换

数据驱动UI

既然选用了ListAdapter,那么我们就不应该再手动操作adapter的数据,再用各种notifyXxx方法来更新列表了。更提倡的做法是,基于data class的**浅拷贝

**,用Collection操作符对数据源的进行转换,然后将转换后的数据提交到adapter。为了提高数据转换性能,我们可以基于协程进行异步处理。

要点::

  • 浅拷贝

低成本生成一个全新的对象,以保证数据源的安全性。

data class Foo(val id: Int, val content: String)

val foo1 = Foo(0, "content")
val foo2 = foo1.copy(content = "updated content")
  • Collection操作符

Kotlin中提供了大量非常好用的Collection操作符,能灵活使用的话,非常有利于咱们向声明式UI转型。

前面我提到了groupByflatMap这两个操作符。怎么使用呢?

以这个需求为例,我们需要显示一级评论、二级评论和展开更多按钮,想要分别用一个data class来表示,但是后端返回的数据中又没有“展开更多”这样的数据,就可以这样处理:

// 从后端获取的数据List,包括有一级评论和二级评论,二级评论的parentId就等于一级评论的id
val loaded: List<CommentItem> = ... 
val grouped = loaded.groupBy { 
    // (1) 以一级评论的id为key,把源list分组为一个Map<Int, List<CommentItem>>
    (it as? CommentItem.Level1)?.id ?: (it as? CommentItem.Level2)?.parentId
    ?: throw IllegalArgumentException("invalid comment item")
}.flatMap { 
    // (2) 展开前面的map,展开时就可以在每级一级评论的二级评论后面添加一个控制“展开更多”的Item
    it.value + CommentItem.Folding(
        parentId = it.key,
    )
}
  • 异步处理

前面我们描述的数据源的转换过程,在Kotlin中,可以简单地被抽象为一个操作:

List<CommentItem>.() -> List<CommentItem>

对于这个需求,数据源转换操作就包括了:分页加载,展开二级评论,收起二级评论,回复评论等。按照惯例,抽象一个接口出来。既然我们要在协程框架下进行异步处理,需要给这个操作加一个suspend关键字。

interface Reducer {
    val reduce: suspend List<CommentItem>.() -> List<CommentItem>
}

为啥我给这个接口取名Reducer?如果你知道它的意思,说明你可能已经了解过MVI架构了;如果你还不知道它的意思,说明你可以去了解一下MVI了。哈哈!

不过今天不谈MVI,对于这样一个小Demo,完全没必要上架构。但是,优秀架构为我们提供的代码构建思路是有必要的!

这个Reducer,在这里就算是咱们的小小业务架构了。

  • 异步2.0

前面谈到异步,我们印象中可能主要是网络请求、数据库/文件读写等IO操作。

这里我想要延伸一下。

ActivitystartActivityForResult/onActivityResultDialog的拉起/回调,其实也可以看着是异步操作。异步与是否在主线程无关,而在于是否是实时返回结果。毕竟在主线程上跳转到其他页面,获取数据再回调回去使用,也是花了时间的啊。所以在协程的框架下,有一个更适合描述异步的词语:挂起(suspend)

说这有啥用呢?仍以这个需求为例,我们点击“回复”后拉起一个对话框,输入评论确认后回调给Activity,再进行网络请求:

class ReplyDialog(context: Context, private val callback: (String) -> Unit) : Dialog(context) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.dialog_reply)
        val editText = findViewById<EditText>(R.id.content)
        findViewById<Button>(R.id.submit).setOnClickListener {
            if (editText.text.toString().isBlank()) {
                Toast.makeText(context, "评论不能为空", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            callback.invoke(editText.text.toString())
            dismiss()
        }
    }
}

suspend List<CommentItem>.() -> List<CommentItem> = {
    val content = withContext(Dispatchers.Main) {
        // 由于整个转换过程是在IO线程进行,Dialog相关操作需要转换到主线程操作
        suspendCoroutine { continuation ->
            ReplyDialog(context) {
                continuation.resume(it)
            }.show()
        }
    }
    ...进行其他操作,如网络请求
}

技术选型,或者说技术框架,咱们就实现了,甚至还谈到了部分细节了。接下来进行完整实现细节分享。

实现细节

MainActivity

基于上一章节的技术选型,咱们的MainActivity的完整代码就是这样了。

class MainActivity : AppCompatActivity() {
    private lateinit var commentAdapter: CommentAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        commentAdapter = CommentAdapter {
            lifecycleScope.launchWhenResumed {
                val newList = withContext(Dispatchers.IO) {
                    reduce.invoke(commentAdapter.currentList)
                }
                val firstSubmit = commentAdapter.itemCount == 1
                commentAdapter.submitList(newList) {
                    // 这里是为了处理submitList后,列表滑动位置不对的问题
                    if (firstSubmit) {
                        recyclerView.scrollToPosition(0)
                    } else if (this@CommentAdapter is FoldReducer) {
                        val index = commentAdapter.currentList.indexOf(this@CommentAdapter.folding)
                        recyclerView.scrollToPosition(index)
                    }
                }
            }
        }
        recyclerView.adapter = commentAdapter
    }
}

RecyclerView设置一个CommentAdapter就行了,回调时也只需要把回调过来的Reducer调度到IO线程跑一下,得到新的数据listsubmitList就完事了。如果不是submitList后有列表的定位问题,代码还能更精简。如果有知道更好的解决办法的朋友,麻烦留言分享一下,感谢!

CommentAdapter

别以为我把逻辑处理扔到adapter中了哦!

AdapterViewHolder都是UI组件,我们也需要尽量保持它们的清洁。

贴一下CommentAdapter

class CommentAdapter(private val reduceBlock: Reducer.() -> Unit) :
    ListAdapter<CommentItem, VH>(object : DiffUtil.ItemCallback<CommentItem>() {
        override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
            if (oldItem::class.java != newItem::class.java) return false
            return (oldItem as? CommentItem.Level1) == (newItem as? CommentItem.Level1)
                    || (oldItem as? CommentItem.Level2) == (newItem as? CommentItem.Level2)
                    || (oldItem as? CommentItem.Folding) == (newItem as? CommentItem.Folding)
                    || (oldItem as? CommentItem.Loading) == (newItem as? CommentItem.Loading)
        }
    }) {

    init {
        submitList(listOf(CommentItem.Loading(page = 0, CommentItem.Loading.State.IDLE)))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            TYPE_LEVEL1 -> Level1VH(
                inflater.inflate(R.layout.item_comment_level_1, parent, false),
                reduceBlock
            )

            TYPE_LEVEL2 -> Level2VH(
                inflater.inflate(R.layout.item_comment_level_2, parent, false),
                reduceBlock
            )

            TYPE_LOADING -> LoadingVH(
                inflater.inflate(
                    R.layout.item_comment_loading,
                    parent,
                    false
                ), reduceBlock
            )

            else -> FoldingVH(
                inflater.inflate(R.layout.item_comment_folding, parent, false),
                reduceBlock
            )
        }
    }

    override fun onBindViewHolder(holder: VH, position: Int) {
        holder.onBind(getItem(position))
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is CommentItem.Level1 -> TYPE_LEVEL1
            is CommentItem.Level2 -> TYPE_LEVEL2
            is CommentItem.Loading -> TYPE_LOADING
            else -> TYPE_FOLDING
        }
    }

    companion object {
        private const val TYPE_LEVEL1 = 0
        private const val TYPE_LEVEL2 = 1
        private const val TYPE_FOLDING = 2
        private const val TYPE_LOADING = 3
    }
}

可以看到,就是一个简单的多ItemTypeAdapter,唯一需要注意的就是,在Activity里传入的reduceBlock: Reducer.() -> Unit,也要传给每个ViewHolder

ViewHolder

篇幅原因,就只贴其中一个:

abstract class VH(itemView: View, protected val reduceBlock: Reducer.() -> Unit) :
    ViewHolder(itemView) {
    abstract fun onBind(item: CommentItem)
}

class Level1VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) {
    private val avatar: TextView = itemView.findViewById(R.id.avatar)
    private val username: TextView = itemView.findViewById(R.id.username)
    private val content: TextView = itemView.findViewById(R.id.content)
    private val reply: TextView = itemView.findViewById(R.id.reply)
    override fun onBind(item: CommentItem) {
        avatar.text = item.userName.subSequence(0, 1)
        username.text = item.userName
        content.text = item.content
        reply.setOnClickListener {
            reduceBlock.invoke(ReplyReducer(item, itemView.context))
        }
    }
}

也是很简单,唯一特别一点的处理,就是在onClickListener中,让reduceBlockinvoke一个Reducer实现。

Reducer

刚才在技术选型章节,已经提前展示了“回复评论”这一操作的Reducer实现了,其他Reducer也差不多,比如展开评论操作,也封装在一个Reducer实现ExpandReducer中,以下是完整代码:

data class ExpandReducer(
    val folding: CommentItem.Folding,
) : Reducer {
    private val mapper by lazy { Entity2ItemMapper() }
    override val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {
        val foldingIndex = indexOf(folding)
        val loaded =
            FakeApi.getLevel2Comments(folding.parentId, folding.page, folding.pageSize).getOrNull()
                ?.map(mapper::invoke) ?: emptyList()
        toMutableList().apply {
            addAll(foldingIndex, loaded)
        }.map {
            if (it is CommentItem.Folding && it == folding) {
                val state =
                    if (it.page > 5) CommentItem.Folding.State.LOADED_ALL else CommentItem.Folding.State.IDLE
                it.copy(page = it.page + 1, state = state)
            } else {
                it
            }
        }
    }

}

短短一段代码,我们做了这些事:

  • 请求网络数据Entity list(假数据)
  • 通过mapper转换成显示用的Item数据list
  • Item数据插入到“展开更多”按钮前面
  • 最后,根据二级评论加载是否完成,将“展开更多”的状态置为IDLELOADED_ALL

一个字:丝滑!

用于转换EntityItemmapper的代码也贴一下吧:

// 抽象
typealias Mapper<I, O> = (I) -> O
// 实现
class Entity2ItemMapper : Mapper<ICommentEntity, CommentItem> {
    override fun invoke(entity: ICommentEntity): CommentItem {
        return when (entity) {
            is CommentLevel1 -> {
                CommentItem.Level1(
                    id = entity.id,
                    content = entity.content,
                    userId = entity.userId,
                    userName = entity.userName,
                    level2Count = entity.level2Count,
                )
            }

            is CommentLevel2 -> {
                CommentItem.Level2(
                    id = entity.id,
                    content = if (entity.hot) entity.content.makeHot() else entity.content,
                    userId = entity.userId,
                    userName = entity.userName,
                    parentId = entity.parentId,
                )
            }

            else -> {
                throw IllegalArgumentException("not implemented entity: $entity")
            }
        }
    }
}

细心的朋友可以看到,在这里我顺便也将热评也处理了:

if (entity.hot) entity.content.makeHot() else entity.content

makeHot()就是用buildSpannedString来实现的:

fun CharSequence.makeHot(): CharSequence {
    return buildSpannedString {
        color(Color.RED) {
            append("热评  ")
        }
        append(this@makeHot)
    }
}

这里可以提一句:尽量用CharSequence来抽象表示字符串,可以方便我们灵活地使用Span来减少UI代码。

data class

也贴一下相关的数据实体得了。

  • 网络数据(假数据)
interface ICommentEntity {
    val id: Int
    val content: CharSequence
    val userId: Int
    val userName: CharSequence
}

data class CommentLevel1(
    override val id: Int,
    override val content: CharSequence,
    override val userId: Int,
    override val userName: CharSequence,
    val level2Count: Int,
) : ICommentEntity
  • RecyclerView Item数据
sealed interface CommentItem {
    val id: Int
    val content: CharSequence
    val userId: Int
    val userName: CharSequence

    data class Loading(
        val page: Int = 0,
        val state: State = State.LOADING
    ) : CommentItem {
        override val id: Int=0
        override val content: CharSequence
            get() = when(state) {
                State.LOADED_ALL -> "全部加载"
                else -> "加载中..."
            }
        override val userId: Int=0
        override val userName: CharSequence=""

        enum class State {
            IDLE, LOADING, LOADED_ALL
        }
    }

    data class Level1(
        override val id: Int,
        override val content: CharSequence,
        override val userId: Int,
        override val userName: CharSequence,
        val level2Count: Int,
    ) : CommentItem

    data class Level2(
        override val id: Int,
        override val content: CharSequence,
        override val userId: Int,
        override val userName: CharSequence,
        val parentId: Int,
    ) : CommentItem

    data class Folding(
        val parentId: Int,
        val page: Int = 1,
        val pageSize: Int = 3,
        val state: State = State.IDLE
    ) : CommentItem {
        override val id: Int
            get() = hashCode()
        override val content: CharSequence
            get() = when  {
                page <= 1 -> "展开20条回复"
                page >= 5 -> ""
                else -> "展开更多"
            }
        override val userId: Int = 0
        override val userName: CharSequence = ""

        enum class State {
            IDLE, LOADING, LOADED_ALL
        }
    }
}

这部分没啥好说的,可以注意两个点:

  • data class也是可以抽象的。但这边我处理不是很严谨,比如CommentItem我把userIduserName也抽象出来了,其实不应该抽象出来。
  • 在基于Reducer的框架下,最好是把data class的属性都定义为val

总结一下实现心得:

  • 数据驱动UI
  • 对业务的精准抽象
  • 对异步的延伸理解
  • 灵活使用Collection操作符
  • 没有UI和PM,写代码真TM爽!

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

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

相关文章

Spring Authorization Server入门 (十七) Vue项目使用授权码模式对接认证服务

Vue单页面项目使用授权码模式对接流程说明 以下流程摘抄自官网 在本例中为授权代码流程。 授权码流程的步骤如下&#xff1a; 客户端通过重定向到授权端点来发起 OAuth2 请求。 如果用户未通过身份验证&#xff0c;授权服务器将重定向到登录页面。 身份验证后&#xff0c;用…

Spring Bean循环依赖学习与探究

文章目录 原理学习源码溯源 本文参考&#xff1a; 画图带你彻底弄懂三级缓存和循环依赖的问题 Spring 三级缓存解决bean循环依赖&#xff0c;为何用三级缓存而非二级_笑矣乎的博客-CSDN博客 Spring为何需要三级缓存解决循环依赖&#xff0c;而不是二级缓存&#xff1f;_石杉…

如何用在线模版快速制作活动海报?

在时代的发展和信息传播的快速发展下&#xff0c;活动海报成为了宣传活动的重要方式之一。设计一张吸引眼球的活动海报&#xff0c;不仅能够有效传递信息&#xff0c;还能够吸引人们的注意力。那么&#xff0c;在这里我将教会大家如何设计活动海报&#xff0c;只需要三分钟&…

SAP MM学习笔记31 - 已割当供给元的购买依赖

上次学习了未割当供给元的购买依赖&#xff08;未分配供应商采购申请&#xff09;&#xff0c;咱们本章来学习一下 已割当供给元的购买依赖如何处理。 SAP MM学习笔记30 - 未割当供给元的购买依赖_东京老树根的博客-CSDN博客 如下图所示&#xff0c;利用 - 购买依赖割当一览&…

Vue记录(下篇)

Vuex getters配置项 *Count.vue <template><div><h1>当前求和为&#xff1a;{{$store.state.sum}}</h1><h3>当前求和的10倍为&#xff1a;{{$store.getters.bigSum}}</h3><select v-model.number"n"><option value&q…

150.逆波兰表达式求值

目录 一、题目 二、分析代码 三、中缀表达式转后缀表达式 一、题目 150. 逆波兰表达式求值 - 力扣&#xff08;LeetCode&#xff09; 二、分析代码 class Solution { public:int evalRPN(vector<string>& tokens) {stack<int>s;for(auto ch:tokens){if(ch!…

小白备战大厂算法笔试(九)——九大排序算法

文章目录 排序选择排序冒泡排序插入排序快速排序基准数优化尾递归优化 归并排序堆排序桶排序计数排序基数排序排序算法对比 排序 评价维度&#xff1a; 运行效率&#xff1a;我们期望排序算法的时间复杂度尽量低&#xff0c;且总体操作数量较少&#xff08;即时间复杂度中的常…

基于Yolov8的交通标志牌(TT100K)识别检测系统

1.Yolov8介绍 Ultralytics YOLOv8是Ultralytics公司开发的YOLO目标检测和图像分割模型的最新版本。YOLOv8是一种尖端的、最先进的&#xff08;SOTA&#xff09;模型&#xff0c;它建立在先前YOLO成功基础上&#xff0c;并引入了新功能和改进&#xff0c;以进一步提升性能和灵活…

在gazebo仿真环境中加载多个机器人

文章目录 前言一、基本概念1、xacro2、Gazebo 加载单个机器人模型 二、原先launch文件代码三、 修改launch文件加载多个机器人总结 前言 单个机器人的各项仿真实验都基本完成&#xff0c;也实现了远程控制&#xff0c;接下来主要对多机器人编队进行仿真实验&#xff0c;在进行…

Git 命令图形化在线练习

git 命令在线练习网址如下: http://onlywei.github.io/explain-git-with-d3/ 在master上先提交2个commit,创建3个分支,分支1打5个commit,分支2打6commit ,分支3commit,master分支打9commit. git commit -m "master c 1" git commit -m "master c 1"git …

程序员必备神器:He3万能工具箱全解析

He3是一个为前端、后端开发者打造的终极工具箱软件&#xff0c;提供了近400的功能&#xff0c;将开发效率提升到一个新的水平。。本文将介绍 He3 开发者工具箱的主要功能和特点。 He3包含Web版和客户端&#xff0c;客户端支持windows和mac。下载后即可使用非常方便。 先睹为快…

FL Studio v21.1.1.3750 Producer Edition inc crack官方中文免费激活版功能介绍及百度网盘下载

FL Studio v21.1.1.3750 Producer Edition inc crack官方中文免费激活版是一款功能强大的软件音乐制作环境或数字音频工作站&#xff08;DAW&#xff09;。它代表了25多年的创新发展&#xff0c;在一个软件包中拥有您所需的一切&#xff0c;以创作、编排、录制、编辑、混音和掌…

C++之vector元素访问函数operator[]、at、front、back、data总结(二百零三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

如何在 Excel 中计算日期之间的天数

计算两个日期之间的天数是 Excel中的常见操作。无论您是规划项目时间表、跟踪时间还是分析一段时间内的趋势&#xff0c;了解如何在 Excel 中查找日期之间的天数都可以提供强大的日期计算功能。 幸运的是&#xff0c;Excel 提供了多种简单的方法来获取两个日期之间的天数。继续…

Javascript EventListener 事件监听 (mouseover、mouseout)

事件指的是在html元素上发生的事情&#xff0c;例如图片元素被点击事件触发时&#xff0c;可设置执行一段js代码。对事件作出反应&#xff0c;通过元素的事件属性&#xff0c;启用事件监听器。 事件监听器是指 addEventListener (给DOM对象添加事件处理程序) 和 removeEventLis…

441分2023级东南大学920专业基础综合信号和数字电路考研上岸经验分享信息科学与工程学院

写在前面的话 本人是23年考生&#xff0c;本科就读于西电电子信息工程&#xff0c;以441分总分&#xff08;数学一149&#xff0c;英语83&#xff0c;专业课137&#xff0c;政治73&#xff09;考上东南信院电路与系统专业。以下所言皆是考研历程中的重要感悟&#xff0c;因为一…

c语言练习61:malloc和free

malloc和free malloc C语⾔提供了⼀个动态内存开辟的函数&#xff1a; 1 void* malloc (size_t size); 这个函数向内存申请⼀块连续可⽤的空间&#xff0c;并返回指向这块空间的指针。 • 如果开辟成功&#xff0c;则返回⼀个指向开辟好空间的指针。 • 如果开辟失败&…

mysql的变量

在 MySQL 中变量分为三种类型 : 系统变量、用户定义变量、局部变量。 系统变量 系统变量 是 MySQL 服务器提供&#xff0c;不是用户定义的&#xff0c;属于服务器层面。分为全局变量&#xff08; GLOBAL &#xff09;、会话变量&#xff08;SESSION &#xff09;。 查看系统变…

选择适合您网站的SSL证书,保障安全与信任

在如今数字化的时代&#xff0c;拥有一个安全可靠的网站是至关重要的。而SSL证书作为保护网站和用户数据安全的关键工具&#xff0c;选择适合自己网站的SSL证书成为了每个网站管理员必须面对的重要任务。下面将为您分享几个关键因素&#xff0c;帮助您做出明智的选择。 1. 网站…

Linux的调试工具 - gdb(超详细)

Linux的调试工具 - gdb 1. 背景2. 开始使用指令的使用都用下面这个C语言简单小代码来进行演示&#xff1a;1. list或l 行号&#xff1a;显示文件源代码&#xff0c;接着上次的位置往下列&#xff0c;每次列10行。2. list或l 函数名:列出某个函数的源代码。3. r或run: 运行程序。…