幫你讀懂preact的源碼(一)

做爲一名前端,咱們須要深刻學習react的運行機制,可是react源碼量已經至關龐大,從學習的角度,性價比不高,因此學習一個react mini庫是一個深刻學習react的一個不錯的方法。html

preact是一個最小的react mini庫,但因爲其對尺寸的追求,它的不少代碼可讀性比較差,市面上也不多有全面且詳細介紹的文章,本篇文章但願能幫助你學習preact的源碼。前端

在最開始我會先介紹preact總體流程,幫助您有一個總體概念,以便不會陷入源碼的細枝末節裏,而後會分別講解preact各個值得學習的機制。建議與preact源碼一塊兒閱讀本文。vue

但願能幫你理清以下問題:node

  • JSX是怎麼被處理的?
  • diff算法是如何工做的?
  • vue和react中咱們爲何須要一個穩定的key?
  • preact是怎麼處理事件的?
  • preact的回收機制是如何提升性能的?
  • setState以後會發生什麼?
  • fiber是用來解決什麼問題的?

如下圖是preact源碼大體流程圖,如今看不懂不要緊,也不須要刻意記,在學習的過程當中,不妨根據此圖試着猜測preact每一步都作了什麼,下一步要作什麼。react

圖片描述

JSX

在react的官方文檔中,咱們能夠得知,jsx內容在會被babel編譯爲如下格式:git

In

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
  
Out

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

這樣經過createElement就能夠生成虛擬dom樹,在preact裏面對應的函數是h。
h函數根據nodeName,attributes,children,返回一個虛擬dom樹,這個虛擬dom樹每每有三個屬性:github

function h(nodeName, props, ...children){
    .... // 其餘代碼
    return {
        nodeName,
        props,     // props中包含children
        key,       // 爲diff算法作準備
    }
}

這裏不貼出preact的源代碼,由於h函數的實現方式有不少,不但願最開始的學習就陷入到細枝末節,只須要明白h函數的做用便可。算法

diff

從上圖中能夠看到,preact主流程調用的第一個函數就是render,render函數很簡單就是調用了一下diff函數。babel

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

diff函數的主要做用是調用idiff函數,而後將idff函數返回的真實dom append到dom中app

function diff(dom, vnode, context, mountAll, parent, componentRoot) {
    // 返回的是一個真實的dom節點
    let ret = idiff(dom, vnode, context, mountAll, componentRoot);

    // append the element if its a new parent
    if (parent && ret.parentNode !== parent) parent.appendChild(ret);
}

idiff

接下來咱們要介紹idff函數,開啓react高性能diff算法的大門,但在這以前,咱們應該瞭解react diff算法的前提:

  • 兩個不一樣類型的element會產生不一樣類型的樹。
  • 開發者經過一個key標識同一層級的子節點。

基於第一個前提,不一樣類型的節點就能夠再也不向下比較,直接銷燬,而後從新建立便可。

idiff函數主要分爲三塊,分別處理vnode三種狀況:

  • vnode是string或者Number,相似於上面例子的'Hello World',通常是虛擬dom樹的葉子節點。
  • vnode中的nodeName是一個function,即vnode對應一個組件,例如上例中的<App/>。
  • vnode中nodeName是一個字符串,即vnode對應一個html元素,例如上例中的h1。

對於string或Number:

// 若是要比較的dom是一個textNode,直接更改dom的nodeValue
// 若是要比較的dom不是一個textNode,就建立textNode,而後回收老的節點樹,回收的節點樹會保留結構,而後保存在內存中,在// 須要的時候複用。(回收相關的處理會在以後詳細說明)
if (typeof vnode === 'string' || typeof vnode === 'number') {

    // update if it's already a Text node:
    if (dom && dom.splitText !== undefined && dom.parentNode && (!dom._component || componentRoot)) {
        /* istanbul ignore if */
        /* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
        if (dom.nodeValue != vnode) {
            dom.nodeValue = vnode;
        }
    } else {
        // it wasn't a Text node: replace it with one and recycle the old Element
        out = document.createTextNode(vnode);
        if (dom) {
            if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
            recollectNodeTree(dom, true);
        }
    }

    out.__preactattr_ = true;

    return out;
}

若是nodeName是一個function,會直接調用buildComponentFromVNode方法

let vnodeName = vnode.nodeName;
if (typeof vnodeName === 'function') {
    return buildComponentFromVNode(dom, vnode, context, mountAll);
}

若是nodeName是一個字符串,如下很長的代碼,就是作三步:

  • 對於類型不一樣的節點,直接作替換操做,不作diff比較。
  • diffAttrites
  • diffChildren
// Tracks entering and exiting SVG namespace when descending through the tree.
isSvgMode = vnodeName === 'svg' ? true : vnodeName === 'foreignObject' ? false : isSvgMode;

// If there's no existing element or it's the wrong type, create a new one:
vnodeName = String(vnodeName);
// 若是不存在dom對象,或者dom的nodeName和vnodeName不同的狀況下
if (!dom || !isNamedNode(dom, vnodeName)) {
    out = createNode(vnodeName, isSvgMode);

    if (dom) {
        // 在後面你會發現preact的diffChildren的方式,是經過把真實dom的子節點與虛擬dom的子節點相比較,因此須要老的// 孩子暫時先移動到新的節點上
        // move children into the replacement node
        while (dom.firstChild) {
            out.appendChild(dom.firstChild);
        } // if the previous Element was mounted into the DOM, replace it inline
        if (dom.parentNode) dom.parentNode.replaceChild(out, dom);

        // recycle the old element (skips non-Element node types)
        recollectNodeTree(dom, true);
    }
}

let fc = out.firstChild,
    props = out.__preactattr_,
    vchildren = vnode.children;
// 把dom節點的attributes都放在了dom['__preactattr_']上
if (props == null) {
    props = out.__preactattr_ = {};
    for (let a = out.attributes, i = a.length; i--;) {
        props[a[i].name] = a[i].value;
    }
}

// 若是vchildren只有一個節點,且是textnode節點時,直接更改nodeValue,優化性能
// 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];
    }
}

// 比較子節點,將真實dom的children與vhildren比較
// otherwise, if there are existing or new children, diff them:
else if (vchildren && vchildren.length || fc != null) {
    innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);
}

diffAttributes(out, vnode.attributes, props);

return out;
相關文章
相關標籤/搜索