使用并发复合命令模式对齐代码执行点
[视频] 你好,我是李艺。 上节课我们主要学习了异步转同步的编程范式,这节课学习使用复合命令模式对齐代码的执行点。 复合命令模式是一个设计模式,它和异步转同步的编程范式一样主要作用在于使我们这个代码结构变得更加清晰 易于维护,其次它还可以优化代码的调用逻辑,统筹安排代码的一个执行时机。 首先我们看一下问题,在编程开发里面,尤其在JS语言的异步编程里面经常会涉及这样一个场景,条件A和条件B全部达成以后才可以执行这个代码C,并且条件A和条件B它不一定是在一个地方,例如不在一个页面里面它们达成的先后的时机也不确定,完全具有随机性,在这种场景之下我们绝不能采用排队的方式,先达成条件A然后再达成条件B 或者是反过来,这样都是不行,因为这样本来可以并发提早完成的这个事情人为的被我们延后了。我们应该怎么办?可以使用并发的复合命令模式,将两个或者是多个操作封装成命令对象,让它们并发竞速完成,当它们全部完成以后对齐这个时间点,再执行下一步的逻辑代码。接下来看项目实践。 首先看实践一,实现并发的复合命令模式。 在我们源码里面,在optimus/command目录下面需要实现一套命令模式,包括普通的单一命令、闭包函数命令还有并发命令和串发命令,并发命令ParallelCommand对象是我们将要用到的一个类对象。 接下来我们看实践一的代码演示。 我们需要在这个library下面,这个optimus这个目录下添加一个子目录叫做command,所有的命令对象全部在这个目录下进行定义,为了编码方便我们仍然使用VSCode进行编写,首先我们要将基础的一个类把它给定义出来,找到我们的最终源码里面 library然后optimus command,基础类command,这是我们那个基础类,我们把这个文件给它拷贝过来放在command目录下面,这个里面我们间接地还引入了一个event_dispatcher,这样的基类它是一个事件派发对象,它的实现其实很简单,实现的是一个观察者模式,一个观察者模式 主要的方法是on就是监听事件,off是移除事件监听,然后once这个是只监听一次事件,emit是发布一个订阅通知,也就是派发事件,通知其他的监听者开始执行代码,我们在这个地方Command其实因为我们要监听和派发事件,所以我们让它继承于我们EventDispatcher。 这个里面我们简单看一下,有临时数据仓,然后有一个只读的一个属性complete代表的是否已完成,这个是一个excute是执行 是一个很关键的方法,调用这个方法代表我们当前的命令对象开始执行,markComplete是标记我们当前的命令完成,这个地方是一个onComplete,是添加一个事件的监听,因为EventDispatcher它本身也是有on这个方法的,但是我们为了方便的话,这个地方多加了一个方法可以直接调这个onComplete然后监听,这个事件的名称就不需要写了。 同时这个地方我们还做了一个,就是在我们添加监听的时候,如果你本身这个事件它已经完成了,直接就派发一个complete的事件,然后监听这个事件的代码都可以得到通知,最后面是一个dispose,它是释放,首先我们调父类里面的off,将我们所有的监听全部去掉,再往下就是要把data数据对象然后置空,这个过程其实是为了方便GC进行垃圾内存回收的这样的一个设置,这是基类,有了基类以后接下来我们还要去定义复合类型。 第一个类型我们看一下这个目录,是CommandGroup,这个类会继承于我们Command,我们将这个类先给它拷贝一下,同时还有另外还有两个,一个是parallel_command,还有一个serial_command,同时我们也拷贝一下,我们先看command_group。CommandGroup继承于它,它主要的一个不同是它是可以复合的,它里面有一个subCommands这样的一个数组,在这个里面我们可以添加子命令,但是它同时它本身也是一个Command,它也是可以执行的,当它执行的时候它怎么样去做事情呢,其实它相当于是所有的子命令然后依次去执行,但是执行的策略不是它本身决定的,而是在它的子类里面。 这个地方有两个子类,一个是parallel是并发复合命令对象,在这个里面,我们可以看到这ParallelCommand它继承于我们的CommandGroup,继承以后,我们在这个地方主要是重写了execute的方法,也就是执行方法,它是并发的是相当于我们要让所有的子命令全部的同时的去执行,然后执行我们还要去监听它是否完成,完成以后我们要将完成的数字加1 加1,直到所有完成的数字达到了我们子命令的数字 就是个数,所有子命令都全部完成以后再将我们自身的命令,就复合命令,它作为一个命令,然后再把它的markComplete这个方法调用,调用就代表把它置为已完成的这样一种状态。 这个方法里面我们还用了一个Iterator,这是一个迭代器,迭代器的实现其实也在我们的里面,大概是在这个地方 这个实现其实也很简单,它主要是传入一个可迭代的对象,可以传ObjectArray String Set Map和TypedArray,传进来以后去调它这个方法进行一个迭代 这样的实现,这是Parallel它的规则就是所有的子命令一起执行,直到最后一个完成,当然这个里面它还有一种竞赛模式,isRaceMode它默认等于false,如果我们将它改成true的话,稍后我们会看到,当然这个模式稍后我们还会用到,如果我们将它改成true的话,只要有一个子命令它完成了就意味着我们复合命令的完成,这是它的策略的小小的改变,另外一个serial_command,这是一个串发复合命令对象,在这个里面我们看它的execute的方法,也是拿iterator一个迭代器,跟刚才复合命令对象最大的一个不同就是它的执行策略,它是一个一个执行 就是排队 一个个执行按顺序来,前面的先执行,前面执行了后面再执行,直到后面的一个子命令执行完成以后才视为整体的串发的复合命令对象,然后执行完成了,就是这样的一个策略,这是复合的命令对象。 下面还有两个,一个是closure_command,还有一个sync_closure_command,这两个文件我们也给它拿过来,简单说一下这个closure_command是什么意思,就是我们可以传一个闭包,传个闭包,然后execute的时候,就是执行的时候我们让闭包去执行,但是什么时候完成,其实完成也是我们在外部调用的,我们外部可以调用它的markComplete的方法来标记一个指令对象的完成,另外还有一个sync_closure_command ,这个是另外的一个,相当于是一个异步的,就是同步的一个同步闭包命令,它的特点就是执行以后它可以马上完成,所以我们这个地方看到它execute的方法会重写了父类以后马上调用它自身的markComplete,就把自身给它标记为完成了,这个类型适合我们做一些不占用时间的,它执行以后马上就可以完成了,用于在一个比较长的执行序列里边,去在中间去执行一些额外的一些事情 做一些设置,做这个事情,这就是我们要用到的,接下来要用到的命令对象,所有的文件都在这里了,代码演示我们就说到这里。 下面看实践二,在App.onLaunch时就开始拉取首页动态数据。 我们在首页需要的列表数据,原来是在首页的onReady周期函数里面开始加载的,拉取的时机说实话有些晚了,其实在App.onLaunch事件派发的时候就已经可以执行网络请求了,但是App.onLaunch这个周期函数它在app.js这个文件里边,如果我们在这里开始拉取这个数据的话,怎么样把拉取得到的数据传递给我们首页并且在首页上它还有一个onLoad和onReady时间节点,只有这个页面Load事件触发以后才可以修改data数据对象,然后对这个页面才可以触发渲染,只有在主页的onReady这个事件派发以后才可以访问这个页面上的这些元素节点。 在这种情况之下,我们可以将App.onLaunch和首页的Page.onLoad这两个时间节点使用并发的复合命令模式管理起来,让它们可以并发执行,实际上其实是前者先执行,在它们全部这个条件达成以后再把这个数据取出来,修改这个页面的data数据对象,为了方便在两个文件里面同时引用,我们可以创建一个单独的叫做retrieve_home_data这样的JS文件,这个文件主要代码就是为了实现这个数据的拉取,同时在这两个文件里面可以不同的引用和调用。 在并发命令对象里面可以添加两个ClosureCommand实例,第一个命令实例它默认在开始的时候就开始执行,开始拉取这个数据,执行以后然后设置完成,第二个命令实例是留给首页里面的onReady周期函数去调用的,然后我们在app.js文件里面去引入和调用retrieve_home_data.js文件,让复合命令开始执行,在主页的JS文件里面,在onReady周期函数里面再将第二个子命令实例设置完成,因为这时候长列表元素它已经可以查询到了,在onLoad的周期函数里面可以使用立即执行函数监听数据,加载完成的事件,那么为什么在这里要使用IIEF,IIEF也就是立即执行函数,这是因为将整个代码逻辑挂载到onLoad的周期函数里面,同时方便在立即执行函数内部使用了一个async和await关键字,我们也可以将立即执行函数里面这个代码拿出来,放在一个单独的方法里面去定义,但是对于只执行一次的这样的一个代码,这样做的意义其实并不大。 下面我们看实践二的代码演示。 首先我们要定义一个retrieve_home_data的这样的一个文件,在这里面要实现数据拉取的一个代码,这个文件我们要放在我们的library,optimus放在这个下面,目前还没有,这个地方有一个services,但下面没有代码,我们就去最终源码里面去看一眼,找到6.2.2,这是我们需要的文件,将这个文件给它拷贝一下放到我们library,然后services下面,这个文件我们看一下它主要干了一个什么事情,主要是导出一个方法,它直接上来就是export default function导出一个方法,在这个里边首先是对三个模块的一个引入,引入了三个模块,第一个是ClosureCommand,这个是闭包指令对象对吧,第二个是ParallelCommand,这是我们的并发复合命令对象,最后这个地方引入了promisify这个工具函数,我们在这个地方我们可以看到前面我们所有的代码,基本上你可以看到在定义它的时候 导出的时候,我用的是ES6的Module这样的一个写法,而我们在引入的时候用的却是CommonJS。 CommonJS它其实有一个优势就是在于它可以运行时动态导入,比如说这个地方 这本来是函数内,函数内它需要这个方法的时候,我们就把它引入,引入之后然后直接就使用,对于这样一次性 使用一次的这样的一个代码,其实我们很多时候没有必要把它写到这个页面的顶部统一使用,其实是没有必要的,然后这个地方它如果只使用一次的话,那就放在这里就可以了,再往下我们这个地方有一个定义的requestCmd ,这是一个ClosureCommand,它是这样的一个实例,在这个里面。这个代码我们看起来很熟悉,这就是我们原来的拉取,从后端拉取首页数据的代码,但是拉完以后我们没有马上触发这个页面的渲染,因为这个代码现在不在那个页面里面,拉完以后其实将我们这个数据存在了global.retrieveHomeData对象的data上面,这个是我们ParallelCommand的一个复合命令。 前面我们提到了我们这个命令对象,它里面其实是有一个data的临时数据仓,临时数据仓就是我们可以在这个上面挂载一些,就在我们请求这个序列里面挂载一些自己的临时的一些数据,放在这个地方,以便我们后续的指令对象在执行的时候可以直接取用,这个数据我们取完以后直接就挂在这个上面,同时在最后这个地方设置我们requestCmd 设置它已完成,最后在这个地方retrieveHomeData 等于ParallelCommand,在这个地方看一下它的示例 它的子命令,第一个子命令是requestCmd,就是前面这个代码 我们已经让它默认执行了,然后执行完它会自己标记为完成,第二个就是,我们新建了一个ClosureCommand,但是我们没有给它传任何的闭包函数,也没有给它传任何的代码,其实在这个地方它的存在的意义仅限于它本身,它本身就是它存在的一个意义,稍后我们还会在后面还会看到,我们对它其实是有调用的。 初始化以后默认就调它的execute方法,因为这个方法它会返回自身,所以调用以后,我们再给它赋值一点问题都没有,这是这个方法 我们现在已经给它搞进来了,接下来第二步我们要做什么事情,就是在我们app.js里面要引入我们的文件,我们看一下我们怎么引入和调用的,找到app.js,然后这个地方 现在就开始加载主页数据,是在这个地方,我们仍然使用的是CommonJS的模块规范,把它取到以后找到我们的现在的项目文件,找到onLaunch这个时间节点,因为我们的本意就是要在这个地方,在这个程序已启动的时候,马上就开始加载我们主页数据,这是我们可以掌控的最好的一个加载时机,最靠前的一个加载时机了。 这个地方有一点我们还要说一下,你看到这个地方我把这个模块引入以后马上就去调用这个方法了,这样调用是没有问题的,因为我们在这个地方没有使用await关键字,也就是我们没有故意人为去阻塞这个代码的一个执行,它其实只是调用,其实无所谓的 ,这样可以直接这样去调用的,调用以后,接下来我们再看另外一个地方,就是在主页文件里面我们也要做事情,看一下我们主页文件里面做了一个什么事情,首先我们要找到onLoad 页面的onLoad周期函数,onLoad周期函数在这里有一个主页已经加载,可以设置数据了,如果已经请求完成 有这样的一段代码对吧,这个是IIE,刚才我们说到了它是一个立即执行函数在我们这个项目里面,其实用了很多这样的一个代码,将这个代码然后复制到我们这个项目里面来,找到onLoad,在这里对吧,放到这个地方,看一下这个地方,首先是我们判断一下global.retrieveHomeData它是否存在,为什么要有这个判断,因为有时候我们这个页面其实不是从我们默认的主包开始启动的,有时候它是从独立页面开始启动的,它如果从另外一个地方启动的话,可能是一个null是不存在的,如果它不存在的话,我们可以在这个地方再次去引入和调用它,这样也是没有问题的,一般情况下这个代码其实它是不会执行的。 然后在这个地方我们去在对象上面去用onComplete监听了它的完成的事件,完成以后我们就调dealWithListData,后面这个是数据,把这个数据然后传递给它,让它进行渲染这样的一个事情,另外还有一个事情就是我们要在onReady这个地方去干的一个事情 我们看一下,在这个地方 只有这一行代码 在这个地方,把这个代码拷贝一下放在我们的文件里边onReady,onReady我们要靠前放,让它尽快地执行,这个地方就是我们要调用我们复合命令对象里面它的第二个子指令,也就是子命令,这个地方我们传1对不对 因为它是从0开始,所以传1其实就是第二个,然后把第二个Complete给它标记为完成,这个非常重要,然后通过这个事件就将两个代码的执行点这个条件已经对齐了。 这个标记为完成,同时我们前面的从主页拉数据那个代码,它再执行完成,两个都执行完成以后它会走到这里,因为我们在这个地方监听了它的复合的命令,它完成时间对不对,它两个条件都具备了就会走到这个地方来,然后去触发页面数据的设置以及渲染,执行我们这个代码,在我们这个里边原来还有一个数据,就这个地方 这是我们原来的代码,从后端拉取数据我们可以看到我们原来的拉取时机它是在什么地方拉的,开始拉取点,是在我们主页的Page.onReady,页面完成以后你才开始去拉取数据,前面其实已经浪费了很多时间,所以从优化上来讲,执行的时机已经很晚了,所以我们这个代码其实已经不需要了,可以把它给它删掉,这个代码已经不需要,看一下执行效果。 首先看调试区有没有编译错误,没有错误我们就看首页的数据它显示是否正常,能否显示,数据也可以显示没有问题,在我们调试区其实有一个Network面板,我们所有的数据的拉取在这个地方其实都可以看到,比如这个地方就有一个home,这就是我们的主页的列表数据的一个拉取,这个地方我们可以看到,但这个地方其实它也是有问题的,我们可以看到我们接口调用以后,然后它返回数据size竟然是8.3kB,这个数据量其实已经是很大的,这个地方还是有优化空间的,这个代码演示我们就说到这里。 最后我们总结一下,使用并发的复合命令对象可以将跨文件的异步执行的随机达成的多个条件,在一个节点对齐,然后再执行其他的一个逻辑,在这里已经定义的命令模式,它们其实只是结构 并且提供释放方法,这样的一个实现方式方便开发者编写自己的业务逻辑代码还有在主页的JS文件里面,当我们监听并发复合事件的完成事件的时候,我们并没有将这个代码写在别的地方而是直接写在了周期函数里面,而且是以立即执行函数这样的一种方式去写的,在哪里需要就写在哪里,这样的一种编码方式会使我们这个代码更加的紧凑 易读,这节课我们就讲到这里,我们这节课用到的文档就如我们现在屏幕上显示的网址。 点击查看开放文档: 小程序框架 /逻辑层 /页面生命周期 这节课我们主要学习了如何使用并发复合命令对齐跨文件的异步时间点,下节课我们学习使用worker在小程序里面开启新线程。 最后说一下思考题。这里有个问题请你思考一下,JS基本上可以说是单线程语言,但是在HTML5开发里面,我们可以通过开一个worker,通过这样一种方式可以开启一个新线程,然后在新线程里面可以执行与主线程并行的代码,在这个小程序里面可以这样操作吗?如果可以这样操作的话,相当于我们可以控制一个异步线程来帮助我们执行一些较为费时的代码,这个问题先留给你思考一下。下节课我们来一起深入探讨一下这个问题。