摘要:想要自己實(shí)現(xiàn)一個(gè)簡易版框架,并不是非常難。為了防止出現(xiàn)這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構(gòu)的區(qū)別。
想要自己實(shí)現(xiàn)一個(gè)React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點(diǎn)
如果你能閱讀以下的文章,那么會更輕松的閱讀本文章:
優(yōu)化你的超大型React應(yīng)用
手寫一個(gè)React腳手架
為了降低本文難度,構(gòu)建工具選擇了parcel,歡迎加入我們的前端交流群~ gitHub倉庫源碼地址和二維碼都會在最后放出來~
什么是虛擬DOM?其實(shí)就是一個(gè)個(gè)的具有固定格式的JS對象,例如:
const obj = { tag:"div", attrs:{ className:"test" }, children:[ tag:"span", attrs:{ className:"text" }, tag:"p", attrs:{ className:"p" }, ] }怎么生成對應(yīng)的虛擬DOM對象?
先把代碼變成抽象語法樹(AST)
然后進(jìn)行對應(yīng)的處理
輸出成瀏覽器可以識別的代碼-即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代碼,都會被轉(zhuǎn)換成React.createElement這種形式
那我們只要自己一個(gè)React全局對象,給它掛載這個(gè)React.createElement方法就可以進(jìn)行接下來的處理:
const React = {}; React.createElement = function(tag, attrs, ...children) { return { tag, attrs, children }; }; export default React;
我們定義的React.createElement方法也很簡單,只是把對應(yīng)的參數(shù)集中變成一個(gè)特定格式的對象,然后返回,再接下來進(jìn)行處理~。Babel的配置會幫我們自動把jsx轉(zhuǎn)換成React.creatElement的代碼,參數(shù)都會默認(rèn)幫我們傳好~
構(gòu)建工具我們使用零配置的parcel ,相比webpack來說,更容易上手,當(dāng)然對于一個(gè)把webpack玩透了的人來說,其實(shí)用什么都一樣~
npm install -g parcel-bundler
parcel index.html即可運(yùn)行項(xiàng)目
// .babelrc 配置 { "presets": ["env"], "plugins": [ ["transform-react-jsx", { "pragma": "React.createElement" }] ] }處理好了jsx代碼,我們?nèi)肟陂_始寫起:
ReactDOM.render方法是我們的入口
先定義ReactDOM對象,以及它的render方法~
const ReactDom = {}; //vnode 虛擬dom,即js對象 //container 即對應(yīng)的根標(biāo)簽 包裹元素 const render = function(vnode, container) { return container.appendChild(_render(vnode)); }; ReactDom.render = render;
思路: 先把虛擬dom對象-js對象變成真實(shí)dom對象,然后插入到根標(biāo)簽內(nèi)。
_render方法,接受虛擬dom對象,返回真實(shí)dom對象:
如果傳入的是null,字符串或者數(shù)字 那么直接轉(zhuǎn)換成真實(shí)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
但是有可能傳入的是個(gè)div標(biāo)簽,而且它有屬性。那么需要處理屬性,由于這個(gè)處理屬性的函數(shù)需要大量復(fù)用,我們多帶帶定義成一個(gè)函數(shù):
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); } } }
但是可能有子節(jié)點(diǎn)的嵌套,于是要用到遞歸:
vnode.children && vnode.children.forEach(child => render(child, dom)); // 遞歸渲染子節(jié)點(diǎn)
上面沒有考慮到組件,只考慮到了div或者字符串?dāng)?shù)字之類的虛擬dom.
其實(shí)加入組件也很簡單:加入新一個(gè)新的處理方式:
我們先定義好Component這個(gè)類,并且掛載到全局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會幫我們把第一個(gè)參數(shù)變成function
if (typeof vnode.tag === "function") { //先創(chuàng)建組件 const component = createComponent(vnode.tag, vnode.attrs); //設(shè)置屬性 setComponentProps(component, vnode.attrs) //返回的是真實(shí)dom對象 return component.base; }
createComponent和setComponentProps都是我們自己定義的方法~后期大量復(fù)用
export function createComponent(component, props) { let inst; // 如果是類定義組件,則直接返回實(shí)例 if (component.prototype && component.prototype.render) { inst = new component(props); // 如果是函數(shù)定義組件,則將其擴(kuò)展為類定義組件 } 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是真實(shí)dom對象 //component.base是將本次渲染好的dom對象掛載到組件上,方便判斷是否首次掛載 component.base = base; //互相飲用,方便后期的隊(duì)列處理 base._component = component; }
最簡單的版本已經(jīng)完成,對應(yīng)的生命簡單周期做了粗糙處理,但是沒有加入diff算法和異步setState,歡迎移步gitHub點(diǎn)個(gè)star
最簡單版React-無diff算法和異步state,選擇master分支
沒有diff算法,更新state后是所有的節(jié)點(diǎn)都要更新,這樣性能損耗非常大。現(xiàn)在我們開始加入React的diff算法
首先改造renderComponent方法
function renderComponent(component, newState = {}) { console.log("renderComponent"); //真實(shí)dom對象 let base; //虛擬dom對象 const renderer = component.render(); //component.base是為了表示是否經(jīng)過初次渲染,好進(jìn)行生命周期函數(shù)調(diào)用 if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } if (component.base && component.shouldComponentUpdate) { //如果組件經(jīng)過了初次渲染,是更新階段,那么可以根據(jù)這個(gè)生命周期判斷是否更新 let result = true; result = component.shouldComponentUpdate && component.shouldComponentUpdate((component.props = {}), newState); if (!result) { return; } } //得到diff算法對比后的真實(shí)dom對象 base = diffNode(component.base, renderer); if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { //為了防止死循環(huán),調(diào)用完`didMount`函數(shù)就結(jié)束。 component.base = base; base._component = component; component.componentDidMount && component.componentDidMount(); return; } component.base = base; base._component = component; }
注意,我們是跟preact一樣,將真實(shí)dom對象和虛擬dom對象進(jìn)行對比:
分為下面幾種diff:
Node節(jié)點(diǎn)diff
Component組件diff
屬性diff
純文本或者數(shù)字的diff...
子節(jié)點(diǎn)的diff(這個(gè)最復(fù)雜)
純文本或者數(shù)字的diff:
純文本和數(shù)字之類的直接替換掉dom節(jié)點(diǎn)的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") { // 如果當(dāng)前的DOM就是文本節(jié)點(diǎn),則直接更新內(nèi)容 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不是文本節(jié)點(diǎn),則新建一個(gè)文本節(jié)點(diǎn)DOM,并移除掉原來的 } else { out = document.createTextNode(vnode); if (dom && dom.parentNode) { dom.parentNode.replaceChild(out, dom); } } return out; }
Component組件diff
如果不是一個(gè)類型組件直接替換掉,否則只更新屬性即可
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 = {}; // 當(dāng)前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; } // 如果原來的屬性不在新的屬性當(dāng)中,則將其移除掉(屬性值設(shè)為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值的真實(shí)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這段,確實(shí)看起來不那么簡單,總結(jié)兩點(diǎn)精髓:
利用key值將節(jié)點(diǎn)分成兩個(gè)隊(duì)列
先對比有key值的節(jié)點(diǎn),然后對比相同類型的節(jié)點(diǎn),然后進(jìn)行dom操作
shouldComponentUpdate的對比優(yōu)化:
shouldComponentUpdate(nextProps, nextState) { if (nextState.test > 5) { console.log("shouldComponentUpdate中限制了更新") alert("shouldComponentUpdate中限制了更新") return false; } return true; }
效果:
建議去倉庫看完整源碼認(rèn)真斟酌:
帶diff算法版mini-React,選擇diff分支
看加入了diff算法后的效果
當(dāng)然state更新后,只是更新了對應(yīng)的節(jié)點(diǎn),所謂的diff算法,就是將真實(shí)dom和虛擬dom對比后,直接dom操作。操作那些有更新的節(jié)點(diǎn)~ 當(dāng)然也有直接對比兩個(gè)虛擬dom對象,然后打補(bǔ)丁上去~我們這種方式如果做SSR同構(gòu)就不行,因?yàn)槲覀兎?wù)端沒dom對象這個(gè)說法,無法運(yùn)行~
這段diff是有點(diǎn)硬核,但是去倉庫認(rèn)真看看,自己嘗試寫寫,也是可以啃下來的。異步合并更新state版
上面的版本,每次setState都會更新組件,這樣很不友好,因?yàn)橛锌赡芤粋€(gè)操作會帶來很多個(gè)setState,而且很可能會頻繁更新state。為了優(yōu)化性能,我們把這些操作都放在一幀內(nèi)去操作~
這里我們使用requestAnimationFrame,去執(zhí)行合并操作~
首先更新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是我們的一個(gè)入口函數(shù):
function enqueueSetState(stateChange, component) { if (setStateQueue.length === 0) { //清空隊(duì)列的辦法是異步執(zhí)行,下面都是同步執(zhí)行的一些計(jì)算 defer(flush); } //向隊(duì)列中添加對象 key:stateChange value:component setStateQueue.push({ stateChange, component }); //如果渲染隊(duì)列中沒有這個(gè)組件 那么添加進(jìn)去 if (!renderQueue.some(item => item === component)) { renderQueue.push(component); } }
上面代碼的精髓:
先執(zhí)行同步代碼
首次setState調(diào)用進(jìn)入if (setStateQueue.length === 0) 的判斷
異步在下一幀執(zhí)行flush函數(shù)
同步執(zhí)行setStateQueue.push
同步執(zhí)行 renderQueue.push(component)
最后執(zhí)行defer函數(shù)
defer函數(shù)
function defer(fn) { //requestIdleCallback的兼容性不好,對于用戶交互頻繁多次合并更新來說 ,requestAnimation更有及時(shí)性高優(yōu)先級,requestIdleCallback則適合處理可以延遲渲染的任務(wù)~ // if (window.requestIdleCallback) { // console.log("requestIdleCallback"); // return requestIdleCallback(fn); // } //高優(yōu)先級任務(wù) return requestAnimationFrame(fn); }
思考了很久,決定還是用requestAnimationFrame,為了體現(xiàn)界面交互的及時(shí)性
flush清空隊(duì)列的函數(shù):
function flush() { let item, component; //依次取出對象,執(zhí)行 while ((item = setStateQueue.shift())) { const { stateChange, component } = item; // 如果沒有prevState,則將當(dāng)前的state作為初始的prevState if (!component.prevState) { component.prevState = Object.assign({}, component.state); } // 如果stateChange是一個(gè)方法,也就是setState的第二種形式 if (typeof stateChange === "function") { Object.assign( component.state, stateChange(component.prevState, component.props) ); } else { // 如果stateChange是一個(gè)對象,則直接合并到setState中 Object.assign(component.state, stateChange); } component.prevState = component.state; } //依次取出組件,執(zhí)行更新邏輯,渲染 while ((component = renderQueue.shift())) { renderComponent(component); } }
flush函數(shù)的精髓:
抽象隊(duì)列,一個(gè)是對應(yīng)的改變state和組件的隊(duì)列, 一個(gè)是需要更新的組件隊(duì)列
每一幀就清空當(dāng)前setState隊(duì)列的需要更新的組件,一次性合并清空
完整代碼倉庫地址,歡迎star:
帶diff算法和異步state的minj-react
當(dāng)我們有100個(gè)節(jié)點(diǎn)需要更新的時(shí)候,我們正在遞歸對比節(jié)點(diǎn),此時(shí)用戶點(diǎn)擊界面需要彈框,那么可能會造成延遲彈出窗口,根據(jù)RAID,超過100ms,用戶就會感覺明顯卡頓。為了防止出現(xiàn)這種情況,我們需要改變整體的diff策略。把遞歸的對比,改成可以暫停執(zhí)行的循環(huán)對比,這樣如果即時(shí)我們在對比階段,有用戶點(diǎn)擊需要交互的時(shí)候,我們可以暫停對比,處理用戶交互。
上面這段話,說的就是stack版本和Fiber架構(gòu)的區(qū)別。
stack版本就是我們上面的版本
Fiber版本:思路:
將對比階段分割成一個(gè)個(gè)小任務(wù)
采用兩個(gè)虛擬dom對象的去diff對比方式,單鏈表結(jié)構(gòu),三根指針,return children sibling。
每幀完成一個(gè)小任務(wù),然后去執(zhí)行requestAnimationFrame,如果還有時(shí)間,那么就去執(zhí)行requestIdleCallback.
這個(gè)版本暫時(shí)就結(jié)束了哦~ 歡迎加入我們的前端交流群,還有前往gitHub給個(gè)star。
本人參考:
hujiulong的博客,感謝這些大佬的無私開源
前端交流群:
現(xiàn)在人數(shù)超過了100人,所以只能加我,然后拉你們進(jìn)群!!
另外深圳招收跨平臺開發(fā)Electron+React的即時(shí)通訊產(chǎn)品前端工程師
歡迎投遞: 453089136@qq.com - Peter
招收中級和高級各一名~團(tuán)隊(duì)氛圍nice 不加班
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/106631.html
摘要:想要自己實(shí)現(xiàn)一個(gè)簡易版框架,并不是非常難。為了防止出現(xiàn)這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構(gòu)的區(qū)別。 showImg(https://segmentfault.com/img/bVbwfRh); 想要自己實(shí)現(xiàn)一個(gè)React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點(diǎn)如果你能閱讀以下的文章,那么會更輕松的閱讀本文章: 優(yōu)化你的超大型React應(yīng)用 ...
摘要:只有動手,你才能真的理解作者的構(gòu)思的巧妙只有動手,你才能真正掌握一門技術(shù)持續(xù)更新中項(xiàng)目地址求求求源碼系列跟一起學(xué)如何寫函數(shù)庫中高級前端面試手寫代碼無敵秘籍如何用不到行代碼寫一款屬于自己的類庫原理講解實(shí)現(xiàn)一個(gè)對象遵循規(guī)范實(shí)戰(zhàn)手摸手,帶你用擼 Do it yourself!!! 只有動手,你才能真的理解作者的構(gòu)思的巧妙 只有動手,你才能真正掌握一門技術(shù) 持續(xù)更新中…… 項(xiàng)目地址 https...
摘要:因?yàn)楣ぷ髦幸恢痹谑褂茫惨恢币詠硐肟偨Y(jié)一下自己關(guān)于的一些知識經(jīng)驗(yàn)。于是把一些想法慢慢整理書寫下來,做成一本開源免費(fèi)專業(yè)簡單的入門級別的小書,提供給社區(qū)。本書的后續(xù)可能會做成視頻版本,敬請期待。本作品采用署名禁止演繹國際許可協(xié)議進(jìn)行許可 React.js 小書 本文作者:胡子大哈本文原文:React.js 小書 轉(zhuǎn)載請注明出處,保留原文鏈接以及作者信息 在線閱讀:http://huzi...
摘要:前言月份開始出沒社區(qū),現(xiàn)在差不多月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會進(jìn)行總結(jié)或者分享會議那么今天我就把看過的一些學(xué)習(xí)資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區(qū),現(xiàn)在差不多9月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了!一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會進(jìn)行總結(jié)或者分享會議!那么今天我就...
閱讀 2203·2021-10-13 09:39
閱讀 3408·2021-09-30 09:52
閱讀 800·2021-09-26 09:55
閱讀 2774·2019-08-30 13:19
閱讀 1888·2019-08-26 10:42
閱讀 3183·2019-08-26 10:17
閱讀 542·2019-08-23 14:52
閱讀 3631·2019-08-23 14:39