聽說你想寫個 React - virtual dom

大家好凡纳,我是微微笑的蝸牛,??领铐。

上一篇文章介紹了 jsx 背后的實現(xiàn)悯森,今天來介紹一下 virtual dom。

如何更新 dom

若采用現(xiàn)有的實現(xiàn)方式绪撵,想要更新 dom呐馆,只能先構(gòu)造不同的節(jié)點描述信息,再重新調(diào)用 render 方法莲兢。

先看一個例子,計時器每隔 1s 刷新頁面续膳。

const rootDom = document.getElementById("root");

function tick() {
  const time = new Date().toLocaleString();
  const clockElement = <h1>{time}</h1>;
  SLReact.render(clockElement, rootDom);
}

tick();
setInterval(tick, 1000);

這個例子中改艇,每秒都會調(diào)用一次 render 方法。

而以現(xiàn)有的實現(xiàn)方式坟岔,render 方法中總是在往 dom 樹上添加節(jié)點谒兄。這樣會造成節(jié)點不斷的增加,就像下圖這個樣子社付。

image

但我們可稍微簡單修改一下 render 的實現(xiàn)承疲,將添加改成替換。

if (!parentDom.lastChild) {
    parentDom.appendChild(dom);     
} else {
    parentDom.replaceChild(dom, parentDom.lastChild);    
}

這樣改過之后鸥咖,比上面的實現(xiàn)方案要好一點燕鸽。但是對于復(fù)雜的結(jié)構(gòu)來說,仍是耗費巨大啼辣,因為在操作真實的 dom 樹啊研。

但如果試想有一種方案,只刷新差異部分鸥拧,那么操作 dom 樹的效率將會大大提升党远。

那么如何才能得到前后 dom 樹的差異呢?

這就需要用到中間層富弦,使用額外的結(jié)構(gòu)來存儲真實 dom 樹的結(jié)構(gòu)沟娱。在重新 render 的時候,進行新老對比腕柜,找出差異部分济似,再進行更新矫废。

這種中間結(jié)構(gòu)稱之為 virtual dom,可簡稱 vdom碱屁。為什么叫做虛擬 dom磷脯,因為它只是內(nèi)存中真實 dom 樹的對照。

virtual dom

  1. 可想而知娩脾,vdom 與 dom 會有一種映射關(guān)系赵誓。vdom 需要跟真實 dom 進行關(guān)聯(lián),這樣就可以方便的找到真實 dom柿赊,進行操作俩功。那么在 vdom 的信息中就會包含 dom。

  2. 此外碰声,vdom 還需包含節(jié)點描述信息诡蜓,不然怎么做對比呢?

  3. vdom 也是樹狀結(jié)構(gòu)胰挑,那么同樣包含子節(jié)點蔓罚。

根據(jù)上述分析,我們便可得到 vdom 的結(jié)構(gòu)瞻颂。它包括節(jié)點描述信息豺谈、關(guān)聯(lián)的真實 dom、子 vdom 節(jié)點贡这。每個 dom 節(jié)點都對應(yīng)著一個 vdom 節(jié)點茬末。

當在做 diff 時,根據(jù)新舊節(jié)點描述信息盖矫,找出差異部分丽惭,盡可能的重用 dom,減少開銷辈双。

那如何來構(gòu)建 vdom 進行 diff 呢责掏?下面我們來一步步的講解。

vdom 結(jié)構(gòu)

虛擬 dom 節(jié)點信息包括三部分:

  • 節(jié)點描述信息
  • 關(guān)聯(lián)的真實 dom
  • 子虛擬 dom 節(jié)點

它的結(jié)構(gòu)如下:

let vdom = { dom, element, childInstances };

根據(jù)之前的 render 方法湃望,其實比較容易改造出這種結(jié)構(gòu)拷橘。因為它返回的是真實 dom,我們只需將返回信息修改為 virtual dom 的結(jié)構(gòu)就好喜爷。

改造過程如下:

  • 根據(jù)節(jié)點類型生成真實 dom
  • 更新 dom 屬性
  • 遞歸處理子節(jié)點
  • 獲取子節(jié)點真實 dom冗疮,逐個添加到父節(jié)點
  • 返回 virtual dom

代碼如下所示:

// virtual dom,保存真實的 dom檩帐,element术幔,childInstance
function instantitate(element) {

  const { type, props } = element;
  const isTextElement = type === TEXT_ELEMENT;

    // 生成真實 dom
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(type);

  // 更新屬性
  updateDomProperties(dom, [], props);

  // 處理子節(jié)點
  const childElements = props.children || [];

    // 生成子虛擬 dom
  const childInstances = childElements.map(instantitate);

    // 獲取子 dom
  const childDoms = childInstances.map((childInstance) => childInstance.dom);

    // 添加到 dom 樹
  childDoms.forEach((childDom) => dom.appendChild(childDom));

    // 組成虛擬 dom 結(jié)構(gòu)
  const instance = { dom, element, childInstances };

  return instance;
}

在得到 virtual dom 結(jié)構(gòu)后,下一步需要做的就是更新真實 dom 節(jié)點湃密。

此時诅挑,render 方法需要進行改造四敞,變?yōu)楸容^前后 vdom 差異。

let rootInstance = null;

function render(element, parentDom) {
  const prevInstance = rootInstance;
  const nextInstance = reconcile(parentDom, prevInstance, element);
  rootInstance = nextInstance;
}

它主要工作是和上一個 virtual dom 實例做對比拔妥,然后進行 dom 樹的更新忿危。

這里我們將 diff 更新的過程叫做 reconcile,它會返回 virtual dom 節(jié)點没龙。如下圖所示:

image

diff 簡單處理

reconcile 的入?yún)⒂腥齻€铺厨,分別是父 dom 節(jié)點、vdom硬纤、節(jié)點描述信息解滓。

先來看一種簡單的處理方式:

  • 如果傳入的 vdom 為空,說明還沒有 dom 節(jié)點筝家,需要將真實 dom 添加到 dom 根節(jié)點上洼裤。
  • 如果不為空,則用新的 dom 節(jié)點替換原有 dom 節(jié)點溪王。

如下所示:

function reconcile(parentDom, instance, element) {
  if (instance == null) {
        // 添加 dom
    const newInstance = instantiate(element);
    parentDom.appendChild(newInstance.dom);
    return newInstance;
  } else {
        // 替換 dom
    const newInstance = instantiate(element);
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
  }
}

不知大家發(fā)現(xiàn)了沒腮鞍,上面的處理中,無論哪種條件下都調(diào)用了 instantiate 方法來重新創(chuàng)建 vdom 結(jié)構(gòu)莹菱。

而 instantiate 中會創(chuàng)建真實 dom缕减,這樣在節(jié)點類型沒變化時會產(chǎn)生不必要的開銷。

重用 dom 節(jié)點

這里我們可以稍微優(yōu)化一下芒珠,當節(jié)點類型一樣時,可以不用重新創(chuàng)建 dom 節(jié)點搅裙,復(fù)用已有就行皱卓,然后再更新屬性。

如下所示:

function reconcile(parentDom, instance, element) {
 if (instance.element.type === element.type) {
    // 復(fù)用 dom部逮,更新屬性
    updateDomProperties(instance.dom, instance.element.props, element.props);
    instance.element = element;
    return instance;
  }
}

子節(jié)點 diff

但是娜汁,還存在一個問題,子節(jié)點的 diff 還沒有進行處理兄朋。

在 React 中掐禁,子節(jié)點會有一個額外的屬性 key,以它為標識來對比之前的節(jié)點颅和。

這里傅事,我們將簡單處理,只將每個位置上的子節(jié)點與新的節(jié)點信息進行對比峡扩,進行新增/更新/替換/刪除操作蹭越。

如下所示:

// 對子節(jié)點做處理
function reconcileChildren(instance, element) {
  const dom = instance.dom;

    // 原有 virutal dom
  const childInstances = instance.childInstances;

    // 新的節(jié)點描述信息
  const nextChildElements = element.props.children || [];

  const newChildInstances = [];

  // 取最大的,若新子節(jié)點數(shù) < 原節(jié)點數(shù)教届,需移除
  const count = Math.max(childInstances.length, nextChildElements.length);
  for (let i = 0; i < count; i++) {
    const childInstance = childInstances[i];
    const childElement = nextChildElements[i];

    const newChildInstance = reconcile(dom, childInstance, childElement);
    newChildInstances.push(newChildInstance);
  }

  return newChildInstances;
}

每個位置上的子節(jié)點 diff 仍然會調(diào)用到 reconcile 方法响鹃。

請注意:新舊子節(jié)點的數(shù)目可能是不一樣的驾霜。

若新的子節(jié)點數(shù)目小于舊子節(jié)點數(shù),需要刪除舊子節(jié)點买置。

因為遍歷次數(shù)是由新舊節(jié)點數(shù)目最大的那個決定粪糙。當遍歷次數(shù)超出新子節(jié)點數(shù)時,這時忿项,childElement 為 null蓉冈。

如下圖所示:

image

對應(yīng)到 reconcile 中的處理,當 element 為空時倦卖,刪除真實 dom 節(jié)點洒擦,然后 vdom 返回 null。

// dom 更新操作
function reconcile(parentDom, instance, element) {
    // 省略...
 if (element == null) {
    console.log("remove dom");

    // remove怕膛,若新子節(jié)點數(shù) < 原節(jié)點數(shù)熟嫩,需移除
    parentDom.removeChild(instance.dom);
    return null;

  } else if (instance.element.type == element.type) {

    console.log("reuse dom");

    // 重用節(jié)點,更新屬性
    updateDomProperties(instance.dom, instance.element.props, element.props);

        // 子節(jié)點處理
    instance.childInstances = reconcileChildren(instance, element);
    instance.element = element;
    return instance;
  }
}

這樣就完成了 dom diff 的簡單處理褐捻。

完整代碼可查看:https://github.com/silan-liu/slreact/tree/master/part3掸茅。

總結(jié)

這篇文章主要介紹了如何構(gòu)建 virutal dom 結(jié)構(gòu),以及進行簡單的 diff 處理柠逞,以重用 dom 節(jié)點昧狮,減少不必要的開銷。

下一篇將介紹 component 和 state 的實現(xiàn)板壮,敬請期待~

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逗鸣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绰精,更是在濱河造成了極大的恐慌撒璧,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笨使,死亡現(xiàn)場離奇詭異卿樱,居然都是意外死亡,警方通過查閱死者的電腦和手機硫椰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門繁调,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人靶草,你說我怎么就攤上這事蹄胰。” “怎么了奕翔?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵烤送,是天一觀的道長。 經(jīng)常有香客問我糠悯,道長帮坚,這世上最難降的妖魔是什么妻往? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮试和,結(jié)果婚禮上讯泣,老公的妹妹穿的比我還像新娘。我一直安慰自己阅悍,他們只是感情好好渠,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著节视,像睡著了一般拳锚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寻行,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天霍掺,我揣著相機與錄音,去河邊找鬼拌蜘。 笑死杆烁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的简卧。 我是一名探鬼主播兔魂,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼举娩!你這毒婦竟也來了析校?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤铜涉,失蹤者是張志新(化名)和其女友劉穎智玻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骄噪,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年蠢箩,在試婚紗的時候發(fā)現(xiàn)自己被綠了链蕊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡谬泌,死狀恐怖滔韵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掌实,我是刑警寧澤陪蜻,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站贱鼻,受9級特大地震影響宴卖,放射性物質(zhì)發(fā)生泄漏滋将。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一症昏、第九天 我趴在偏房一處隱蔽的房頂上張望随闽。 院中可真熱鬧,春花似錦肝谭、人聲如沸掘宪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽魏滚。三九已至,卻和暖如春坟漱,著一層夾襖步出監(jiān)牢的瞬間鼠次,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工靖秩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留须眷,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓沟突,卻偏偏與公主長得像花颗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惠拭,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容