微信扫描登录

斗鱼直播礼物打赏系统的前端组件 – timeline.js

本篇文章整理自斗鱼前端技术负责人杜伟5月10日在『ITA1024前端技术精英群』里的分享实录:斗鱼直播礼物打赏系统的前端组件 – timeline.js。

enter image description here 大家好,我是今天的分享人:杜伟,来自斗鱼。在斗鱼我组建了20人的前端团队,并主导了多个前端重大项目。

很高兴受到“ITA1024“团队的邀请,能有机会与大家交流前端技术,我非常荣幸。本次我不分享架构和设计模式,我为大家带来“斗鱼前端组件 timelime.js”。第一次参与这种文字直播的分享,有哪里描述的不清楚的还请大家见谅。

大纲 1、开发背景
2、开发分析
3、设计原理
4、历史版本(1.0、2.0)
5、开发总结

一、 开发背景

在接手斗鱼前端的时候,发现页面上存在大量的:递归、setTimeout、setInterval,导致页面的性能非常差。

同时基于这种场景,我负责的第一个前端项目是“直播间礼物系统”,首先我们来看看斗鱼直播间前后端交互图:

enter image description here

在斗鱼直播间,需要考虑实时性与性能,比如:当用户在某一个直播间消费礼物,需要将用户消费行为推送给每一个观看当前直播间的客户端并展示礼物特效。

后端使用 C++,前端通过flash与C++ 进行数据交互,而flash 通过 Socket 与 C++ 保持连接。这种结构导致前端大部分的场景受 C++ 驱动,C++如同司令部下达命令,前端收到命令并执行。

前端在上述结构下,收到直播间礼物系统的需求,要求如下:

  1. 礼物类型由后端系统配置和管理,每个礼物的配置信息如下:

礼物样式 礼物在不连击的情况下的停留时间 礼物展示前的动画特效(非必须) 礼物展示是否产生红包 礼物的优先级(比如:高级礼物遮挡低级礼物) 礼物是否全站广播

2.礼物类型包括:鱼丸、520、赞、666、飞机、火箭。

3.礼物特效要求在弹幕区域展示(由前端实现)。

二、开发分析

通过需求分析,前端第一步分析如下:

  1. 礼物由后端动态配置,礼物样式是前端的唯一标示。

  2. 礼物的赠送触发点与特效渲染点,因为前后端的交互结构被强行断开无法关联。

3.礼物有生命周期

到达销毁时间自动销毁
如果用户连击礼物,销毁时间重新计算

4.礼物有等级划分

高级礼物特效遮挡低级礼物特效
特别高级礼物特效出现要销毁所有低级礼物

通过以上初步分析,我们先排除非核心功能,如:礼物样式、礼物特效、礼物优先级,这些功能对于一个初级前端都不在话下。

这个功能的核心点在于如何有效的管理礼物,因为礼物的:个数不确定、生命周期不确定、生命周期被连击和高级礼物所影响。

我们先做出抽象分析:

enter image description here

看到这里,我们想到前端需要一个实时轮询机制,这里我们称为监听。

在我们看来,监听分为:被动和主动。被动监听就是将函数以某种形式缓存起来,在需要的时候触发执行,比如:DOM 事件通过用户行为触发。

而在前端实现主动监听的原生方法为setInterval,但过多的使用 setInterval 会导致性能问题和逻辑问题。Timeline.js 就是解决前端主动监听的一种技术和规范。

三、设计原理

Timeline.js 设计图:

enter image description here

Timeline.js的核心单元为:时间轴、函数队列。

函数队列负责收集和管理函数节点。

时间轴在启动的时候会记录初始时间,便于后续精确计算时间间隔。 时间轴在执行间隔会遍历函数队列,回收已过期的函数节点,因为在心跳扫描

阶段删除节点会有性能问题。

时间轴在执行节点,根据节点的执行间隔,收集可以执行的节点并批量执行。

时间轴的核心代码:

/**
 * fn  : 轮询函数
 * time: 执行间隔
 * right: 首次立刻执行
 */
functionInterval(fn, time, right) {
        setTimeout(function() {
               if( fn() !== false )
                      Interval(fn, time);
});

if(right === true) fn();
}

Interval使用延时递归实现轮询功能。当然也可以使用requestAnimationFrame,看个人风格。

四、历史版本---1.0

开发背景:需求开发时间紧迫,需要快速上线。
设计模式:工厂模式。
节点数据格式:

  {
              // 节点数据
              data: { … },
              // 节点超时时间(单位:毫秒)
              timeout: 1000,
              // 事件回调:节点被加载
              onLoad: function(tline, node),
              // 事件回调:节点被更新
              onUpdate: function(tline, node),
              // 事件回调:节点被销毁
              onDestroy: function(tline, node),
}
Timeline使用代码示例:
var tline = Timeline.create({
              //节点数据集合
              nodes:[ node, node, node… ],
              //轮询间隔
              // *轮询间隔必须远远小于节点的最小执行间隔
// *内部会自动处理,但如果小于 50ms 会抛错
// *IE7 建议设置为 200ms 以上
// *其它浏览器不小于 50 ms 即可
              step:50
});
// 开启
tline.start();
// 节点管理
// 增
tline.node.add(node);
// 删(将节点打标,隐藏节点,被时间轴在执行间隔期间回收)
tline.node.remove(node);
// 改
tline.node.update(node);
// 遍历(如果节点被隐藏,将不会被遍历到)
tline.node.each(fn);
// 停止
tline.stop();

五、历史版本---2.0

在 1.0 上线满足现有业务需求后,我们开始总结和优化。我们发现Timeline在 PC 浏览器上高峰期能处理 500 个左右的节点,页面没有明显卡顿和功能紊乱。

我们发现 Timeline.js 整体设计思路没有问题,但是存在以下问题:

“节点数据”太过冗余且不够抽象
Timeline1.0 使用必须实例化,开发使用过重,侵入式太强
在无线端 H5 页面下测试,当节点达到 100 个左右出现性能问题

针对 1.0 的问题,2.0 做出如下优化:

设计模式:使用混合模式:单例模式、工厂模式,Timeline既是类也是自己的实例。

开发优化:Timeline 加载完成后,会覆盖 setTimeout 、setInterval、clearTimeout、clearInterval,函数的入参不变,只是被Timeline 的全局实例管理,方便于后期维护和版本迭代。

代码如下:

setTimeout = Timeline.SetTimeoutProxy;
setInterval = Timeline.SetIntervalProxy;
clearTimeout = Timeline.ClearTimeoutProxy;
clearInterval = Timeline.MockClearIntervalProxy;

保持原生写法:

var timer = setTimeout(function(){ … }, 500);
clearTimeout(timer);

简单场景中建议使用处理过的setTimeout 和 setInterval特定复杂场景可以使用 Timeline,实现自定义开发。

节点数据优化:

  {
                     // 节点数据
                     data: { … },
                     // 节点心跳
                     onHeart: function(tline,node, ntime)
}

时间轴只负责发送心跳给函数队列中的节点,节点的心跳函数接收到的参数为:timeline 实例、当前节点、当前时间。节点在心跳函数中自主管理,如:删除、更新。

应用场景兼容:

在支持 H5 Worker 的浏览器下面,Timeline 使用 Worker 实现轮询机制;反之,使用 1.0 中的 Interval 实现。

Timeline2.0 设计图:

enter image description here

六、开发总结

在互联网视频直播领域,前端面对的挑战是性能,比如:弹幕渲染、礼物特效等等。通过 Timeline,我们精简业务代码约4500行,提升脚本运行性能约 30%。

Timeline 本身也有限制,业务轮询时间必须大于 Timeline 的轮询时间。在 2.0 中我们在保持 API 不变的情况下重写了setTimeout 与 setInterval ,虽然有点蹩脚,但是在真实的开发过程中对于开发者也并无感知,而且便于组件的版本迭代。

我们计划将 Timeline 移植到node,让前端能在服务端更好的完善“定时任务”机制。比如:在前端统计中的定时数据汇总、在前端监控中的定时数据推送等等。(思维有多远,前端就能走多远)。

分析:找到核心点,排除功能干扰。
设计:进行第一步概要设计,并版本规划。
抽象:提取公共行为,解决一类事物。
应用:在项目中运行,观察、分析和总结。
优化:优化当前,优化全站,若有瓶颈,可以从以上某环节重新开始。

Q&A

问:2.0重写settimeout的意思是开发者仍然用settimeout的api? 杜伟:是的,API 保持不变,Timeline 太笨重,提供原生方法的写法减轻开发压力。

问:如果调用api不变,那怎么做到精简4500行业务代码的呢? 杜伟:斗鱼前端做过一次重构,通过 Timeline 精简了业务代码,由于 Timeline 2.0 节点数据的优化,也更加精简了一部分代码,清楚了冗余的逻辑。

问:重写setTimeout、setInterval、clearTimeout、clearInterval,主要做了什么优化? 杜伟:重写 setTimeout 和 setInterval 主要是将多个轮询逻辑变成一个轮询逻辑,用一个轮询去实现,而且便于前端统计用户行为,数据类型等等。

问:为什么没有选择像socket之类,而是用轮询去拿数据呢? 杜伟:数据是 C++ 通过 socket 推送给 flash,flash 再推送给前端,所以前端是被动执行,只是用 Timeline 去处理需要动态交互的数据。

问:这个与动画库结合使用如何? 杜伟:Timeline 的核心是延时递归,与动画库的核心类似,目前与动画库并没有直接的关联。

问:对于不支持 flash 的ios 是怎么做的? 杜伟 :在移动端浏览器,没有礼物,没有弹幕,只是支持简单的视屏流播放,后续我们会考虑用 websocket 去实现。