preact源碼分析,有毒

最近讀了讀preact源碼,記錄點筆記,這裏採用例子的形式,把代碼的執行過程帶到源碼裏走一遍,順便說明一些重要的點,建議對着preact源碼看

vnode和h()

虛擬結點是對真實DOM元素的一個js對象表示,由h()建立html

h()方法在根據指定結點名稱、屬性、子節點來建立vnode以前,會對子節點進行處理,包括node

  1. 當前要建立的vnode不是組件,而是普通標籤的話,文本子節點是null,undefined,轉成'',文本子節點是number類型,轉成字符串
  2. 連續相鄰的兩個子節點都是文本結點,合併成一個

例如: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()

render()就是react中的ReactDOM.render(vnode,parent,merge),將一個vnode轉換成真實DOM,插入到parent中,只有一句話,重點在diff中git

return diff(merge, vnode, {}, false, parent, false);

diff

diff主要作三件事github

  1. 調用idff()生成真實DOM
  2. 掛載dom
  3. 在組件及全部子節點diff完成後,統一執行收集到的組件的componentDidMount()

重點看idiff數組

idiff(dom,vnode)處理vnode的三種狀況dom

  1. vnode是一個js基本類型值,直接替換dom的文本或dom不存在,根據vnode建立新的文本返回
  2. vnode.nodeName是function 即當前vnode表示一個組件
  3. vnode.nodeName是string 即當前vnode表示一個對普通html元素的js表示

通常咱們寫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>
    //}

}

普通標籤元素及子節點的diff

咱們以一個真實的組件的渲染過程來對照着走一下表示普通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)工做流程

  1. 對dom結點下的子節點遍歷,根據是否有key,放入兩個數組keyed和children(那些沒有key放到這個裏)
  2. 遍歷vchildren,爲當前的vchild找一個相對應的dom下的子節點child,例如,key同樣的,若是vchild沒有key,就從children數組中找標籤名同樣的
  3. child=idiff(child, vchild); 遞歸diff,根據vchild來獲得處理後的child,將child應用到當前父元素dom下

接着看上面的例子

  1. dom子節點遍歷 獲得兩個數組
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>
]
  1. 迭代vnode的children數組

存在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與更新

和組件的渲染,diff相關的方法主要有三個,依次調用關係

buildComponentFromVNode

  1. 組件以前沒有實例化過,實例化組件,爲組件應用props,setComponentProps()
  2. 組件已經實例化過,屬於更新階段,setComponentProps()

setComponentProps

在setComponentProps(compInst)內部進行兩件事

  1. 根據當前組件實例是首次實例化仍是更新屬性來調用組件的componentWillMount或者componentWillReceiveProps
  2. 判斷是否時強制渲染,renderComponent()或者把組件入渲染隊列,異步渲染

renderComponent

renderComponent內會作這些事:

  1. 判斷組件是否更新,更新的話執行componentWillUpdate(),
  2. 判斷shouldComponentUpdate()的結果,決定是否跳過執行組件的render方法
  3. 須要render,執行組件render(),返回一個vnode,diff當前組件表示的頁面結構上的真實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文件,但一篇文章不能覆蓋全部點,這裏只是記錄了一些主要的流程,最後放一張有毒的圖

github

相關文章
相關標籤/搜索