vue3.0时间切片(废除)

一直对时间切片非常感兴趣,虽然最新的vue-next中剔除了时间切片,但是这里还是可以借鉴下其中的原理和思想:

首先要先知道javascript的执行机制,javascript的任务分为macro-task宏任务和micro-task微任务,宏任务主要为同步代码,settimeout,setInterval等,微任务为promise,process.nextTick等。网上有一张图能比较清楚的说明两者相互间的关系:

vue3.0时间切片(废除)_第1张图片

大致明白了宏任务和微任务,理解时间切片就简单多了。切入代码可以先从测试文件开始:

it('queueJob', async () => {
  const calls: any = []
  const job1 = () => {
    calls.push('job1')
  }
  const job2 = () => {
    calls.push('job2')
  }
  queueJob(job1)
  queueJob(job2)
  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(['job1', 'job2'])
})

这里使用了2个函数,queueJob和nextTick,queueJob源码如下:

export function queueJob(rawJob: Function) {
  console.log(rawJob)
  const job = rawJob as Job
  if (currentJob) {
    currentJob.children.push(job)
  }
  // Let's see if this invalidates any work that
  // has already been staged.
  if (job.status === JobStatus.PENDING_COMMIT) {
    // staged job invalidated
    invalidateJob(job)
    // re-insert it into the stage queue
    requeueInvalidatedJob(job)
  } else if (job.status !== JobStatus.PENDING_STAGE) {
    // a new job
    queueJobForStaging(job)
  }
  if (!hasPendingFlush) {
    hasPendingFlush = true
    flushAfterMicroTask()
  }
}

其中rawJob就是我们传入queueJob的匿名函数,通过执行queueJob函数首先会将他推入stageQueue队列,执行queueJob(job1)时,hasPendingFlush为false,则执行flushAfterMicroTask函数,后续的queueJob则不会flushAfterMicroTask。flushAfterMicroTask源码如下:

function flushAfterMicroTask() {
  flushStartTimestamp = getNow()
  return p.then(flush).catch(handleError)
}

代码很简单,就是将在flush函数放入微任务队列中,当宏任务执行完成,就会执行flush函数,简单来说,就是先将要执行的一组任务推入stageQueue,执行完成后执行微任务即执行一次flush。很显然,flush就是时间切片的关键,源码如下:

function flush(): void {
  let job
  while (true) {
    // console.log(stageQueue)
    job = stageQueue.shift()
    // console.log(job)
    if (job) {
      stageJob(job)
    } else {
      break
    }
    if (!__COMPAT__) {
      const now = getNow()
      if (now - flushStartTimestamp > frameBudget && job.expiration > now) {
        break
      }
    }
  }

  if (stageQueue.length === 0) {
    // all done, time to commit!
    for (let i = 0; i < commitQueue.length; i++) {
      commitJob(commitQueue[i])
    }
    commitQueue.length = 0
    flushEffects()
    // some post commit hook triggered more updates...
    if (stageQueue.length > 0) {
      if (!__COMPAT__ && getNow() - flushStartTimestamp > frameBudget) {
        return flushAfterMacroTask()
      } else {
        // not out of budget yet, flush sync
        return flush()
      }
    }
    // now we are really done
    hasPendingFlush = false
    pendingRejectors.length = 0
    for (let i = 0; i < nextTickQueue.length; i++) {
      nextTickQueue[i]()
    }
    nextTickQueue.length = 0
  } else {
    // got more job to do
    // shouldn't reach here in compat mode, because the stageQueue is
    // guarunteed to have been depleted
    flushAfterMacroTask()
  }
}

这段代码其实就干两件事,首先stageQueue不断出栈,然后通过stageJob函数推入commitQueue队列,stageJob源码如下:

function stageJob(job: Job) {
  // job with existing ops means it's already been patched in a low priority queue
  if (job.ops.length === 0) {
    currentJob = job
    job.cleanup = job()
    currentJob = null
    commitQueue.push(job)
    job.status = JobStatus.PENDING_COMMIT
  }
}

当超过某个时长或者任务过去则跳出循环。这里frameBudget为16ms左右。之后就是判断如果stageQueue出栈完了都进入commitQueue队列了,则执行commitQueue队列里的job,在vue中,这些job就是挂载组件,副作用更新组件等。如果stageQueue没有清空,那么执行flushAfterMacroTask函数,flushAfterMacroTask函数源码如下:

function flushAfterMacroTask() {
  window.postMessage(key, `*`)
}

window.addEventListener(
  'message',
  event => {
    if (event.source !== window || event.data !== key) {
      return
    }
    flushStartTimestamp = getNow()
    try {
      flush()
    } catch (e) {
      handleError(e)
    }
  },
  false
)

能看出来接下来就是在宏任务中不断地执行flush函数,直到stageQueue为空,然后执行commitQueue队列中的job。

讲到这里其实关于时间切片的原理也大致说清楚了,Vue通过queueJob函数,将需要组件挂载和更新的job推入stageQueue队列,然后再之后的宏任务中调用flush函数,不停地将stageQueue出栈,推入commitQueue队列,最后执行job。

通过这样的方法,可以减少页面的阻断,每次数据更新将其推入stageQueue队列,并不会立即执行,可以看出对于高帧频的操作,有着明显的效果。

个人认为好像也就能针对高帧频的操作会有效果,也许这就是被废除的原因吧。

通过vue的时间切片,我们可以尝试写一个建议版的,执行代码如下:





    
    test



    
    

每次输入input则在div中插入200条数据,这里使用时间切片,可以发现input连着输入时不会卡顿,但是当稍有停顿在输入时明显卡顿,通过代码可以看出卡顿的原因在于,stageQueue为空执行commitQueue中的job时,其实是同步操作,这个时候肯定会造成阻塞,这也是vue废除这个特性的原因,因为vue的渲染都是以组件单最小单位的,组件渲染时间长卡顿的问题通过时间切片无法解决。个人认为这个功能和节流防抖差不多。当然如果是高帧频操作,time slicing对于性能还是有很大帮助的,我们这里可以尝试不同时time scling 运行这段html:





    
    test



    
    

当连续输入时,卡炸了。

你可能感兴趣的:(vue3,前端,vue)