vue3.0时间切片(废除)

9 篇文章 0 订阅
6 篇文章 0 订阅

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

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

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

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的时间切片,我们可以尝试写一个建议版的,执行代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>test</title>
</head>

<body>
    <input type="text" value="" id="myinput">
    <div id="list"></div>
</body>
<script>
    const input = document.getElementById('myinput')
    const list = document.getElementById('list')

    function wait(time) {
        const startTime = performance.now()
        while (performance.now() - startTime < time) {}
    }

    const stageQueue = []
    const commitQueue = []
    let hasPendingFlush = false
    let flushStartTimestamp = 0

    function queueJob(job) {
        stageQueue.push(job)
        if (!hasPendingFlush) {
            hasPendingFlush = true
            flushAfterMicroTask()
        }
    }

    function flushAfterMicroTask() {
        console.log('触发微任务')
        flushStartTimestamp = performance.now()
        return Promise.resolve().then(flush)
    }
    window.addEventListener('message', event => {
        console.log('触发message宏任务')
        flushStartTimestamp = performance.now()
        flush()
    }, false)

    function flushAfterMacroTask() {
        window.postMessage('$vueTick', `*`)
    }

    function flush() {
        let job
        while (true) {
            job = stageQueue.shift()
            if (job) {
                wait(1) // 模拟生成node节点的时间
                commitQueue.push(job)
            } else {
                break
            }
            if (performance.now() - flushStartTimestamp > 16) {
                break
            }
        }
        if (stageQueue.length === 0) {
            console.log('执行')
            for (let i = 0; i < commitQueue.length; i++) {
                commitQueue[i]()
            }
            commitQueue.length = 0
            hasPendingFlush = false
        } else {
            flushAfterMacroTask()
        }
    }


    function job(value) {
        return function () {
            wait(2)
            var node = document.createElement("div");
            var textnode = document.createTextNode(value)
            node.appendChild(textnode)
            list.appendChild(node)
        }
    }

    input.oninput = function (event) {
        console.log('触发input宏任务')
        for (let i = 0; i < 200; i++) {
            queueJob(job(event.target.value))
        }
    }
</script>

</html>

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

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>test</title>
</head>

<body>
    <input type="text" value="" id="myinput">
    <div id="list"></div>
</body>
<script>
    const input = document.getElementById('myinput')
    const list = document.getElementById('list')

    function wait(time) {
        const startTime = performance.now()
        while (performance.now() - startTime < time) {}
    }


    input.oninput = function (event) {
        for (let i = 0; i < 200; i++) {
            wait(2)
            var node = document.createElement("div");
            var textnode = document.createTextNode(event.target.value)
            node.appendChild(textnode)
            list.appendChild(node)
        }
    }
</script>

</html>

当连续输入时,卡炸了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值