摘要:最近讀了讀源碼,記錄點筆記,這里采用例子的形式,把代碼的執(zhí)行過程帶到源碼里走一遍,順便說明一些重要的點建議對著源碼看和虛擬結(jié)點是對真實元素的一個對象表示由創(chuàng)建方法在根據(jù)指定結(jié)點名稱屬性子節(jié)點來創(chuàng)建之前會對子節(jié)點進行處理,包括當前要創(chuàng)建的不是
最近讀了讀preact源碼,記錄點筆記,這里采用例子的形式,把代碼的執(zhí)行過程帶到源碼里走一遍,順便說明一些重要的點,建議對著preact源碼看vnode和h()
虛擬結(jié)點是對真實DOM元素的一個js對象表示,由h()創(chuàng)建
h()方法在根據(jù)指定結(jié)點名稱、屬性、子節(jié)點來創(chuàng)建vnode之前,會對子節(jié)點進行處理,包括
當前要創(chuàng)建的vnode不是組件,而是普通標簽的話,文本子節(jié)點是null,undefined,轉(zhuǎn)成"",文本子節(jié)點是number類型,轉(zhuǎn)成字符串
連續(xù)相鄰的兩個子節(jié)點都是文本結(jié)點,合并成一個
例如:
h("div",{ id: "foo", name : "bar" },[ h("p",null,"test1"), "hello", null "world", h("p",null,"test2") ] ) 對應的vnode={ nodeName:"div", attributes:{ id:"foo", name:"bar" }, [ { nodeName:"p", children:["test1"] }, "hello world", { nodeName:"p", children:["test2"] } ] }render()
render()就是react中的ReactDOM.render(vnode,parent,merge),將一個vnode轉(zhuǎn)換成真實DOM,插入到parent中,只有一句話,重點在diff中
return diff(merge, vnode, {}, false, parent, false);diff
diff主要做三件事
調(diào)用idff()生成真實DOM
掛載dom
在組件及所有子節(jié)點diff完成后,統(tǒng)一執(zhí)行收集到的組件的componentDidMount()
重點看idiff
idiff(dom,vnode)處理vnode的三種情況
vnode是一個js基本類型值,直接替換dom的文本或dom不存在,根據(jù)vnode創(chuàng)建新的文本返回
vnode.nodeName是function 即當前vnode表示一個組件
vnode.nodeName是string 即當前vnode表示一個對普通html元素的js表示
一般我們寫react應用,最外層有一個類似
第三種情況一般出現(xiàn)在組件的定義是以普通標簽包裹的,組件內(nèi)部狀態(tài)發(fā)生改變了或者初次實例化時,要render組件了,此時,要將當前組件現(xiàn)有的dom與執(zhí)行compoent.render()方法得到的新的vnode進行Diff,來決定當前組件要怎么更新DOM
class Comp1 extends Component{ render(){ return普通標簽元素及子節(jié)點的diff{ list.map(x=>{ return} //而不是 //render(){ // return{x.txt}
}) }//} }
我們以一個真實的組件的渲染過程來對照著走一下表示普通dom及子節(jié)點的vnode和真實dom之間的diff過程
假設(shè)現(xiàn)在有這樣一個組件
class App extends Component { constructor(props) { super(props); this.state = { change: false, data: [1, 2, 3, 4] }; } change(){ this.setState(preState => { return { change: !preState.change, data: [11, 22, 33, 44] }; }); } render(props) { const { data, change } = this.state; return (初次渲染{data.map((x, index) => { if (index == 2 && this.state.change) { return); } }{x}
; } return{x}
; })} {!change ?hello world
: null}
App組件初次掛載后的DOM結(jié)構(gòu)大致表示為
dom = { tageName:"DIV", childNodes:[更新1
,2
,3
,4
,hello world
] }
點擊一下按鈕,觸發(fā)setState,狀態(tài)發(fā)生變化,App組件實例入渲染隊列,一段時間后(異步的),渲染隊列中的組件被渲染,實例.render執(zhí)行,此時生成的vnode結(jié)構(gòu)大致是
vnode= { nodeName:"div" children:[ { nodeName:"button", children:["change"] }, { nodeName:"p", attributes:{key:"0"}, children:[11]}, { nodeName:"p", attributes:{key:"1"}, children:[22]}, { nodeName:"h2", attributes:{key:"2"}, children:[33]}, { nodeName:"p", attributes:{key:"3"}, children:[44]}, ] } //少了最后的h1元素,第三個p元素變成了h2
然后在renderComponent方法內(nèi)diff上面的dom和vnode diff(dom,vnode),此時在diff內(nèi)部調(diào)用的idff方法內(nèi),執(zhí)行的就是上面說的第三種情況vnode.nodeType是普通標簽,關(guān)于renderComponent后面介紹
首先dom和vnode標簽名是一樣的,都是div(如果不一樣,要通過vnode.nodeName來創(chuàng)建一個新元素,并把dom子節(jié)點復制到這個新元素下),并且vnode有多個children,所以直接進入innerDiffNode(dom,vnode.children)函數(shù)
innerDiffNode(dom,vchildren)工作流程
對dom結(jié)點下的子節(jié)點遍歷,根據(jù)是否有key,放入兩個數(shù)組keyed和children(那些沒有key放到這個里)
遍歷vchildren,為當前的vchild找一個相對應的dom下的子節(jié)點child,例如,key一樣的,如果vchild沒有key,就從children數(shù)組中找標簽名一樣的
child=idiff(child, vchild); 遞歸diff,根據(jù)vchild來得到處理后的child,將child應用到當前父元素dom下
接著看上面的例子
dom子節(jié)點遍歷 得到兩個數(shù)組
keyed=[1
,2
,3
,4
] children=[ ,hello world
]
迭代vnode的children數(shù)組
存在key相等的
vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]}, child=keyed[0]=1
存在標簽名改變的
vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]}, child=keyed[2]=3
,
存在標簽名相等的
vchild={ nodeName:"button", children:["change"] }, child=,
然后對vchild和child進行diff
child=idff(child,vchild)
看一組子元素的更新
看上面那組存在keys相等的子元素的diff,vchild.nodeName=="p"是個普通標簽,所以還是走的idff內(nèi)的第三種情況。
但這里vchild只有一個后代元素,并且child只有一個文本結(jié)點,可以明確是文本替換的情況,源碼中這樣處理,而不是進入innerDiffNode,算是一點優(yōu)化
let fc = out.firstChild, props = out[ATTR_KEY], vchildren = vnode.children; if (props == null) { props = out[ATTR_KEY] = {}; for (let a = out.attributes, i = a.length; i--;) props[a[i].name] = a[i].value; } // Optimization: fast-path for elements containing a single TextNode: if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === "string" && fc != null && fc.splitText !== undefined && fc.nextSibling == null) { if (fc.nodeValue != vchildren[0]) { fc.nodeValue = vchildren[0]; } }
所有執(zhí)行child=idiff(child,vchild)后
child=11
//文本值更新了
然后將這個child放入當前dom下的合適位置,一個子元素的更新就完成了
如果vchild.children數(shù)組有多個元素,又會進行vchild的子元素的迭代diff
至此,diff算是說了一半了,另一半是vnode表示一個組件的情況,進行組件渲染或更新diff
組件的渲染、diff與更新和組件的渲染,diff相關(guān)的方法主要有三個,依次調(diào)用關(guān)系
buildComponentFromVNode
組件之前沒有實例化過,實例化組件,為組件應用props,setComponentProps()
組件已經(jīng)實例化過,屬于更新階段,setComponentProps()
setComponentProps
在setComponentProps(compInst)內(nèi)部進行兩件事
根據(jù)當前組件實例是首次實例化還是更新屬性來調(diào)用組件的componentWillMount或者componentWillReceiveProps
判斷是否時強制渲染,renderComponent()或者把組件入渲染隊列,異步渲染
renderComponent
renderComponent內(nèi)會做這些事:
判斷組件是否更新,更新的話執(zhí)行componentWillUpdate(),
判斷shouldComponentUpdate()的結(jié)果,決定是否跳過執(zhí)行組件的render方法
需要render,執(zhí)行組件render(),返回一個vnode,diff當前組件表示的頁面結(jié)構(gòu)上的真實DOM和返回的這個vnode,應用更新.(像上面說明的那個例子一樣)
依然從例子入手,假設(shè)現(xiàn)在有這樣一個組件
class Welcom extends Component{ render(props){ return{props.text}
} } class App extends Component { constructor(props){ super(props) this.state={ text:"hello world" } } change(){ this.setState({ text:"now changed" }) } render(props){ return} } render(preact
,root) vnode={ nodeName:App, }
首次render
render(
程序首次執(zhí)行,頁面還沒有dom結(jié)構(gòu),所以此時buildComponentFromVNode第一個參數(shù)是null,進入實例化App組件階段
c = createComponent(vnode.nodeName, props, context); if (dom && !c.nextBase) { c.nextBase = dom; // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229: oldDom = null; } setComponentProps(c, props, SYNC_RENDER, context, mountAll); dom = c.base;
在setComponentProps中,執(zhí)行component.componentWillMount(),組件入異步渲染隊列,在一段時間后,組件渲染,執(zhí)行
renderComponent()
rendered = component.render(props, state, context); 根據(jù)上面的定義,這里有 rendered={ nodeName:"div", children:[ { nodeName:"button", children:["change"] }, { nodeName:"h1", children:["preact"] },{ nodeName:Welcom, attributes:{ text:"hello world" } } ] }
nodeName是普通標簽,所以執(zhí)行
base = diff(null, rendered) //這里需要注意的是,renderd有一個組件child,所以在diff()-->idiff()[**走第三種情況**]---->innerDiffNode()中,對這個組件child進行idiff()時,因為是組件,所以走第二種情況,進入buildComponentFromVNode,相同的流程 component.base=base //這里的baes是vnode diff完成后生成的真實dom結(jié)構(gòu),組件實例上有個base屬性,指向這個dom base大體表示為 base={ tageName:"DIV", childNodes:[preact
hello world
] } 然后為當前dom元素添加一些組件的信息 base._component = component; base._componentConstructor = component.constructor;
至此,初始化的這次組件渲染就差不多了,buildComponentFromVNode返回dom,即實例化的App的c.base,在diff()中將dom插入頁面
更新
然后現(xiàn)在點擊按鈕,setState()更新狀態(tài),setState源碼中
let s = this.state; if (!this.prevState) this.prevState = extend({}, s); extend(s, typeof state==="function" ? state(s, this.props) : state); /** * _renderCallbacks保存回調(diào)列表 */ if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback); enqueueRender(this);
組件入隊列了,延遲后執(zhí)行renderComponent()
這次,在renderComponent中,因為當前App的實例已經(jīng)有一個base屬性,所以此時實例屬于更新階段isUpdate = component.base =true,執(zhí)行實例的componentWillUpdate()方法,如果實例的shouldComponentUpdate()返回true,實例進入render階段。
這時候根據(jù)新的props,state
rendered = component.render(props, state, context); rendered={ nodeName:"div", children:[ { nodeName:"button", children:["change"] }, { nodeName:"h1", children:["preact"] },{ nodeName:Welcom, attributes:{ text:"now changed" //這里變化 } } ] }
然后,像第一次render一樣,base = diff(cbase, rendered),但這時候,cbase是上一次render后產(chǎn)生的dom,即實例.base,然后頁面引用更新后的新的dom.rendered的那個組件子元素(Welcom)同樣執(zhí)行一次更新過程,進入buildComponentFromVNode(),走一遍buildComponentFromVNode()-->setComponentProps()--->renderComponent()--->render()--->diff(),直到數(shù)據(jù)更新完畢
總結(jié)preact src下只有15個js文件,但一篇文章不能覆蓋所有點,這里只是記錄了一些主要的流程,最后放一張有毒的圖
github
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/93440.html
摘要:是一個最小的庫,但由于其對尺寸的追求,它的很多代碼可讀性比較差,市面上也很少有全面且詳細介紹的文章,本篇文章希望能幫助你學習的源碼。建議與源碼一起閱讀本文。 作為一名前端,我們需要深入學習react的運行機制,但是react源碼量已經(jīng)相當龐大,從學習的角度,性價比不高,所以學習一個react mini庫是一個深入學習react的一個不錯的方法。 preact是一個最小的react mi...
摘要:本系列文章將重點分析類似于的這類框架是如何實現(xiàn)的,歡迎大家關(guān)注和討論。作為一個極度精簡的庫,函數(shù)是屬于本身的。 前言 首先歡迎大家關(guān)注我的掘金賬號和Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現(xiàn),能堅持下去也是靠的是自己的熱情和大家的鼓勵。 之前分享過幾篇關(guān)于React的文章: React技術(shù)內(nèi)幕: key帶來了什么 React技術(shù)內(nèi)幕: setState的秘密...
摘要:對回收的處理在中,回收調(diào)用了兩個方法,節(jié)點的回收一般會調(diào)用,組件的回收會調(diào)用。個人理解從以上源碼閱讀中我們可以看到,最大的性能問題在于遞歸的,中的與也是為了緩解這個問題。為不同類型的更新分配優(yōu)先級。 對回收的處理 在preact中,回收調(diào)用了兩個方法,dom節(jié)點的回收一般會調(diào)用recollectNodeTree,組件的回收會調(diào)用unmountComponent。 preact復用dom...
摘要:最后刪除新的樹中不存在的節(jié)點。而中會記錄對其做了相應的優(yōu)化,節(jié)點的的情況下,不做移動操作。這種情況,在中得到了優(yōu)化,通過四個指針,在每次循環(huán)中先處理特殊情況,并通過縮小指針范圍,獲得性能上的提升。 上篇文章已經(jīng)介紹過idff的處理邏輯主要分為三塊,處理textNode,element及component,但具體怎么處理component還沒有詳細介紹,接下來講一下preact是如何處理...
摘要:司徒正美的一款了不起的化方案,支持到。行代碼內(nèi)實現(xiàn)一個胡子大哈實現(xiàn)的作品其實就是的了源碼學習個人文章源碼學習個人文章源碼學習個人文章源碼學習個人文章這幾片文章的作者都是司徒正美,全面的解析和官方的對比。 前言 在過去的一個多月中,為了能夠更深入的學習,使用React,了解React內(nèi)部算法,數(shù)據(jù)結(jié)構(gòu),我自己,從零開始寫了一個玩具框架。 截止今日,終于可以發(fā)布第一個版本,因為就在昨天,我...
閱讀 2240·2021-09-22 15:54
閱讀 1962·2021-09-04 16:40
閱讀 1003·2019-08-30 15:56
閱讀 2745·2019-08-30 15:44
閱讀 2270·2019-08-30 13:52
閱讀 1243·2019-08-29 16:35
閱讀 3463·2019-08-29 16:31
閱讀 2683·2019-08-29 13:48