React的簡單實現 (三)更新優化算法diff

接下來主要優化一下組件的更新

從之前文章可以知道,我們組件是通過createElement然后遍歷children屬性,完成整個dom的創建,其實是相當耗費性能的,react之所以能實現高性能,是因為它實現了局部dom的更新,而不是像我們_render方法里面那么簡單粗暴。

function _render(vnode) {

  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;
  }
  if (typeof vnode.tag === 'function') {

    const component = createComponent(vnode.tag, vnode.attrs);

    setComponentProps(component, vnode.attrs);

    return component.base;
  }

  const dom = document.createElement(vnode.tag);

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach(key => {
      const value = vnode.attrs[key];
      setAttribute(dom, key, value);
    });
  }

  vnode.children.forEach(child => render(child, dom));    // 遞歸渲染子節點
  return dom;
}

先定義一個diff的算法,傳入原來通過createElement創建的dom和新的node(JSX解析出來的返還),diff主要通過新的對象與已有的dom對比,決定是否需要創建或者更新dom,如下面:

{"tag":"div","attrs":null,"children":["Text",{"tag":"div","attrs":null,"children":["Count: ",0]},{"tag":"button","attrs":{},"children":["Add"]}]}

diff函數

function diff( dom, vnode, container ) {
    const ret = diffNode( dom, vnode );
    if ( container && ret.parentNode !== container ) {
        container.appendChild( ret );
    }
    return ret;
}

diffNode做同級節點對比,同時遞歸便利字節點

function 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;
    }
    if ( typeof vnode.tag === 'function' ) {
        return diffComponent( dom, vnode );
    }
    //
    if ( !dom || !isSameNodeType( dom, vnode ) ) {
        out = document.createElement( vnode.tag );
        if ( dom ) {
            [ ...dom.childNodes ].map( out.appendChild );    // 將原來的子節點移到新節點下
            if ( dom.parentNode ) {
                dom.parentNode.replaceChild( out, dom );    // 移除掉原來的DOM對象
            }
        }
    }

    if ( vnode.children && vnode.children.length > 0 || ( out.childNodes && out.childNodes.length > 0 ) ) {
        diffChildren( out, vnode.children );
    }

    diffAttributes( out, vnode );
    return out;
}

diffChildren相對麻煩,因為要針對key進行優化,具體原因可以參考:https://jdc.jd.com/archives/212685

function diffChildren( dom, vchildren ) {

    const domChildren = dom.childNodes;
    const children = [];

    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 );
                }
            }
        }
    }
}

最后還有diffcomponent的函數,主要是component有unmount的生命周期需要實現。注意一下,vnode返回的的tag就是component的構建函數,這一步是babel幫我們轉換的,加入vnode的tag跟原本_component的constructor不同,可以認為原來的組件已經卸載了

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;
}

使用以下代碼進行測試

import React from './react';
// end with react frame work
class Count extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    }
    setInterval(()=>{
      this.setState({
        count: this.state.count+1,
      })
    }, 1000)
  }
  render() {
    return <div>
      Text
      <div>Count: {this.state.count}</div>
      <button
        onClick={()=>{
          this.setState({
            count: this.state.count+1,
          })
        }}
      >
        Add
      </button>
    </div>;
  }
}

const ReactDOM = {
  render: (vnode, container) => {
    container.innerHTML = '';
    return React.render(vnode, container);
  },
};
const element = <Count name="counter"/>;
console.log(element);
ReactDOM.render(
  element,
  document.getElementById('root'),
);

原本的加載方式,不使用diff,而是每次更新state觸發renderComponent時都是重新創建整個dom,base = _render(renderer),打開chrome可以看到,整個root都是重新掛載的:


image.png

我們把renderComponent里面base = _render(renderer)替換為上面的函數base = diff( component.base, renderer ):

function renderComponent(component) {

  let base;

  const renderer = component.render();

  if (component.base && component.componentWillUpdate) {
    component.componentWillUpdate();
  }
  console.log('component.base');
  console.log(component.base);
  console.log('renderer');
  console.log(JSON.stringify(renderer));
  //base = _render(renderer);
  base = diff(component.base, renderer);
  if (component.base) {
    if (component.componentDidUpdate) component.componentDidUpdate();
  } else if (component.componentDidMount) {
    component.componentDidMount();
  }

  //if (component.base && component.base.parentNode) {
  //  component.base.parentNode.replaceChild(base, component.base);
  //}

  component.base = base;
  base._component = component;

}

打開chrome可以看到接下來,只有text部分是重新更新的。


image.png

diff算法的核心就是同級比較,同index比較,并增加key進行優化,根據每次新的state,決定需要更新的那部分節點,實質上瀏覽器渲染的原理也是類似的。

最后一篇會說一下setState的優化,React中一次同步生命周期中其實setState只進行了一次,目的是減少人為的錯誤導致無效的渲染。
比如有人在onClick里面設置setState({count: this.state.count + 1}),之后在componentWillUpdate里面又使用一次setState({count: this.state.count + 1}),最終只會觸發一次render更新。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容