preact源碼分析

image

前言

前兩個星期花了一些時間學習preact的源碼, 並寫了幾篇博客。可是如今回頭看看寫的並很差,並且源碼的有些地方(diffChildren的部分)我還理解🙅錯了。實在是很差意思。因此此次準備從新寫一篇博客從新作下分析。css

preact雖然是react的最小實現, 不少react的特性preact裏一點都沒有少, 好比contextAPI, Fragment等。咱們分析時更注重實現過程,會對一些API的實現進行忽略。請見諒node

preact是什麼?

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOMreact

preact能夠說是類react框架的最小實現算法

虛擬DOM

關於jsx

咱們首先看下preact官網上的demo。數組

import { h, render } from 'preact';

render((
  <h1 id="title" >Hello, world!</h1>
), document.body);

其實上面👆的jsx代碼,本質是下面👇代碼的語法糖babel

h(
  'h1',
  { id: 'title' },
  'Hello, world!'
)

preact是如何作到的呢?preact自己並無實現這個語法轉換的功能,preact是依賴transform-react-jsx的babel插件作到的。app

createElement

前面咱們看到了jsx的代碼會被轉換爲用h函數包裹的代碼, 咱們接下來看下h函數是如何實現的。createElement函數位於create-element.js這個文件中。框架

文件中主要爲3個函數, createElement和createVNode, 以及coerceToVNode。dom

createElement和createVNode是一對的, createElement會將children掛載到VNode的props中。既props.children的數組中。createVNode則會將根據這些參數返回一個對象, 這個對象就是虛擬DOM。異步

在createElement中咱們還能夠看到對defaultProps的處理, 而defaultProps能夠爲咱們設置props的默認的初始值。

export function createElement(type, props, children) {
    if (props==null) props = {};
    if (arguments.length>3) {
        children = [children];
        for (let i=3; i<arguments.length; i++) {
            children.push(arguments[i]);
        }
  }
  
    if (children!=null) {
        props.children = children;
  }

    if (type!=null && type.defaultProps!=null) {
        for (let i in type.defaultProps) {
            if (props[i]===undefined) props[i] = type.defaultProps[i];
        }
    }
    let ref = props.ref;
    if (ref) delete props.ref;
    let key = props.key;
    if (key) delete props.key;

    return createVNode(type, props, null, key, ref);
}

export function createVNode(type, props, text, key, ref) {

    const vnode = {
        type,
        props,
        text,
        key,
        ref,
        _children: null,
        _dom: null,
        _lastDomChild: null,
        _component: null
    };

    return vnode;
}

而coerceToVNode函數的做用則是將一些沒有type類型的節點。好比一段字符串, 一個數字強制轉換爲VNode節點, 這些節點的type值爲null, text屬性中保留了字符串和數字的值。

export function coerceToVNode(possibleVNode) {
    if (possibleVNode == null || typeof possibleVNode === 'boolean') return null;
    if (typeof possibleVNode === 'string' || typeof possibleVNode === 'number') {
        return createVNode(null, null, possibleVNode, null, null);
    }

    if (Array.isArray(possibleVNode)) {
        return createElement(Fragment, null, possibleVNode);
    }

    if (possibleVNode._dom!=null) {
        return createVNode(possibleVNode.type, possibleVNode.props, possibleVNode.text, possibleVNode.key, null);
    }

    return possibleVNode;
}

到這裏create-element的這個模塊咱們就介紹完了。這是一個很是簡單的模塊, 作的功能就是根據對應的jsx->虛擬DOM。咱們這裏尚未涉及如何渲染出真正的DOM節點, 這是由於preact中渲染的過程是直接在diff算法中實現,一邊比對一邊跟更新真實的dom。

組件

preact中有一個通用Component類, 組件的實現須要繼承這個通用的Component類。咱們來看下preact中Component類是如何實現的。它位於component.js文件📃中。

咱們首先看下Component類的構造函數,很是的簡單。只有兩個屬性props, context。由於通用的Component類實現了props屬性,因此咱們的組件類在繼承Component類後,須要顯式的使用super做爲函數調用,並將props傳入。

export function Component(props, context) {
    this.props = props
    this.context = context
}

Component類中實現了setState方法, forceUpdate方法,render方法,以及其餘的一些輔助函數。forceUpdate涉及到了setState的異步更新, 咱們將在setState一節中專門介紹。這裏暫不作介紹。咱們接下來看看setState的實現。

Component.prototype.setState = function(update, callback) {
    let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));

    if (typeof update!=='function' || (update = update(s, this.props))) {
        assign(s, update);
    }

    if (update==null) return;

    if (this._vnode) {
        if (callback) this._renderCallbacks.push(callback);
        enqueueRender(this);
    }
};

// src/util.js
export function assign(obj, props) {
    for (let i in props) obj[i] = props[i];
    return obj;
}

在preact的setState方法, 同react同樣支持函數或者Object兩種方式更新state, 而且支持setState的回調。咱們這裏看到了兩個個私有屬性_nextState, _renderCallbacks。_renderCallbacks則是存儲了setState回調的隊列。

_nextState裏存儲了最新的state, 爲何咱們不去直接更新state呢?由於咱們要實現生命週期, 好比getDerivedStateFromProps生命週期中組件的state並無更新呢。咱們須要使用_nextState存儲最新的state😊。enqueueRender函數涉及到了state的異步更新, 咱們在本節先不介紹。

// src/component.js
export function Fragment() { }

Component.prototype.render = Fragment;

基類的render方法自己是一個空函數, 須要繼承的子類本身具體實現。

🎉component.js的模塊的部份內容,咱們已經介紹完成了, 一樣不是很複雜。component.js的模塊的其餘的內容由於涉及了setState異步更新隊列,因此咱們將在setState一節中。回過頭來介紹它。

diff算法

image

ps: 👆咱們只須要比較同級的節點(相同顏色框內的), 若是兩個節點type不一致, 咱們會銷燬當前的節點。不進行比較子節點的操做。

在preact中diff算法以及真實dom的更新和渲染是雜糅在一塊兒的。因此本節內容會比較多。

preact會存儲上一次的渲染的VNode(存儲在_prevVNode的私有屬性上)。而本次渲染過程當中咱們會比較本次的VNode上前一次的_prevVNode。判斷是否須要生成新的Dom, 卸載Dom的操做, 更新真實dom的操做(咱們將VNode對應的真實的dom存儲在VNode的私有屬性_dom, 能夠實如今diff的過程當中更新dom的操做)。

render

對比文本節點

咱們首先回憶一下文本節點的VNode的結構是怎麼樣的

// 文本節點VNode
{
  type: null,
  props: null,
  text: '你的文本'
  _dom: TextNode
}

咱們首先進入diff方法。diff方法中會對VNode類型進行判斷, 若是不是function類型(組件類型), 和Fragment類型。咱們的會調用diffElementNodes函數。

// src/diff/index.js
// func diff

// 參數不少, 咱們來講下幾個參數的具體含義
// dom爲VNode對應的真實的Dom節點
// newVNode新的VNode
// oldVNode舊的VNode
// mounts存儲掛載組件的列表
dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent)

若是此時dom尚未建立。初次渲染, 那麼咱們根據VNode類型建立對應的真實dom節點。文本類型會使用createTextNode建立文本節點。

接下來咱們會標籤以前VNode的text的內容, 若是新舊不相等。咱們將新VNode的text屬性,賦值給dom節點。完成對dom的更新操做。

// src/diff/index.js
// func diffElementNodes

if (dom==null) {
    dom = newVNode.type===null ? document.createTextNode(newVNode.text) : isSvg ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type) : document.createElement(newVNode.type);

    excessDomChildren = null;
}

newVNode._dom = dom;

if (newVNode.type===null) {
    if ((d===null || dom===d) && newVNode.text!==oldVNode.text) {
        dom.data = newVNode.text;
    }
}

對比非文本DOM節點

非文本DOM節點🈯️的是那些type爲div, span, h1的VNode節點。這些類型的節點在diff方法中, 咱們依舊會調用diffElementNodes函數去處理。

// src/diff/index.js
// func diff

dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent)

進入diffElementNodes方法後, 若是是初次渲染咱們會使用createElement建立真實的dom節點掛載到VNode的_dom屬性上。

接下來咱們會比較新舊VNode的屬性props。可是以前會調用diffChildren方法, 對當前的VNode子節點進行比較。咱們這裏先不進入diffChildren函數中。咱們只須要知道咱們在更新當前節點屬性的時候, 咱們已經經過遞歸形式, 完成了對當前節點的子節點的更新操做。接下來咱們進入diffProps函數中。

// src/diff/index.js
// func diffElementNodes

if (dom==null) {
    dom = newVNode.type===null ? document.createTextNode(newVNode.text) : isSvg ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type) : document.createElement(newVNode.type);
}

newVNode._dom = dom;

if (newVNode !== oldVNode) {
    let oldProps = oldVNode.props;
    let newProps = newVNode.props;

    if (oldProps == null) {
        oldProps = {};
    }
    diffChildren(dom, newVNode, oldVNode, context, newVNode.type === 'foreignObject' ? false : isSvg, excessDomChildren, mounts, ancestorComponent);
    diffProps(dom, newProps, oldProps, isSvg);
}

在diffProps函數中咱們會作兩件事。設置, 更新屬性。刪除新的props中不存在的屬性。setProperty在preact中的具體實現, 咱們往下看。

// src/diff/props.js

export function diffProps(dom, newProps, oldProps, isSvg) {
  // 設置或更新屬性值
    for (let i in newProps) {
        if (i!=='children' && i!=='key' && (!oldProps || ((i==='value' || i==='checked') ? dom : oldProps)[i]!==newProps[i])) {
            setProperty(dom, i, newProps[i], oldProps[i], isSvg);
        }
  }
  // 刪除屬性
    for (let i in oldProps) {
        if (i!=='children' && i!=='key' && (!newProps || !(i in newProps))) {
            setProperty(dom, i, null, oldProps[i], isSvg);
        }
    }
}

在setProperty方法中, 若是value(新的屬性值)爲null, 咱們會刪除對應的屬性。若是不爲null, 咱們將會更新或者設置新的屬性。同時還會對事件進行處理, 例如onClick屬性, 咱們會使用addEventListener添加原生的click事件。

// src/diff/props.js

function setProperty(dom, name, value, oldValue, isSvg) {
  let v;
  // 對class處理
    if (name==='class' || name==='className') name = isSvg ? 'class' : 'className';

  // 對style處理, style傳入Object或者字符串都會獲得兼容的處理
    if (name==='style') {

        let s = dom.style;

    // 若是style是string類型
        if (typeof value==='string') {
            s.cssText = value;
        }
        else {
      // 若是style是object類型
            if (typeof oldValue==='string') s.cssText = '';
            else {
                for (let i in oldValue) {
                    if (value==null || !(i in value)) s.setProperty(i.replace(CAMEL_REG, '-'), '');
                }
            }
            for (let i in value) {
                v = value[i];
                if (oldValue==null || v!==oldValue[i]) {
                    s.setProperty(i.replace(CAMEL_REG, '-'), typeof v==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (v + 'px') : v);
                }
            }
        }
    }
    else if (name==='dangerouslySetInnerHTML') {
        return;
    }
    else if (name[0]==='o' && name[1]==='n') {
    // 對事件處理
        let useCapture = name !== (name=name.replace(/Capture$/, ''));
        let nameLower = name.toLowerCase();
        name = (nameLower in dom ? nameLower : name).substring(2);

        if (value) {
            if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
        }
        else {
            dom.removeEventListener(name, eventProxy, useCapture);
        }
        (dom._listeners || (dom._listeners = {}))[name] = value;
    }
    else if (name!=='list' && name!=='tagName' && !isSvg && (name in dom)) {
        dom[name] = value==null ? '' : value;
    }
    else if (value==null || value===false) {
    // 刪除以及爲null的屬性
        if (name!==(name = name.replace(/^xlink:?/, ''))) dom.removeAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase());
        else dom.removeAttribute(name);
    }
    else if (typeof value!=='function') {
    // 更新或設置新的屬性
        if (name!==(name = name.replace(/^xlink:?/, ''))) dom.setAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase(), value);
        else dom.setAttribute(name, value);
    }
}

對比組件

image

若是VNode是組件類型。在diff函數中, 會在不一樣的時刻執行組件的生命週期。在diff中, 執行組件實例的render函數。咱們將會拿到組件返回的VNode, 而後再將VNode再一次帶入diff方法中進行diff比較。大體的流程能夠如上圖所示。

// src/diff/index.js
// func diff

let c, p, isNew = false, oldProps, oldState, snapshot,
  newType = newVNode.type;
  
let cxType = newType.contextType;
let provider = cxType && context[cxType._id];
let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;

if (oldVNode._component) {
    c = newVNode._component = oldVNode._component;
    clearProcessingException = c._processingException;
}
else {
    isNew = true;

  // 建立組件的實例
    if (newType.prototype && newType.prototype.render) {
        newVNode._component = c = new newType(newVNode.props, cctx);
    }
    else {
        newVNode._component = c = new Component(newVNode.props, cctx);
        c.constructor = newType;
        c.render = doRender;
  }
  
    c._ancestorComponent = ancestorComponent;
    if (provider) provider.sub(c);

  // 初始化,組件的state, props的屬性
    c.props = newVNode.props;
    if (!c.state) c.state = {};
    c.context = cctx;
    c._context = context;
    c._dirty = true;
    c._renderCallbacks = [];
}

// 組件的實例上掛載組件所對應的VNode節點
c._vnode = newVNode;

let s = c._nextState || c.state;

// 執行getDerivedStateFromProps生命週期函數, 返回只會更新組件的state
if (newType.getDerivedStateFromProps != null) {
    oldState = assign({}, c.state);
    if (s === c.state) s = c._nextState = assign({}, s);
    assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
}

if (isNew) {
  // 執行componentWillMount生命週期
  if (newType.getDerivedStateFromProps == null && c.componentWillMount != null) c.componentWillMount();
  // 將須要執行componentDidMount生命週期的組件, push到mounts隊列中
    if (c.componentDidMount != null) mounts.push(c);
}
else {
  // 執行componentWillReceiveProps生命週期
    if (newType.getDerivedStateFromProps == null && force == null && c.componentWillReceiveProps != null) {
        c.componentWillReceiveProps(newVNode.props, cctx);
        s = c._nextState || c.state;
    }

  // 執行shouldComponentUpdate生命週期, 並將_dirty設置爲false, 當_dirty被設置爲false時, 執行的更新操做將會被暫停
    if (!force && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newVNode.props, s, cctx) === false) {
        c.props = newVNode.props;
        c.state = s;
    c._dirty = false;
    // break後,不在執行如下的代碼
        break outer;
    }

  // 執行componentWillUpdate生命週期
    if (c.componentWillUpdate != null) {
        c.componentWillUpdate(newVNode.props, s, cctx);
    }
}

oldProps = c.props;
if (!oldState) oldState = c.state;

c.context = cctx;
c.props = newVNode.props;
// 將更新後的state的s,賦予組件的state
c.state = s;

// prev爲上一次渲染時對應的VNode節點
let prev = c._prevVNode;
// 調用組件的render方法獲取組件的VNode
let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
c._dirty = false;

if (c.getChildContext != null) {
    context = assign(assign({}, context), c.getChildContext());
}

// 執行getSnapshotBeforeUpdate生命週期
if (!isNew && c.getSnapshotBeforeUpdate != null) {
    snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}

// 更新組件所對應的VNode,返回對應的dom
c.base = dom = diff(dom, parentDom, vnode, prev, context, isSvg, excessDomChildren, mounts, c, null);

if (vnode != null) {
    newVNode._lastDomChild = vnode._lastDomChild;
}

c._parentDom = parentDom;

在diff函數的頂部有這樣一段代碼上面有一句英文註釋(If the previous type doesn't match the new type we drop the whole subtree), 若是oldVNode和newVNode類型不一樣,咱們將會卸載整個子樹🌲。

if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) {
  // 若是newVNode爲null, 咱們將會卸載整個組件, 並刪除對應的dom節點 
    if (oldVNode!=null) unmount(oldVNode, ancestorComponent);
    if (newVNode==null) return null;
    dom = null;
    oldVNode = EMPTY_OBJ;
}

對比子節點

export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent) {
    let childVNode, i, j, p, index, oldVNode, newDom,
        nextDom, sibDom, focus,
        childDom;

    let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode);
    let oldChildren = oldParentVNode!=null && oldParentVNode!=EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR;

    let oldChildrenLength = oldChildren.length;

  childDom = oldChildrenLength ? oldChildren[0] && oldChildren[0]._dom : null;
  
    for (i=0; i<newChildren.length; i++) {
        childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
        oldVNode = index = null;

    p = oldChildren[i];
    
    // 
        if (p != null && (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key))) {
            index = i;
        }
        else {
            for (j=0; j<oldChildrenLength; j++) {
                p = oldChildren[j];
                if (p!=null) {
                    if (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key)) {
                        index = j;
                        break;
                    }
                }
            }
        }

        if (index!=null) {
            oldVNode = oldChildren[index];
            oldChildren[index] = null;
        }

    nextDom = childDom!=null && childDom.nextSibling;
    
        newDom = diff(oldVNode==null ? null : oldVNode._dom, parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, null);

        if (childVNode!=null && newDom !=null) {
            focus = document.activeElement;

            if (childVNode._lastDomChild != null) {
                newDom = childVNode._lastDomChild;
            }
            else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {

                outer: if (childDom==null || childDom.parentNode!==parentDom) {
                    parentDom.appendChild(newDom);
                }
                else {
                    sibDom = childDom;
                    j = 0;
                    while ((sibDom=sibDom.nextSibling) && j++<oldChildrenLength/2) {
                        if (sibDom===newDom) {
                            break outer;
                        }
                    }
                    parentDom.insertBefore(newDom, childDom);
                }
            }

            if (focus!==document.activeElement) {
                focus.focus();
            }

            childDom = newDom!=null ? newDom.nextSibling : nextDom;
        }
    }


    for (i=oldChildrenLength; i--; ) {
        if (oldChildren[i]!=null) {
            unmount(oldChildren[i], ancestorComponent);
        }
    }
}

diffChildren是最爲複雜的一部份內容。子VNode做爲一個數組, 數組中的內容可能改變了順序或者數目, 很難肯定新的VNode要和那一箇舊的VNode比較。因此preact中當面對列表時,咱們將要求用戶提供key, 幫助咱們比較VNode。達到複用Dom的目的。

在diffChildren中,咱們會首先經過toChildArray函數將子節點以數組的形式存儲在_children屬性上。

childDom爲第一個子節點真實的dom(這頗有用, 咱們在後面將經過它來判斷是使用appendChild插入newDom仍是使用insertBefore插入newDom,或者什麼都不作)

接下來遍歷_children屬性。若是VNode有key屬性, 則找到key與key相等的舊的VNode。若是沒有key, 則找到最近的type相等的舊的VNode。而後將oldChildren對應的位置設置null, 避免重複的查找。使用diff算法對比, 新舊VNode。返回新的dom。

若是childDom爲null, 則將新dom, append的到父DOM中。若是找到了與新的dom相等的dom(引用類型), 咱們則不作任何處理(props已經在diffElementNode中更新了)。若是在childDom的nextSibling沒有找到和新的dom相等的dom, 咱們將dom插入childDom的前面。接着更新childom。

遍歷剩餘沒有使用到oldChildren, 卸載這些節點或者組件。

異步setState

preact除了使用diff算法減小dom操做優化性能外, preact會將一段時間內的屢次setState合併減小組件渲染的次數。

咱們首先在setState中, 並無直接更新state, 或者直接從新渲染函數函數。而是將組件的實例帶入到了enqueueRender函數中。

Component.prototype.setState = function(update, callback) {
    let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));

    if (typeof update!=='function' || (update = update(s, this.props))) {
        assign(s, update);
    }

    if (update==null) return;

    if (this._vnode) {
        if (callback) this._renderCallbacks.push(callback);
        enqueueRender(this);
    }
};

在enqueueRender函數中, 咱們將組件push到隊列q中。

同時使用_dirty控制, 避免q隊列中被push了相同的組件。咱們應該在多長時間內清空q隊列呢?

咱們該如何定義這麼一段時間呢?比較好的作法是使用Promise.resolve()。在這一段時間的setState操做都會被push到q隊列中。_nextState將會被合併在清空隊列的時候,一併更新到state上,避免了重複的渲染。

let q = [];

export function enqueueRender(c) {
    if (!c._dirty && (c._dirty = true) && q.push(c) === 1) {
        (options.debounceRendering || defer)(process);
    }
}

function process() {
    let p;
    while ((p=q.pop())) {
        if (p._dirty) p.forceUpdate(false);
    }
}

const defer = typeof Promise=='function' ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout;

在宏任務完成後,咱們執行微任務Promise.resolve(), 清空q隊列,使用diff方法更新隊列中的組件。

Component.prototype.forceUpdate = function(callback) {
    let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
    if (parentDom) {
        const force = callback!==false;

        let mounts = [];
        dom = diff(dom, parentDom, vnode, vnode, this._context, parentDom.ownerSVGElement!==undefined, null, mounts, this._ancestorComponent, force);
        if (dom!=null && dom.parentNode!==parentDom) {
            parentDom.appendChild(dom);
        }
        commitRoot(mounts, vnode);
    }
    if (callback) callback();
};

結語

到這裏咱們已經吧preact的源碼大體瀏覽了一遍。咱們接下來能夠參考preact的源碼,實現本身的react。話說我還給preact的項目提交了pr😊,不過尚未merge😢。

相關文章
相關標籤/搜索