三、調度Scheduler
scheduling(調度)是fiber reconciliation的一個過程,主要決定應該在何時做什么?在stack reconciler中,reconciliation是“一氣呵成”,對于函數來說,這沒什么問題,因為我們只想要函數的運行結果,但對于UI來說還需要考慮以下問題:
并不是所有的state更新都需要立即顯示出來,比如屏幕之外的部分的更新;
并不是所有的更新優先級都是一樣的,比如用戶輸入的響應優先級要比通過請求填充內容的響應優先級更高;
理想情況下,對于某些高優先級的操作,應該是可以打斷低優先級的操作執行的,比如用戶輸入時,頁面的某個評論還在reconciliation,應該優先響應用戶輸入。比如18版本里提示一些不安全的生命周期主要時它被打斷了可能會被執行多次。
所以理想狀況下reconciliation的過程應該是每次只做一個很小的任務,做完后能夠“喘口氣兒”,回到主線程看下有沒有什么更高優先級的任務需要處理,如果有則先處理更高優先級的任務,沒有則繼續執行(cooperative scheduling 合作式調度)。
當用戶操作時,調用setState,react會把當前的更新送入對應組件對應的update queue中。但是react并不會立即執行對比并修改DOM的操作。而是交給scheduler去處理。
scheduler會根據當前主線程的使用情況去處理這次update。為了實現這種特性,最開始考慮使用了requestIdelCallback API。
總的來講,通常,客戶端線程執行任務時會以幀的形式劃分,大部分設備控制在30-60幀是不會影響用戶體驗;在兩個執行幀之間,主線程通常會有一小段空閑時間,requestIdleCallback可以在這個空閑期(Idle Period)調用空閑期回調(Idle Callback),執行一些任務。
低優先級任務由requestIdleCallback處理;
高優先級任務,如動畫相關的由requestAnimationFrame處理;
requestIdleCallback可以在多個空閑期調用空閑期回調,執行任務;
requestIdleCallback方法提供deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞UI渲染而導致掉幀;
但是由于requestIdleCallback有以下兩個問題就采用了messageChannel模擬實現了requestIdleCallback。
1)兼容性;
2)50ms 渲染問題;(可能在一些任務很長時這個回調不會執行)
|— task queue —|— micro task —|— raf —|— render —|— requestIdleCallback – -|
requestIdleCallback是宏任務,messageChannel也宏任務。
為什么沒有? generator ?因為它是有狀態的,無法從中間中斷。
為什么沒有? setTimeout ?因為setTimeout有4-5ms的延時。
模擬了requestIdleCallback行為:
/*** schedule —> 把我的任務放進一個隊列里,然后以某一種節奏進行執行;* */// task 的任務隊列
const queue = [];
const threshold = 1000 / 60;const transtions = [];
let deadline = 0;// 獲取當前時間, bi date-now 精確
const now = () => performance.now(); // 時間 ,精確
// 從任務queue中,選擇第一個 任務
const peek = arr => arr.length === 0 ? null : arr[0];// schedule —> 把我的任務放進一個隊列里,然后以某一種節奏進行執行;
export function schedule (cb) {queue.push(cb);startTranstion(flush);
}// 此時,是否應該交出執行權
function shouldYield() {return navigator.scheduling.isInputPending() || now() >= deadline;
}// 執行權的切換
function startTranstion(cb) {transtions.push(cb) && postMessage();
}// 執行權的切換
const postMessage = (() => {const cb = () => transtions.splice(0, 1).forEach(c => c());const { port1, port2 } = new MessageChannel();port1.onmessage = cb;return () => port2.postMessage(null);
})()// 模擬實現 requestIdleCallback 方法
function flush() {// 生成時間,用于判斷deadline = now() + threshold;let task = peek(queue);// 我還沒有超出 16.666ms 同時,也沒有更高的優先級打斷我while(task && !shouldYield()) {const { cb } = task;const next = cb();// 相當于有一個約定,如果,你這個task 返回的是一個函數,那下一次,就從你這里接著跑// 那如果 task 返回的不是函數,說明已經跑完了。不需要再從你這里跑了if(next && typeof next === "function") {task.cb = next;} else {queue.shift()}task = peek(queue);}// 如果我的這一個時間片,執行完了,到了這里。task && startTranstion(flush)
}
一旦reconciliation過程得到時間片,就開始進入work loop。work loop機制可以讓react在計算狀態和等待狀態之間進行切換。為了達到這個目的,對于每個loop而言,需要追蹤兩個東西:下一個工作單元(下一個待處理的fiber);當前還能占用主線程的時間。第一個loop,下一個待處理單元為根節點。
每個工作單元(fiber)執行完成后,都會查看是否還繼續擁有主線程時間片,如果有繼續下一個,如果沒有則先處理其他高優先級事務,等主線程空閑下來繼續執行
react17版本有時間切片ric,但是沒有使用。18版本里才使用了。
宏任務微任務執行示例
四、diff算法
react diff算法最好時是O(n), 最差的話,是 O(mn),而傳統的diff算法是O(n^3)。
react 是如何將 diff 算法的復雜度降下來的?
其實就是在算法復雜度、虛擬 dom 渲染機制、性能中找了?個平衡,react 采?了啟發式的算法,做了如下最優假設:
a. 如果節點類型相同,那么以該節點為根節點的 tree 結構,?概率是相同的,所以如果類型不同,可以直接「刪除」原節點,「插?」新節點;
b. 跨層級移動? tree 結構的情況?較少?,或者可以培養?戶使?習慣來規避這種情況,遇到這種情況同樣是采?先「刪除」再「插?」的?式,這樣就避免了跨層級移動
c. 同?層級的?元素,可以通過 key 來緩存實例,然后根據算法采取「插?」「刪除」「移動」的操作,盡量復?,減少性能開銷
d. 完全相同的節點,其虛擬 dom 也是完全?致的;
react為什么不去優化diff算法?
因為新版本下,diff算法不是約束性能瓶頸的問題了。
為什么要有key?
在?較時,會以 key 和 type 是否相同進??較,如果相同,則直接復制
vue diff算法和react diff算法相同/不同點:
共同點:
vue和diff算法,都是不進行跨層級比較,只做同級比較
不同點:
1.vue進行diff時,調用patch打補丁函數,一邊比較一邊給真實的dom打補丁,vue對比節點時,當節點元素類型相同,類名不同時,認為是不同的元素,刪除重新創建,而react認為是同類型的節點,進行修改操作
2.vue列表對比的時候,采用從兩端到中間的方式,舊集合和新集合兩端各存在兩個指針,兩兩進行比較,每次對比結束后,指針向隊列中間移動;react則是從左往右一次對比,利用元素的index和lastindex進行比較
3.當一個集合把最后一個節點移動到最前面,react會把前面的節點依次向后移動,而Vue只會把最后一個節點放在最前面,這樣的操作來看,Vue的diff性能是高于react的。
四、模擬實現react流程
react.js
const normalize = (children = []) => children.map(child => typeof child === 'string' ? createVText(child): child)export const NODE_FLAG = {EL: 1, // 元素 elementTEXT: 1 << 1
};
// El & TEXT = 0const createVText = (text) => {return {type: "",props: {nodeValue: text + ""},$$: { flag: NODE_FLAG.TEXT }}
}const createVNode = (type, props, key, $$) => {return {type, props,key,$$,}
}export const createElement = (type, props, ...kids) => {props = props || {};let key = props.key || void 0;kids = normalize(props.children || kids);if(kids.length) props.children = kids.length === 1? kids[0] : kids;// 定義一下內部的屬性const $$ = {};$$.staticNode = null;$$.flag = type === "" ? NODE_FLAG.TEXT: NODE_FLAG.EL;return createVNode(type, props, key, $$)
}
path.js
import { mount } from "./mount";
import { diff } from './diff';function patchChildren(prev, next, parent) {// diff 整個的邏輯還是耗性能的,所以,我們可以先提前做一些處理。if(!prev) {if(!next) {// nothing} else {next = Array.isArray(next) ? next : [next];for(const c of next) {mount(c, parent);}}} else if (prev && !Array.isArray(prev)) {// 只有一個 childrenif(!next) parent.removeChild(prev.staticNode);else if(next && !Array.isArray(next)) {patch(prev, next, parent)} else {// 如果prev 只有一個節點,next 有多個節點parent.removeChild(prev.staticNode);for(const c of next) {mount(c, parent);}}} else diff(prev, next, parent);
}export function patch (prev, next, parent) {// type: 'div' -> 'ul'if(prev.type !== next.type) {parent.removeChild(prev.staticNode);mount(next, parent);return;}// type 一樣,diff props // 先不看 children const { props: { children: prevChildren, ...prevProps}} = prev;const { props: { children: nextChildren, ...nextProps}} = next;// patch Porpsconst staticNode = (next.staticNode = prev.staticNode);for(let key of Object.keys(nextProps)) {let prev = prevProps[key],next = nextProps[key]patchProps(key, prev, next, staticNode)}for(let key of Object.keys(prevProps)) {if(!nextProps.hasOwnProperty(key)) patchProps(key, prevProps[key], null, staticNode);}// patch Children !!!patchChildren(prevChildren,nextChildren,staticNode)}export function patchProps(key, prev, next, staticNode) {// style if(key === "style") {// margin: 0 padding: 10if(next) {for(let k in next) {staticNode.style[k] = next[k];}}if(prev) {// margin: 10; color: redfor(let k in prev) {if(!next.hasOwnProperty(k)) {// style 的屬性,如果新的沒有,老的有,那么老的要刪掉。staticNode.style[k] = "";}}}}else if(key === "className") {if(!staticNode.classList.contains(next)) {staticNode.classList.add(next);}}// eventselse if(key[0] === "o" && key[1] === 'n') {prev && staticNode.removeEventListener(key.slice(2).toLowerCase(), prev);next && staticNode.addEventListener(key.slice(2).toLowerCase(), next);} else if (/\[A-Z]|^(?:value|checked|selected|muted)$/.test(key)) {staticNode[key] = next} else {staticNode.setAttribute && staticNode.setAttribute(key, next);}
}
mount.js
import { patchProps } from "./patch";
import { NODE_FLAG } from "./react";export function mount(vnode, parent, refNode) {// 為什么會有一個 refNode?/** |* 假如: ul -> li li li(refNode) */if(!parent) throw new Error('no container');const $$ = vnode.$$;if($$.flag & NODE_FLAG.TEXT) {// 如果是一個文本節點const el = document.createTextNode(vnode.props.nodeValue);vnode.staticNode = el;parent.appendChild(el);} else if($$.flag & NODE_FLAG.EL) {// 如果是一個元素節點的情況,先不考慮是一個組件的情況;const { type, props } = vnode;const staticNode = document.createElement(type);vnode.staticNode = staticNode;// 我們再來處理,children 和后面的內容const { children, ...rest} = props;if(Object.keys(rest).length) {for(let key of Object.keys(rest)) {// 屬性對比的函數patchProps(key, null, rest[key], staticNode);}}if(children) {// 遞歸處理子節點const __children = Array.isArray(children) ? children : [children];for(let child of __children) {mount(child, staticNode);}}refNode ? parent.insertBefore(staticNode, refNode) : parent.appendChild(staticNode);}}
diff.js
import { mount } from './mount.js'
import { patch } from './patch.js'export const diff = (prev, next, parent) => {let prevMap = {}let nextMap = {}// 遍歷我的老的 childrenfor (let i = 0; i < prev.length; i++) {let { key = i + '' } = prev[i]prevMap[key] = i}let lastIndex = 0// 遍歷我的新的 childrenfor (let n = 0; n < next.length; n++) {let { key = n + '' } = next[n]// 老的節點let j = prevMap[key]// 新的 childlet nextChild = next[n]nextMap[key] = n// 老的children 新的children// [b, a] [c, d, a] => [c, b, a] --> c// [b, a] [c, d, a] => [c, d, b, a] --> dif (j == null) {// 從老的里面,沒有找到。新插入let refNode = n === 0 ? prev[0].staticNode : next[n - 1].staticNode.nextSiblingmount(nextChild, parent, refNode)}else {// [b, a] [c, d, a] => [c, d, a, b] --> a// 如果找到了,我 patch patch(prev[j], nextChild, parent)if (j < lastIndex) {// 上一個節點的下一個節點的前面,執行插入let refNode = next[n - 1].staticNode.nextSibling;parent.insertBefore(nextChild.staticNode, refNode)}else {lastIndex = j}}}// [b, a] [c, d, a] => [c, d, a] --> bfor (let i = 0; i < prev.length; i++) {let { key = '' + i } = prev[i]if (!nextMap.hasOwnProperty(key)) parent.removeChild(prev[i].staticNode)}
}
render.js
import { mount } from "./mount";
import { patch } from "./patch";// step 1
// setTimeout(() => render(vnode, document.getElementById("app")))// step 2
// setTimeout(() => render(null, document.getElementById("app")),5000)export function render(vnode, parent) {let prev = parent.__vnode;if(!prev) {mount(vnode, parent);parent.__vnode = vnode;} else {if(vnode) {// 新舊兩個patch(prev, vnode, parent);parent.__vnode = vnode;} else {parent.removeChild(prev.staticNode)}}
}
index.js
import { render } from "./render";
import { createElement } from "./react";// 用戶的開發:
// react / preact / vueconst vnode = createElement("ul",{id: "ul-test",className: "padding-20",style: {padding: "10px",},},createElement("li", { key: "li-0" }, "this is li 01")
);const nextVNode = createElement("ul",{style: {width: "100px",height: "100px",backgroundColor: "green",},},[createElement("li", { key: "li-a" }, "this is li a"),createElement("li", { key: "li-b" }, "this is li b"),createElement("li", { key: "li-c" }, "this is li c"),createElement("li", { key: "li-d" }, "this is li d"),]
);const lastVNode = createElement("ul",{style: {width: "100px",height: "200px",backgroundColor: "pink",},},[createElement("li", { key: "li-a" }, "this is li a"),createElement("li", { key: "li-c" }, "this is li c"),createElement("li", { key: "li-d" }, "this is li d"),createElement("li", { key: "li-f" }, "this is li f"),createElement("li", { key: "li-b" }, "this is li b"),]
);setTimeout(() => render(vnode, document.getElementById("app")))
setTimeout(() => render(nextVNode, document.getElementById("app")),6000)
setTimeout(() => render(lastVNode, document.getElementById("app")),8000)
console.log(nextVNode);
使用rollup進行編譯運行:
下載rollup插件,創建rollup.config.js文件
const livereload = require('rollup-plugin-livereload');
const serve = require('rollup-plugin-serve');module.exports = {input: './react/index.js',output: {file: './dist/bundle.js',format: "iife" // es, umd, amd, cjs,iife以script腳本加載執行},plugins: [livereload(),serve({openPage: "/public/index.html",port: 3020,contentBase:'./'})]
}// rollup -c // 我默認去找根目錄下的 rollup.config.js -w 監聽文件變化,重新編譯。
執行 rollup -c // 我默認去找根目錄下的 rollup.config.js -w 監聽文件變化,重新編譯。
將輸入開始的文件,編譯打包到output目錄下。這樣就可以訪問對應端口查看頁面。
react 冒泡到fiberroot 而不是到root,是因為render函數可能調用多次,會導致錯亂。
react為什么實現合成事件,是因為如果寫很多監聽事件會導致性能下降,還有兼容性問題。
react18的新特性
React 18 中的重大更改僅限于幾個簡單的 API 更改,以及對 React 中多個行為的穩定性和一致性的一些改進,比較重要的一點是,不再支持 IE 瀏覽器。
1、客戶端渲染 API
帶有 createRoot() 的 root API,替換現有的 render() 函數,提供更好的人體工程學并啟用新的并發渲染特性。
2、自動批量處理
以下都是批量處理了,以優化性能并避免重渲染。但是之前的版本在settimeout里是會渲染兩次的。
const App = () => {const handleClick = () => {setA((a) => a + 1);setB((b) => b - 1);// Updates batched - single re-render};setTimeout(() => {setA((a) => a + 1);setB((b) => b - 1);// New (v18): Updates batched - single re-render}, 1000);// ...
};
3、并發渲染特性,比圖startansition等,它是基于任務優先級,時間分片實現的。