最近讀了讀preact源碼,記錄點筆記,這裏採用例子的形式,把代碼的執行過程帶到源碼裏走一遍,順便說明一些重要的點,建議對着preact源碼看
虛擬結點是對真實DOM元素的一個js對象表示
,由h()建立html
h()方法在根據指定結點名稱、屬性、子節點來建立vnode以前,會對子節點進行處理,包括node
例如:react
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()就是react中的ReactDOM.render(vnode,parent,merge),將一個vnode轉換成真實DOM,插入到parent中,只有一句話,重點在diff中git
return diff(merge, vnode, {}, false, parent, false);
diff主要作三件事github
重點看idiff數組
idiff(dom,vnode)處理vnode的三種狀況dom
通常咱們寫react應用,最外層有一個相似<App>的組件,渲染時ReactDOM.render(<App/>>,root)
,這時候diff走的就是第二步,根據vnode.nodeName==='function'
來構建組件,執行buildComponentFromVNode()
,實例化組件,子組件等等異步
第三種狀況通常出如今組件的定義是以普通標籤包裹的,組件內部狀態發生改變了或者初次實例化時,要render組件了,此時,要將當前組件現有的dom與執行compoent.render()方法獲得的新的vnode進行Diff,來決定當前組件要怎麼更新DOM
函數
class Comp1 extends Component{ render(){ return <div> { list.map(x=>{ return <p key={x.id}>{x.txt}</p> }) } <Comp2></Comp2> </div> } //而不是 //render(){ // return <Comp2></Comp2> //} }
咱們以一個真實的組件的渲染過程來對照着走一下表示普通dom及子節點的vnode和真實dom之間的diff過程
優化
假設如今有這樣一個組件
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 ( <div> <button onClick={this.change.bind(this)}>change</button> {data.map((x, index) => { if (index == 2 && this.state.change) { return <h2 key={index}>{x}</h2>; } return <p key={index}>{x}</p>; })} {!change ? <h1>hello world</h1> : null} </div> ); } }
App組件初次掛載後的DOM結構大體表示爲
dom = { tageName:"DIV", childNodes:[ <button>change</button> <p key="0">1</p>, <p key="1">2</p>, <p key="2">3</p>, <p key="3">4</p>, <h1>hello world</h1> ] }
點擊一下按鈕,觸發setState,狀態發生變化,App組件實例入渲染隊列,一段時間後(異步的),渲染隊列中的組件被渲染,實例.render執行,此時生成的vnode結構大體是
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方法內diff上面的dom和vnode diff(dom,vnode)
,此時在diff內部調用的idff方法內,執行的就是上面說的第三種狀況vnode.nodeType是普通標籤,關於renderComponent後面介紹
首先dom和vnode標籤名是同樣的,都是div(若是不同,要經過vnode.nodeName來建立一個新元素,並把dom子節點複製到這個新元素下),而且vnode有多個children,因此直接進入innerDiffNode(dom,vnode.children)函數
innerDiffNode(dom,vchildren)工做流程
child
,例如,key同樣的,若是vchild沒有key,就從children數組中找標籤名同樣的 接着看上面的例子
keyed=[ <p key="0">1</p>, <p key="1">2</p>, <p key="2">3</p>, <p key="3">4</p> ] children=[ <button>change</button>, <h1>hello world</h1> ]
存在key相等的
vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]}, child=keyed[0]=<p key="0">1</p>
存在標籤名改變的
vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]}, child=keyed[2]=<p key="2">3</p>,
存在標籤名相等的
vchild={ nodeName:"button", children:["change"] }, child=<button>change</button>,
而後對vchild和child進行diff
child=idff(child,vchild)
看一組子元素的更新
看上面那組存在keys相等的
子元素的diff,vchild.nodeName=='p'是個普通標籤,因此仍是走的idff內的第三種狀況。
但這裏vchild只有一個後代元素,而且child只有一個文本結點,能夠明確是文本替換的狀況,源碼中這樣處理,而不是進入innerDiffNode,算是一點優化
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]; } }
全部執行child=idiff(child,vchild)
後
child=<p key="0">11</p> //文本值更新了
而後將這個child放入當前dom下的合適位置,一個子元素的更新就完成了
若是vchild.children數組有多個元素,又會進行vchild的子元素的迭代diff
至此,diff算是說了一半了,另外一半是vnode表示一個組件的狀況,進行組件渲染或更新diff
和組件的渲染,diff相關的方法主要有三個,依次調用關係
buildComponentFromVNode
setComponentProps
在setComponentProps(compInst)內部進行兩件事
renderComponent
renderComponent內會作這些事:
當前組件表示的頁面結構上的真實DOM
和返回的這個vnode,應用更新.(像上面說明的那個例子同樣)依然從例子入手,假設如今有這樣一個組件
class Welcom extends Component{ render(props){ return <p>{props.text}</p> } } class App extends Component { constructor(props){ super(props) this.state={ text:"hello world" } } change(){ this.setState({ text:"now changed" }) } render(props){ return <div> <button onClick={this.change.bind(this)}>change</button> <h1>preact</h1> <Welcom text={this.state.text} /> </div> } } render(<App></App>,root) vnode={ nodeName:App, }
首次render
render(<App/>
,root)執行,進入diff(),vnode.nodeName==App,進入buildComponentFromVNode(null,vnode)
程序首次執行,頁面尚未dom結構,因此此時buildComponentFromVNode第一個參數是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中,執行component.componentWillMount(),組件入異步渲染隊列,在一段時間後,組件渲染,執行
renderComponent()
rendered = component.render(props, state, context); 根據上面的定義,這裏有 rendered={ nodeName:"div", children:[ { nodeName:"button", children:['change'] }, { nodeName:"h1", children:['preact'] },{ nodeName:Welcom, attributes:{ text:'hello world' } } ] }
nodeName是普通標籤,因此執行
base = diff(null, rendered) //這裏須要注意的是,renderd有一個組件child,因此在diff()-->idiff()[**走第三種狀況**]---->innerDiffNode()中,對這個組件child進行idiff()時,由於是組件,因此走第二種狀況,進入buildComponentFromVNode,相同的流程 component.base=base //這裏的baes是vnode diff完成後生成的真實dom結構,組件實例上有個base屬性,指向這個dom base大致表示爲 base={ tageName:"DIV", childNodes:[ <button>change</button> <h1>preact</h1> <p>hello world</p> ] } 而後爲當前dom元素添加一些組件的信息 base._component = component; base._componentConstructor = component.constructor;
至此,初始化的此次組件渲染就差很少了,buildComponentFromVNode返回dom,即實例化的App的c.base,在diff()中將dom插入頁面
更新
而後如今點擊按鈕,setState()更新狀態,setState源碼中
let s = this.state; if (!this.prevState) this.prevState = extend({}, s); extend(s, typeof state==='function' ? state(s, this.props) : state); /** * _renderCallbacks保存回調列表 */ if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback); enqueueRender(this);
組件入隊列了,延遲後執行renderComponent()
此次,在renderComponent中,由於當前App的實例已經有一個base屬性,因此此時實例屬於更新階段isUpdate = component.base =true
,執行實例的componentWillUpdate()方法,若是實例的shouldComponentUpdate()返回true,實例進入render階段。
這時候根據新的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後產生的dom,即實例.base,而後頁面引用更新後的新的dom.rendered的那個組件子元素(Welcom)一樣執行一次更新過程,進入buildComponentFromVNode(),走一遍buildComponentFromVNode()-->setComponentProps()--->renderComponent()--->render()--->diff(),直到數據更新完畢
preact src下只有15個js文件,但一篇文章不能覆蓋全部點,這裏只是記錄了一些主要的流程,最後放一張有毒的圖