JavaScript 俄罗斯方块 - setTimeout和rAF

本节内容需要有些基础知识,如进程和线程,队列数据结构

一、setTimeout和setInterval

只要使用过JavaScript的朋友,对setTimeout和setInterval应该不会默生,如果光说怎样去使用这个API,并不难,无非就是隔多少毫秒再执行某个函数,把变化的内容封装在函数中,就可以制作出动画效果,这也是最初写JavaScript时的常见写法,多年以前的IE6时代我对这个函数的印象就是它是用来做网页动画特效的,由于IE6浏览器并未开放源码,那这个定时任务到底是怎样执行的呢?只能去官方通过文档了解。后来,Google浏览器Chrome V8引擎是开源,通过源码,可以了解到JavaScript的setTimeout是怎样实现的了,关键部分就是消息队列和事件循环, 把任务放到了队列中,隔一定时间再把它取出来执行,我们知道浏览器渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务.

在Java NIO中的也是相类似的玩法,开一个线程,在线程中不断的轮询是否有TCP连接事件或是读写事件,如果有则进入循环并判断是哪种任务并给出相应的处理逻辑,在NIO出现前,用IO写服务器,同样会开一个线程,只是accept方法是要阻塞并等待

// 轮询,且返回时有就绪事件
while (selector.select() > 0){ 
    // 获取就绪事件集合
    Set<SelectionKey> keys = selector.selectedKeys(); 
    ......
}
ServerSocket serverSocket = new ServerSocket(9999);
while(true) {
    Socket socket = serverSocket.accept()
    ....
}
//一个阻塞队列
Queue<T> q = new LinkedBlockingQueue<>();
...
while(true){
    T t = q.take();
    ......
}

我们知道,在JavaScript中一旦有异步执行的回调函数就会将它添加到消息队列中,为什么要这样做,还是因为JavaScript是单线程的,这里并不是说浏览器是单线程。

且看Chrome源码:base/task/sequence_manager/task_queue_impl.h, 这里定义了一个队列结构,包含出队入队等方法

  struct DelayedIncomingQueue {
   public:
    DelayedIncomingQueue();
    DelayedIncomingQueue(const DelayedIncomingQueue&) = delete;
    DelayedIncomingQueue& operator=(const DelayedIncomingQueue&) = delete;
    ~DelayedIncomingQueue();

    void push(Task task);
    void remove(HeapHandle heap_handle);
    Task take_top();
    bool empty() const { return queue_.empty(); }
    size_t size() const { return queue_.size(); }
    const Task& top() const { return queue_.top(); }
    void swap(DelayedIncomingQueue* other);

我们知道浏览器本身是多线程的,其中有一个渲染主线程,渲染主线程会频繁收到来自IO线程的任务,收到任务后就会将它们封装成Task,添加到队列中,比如收到图片加载完成后的消息,渲染进程就可以去调用注册在其中的函数(可以称它为任务), 在上面的代码中 Chrome中定义一种数据结构,称为DelayedIncomingQueue,可以存储要执行的任务 Task, 可以看出定义了push(Task task)方法,添加任务到队尾,取出任务从队头获取 Task take_top()

const cherry = new Image()  
cherry.src = 'imgs/cherry.png' 
cherry.addEventListener('load', function(){
    draw()
})  

如果有任务添加进来了,比如运行了下面的代码,那么会创建一个任务对象并添加到任务队列中

setTimeout('f()', 1000)

只要队列中有任务,那么主线程会从消息队列中读一个任务出来,然后去执行

base/task/sequence_manager/sequence_manager_impl.cc

其实这里还用到了观察者

base/task/sequence_manager/task_time_observer.h

Chrome中消息队列中的任务类型有很多,用枚举方式描述,点击下面的链接可查看任务类型源码

Chrome中的任务类型

仅截取一部分代码,如:微任务kMicrotask,读文件kFileReading,kWebSocket

namespace blink {

// A list of task sources known to Blink according to the spec.
// This enum is used for a histogram and it should not be re-numbered.
//
// For the task type usage guideline, see https://bit.ly/2vMAsQ4
//
// When a new task type is created:
// * Set the new task type's value to "Next value"
// * Update kMaxValue to point to the new task type
// * Increment "Next value"
// * in tools/metrics/histograms/enums.xml update the
//   "RendererSchedulerTaskType" enum
// * update TaskTypes.md
//
// Next value: 83
enum class TaskType : unsigned char {

  //响应用户交互的任务类型,也就是鼠标点击、移动等等  
  kUserInteraction = 2,
  
  // https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
  // This task source is used when an algorithm requires a microtask to be
  // queued.
  kMicrotask = 9,

  // 与setTimeout和setInterval的相关的任务类型
  // https://html.spec.whatwg.org/multipage/webappapis.html#timers
  // For tasks queued by setTimeout() or setInterval().
  //
  // Task nesting level is < 5 and timeout is zero.
  kJavascriptTimerImmediate = 72,
  // Task nesting level is < 5 and timeout is > 0.
  kJavascriptTimerDelayedLowNesting = 73,
  // Task nesting level is >= 5.
  kJavascriptTimerDelayedHighNesting = 10,
  // Note: The timeout is increased to be at least 4ms when the task nesting
  // level is >= 5. Therefore, the timeout is necessarily > 0 for
  // kJavascriptTimerDelayedHighNesting.

  // https://html.spec.whatwg.org/multipage/comms.html#sse-processing-model
  // This task source is used for any tasks that are queued by EventSource
  // objects.
  kRemoteEvent = 11,

  // https://html.spec.whatwg.org/multipage/comms.html#feedback-from-the-protocol
  // The task source for all tasks queued in the [WebSocket] section of the
  // spec.
  kWebSocket = 12,

  // https://html.spec.whatwg.org/multipage/comms.html#web-messaging
  // This task source is used for the tasks in cross-document messaging.
  kPostedMessage = 13,

  // https://html.spec.whatwg.org/multipage/comms.html#message-ports
  kUnshippedPortMessage = 14,

  // https://www.w3.org/TR/FileAPI/#blobreader-task-source
  // This task source is used for all tasks queued in the FileAPI spec to read
  // byte sequences associated with Blob and File objects.
  kFileReading = 15,
  kWebSocket = 12,
  //省略其它代码......
}

网上也有很多关于消息队列和事件循环的解析,有兴趣的朋友自行搜索。

为什么在编写动画时不建议使用setTimeout或setInterval呢?看下面的代码

function f() {
    console.log("f function");
}

setTimeout(f, 0)

let sum = 0;
for(let i=0;i<10000;i++) {
    sum+=i
}
console.log(sum);

使用setTimeout定义了一个延时异步任务,虽然时间为0但并不会立即执行,因为在当前线程中还有一个循环未执行,需要等循环执行完才能执行异步任务f, 如果这个异步任务是动画,那么用户看到动画时将会感觉到卡顿。

二、rAF window.requestAnimationFrame()

在电影中画面切换速度为1秒24帧,也就是24fps, 肉眼的视觉残留是下限为24, 这个24是指1秒闪过的连续画面达到24张,那么整个画面在肉眼看起来就是连续的,实际上下限应该为16帧,黑白电影时代用的就是16帧,技术进步后就有了24帧,所以电影中24帧成为一种主流,游戏也是如此.

下面使用requestAnimationFrame,执行时将当前时间和上一次时间相减得出的结果是16.66毫秒,也就是1/60毫秒,我们的显示器将一帧画面绘制完成后读取下一帧之前,会发出一个同步信号,这个称为Vertical Synchronization 给GPU,当GPU收到这个信号后,就会将它同步给浏览器进程,浏览器又会将它同步到渲染进程,之后浏览器就准备画新的一帧了

let pTime
function animate(time) {
    console.log(time-pTime);
    pTime = time
    window.requestAnimationFrame(animate)
}
animate()

JavaScript是由用户控制的,如果采用setTimeout来触发动画中的每一帧绘制就很难和Vertical Synchronization保持一致,所以引入了requestAnimationFrame函数,用来和Vertical Synchronization时钟周期同步,这时rFA的回调任务会在显示器每一帧开始时执行,也就是浏览器引擎收到Vertical Synchronization信号时重绘,setTimeout回调的运行时机很不确定,如果使用setTimeout就可能造成fps不稳定或丢帧.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值