3.调度开始 上一节讲到,react根据更新任务的优先级lanePriority来执行不同的调度任务.
lanePriority=SyncLanePriority,执行同步调度,scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
lanePriority!=SyncLanePriority&&!=SyncBatchedLanePriority,执行异步调度,scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root))
同步调度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function scheduleSyncCallback (callback ) { if (syncQueue === null ) { syncQueue = [callback]; immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl); } else { syncQueue.push(callback); } return fakeCallbackNode; }
异步调度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function scheduleCallback (reactPriorityLevel, callback, options ) { var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); return Scheduler_scheduleCallback(priorityLevel, callback, options); } function reactPriorityToSchedulerPriority (reactPriorityLevel ) { switch (reactPriorityLevel) { case ImmediatePriority$1 : return Scheduler_ImmediatePriority; case UserBlockingPriority$2 : return Scheduler_UserBlockingPriority; case NormalPriority$1 : return Scheduler_NormalPriority; case LowPriority$1 : return Scheduler_LowPriority; case IdlePriority$1 : return Scheduler_IdlePriority; default : } }
由上可知,无论是同步任务调度还是异步任务调度,最终都会执行Scheduler_scheduleCallback。
3.1 前置说明 存和取任务
更新任务进入调度程序时,会根据任务是否是延时任务,存入taskQueue或timerQueue任务队列中。其中,taskQueue存放需要立即执行的任务;timerQueue存放延时执行的任务。
任务插入(push)任务队列时,会根据sortIndex、id 属性进行优先级排序(siftUp),优先级最高的任务排在队列首位。 Scheduler会优先取出(peek)taskQueue中的任务去执行,任务执行完成之后,从taskQueue移除(pop),移除之后对任务队列重新排序(siftDown)。
若taskQueue为空,timerQueue中的任务会开始定时器任务,到达任务开始执行时间后,从timerQueue中取出(peek、pop)首个任务,存入(push)taskQueue中.
任务队列采用的是二叉堆(最小堆),即父节点的键值总是小于或等于任何一个子节点的键值。添加元素时,在数组的最末尾插入新节点,然后自下而上调整子节点和父节点的位置(siftUp);删除元素时,将数组的末尾元素放入到根节点,然后自上而下调整子节点和父节点的位置(siftDown).
二叉堆添加和删除元素代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 function push (heap, node ) { var index = heap.length; heap.push(node); siftUp(heap, node, index); } function siftUp (heap, node, i ) { var index = i; while (true ) { var parentIndex = index - 1 >>> 1 ; var parent = heap[parentIndex]; if (parent !== undefined && compare(parent, node) > 0 ) { heap[parentIndex] = node; heap[index] = parent; index = parentIndex; } else { return ; } } } function compare (a, b ) { var diff = a.sortIndex - b.sortIndex; return diff !== 0 ? diff : a.id - b.id; } function peek (heap ) { var first = heap[0 ]; return first === undefined ? null : first; } function pop (heap ) { var first = heap[0 ]; if (first !== undefined ) { var last = heap.pop(); if (last !== first) { heap[0 ] = last; siftDown(heap, last, 0 ); } return first; } else { return null ; } } function siftDown (heap, node, i ) { var index = i; var length = heap.length; while (index < length) { var leftIndex = (index + 1 ) * 2 - 1 ; var left = heap[leftIndex]; var rightIndex = leftIndex + 1 ; var right = heap[rightIndex]; if (left !== undefined && compare(left, node) < 0 ) { if (right !== undefined && compare(right, left) < 0 ) { heap[index] = right; heap[rightIndex] = node; index = rightIndex; } else { heap[index] = left; heap[leftIndex] = node; index = leftIndex; } } else if (right !== undefined && compare(right, node) < 0 ) { heap[index] = right; heap[rightIndex] = node; index = rightIndex; } else { return ; } } }
peek、pop都是用于从二叉堆任务队列里取出首个任务,pop会从二叉堆中移除首个任务,然后进行堆排序;peek只是单纯的取出二叉堆的首个任务,不会移除元素。
调度过程 1)调度分为两个阶段 :
第一阶段:新任务进来后,根据任务过期时间和插入顺序,对任务进行二叉堆的堆排序,优先级最高的任务放在队列的首位。此时并不会立即执行任务队列里的任务,而是port.postMessage(null),等到下一个宏任务再执行;此时设置isHostCallbackScheduled=true,标志着回调正在进行,任务调度中;
第二阶段:浏览器渲染完成,下一轮宏任务开始执行(通过channel.port1.onmessage回调)。执行回调函数flushWork,在时间片的范围内循环执行taskQueue中的任务,此时设置isHostCallbackScheduled=false,标志着第一阶段的任务调度结束,开始执行调度的更新任务了
2)宏任务触发机制
浏览器一帧的执行顺序: 一个宏任务 => 队列中全部微任务 => requestAnimationFrame => 重排/重绘 => requestIdleCallback 其中,requestIdleCallback并不是每次都执行,而是在重绘重排之后,如果还有空闲时间,才会执行requestIdleCallback。
若js执行时间过长,会导致浏览器没时间绘制dom,造成丢帧和卡顿的现象。不影响浏览器重排/重绘最好的方式,是等浏览器渲染完之后,再执行js,即在requestIdleCallback中执行。由于requestIdleCallback存在兼容性和触发时机的问题,react并未采用requestIdleCallback,而是在每一帧分配一个时间片(5ms)给js执行,在这个时间片内,若是还没执行完,那就暂停js,把主线程交个浏览器去绘制,等下一帧继续执行js.
Scheduler的时间切片功能是通过task(宏任务)实现的,浏览器渲染完成之后,才会执行下一轮的宏任务。Scheduler会在宏任务开始时,执行react的更新任务。
浏览器中宏任务优先级排序:主代码块 > setImmediate(node) > MessageChannel > setTimeout / setInterval。可以看出,宏任务中的MessageChannel优先级高于setTimeout / setInterval。
若当前宿主环境支持MessageChannel(浏览器环境),则采用MessageChannel;不支持MessageChannel(非浏览器环境),则采用setTimeout。
1 2 3 4 5 6 7 8 9 var channel = new MessageChannel();var port = channel.port2;channel.port1.onmessage = performWorkUntilDeadline; requestHostCallback = function (callback ) { scheduledHostCallback = callback; port.postMessage(null ); };
1 2 3 4 5 6 7 8 9 requestHostCallback = function (cb ) { if (_callback !== null ) { setTimeout (requestHostCallback, 0 , cb); } else { _callback = cb; setTimeout (_flushCallback, 0 ); } }
Sheduler调度优先级
Immediate 立即执行优先级,需要同步执行的任务
UserBlocking 用户阻塞型优先级(250 ms 后过期),需要作为用户交互结果运行的任务(例如,按钮点击)
Normal 普通优先级(5 s 后过期),不必让用户立即感受到的更新
Low 低优先级(10 s 后过期),可以推迟但最终仍然需要完成的任务(例如,分析通知)
Idle 空闲优先级(永不过期),不必运行的任务(例如,隐藏界面以外的内容)
sheduler中的调度优先级
1 2 3 4 5 6 var NoPriority = 0 ;var ImmediatePriority = 1 ;var UserBlockingPriority = 2 ;var NormalPriority = 3 ;var LowPriority = 4 ;var IdlePriority = 5 ;
sheduler调度优先级对应的过期时间
1 2 3 4 5 var IMMEDIATE_PRIORITY_TIMEOUT = -1 ; var USER_BLOCKING_PRIORITY_TIMEOUT = 250 ;var NORMAL_PRIORITY_TIMEOUT = 5000 ;var LOW_PRIORITY_TIMEOUT = 10000 ; var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
3.2 代码
isHostCallbackScheduled:表示当前是否有调度任务(第二阶段中宏任务里要执行的回调函数是否被执行了)。若是当前有下一轮宏任务需要执行的回调函数(flushWork),设置isHostCallbackScheduled=true;若回调函数任务开始执行了,设置为isHostCallbackScheduled=false;(针对taskQueue任务的第一阶段)
isHostTimeoutScheduled: 表示当前是否有定时器任务。timerQueue中的任务设置定时器延时执行,此时isHostTimeoutScheduled=true;若是延时任务到了开始执行的时间,设置isHostTimeoutScheduled=false(针对timerQueue中的任务)
isPerformingWork:当前是否有正在执行的更新任务(针对taskQueue任务的第二阶段)
第一阶段
计算任务的开始时间
根据任务优先级priorityLevel计算任务的过期时间
创建调度任务
任务还没到开始执行时间,存入timerQueue;否则,存入taskQueue
taskQueue不为空,调用requestHostCallback,port.postMessage(null),等待下一个宏任务
taskQueue为空,timerQueue中首个任务开始定时器任务,直到任务开始时间,存入taskQueue中,调用requestHostCallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 function unstable_scheduleCallback (priorityLevel, callback, options ) { var currentTime = exports .unstable_now(); var startTime; if (typeof options === 'object' && options !== null ) { var delay = options.delay; if (typeof delay === 'number' && delay > 0 ) { startTime = currentTime + delay; } else { startTime = currentTime; } } else { startTime = currentTime; } var timeout; switch (priorityLevel) { case ImmediatePriority: timeout = IMMEDIATE_PRIORITY_TIMEOUT; break ; case UserBlockingPriority: timeout = USER_BLOCKING_PRIORITY_TIMEOUT; break ; case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break ; case LowPriority: timeout = LOW_PRIORITY_TIMEOUT; break ; case NormalPriority: default : timeout = NORMAL_PRIORITY_TIMEOUT; break ; } var expirationTime = startTime + timeout; var newTask = { id: taskIdCounter++, callback: callback, priorityLevel: priorityLevel, startTime: startTime, expirationTime: expirationTime, sortIndex: -1 }; if (startTime > currentTime) { newTask.sortIndex = startTime; push(timerQueue, newTask); if (peek(taskQueue) === null && newTask === peek(timerQueue)) { if (isHostTimeoutScheduled) { cancelHostTimeout(); } else { isHostTimeoutScheduled = true ; } requestHostTimeout(handleTimeout, startTime - currentTime); } } else { newTask.sortIndex = expirationTime; push(taskQueue, newTask); if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true ; requestHostCallback(flushWork); } } return newTask; }
taskQueue中的任务操作:
1 2 3 4 5 6 7 8 9 10 requestHostCallback = function (callback ) { scheduledHostCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true ; port.postMessage(null ); } }
timerQueue中的任务操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 function handleTimeout (currentTime ) { isHostTimeoutScheduled = false ; advanceTimers(currentTime); if (!isHostCallbackScheduled) { if (peek(taskQueue) !== null ) { isHostCallbackScheduled = true ; requestHostCallback(flushWork); } else { var firstTimer = peek(timerQueue); if (firstTimer !== null ) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); } } } } function advanceTimers (currentTime ) { var timer = peek(timerQueue); while (timer !== null ) { if (timer.callback === null ) { pop(timerQueue); } else if (timer.startTime <= currentTime) { pop(timerQueue); timer.sortIndex = timer.expirationTime; push(taskQueue, timer); } else { return ; } timer = peek(timerQueue); } } requestHostTimeout = function (callback, ms ) { taskTimeoutID = _setTimeout(function ( ) { callback(exports .unstable_now()); }, ms); }; cancelHostTimeout = function ( ) { _clearTimeout(taskTimeoutID); taskTimeoutID = -1 ; };
第二阶段 在下一轮宏任务中执行更新任务。真正执行更新任务的入口:performWorkUntilDeadline
计算任务的暂停时间,当前时间加上时间片(5ms);
在时间片的范围内,循环执行taskQueue中的任务,并发模式是performConcurrentWorkOnRoot;block模式是performSyncWorkOnRoot;
时间片内,调度的任务taskQueue执行完毕,重置isMessageLoopRunning以及scheduledHostCallback,停止调度;从timerQueue找到优先级最高的任务,开启定时器任务,直到任务的开始时间
时间片结束,还有任务没有执行完成,继续port.postMessage(null),通过宏任务进入下一轮performWorkUntilDeadlined,继续执行workLoop
1 2 3 4 var channel = new MessageChannel();var port = channel.port2;channel.port1.onmessage = performWorkUntilDeadline;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 var yieldInterval = 5 ; var performWorkUntilDeadline = function ( ) { if (scheduledHostCallback !== null ) { var currentTime = exports .unstable_now(); deadline = currentTime + yieldInterval; var hasTimeRemaining = true ; try { var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); if (!hasMoreWork) { isMessageLoopRunning = false ; scheduledHostCallback = null ; } else { port.postMessage(null ); } } catch (error) { port.postMessage(null ); throw error; } } else { isMessageLoopRunning = false ; } }; function flushWork (hasTimeRemaining, initialTime ) { isHostCallbackScheduled = false ; if (isHostTimeoutScheduled) { isHostTimeoutScheduled = false ; cancelHostTimeout(); } isPerformingWork = true ; var previousPriorityLevel = currentPriorityLevel; try { return workLoop(hasTimeRemaining, initialTime) } finally { currentTask = null ; currentPriorityLevel = previousPriorityLevel; isPerformingWork = false ; } } function workLoop (hasTimeRemaining, initialTime ) { var currentTime = initialTime; advanceTimers(currentTime); currentTask = peek(taskQueue); while (currentTask !== null && !(enableSchedulerDebugging )) { if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) { break ; } var callback = currentTask.callback; if (typeof callback === 'function' ) { currentTask.callback = null ; currentPriorityLevel = currentTask.priorityLevel; var didUserCallbackTimeout = currentTask.expirationTime <= currentTime; var continuationCallback = callback(didUserCallbackTimeout); currentTime = getCurrentTime(); if (typeof continuationCallback === 'function' ) { currentTask.callback = continuationCallback; } else { if (currentTask === peek(taskQueue)) { pop(taskQueue); } } advanceTimers(currentTime); } else { pop(taskQueue); } currentTask = peek(taskQueue); } if (currentTask !== null ) { return true ; } else { var firstTimer = peek(timerQueue); if (firstTimer !== null ) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); } return false ; } }