virtual-dom 分析梳理【一】

目前前端的三個主流的框架都使用virtual-dom來處理dom的渲染,每一個框架都會在virtual-dom的核心原理上進行了一些特點的擴展,這篇文章主要是經過github.com/Matt-Esch/v…源碼來分析最基礎核心的原理。html

virtual-dom簡介

virtual-dom也就是使用js的數據結構來表示dom元素的結構,由於不是真是的dom節點,也就稱爲虛擬DOM。它最大的特色是將頁面進行抽象成JS對象形式,配合不一樣的工具使跨平臺成爲可能,能夠根據不一樣的平臺渲染出相應的真實「DOM」。除此以外,在頁面進行更新的時候,能夠將DOM元素的變更放在內存比較,再結合一些框架的機制,將屢次的渲染合併成一次渲染更新。前端

開始

先看看virtual-dom的庫是怎麼使用的,平時接觸的都是框架或者webpack轉換以前的代碼,好比jsxvue的模板等。如下將virtual-dom簡稱爲VDvue

var h = require('virtual-dom/h');
var diff = require('virtual-dom/diff');
var patch = require('virtual-dom/patch');
var createElement = require('virtual-dom/create-element');

// 1: Create a function that declares what the DOM should look like
function render(count) {
    return h('div', {
        style: {
            textAlign: 'center',
            lineHeight: (100 + count) + 'px',
            border: '1px solid red',
            width: (100 + count) + 'px',
            height: (100 + count) + 'px'
        }
    }, [String(count)]);
}

// 2: Initialise the document
var count = 0;      // We need some app data. Here we just store a count.

var tree = render(count);               // We need an initial tree
var rootNode = createElement(tree);     // Create an initial root DOM node ...
document.body.appendChild(rootNode);    // ... and it should be in the document

// 3: Wire up the update logic
setInterval(function () {
      count++;

      var newTree = render(count);
      var patches = diff(tree, newTree);
      rootNode = patch(rootNode, patches);
      tree = newTree;
}, 1000);
複製代碼

以上代碼爲我上面說的那個github上庫的代碼的例子。其實從這上面咱們就能夠看到VD主要分爲四大部分或者說是功能:node

  1. h(能夠叫其餘名字,好比 React.creatElement) 函數,用來建立VD對象,好比jsx書寫的代碼就會被轉換成React.creatElement(tag, props, ...children)這樣的。
  2. diff 函數,用來比較兩個VD對象的具體不一樣,造成一個描述(描述兩個VD的不一樣點)對象。
  3. patch 函數,用來經過對比後產生的描述對象對前一個DOM樹進行更新或者成爲打補丁(我的理解)。
  4. createElement 函數,根據VD對象產生真實的DOM樹。

以後內容就會根據這四個部分進行梳理,可能順序不一樣,會先梳理h函數和createElement函數這兩個比較簡單 :joy: 。該篇就是講這兩點。webpack

建立 VD 對象

看一下h函數的實現git

function h(tagName, properties, children) {
   var childNodes = [];
    var tag, props, key, namespace;
    // 若是第三參數爲空,就先看看第二個參數是否爲子節點,
    // 也就是能夠這樣寫 h('div', ['children']) ,若是沒有props就能夠不用穿,
    // 能夠不須要這樣 h('div', null, ['children'])
    if (!children && isChildren(properties)) {   
        children = properties;
        props = {};
    }

    props = props || properties || {};  // props 賦值
    // 作一下檢查和解析,好比,若是tagName參數爲空,則返回 div 做爲兜底
    tag = parseTag(tagName, props); 
    ...
    if (children !== undefined && children !== null) {
        // 爲子元素作一些判斷,若是是字符串就轉成 VText 對象,數值就先 String 化再轉,這些
        // 最終目的是將傳入的 children 進行標準話,作一下邊界處理,
        // 使得符合 VNode 構造須要的幾個參數
        addChild(children, childNodes, tag, props);
    }
   ... 
   return new VNode(tag, props, childNodes, key, namespace);
}
複製代碼

簡單的先看一下函數的結構。h接受三個參數,如最開始的代碼中:github

h('div', {
        style: {
            textAlign: 'center',
            lineHeight: (100 + count) + 'px',
            border: '1px solid red',
            width: (100 + count) + 'px',
            height: (100 + count) + 'px'
        }
    }, [String(count)]);
複製代碼

這裏就是對於的個參數,其實轉換成DOM就是這樣的(jsx的僞代碼):web

<div style={{...}}>count</div>
複製代碼

因此h函數的三個參數是比較好理解的。分別是 標籤名稱、屬性和子節點。算法

該函數最終返回了一個 VNode 對象,除了傳入重要的標籤名屬性子節點以外,還傳入了兩個其餘屬性,其實在ReactVue中都會本身對VNode這個對象進行擴展,因此這裏也是該庫的優化可擴展,暫時先不看,主要是梳理過程。數據結構

VNode

經過h函數能夠new一個VNode對象,那麼這個VNode對象,本質上也就是一個描述Dom的字面量對象。

function VirtualNode(tagName, properties, children, key, namespace) {
    this.tagName = tagName
    this.properties = properties || noProperties
    this.children = children || noChildren
    this.key = key != null ? String(key) : undefined
    this.namespace = (typeof namespace === "string") ? namespace : null
    ...
    this.count = count + descendants
    this.hasWidgets = hasWidgets
    this.hasThunks = hasThunks
    this.hooks = hooks
    this.descendantHooks = descendantHooks
}
複製代碼

省略的代碼主要是來計算和根據傳入參數產生一些附加信息,這些我以爲屬性擴展吧,因此先不說吧,仍是關注流程。經過h函數,咱們就能夠獲得這樣的一個字面對象。h函數嵌套使用就能夠獲得一顆這樣的字面對象的「樹」。

h('div', {}, [
  h('p', {}, ['demo']),
  ....
])
複製代碼

根據 VNode 子面量對象,建立真實 DOM

當咱們使用 h 函數獲得整顆VD樹以後,咱們就須要經過createElement函數建立真實的DOM,最後插入到某個節點裏面,頁面就這樣生成了(頁面更新的過程,舊 VD Tree 和 新 VD Tree 還須要進行比較)。

function createElement(vnode, opts) {
    var doc = opts ? opts.document || document : document
    var warn = opts ? opts.warn : null

    vnode = handleThunk(vnode).a

    if (isWidget(vnode)) {
         // 不是很清楚 Widget
         // 我好像沒有試過用,你們要是知道,能夠指點指點,謝謝了。
         // 應該是用來初始化一些能夠複用的小部件
        return vnode.init() 
    } else if (isVText(vnode)) {
        // 若是是文本,就建立一個簡單的文本節點
        return doc.createTextNode(vnode.text)
    } else if (!isVNode(vnode)) {
        // 若是都不是VD就警告
        if (warn) {
            warn("Item is not a valid virtual dom node", vnode)
        }
        return null
    }
    ...
    var props = vnode.properties
    // 這個方法就是給節點賦值屬性,好比: node.setAttribute('key', props[key])
    applyProperties(node, props)

    var children = vnode.children
    
    // 遞歸建立子節點。
    for (var i = 0; i < children.length; i++) {
        var childNode = createElement(children[i], opts)
        if (childNode) {
            node.appendChild(childNode)
        }
    }

    return node
}
複製代碼

代碼上加了一些註釋,其實createElement是相對簡單的,核心就是判斷 VD 是個什麼類型的節點,建立相應的 DOM ,而後再低櫃建立子節點。

建立好 DOM 後就能夠如官方給出的示例,將這份 DOM 樹插入到指點的頁面元素下面了。VD 到真實的 DOM就是這樣一個過程。內容比較基礎,梳理爲主。

小結

經過上面的梳理,能夠知道咱們日常寫的jsx就是先經過Babel這樣的工具,先將jsx轉出h('div', {}, [...])這樣的函數,而後執行獲得 VD Tree,再經過createElement建立對象的DOM節點,而後插入到頁面某個節點下面。

下一章就來梳理一下。diff 算法的流程和基礎原理。

原文地址

相關文章
相關標籤/搜索