摘要:想要自己實現一個簡易版框架,并不是非常難。為了防止出現這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構的區別。
想要自己實現一個React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點
如果你能閱讀以下的文章,那么會更輕松的閱讀本文章:
優化你的超大型React應用
手寫一個React腳手架
為了降低本文難度,構建工具選擇了parcel,歡迎加入我們的前端交流群~ gitHub倉庫源碼地址和二維碼都會在最后放出來~
什么是虛擬DOM?其實就是一個個的具有固定格式的JS對象,例如:
const obj = { tag:"div", attrs:{ className:"test" }, children:[ tag:"span", attrs:{ className:"text" }, tag:"p", attrs:{ className:"p" }, ] }怎么生成對應的虛擬DOM對象?
先把代碼變成抽象語法樹(AST)
然后進行對應的處理
輸出成瀏覽器可以識別的代碼-即js對象
這一切都是基于Babel做的 babel在線編譯測試
class App extends React.Component{ render(){ return123} }
上面這段代碼 會被編譯成:
... _createClass(App, [{ key: "render", value: function render() { return React.createElement("div", null, "123"); } }]); //省略掉一部分代碼
最核心的一段jsx代碼, return
我們寫的jsx代碼,都會被轉換成React.createElement這種形式
那我們只要自己一個React全局對象,給它掛載這個React.createElement方法就可以進行接下來的處理:
const React = {}; React.createElement = function(tag, attrs, ...children) { return { tag, attrs, children }; }; export default React;
我們定義的React.createElement方法也很簡單,只是把對應的參數集中變成一個特定格式的對象,然后返回,再接下來進行處理~。Babel的配置會幫我們自動把jsx轉換成React.creatElement的代碼,參數都會默認幫我們傳好~
構建工具我們使用零配置的parcel ,相比webpack來說,更容易上手,當然對于一個把webpack玩透了的人來說,其實用什么都一樣~
npm install -g parcel-bundler
parcel index.html即可運行項目
// .babelrc 配置 { "presets": ["env"], "plugins": [ ["transform-react-jsx", { "pragma": "React.createElement" }] ] }處理好了jsx代碼,我們入口開始寫起:
ReactDOM.render方法是我們的入口
先定義ReactDOM對象,以及它的render方法~
const ReactDom = {}; //vnode 虛擬dom,即js對象 //container 即對應的根標簽 包裹元素 const render = function(vnode, container) { return container.appendChild(_render(vnode)); }; ReactDom.render = render;
思路: 先把虛擬dom對象-js對象變成真實dom對象,然后插入到根標簽內。
_render方法,接受虛擬dom對象,返回真實dom對象:
如果傳入的是null,字符串或者數字 那么直接轉換成真實dom然后返回就可以了~
if (vnode === undefined || vnode === null || typeof vnode === "boolean") vnode = ""; if (typeof vnode === "number") vnode = String(vnode); if (typeof vnode === "string") { let textNode = document.createTextNode(vnode); return textNode; } const dom = document.createElement(vnode.tag); return dom
但是有可能傳入的是個div標簽,而且它有屬性。那么需要處理屬性,由于這個處理屬性的函數需要大量復用,我們多帶帶定義成一個函數:
if (vnode.attrs) { Object.keys(vnode.attrs).forEach(key => { const value = vnode.attrs[key]; handleAttrs(dom, key, value); }); } function setAttribute(dom, name, value) { if (name === "className") name = "class"; if (/onw+/.test(name)) { name = name.toLowerCase(); dom[name] = value || ""; } else if (name === "style") { if (!value || typeof value === "string") { dom.style.cssText = value || ""; } else if (value && typeof value === "object") { for (let name in value) { dom.style[name] = typeof value[name] === "number" ? value[name] + "px" : value[name]; } } } else { if (name in dom) { dom[name] = value || ""; } if (value) { dom.setAttribute(name, value); } else { dom.removeAttribute(name); } } }
但是可能有子節點的嵌套,于是要用到遞歸:
vnode.children && vnode.children.forEach(child => render(child, dom)); // 遞歸渲染子節點
上面沒有考慮到組件,只考慮到了div或者字符串數字之類的虛擬dom.
其實加入組件也很簡單:加入新一個新的處理方式:
我們先定義好Component這個類,并且掛載到全局React的對象上
export class Component { constuctor(props = {}) { this.state = {}; this.props = props; } setState(stateChange) { // 將修改合并到state console.log("setstate"); const newState = Object.assign(this.state, stateChange); console.log("state:", newState); renderComponent(this); } } .... //掛載Component類到全局React上 React.Component = Component
如果是組件,Babel會幫我們把第一個參數變成function
if (typeof vnode.tag === "function") { //先創建組件 const component = createComponent(vnode.tag, vnode.attrs); //設置屬性 setComponentProps(component, vnode.attrs) //返回的是真實dom對象 return component.base; }
createComponent和setComponentProps都是我們自己定義的方法~后期大量復用
export function createComponent(component, props) { let inst; // 如果是類定義組件,則直接返回實例 if (component.prototype && component.prototype.render) { inst = new component(props); // 如果是函數定義組件,則將其擴展為類定義組件 } else { inst = new Component(props); inst.constructor = component; inst.render = function() { return this.constructor(props); }; } return inst; }
export function setComponentProps(component, props) { if (!component.base) { if (component.componentWillMount) component.componentWillMount(); } else if (component.base && component.componentWillReceiveProps) { component.componentWillReceiveProps(props); } component.props = props; renderComponent(component); }
renderComponent也是我們自己定義的方法,用來渲染組件:
export function renderComponent(component) { console.log("renderComponent"); let base; const renderer = component.render(); if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } base = _render(renderer); if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { component.base = base; component.componentDidMount && component.componentDidMount(); if (component.base && component.base.parentNode) { component.base.parentNode.replaceChild(base, component.base); } return; } if (component.base && component.base.parentNode) { component.base.parentNode.replaceChild(base, component.base); } //base是真實dom對象 //component.base是將本次渲染好的dom對象掛載到組件上,方便判斷是否首次掛載 component.base = base; //互相飲用,方便后期的隊列處理 base._component = component; }
最簡單的版本已經完成,對應的生命簡單周期做了粗糙處理,但是沒有加入diff算法和異步setState,歡迎移步gitHub點個star
最簡單版React-無diff算法和異步state,選擇master分支
沒有diff算法,更新state后是所有的節點都要更新,這樣性能損耗非常大。現在我們開始加入React的diff算法
首先改造renderComponent方法
function renderComponent(component, newState = {}) { console.log("renderComponent"); //真實dom對象 let base; //虛擬dom對象 const renderer = component.render(); //component.base是為了表示是否經過初次渲染,好進行生命周期函數調用 if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } if (component.base && component.shouldComponentUpdate) { //如果組件經過了初次渲染,是更新階段,那么可以根據這個生命周期判斷是否更新 let result = true; result = component.shouldComponentUpdate && component.shouldComponentUpdate((component.props = {}), newState); if (!result) { return; } } //得到diff算法對比后的真實dom對象 base = diffNode(component.base, renderer); if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { //為了防止死循環,調用完`didMount`函數就結束。 component.base = base; base._component = component; component.componentDidMount && component.componentDidMount(); return; } component.base = base; base._component = component; }
注意,我們是跟preact一樣,將真實dom對象和虛擬dom對象進行對比:
分為下面幾種diff:
Node節點diff
Component組件diff
屬性diff
純文本或者數字的diff...
子節點的diff(這個最復雜)
純文本或者數字的diff:
純文本和數字之類的直接替換掉dom節點的textContent即可
diffNode(dom, vnode) { let out = dom; if (vnode === undefined || vnode === null || typeof vnode === "boolean") vnode = ""; if (typeof vnode === "number") vnode = String(vnode); // diff text node if (typeof vnode === "string") { // 如果當前的DOM就是文本節點,則直接更新內容 if (dom && dom.nodeType === 3) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType if (dom.textContent !== vnode) { dom.textContent = vnode; } // 如果DOM不是文本節點,則新建一個文本節點DOM,并移除掉原來的 } else { out = document.createTextNode(vnode); if (dom && dom.parentNode) { dom.parentNode.replaceChild(out, dom); } } return out; }
Component組件diff
如果不是一個類型組件直接替換掉,否則只更新屬性即可
function diffComponent(dom, vnode) { let c = dom && dom._component; let oldDom = dom; // 如果組件類型沒有變化,則重新set props if (c && c.constructor === vnode.tag) { setComponentProps(c, vnode.attrs); dom = c.base; // 如果組件類型變化,則移除掉原來組件,并渲染新的組件 } else { if (c) { unmountComponent(c); oldDom = null; } c = createComponent(vnode.tag, vnode.attrs); setComponentProps(c, vnode.attrs); dom = c.base; if (oldDom && dom !== oldDom) { oldDom._component = null; removeNode(oldDom); } } return dom; }
屬性的diff
export function diffAttributes(dom, vnode) { const old = {}; // 當前DOM的屬性 const attrs = vnode.attrs; // 虛擬DOM的屬性 for (let i = 0; i < dom.attributes.length; i++) { const attr = dom.attributes[i]; old[attr.name] = attr.value; } // 如果原來的屬性不在新的屬性當中,則將其移除掉(屬性值設為undefined) for (let name in old) { if (!(name in attrs)) { handleAttrs(dom, name, undefined); } } // 更新新的屬性值 for (let name in attrs) { if (old[name] !== attrs[name]) { handleAttrs(dom, name, attrs[name]); } } }
children的diff
function diffChildren(dom, vchildren) { const domChildren = dom.childNodes; //沒有key值的真實dom集合 const children = []; //有key值的集合 const keyed = {}; if (domChildren.length > 0) { for (let i = 0; i < domChildren.length; i++) { const child = domChildren[i]; const key = child.key; if (key) { keyed[key] = child; } else { children.push(child); } } } if (vchildren && vchildren.length > 0) { let min = 0; let childrenLen = children.length; for (let i = 0; i < vchildren.length; i++) { const vchild = vchildren[i]; const key = vchild.key; let child; if (key) { if (keyed[key]) { child = keyed[key]; keyed[key] = undefined; } } else if (min < childrenLen) { for (let j = min; j < childrenLen; j++) { let c = children[j]; if (c && isSameNodeType(c, vchild)) { child = c; children[j] = undefined; if (j === childrenLen - 1) childrenLen--; if (j === min) min++; break; } } } child = diffNode(child, vchild); const f = domChildren[i]; if (child && child !== dom && child !== f) { if (!f) { dom.appendChild(child); } else if (child === f.nextSibling) { removeNode(f); } else { dom.insertBefore(child, f); } } } } }
children的diff這段,確實看起來不那么簡單,總結兩點精髓:
利用key值將節點分成兩個隊列
先對比有key值的節點,然后對比相同類型的節點,然后進行dom操作
shouldComponentUpdate的對比優化:
shouldComponentUpdate(nextProps, nextState) { if (nextState.test > 5) { console.log("shouldComponentUpdate中限制了更新") alert("shouldComponentUpdate中限制了更新") return false; } return true; }
效果:
建議去倉庫看完整源碼認真斟酌:
帶diff算法版mini-React,選擇diff分支
看加入了diff算法后的效果
當然state更新后,只是更新了對應的節點,所謂的diff算法,就是將真實dom和虛擬dom對比后,直接dom操作。操作那些有更新的節點~ 當然也有直接對比兩個虛擬dom對象,然后打補丁上去~我們這種方式如果做SSR同構就不行,因為我們服務端沒dom對象這個說法,無法運行~
這段diff是有點硬核,但是去倉庫認真看看,自己嘗試寫寫,也是可以啃下來的。異步合并更新state版
上面的版本,每次setState都會更新組件,這樣很不友好,因為有可能一個操作會帶來很多個setState,而且很可能會頻繁更新state。為了優化性能,我們把這些操作都放在一幀內去操作~
這里我們使用requestAnimationFrame,去執行合并操作~
首先更新setState入口,不要直接重新渲染組件:
import { _render } from "../reactDom/index"; import { enqueueSetState } from "./setState"; export class Component { constuctor(props = {}) { this.state = {}; this.props = props; } setState(stateChange) { // 將修改合并到state console.log("setstate"); const newState = Object.assign(this.state, stateChange); console.log("state:", newState); this.newState = newState; enqueueSetState(newState, this); } }
enqueueSetState是我們的一個入口函數:
function enqueueSetState(stateChange, component) { if (setStateQueue.length === 0) { //清空隊列的辦法是異步執行,下面都是同步執行的一些計算 defer(flush); } //向隊列中添加對象 key:stateChange value:component setStateQueue.push({ stateChange, component }); //如果渲染隊列中沒有這個組件 那么添加進去 if (!renderQueue.some(item => item === component)) { renderQueue.push(component); } }
上面代碼的精髓:
先執行同步代碼
首次setState調用進入if (setStateQueue.length === 0) 的判斷
異步在下一幀執行flush函數
同步執行setStateQueue.push
同步執行 renderQueue.push(component)
最后執行defer函數
defer函數
function defer(fn) { //requestIdleCallback的兼容性不好,對于用戶交互頻繁多次合并更新來說 ,requestAnimation更有及時性高優先級,requestIdleCallback則適合處理可以延遲渲染的任務~ // if (window.requestIdleCallback) { // console.log("requestIdleCallback"); // return requestIdleCallback(fn); // } //高優先級任務 return requestAnimationFrame(fn); }
思考了很久,決定還是用requestAnimationFrame,為了體現界面交互的及時性
flush清空隊列的函數:
function flush() { let item, component; //依次取出對象,執行 while ((item = setStateQueue.shift())) { const { stateChange, component } = item; // 如果沒有prevState,則將當前的state作為初始的prevState if (!component.prevState) { component.prevState = Object.assign({}, component.state); } // 如果stateChange是一個方法,也就是setState的第二種形式 if (typeof stateChange === "function") { Object.assign( component.state, stateChange(component.prevState, component.props) ); } else { // 如果stateChange是一個對象,則直接合并到setState中 Object.assign(component.state, stateChange); } component.prevState = component.state; } //依次取出組件,執行更新邏輯,渲染 while ((component = renderQueue.shift())) { renderComponent(component); } }
flush函數的精髓:
抽象隊列,一個是對應的改變state和組件的隊列, 一個是需要更新的組件隊列
每一幀就清空當前setState隊列的需要更新的組件,一次性合并清空
完整代碼倉庫地址,歡迎star:
帶diff算法和異步state的minj-react
當我們有100個節點需要更新的時候,我們正在遞歸對比節點,此時用戶點擊界面需要彈框,那么可能會造成延遲彈出窗口,根據RAID,超過100ms,用戶就會感覺明顯卡頓。為了防止出現這種情況,我們需要改變整體的diff策略。把遞歸的對比,改成可以暫停執行的循環對比,這樣如果即時我們在對比階段,有用戶點擊需要交互的時候,我們可以暫停對比,處理用戶交互。
上面這段話,說的就是stack版本和Fiber架構的區別。
stack版本就是我們上面的版本
Fiber版本:思路:
將對比階段分割成一個個小任務
采用兩個虛擬dom對象的去diff對比方式,單鏈表結構,三根指針,return children sibling。
每幀完成一個小任務,然后去執行requestAnimationFrame,如果還有時間,那么就去執行requestIdleCallback.
這個版本暫時就結束了哦~ 歡迎加入我們的前端交流群,還有前往gitHub給個star。
本人參考:
hujiulong的博客,感謝這些大佬的無私開源
前端交流群:
現在人數超過了100人,所以只能加我,然后拉你們進群!!
另外深圳招收跨平臺開發Electron+React的即時通訊產品前端工程師
歡迎投遞: 453089136@qq.com - Peter
招收中級和高級各一名~團隊氛圍nice 不加班
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/116234.html
摘要:想要自己實現一個簡易版框架,并不是非常難。為了防止出現這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構的區別。 showImg(https://segmentfault.com/img/bVbwfRh); 想要自己實現一個React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點如果你能閱讀以下的文章,那么會更輕松的閱讀本文章: 優化你的超大型React應用 ...
摘要:因為工作中一直在使用,也一直以來想總結一下自己關于的一些知識經驗。于是把一些想法慢慢整理書寫下來,做成一本開源免費專業簡單的入門級別的小書,提供給社區。本書的后續可能會做成視頻版本,敬請期待。本作品采用署名禁止演繹國際許可協議進行許可 React.js 小書 本文作者:胡子大哈本文原文:React.js 小書 轉載請注明出處,保留原文鏈接以及作者信息 在線閱讀:http://huzi...
摘要:前言月份開始出沒社區,現在差不多月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了一般來說,差不多到了轉正的時候,會進行總結或者分享會議那么今天我就把看過的一些學習資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區,現在差不多9月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了!一般來說,差不多到了轉正的時候,會進行總結或者分享會議!那么今天我就...
閱讀 3062·2021-10-12 10:12
閱讀 1568·2021-09-09 11:39
閱讀 1845·2019-08-30 15:44
閱讀 2339·2019-08-29 15:23
閱讀 2898·2019-08-29 15:18
閱讀 2960·2019-08-29 13:02
閱讀 2687·2019-08-26 18:36
閱讀 733·2019-08-26 12:08