深刻Preact源碼分析(4.20更新)

React的源碼多達幾萬行,對於咱們想要快速閱讀並看懂是至關有難度的,而Preact是一個輕量級的類react庫,幾千行代碼就實現了react的大部分功能。所以閱讀preact源碼,對於咱們學習react的思想並增強認識是很是有用的。javascript

本文的倉庫在github上,持續更新中。歡迎大佬們star或提意見。html

下面是正文部分java

源碼結構node

Preact導出的函數結構 react

import { h, h as createElement } from './h';
import { cloneElement } from './clone-element';
import { Component } from './component';
import { render } from './render';
import { rerender } from './render-queue';
import options from './options';
/**
 * h函數和createElement函數是同一個函數
 *
 * */
export default {
    h,
    createElement,
    cloneElement,
    Component,
    render,
    rerender,
    options
};

export {
    h,
    createElement,
    cloneElement,
    Component,
    render,
    rerender,
    options
};
複製代碼

jsx是如何轉化成virtualDOM的

jsx要轉化成virtualDOM,首先通過babel,再通過h函數的調用造成virtualDOM。具體以下git

源碼連接 src/h.jsgithub

至關於react得createElement(),jsx通過babel轉碼後是h的循環調用,生成virtualDOM。算法

// jsx
<div>
<span className="sss" fpp="xxx">123</span>
<Hello/>
<span>xxx</span>
</div>

// h結果
h(
  "div",
  null,
  h(
    "span",
    { className: "sss", fpp: "xxx" },
    "123"
  ),
h(Hello, null),
  h(
    "span",
    null,
    "xxx"
  )
);
複製代碼

經過源碼中h的函數定義也能夠看見。h的函數第一個參數是標籤名(若是是組件類型的化就是組件名)、第二個參數是屬性值的key-value對象,後面的參數是全部子組件。數組

vnode的結構bash

h函數會根據子組件的不一樣類型進行封裝,具體以下

  • bool 返回 null
  • null 返回 ""
  • number 返回 String(number)

最後賦值給child變量並存進childdren數組中,再封裝成下面的vnode結構並返回

{
    nodeName:"div",//標籤名
    children:[],//子組件組成的數組,每一項也是一個vnode
    key:"",//key
    attributes:{}//jsx的屬性
}
複製代碼

virtualDOM如何變爲真實dom

// 一個簡單的Preact demo
import { h, render, Component } from 'preact';

class Clock extends Component {
	render() {
		let time = new Date().toLocaleTimeString();
		return <span>{ time }</span>;
	}
}

render(<Clock />, document.body); 複製代碼

調用了preact的render方法將virtualDOM渲染到真實dom。

// render.js
import { diff } from './vdom/diff';
export function render(vnode, parent, merge) {
	return diff(merge, vnode, {}, false, parent, false);
}
複製代碼

可見,render方法的第一個參數一個vnode,第二個參數是要掛載到的dom的節點,這裏暫時不考慮第三個參數。而render方法實際上又是 去調用/vdom/diff.js下的diff方法

//diff函數的定義
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {}
複製代碼

render函數使vnode轉換成真實dom主要進行了如下操做

  • render函數實際上調用了diff方法,diff方法進而調用了idiff。
  • idiff方法會返回真實的html。idiff內將vnode分爲4大類型進行處理封裝在html
  • 而後調用diffAttributes,將vnode上的屬性值更新到html domnode的屬性上。(經過setAccessor)
  • 初次render時,下面if條件恆爲真,因此真實html就這樣被裝進了。
if (parent && ret.parentNode !== parent) parent.appendChild(ret);
複製代碼

這樣初次的vnode轉化成真實html就完成了

流程圖以下

tips:在diff中會見到不少的out[ATTR_KEY],這個是用來將dom的attributrs數組每一項的name value轉化爲鍵值對存進 out[ATTR_KEY]。

組件的buildComponentFromNode是怎樣的?

buildComponentFromNode的定義

/** Apply the Component referenced by a VNode to the DOM. * @param {Element} dom The DOM node to mutate * @param {VNode} vnode A Component-referencing VNode * @returns {Element} dom The created/mutated element * @private */
export function buildComponentFromVNode(dom, vnode, context, mountAll) {}
複製代碼

初次調用時 buildComponentFromNode(undefined,vnode,{},false)。所以,初次render時的buildComponentFromVNode內部只是調用了以下的邏輯(不執行的代碼去掉了)

export function buildComponentFromVNode(dom, vnode, context, mountAll) {
   let c = dom && dom._component, // undefined
   	originalComponent = c,//undefined
   	oldDom = dom,// undefined
   	isDirectOwner = c && dom._componentConstructor===vnode.nodeName,//undefined
   	props = getNodeProps(vnode);// 這個函數除了通常的props獲取外,還會加上defaultProps。
   	c = createComponent(vnode.nodeName, props, context);// 建立組件
   	setComponentProps(c, props, SYNC_RENDER, context, mountAll);
   	dom = c.base;
   return dom;
}
複製代碼

緊接上節,Preact組件從vnode到真實html的過程發生了什麼?

...
// buildComponentFromVNode方法內部
// buildComponentFromVNode(undefined, vnode, {}, false);
c = createComponent(vnode.nodeName, props, context);// 建立組件
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
    return dom;
....
複製代碼

從上節組件變成真實dom的過程當中最重要的函數就是createComponentsetComponentProps。咱們能夠發現,在前後執行了createComponentsetComponentProps後,真實dom就是c.base了。那麼 這個createComponent幹了什麼?去掉一些初始渲染時不會去執行的代碼,簡化後的代碼以下:

// 若是是用class定義的那種有生命週期的組件,上文代碼中的```vnode.nodeName```其實就是咱們定義的那個class。
export function createComponent(Ctor, props, context) {
    let inst;
    if (Ctor.prototype && Ctor.prototype.render) {
        // 正常的組件 class xxx extends Component{} 定義的
        //首先是對本身的組件實例化
        inst = new Ctor(props, context);
        //而後再在咱們實例化的組件,去得到一些Preact的內置屬性(props、state,這兩個是掛在實例上的)和一些內置方法(setState、render之類的,這些方法是掛在原型上的)
        Component.call(inst, props, context);
    } else {
        // 無狀態組件
        //無狀態組件是沒有定義render的,它的render方法就是這個無狀態組件自己
        inst = new Component(props, context);
        inst.constructor = Ctor;
        inst.render = doRender;
    }
    return inst;
}

function doRender(props, state, context) {
    // 無狀態組件的render方法就是本身自己
    return this.constructor(props, context);
}
複製代碼

Component的定義以下。經過上面和下面的代碼能夠知道,createComponent的主要做用就是讓咱們編寫的class型和無狀態型組件實例化, 這個實例是具備類似的結構。並供後面的setComponentProps去使用產生真實dom。

// Component的定義
export function Component(props, context) {
	this._dirty = true;// 這個東西先無論,應該是和diff有關
	this.context = context;// context這個東西我也暫時不知道有什麼用
	this.props = props;
	this.state = this.state || {};
}
// 這裏的extend就是一個工具函數,把setState、forceUpdate、render方法掛載到原型上
extend(Component.prototype,{
    setState(state,callback){},
    forceUpdate(callback){},
    render() {}
})
複製代碼

setComponentProps產生真實dom的過程。

setComponentProps(c, props, SYNC_RENDER, {}, false);

export function setComponentProps(component, props, opts, context, mountAll) {
    // 同理去除條件不成立的代碼,只保留首次渲染時運行的關鍵步驟
    if (!component.base || mountAll) {
        // 可見。componentWillMount生命週期方法只會在未加載以前執行,
        if (component.componentWillMount) component.componentWillMount();
    }
    renderComponent(component, SYNC_RENDER, mountAll);
}
複製代碼

由上面代碼可見,setComponentProps內部,實際上關鍵是調用了renderComponent方法。renderComponent邏輯有點繞, 精簡版代碼以下。

renderComponent主要邏輯簡單來講以下: 一、調用組件實例的render方法去產生vnode。

二、若是這個組件產生的vnode再也不是組件了。則經過diff函數去產生真實dom並掛載(前面已經分析過)diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);

三、若是這個組件的子vnode仍是子組件的話。則再次調用setComponentPropsrenderComponent去進一步生成真實dom,直到2中條件成立。(判斷步驟和二、3相似),可是有點區別的是。這種調用代碼是

setComponentProps(inst, childProps, NO_RENDER, context, false);// 不渲染。只是去執行下生命週期方法,在這個setComponentProps內部是不調用 renderComponent的。 至於爲啥。。暫時我也不知道。NO_RENDER標誌位
renderComponent(inst, SYNC_RENDER, mountAll, true);
複製代碼

精簡版代碼

export function renderComponent(component, opts, mountAll, isChild) {
    // 這個函數其實很長有點複雜的,只保留了初次渲染時執行的部分和關鍵的部分。
        // 調用組件的render方法,返回vnode
        rendered = component.render(props, state, context);//*****
        let childComponent = rendered && rendered.nodeName,base;
        if (typeof childComponent === 'function') {
            // 子節點也是自定義組件的狀況
            let childProps = getNodeProps(rendered);
                component._component = inst = createComponent(childComponent, childProps, context);
				setComponentProps(inst, childProps, NO_RENDER, context, false);// 不渲染啊。只是去執行下生命週期方法
                renderComponent(inst, SYNC_RENDER, mountAll, true);// 對比 renderComponent(component, SYNC_RENDER, mountAll);
        } else {
            base = diff(。。。);// 掛載
        }
        component.base = base; //把真實dom掛載到base屬性上
        if (!diffLevel && !isChild) flushMounts();
}
複製代碼

前面看到了componentWillMount生命週期了,那麼componentDidMount這個生命週期呢?它就是在flushMounts。這個if語句成立的條件是在祖先組件而且初次渲染時才執行(初次渲染的diffLevel值爲0)。

export function flushMounts() {
    let c;
    while ((c = mounts.pop())) {
        if (options.afterMount) options.afterMount(c);
        if (c.componentDidMount) c.componentDidMount();
    }
}
複製代碼

flushMounts中的mounts就是當前掛載的組件的實例。它是一個棧的結構並依次出棧執行componentDidMount。因此, 這就能說明了Preact(React也同樣)父子組件的生命週期執行順序了 parentWillMount -> parentRender -> childWillMount -> childRender -> childDidMount -> parentDidParent。

至此組件類型的vnode產生真實dom的分析就結束了。

流程圖以下

setState發生了什麼

setState(state, callback) {
    let s = this.state;
    if (!this.prevState) this.prevState = extend({}, s);
    extend(s, typeof state==='function' ? state(s, this.props) : state);// 語句3
    if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
    enqueueRender(this);
},
複製代碼

setState的定義如上,代碼邏輯很容易看出

一、prevState若不存在,將要更新的state合併到prevState上

二、能夠看出Preact中setState參數也是能夠接收函數做爲參數的。將要更新的state合併到當前的state

三、若是提供了回調函數,則將回調函數放進_renderCallbacks隊列

四、調用enqueueRender進行組件更新

why?我剛看到setState的第二、3行代碼的時候也是一臉矇蔽。爲何它要這樣又搞一個this.prevState又搞一個this.state,又有個state呢?WTF。 經過理清Preact的setState的執行原理。

應該是用於處理一個組件在一次流程中調用了兩次setState的狀況。

// 例如這裏的handleClick是綁定click事件

handleClick = () =>{
    // 注意,preact中setState後state的值是會立刻更新的
    this.setState({a:this.state.a+1});
    console.log(this.state.a);
    this.setState({a:this.state.a+1});
    console.log(this.state.a);
} 
複製代碼

基本上每個學react的人,都知道上述代碼函數在react中執行以後a的值只會加一,but!!!!在Preact中是加2的!!!!經過分析Preact的setState能夠解釋這個緣由。 在上面的語句3,extend函數調用後,當前的state值已經改變了。可是即便state的值改變了,可是屢次setState仍然是會只進行一次組件的更新(經過setTimeout把更新操做放在當前事件循環的最後),以最新的state爲準。因此,這裏的prevState應該是用於記錄當前setState以前的上一次state的值,用於後面的diff計算。在enqueueRender執行diff時比較prevState和當前state的值

關於enqueueRender的相關定義

let items = [];

export function enqueueRender(component) {
	// dirty 爲true代表這個組件從新渲染
    if (!component._dirty && (component._dirty = true) && items.push(component) == 1) {//語句1
        // 只會執行一遍
        (options.debounceRendering || defer)(rerender); // 至關於setTimeout render 語句2
    }
}

export function rerender() {
    let p, list = items;
    items = [];
    while ((p = list.pop())) {
        if (p._dirty) renderComponent(p);
    }
}
複製代碼

enqueueRender的邏輯主要是

一、語句1: 將調用了setState的組件的_dirty屬性設置爲false。經過這段代碼咱們還能夠發現, 若是在一次流程中,調用了屢次setState,rerender函數實際上仍是隻執行了一遍(經過判斷component._dirty的值來保證一個組件內的屢次setState只執行一遍rerender和判斷items.push(component) == 1確保若是存在父組件調用setState,而後它的子組件也調用了setState,仍是隻會執行一次rerender)。items隊列是用來存放當前全部dirty組件。

二、語句2。能夠看做是setTimeout,將rerender函數放在本次事件循環結束後執行。rerender函數對全部的dirty組件執 行renderComponent進行組件更新。

在renderComponent中將會執行的代碼。只列出和初次渲染時有區別的主要部分

export function renderComponent(component, opts=undefined, mountAll=undefined, isChild=undefined) {
    ....
    if (isUpdate) {
        component.props = previousProps;
        component.state = previousState;
        component.context = previousContext;
        if (opts !== FORCE_RENDER && // FORCE_RENDER是在調用組件的forceUpdate時設置的狀態位
            component.shouldComponentUpdate &&
            component.shouldComponentUpdate(props, state, context) === false) {
            skip = true;// 若是shouldComponentUpdate返回了false,設置skip標誌爲爲true,後面的渲染部分將會被跳過
        } else if (component.componentWillUpdate) {
            component.componentWillUpdate(props, state, context);//執行componentWillUpdate生命週期函數
        }

        // 更新組件的props state context。由於componentWillUpdate裏面有可能再次去修改它們的值
        component.props = props;
        component.state = state;
        component.context = context;
    }
    ....
    component._dirty = false;
    ....
    // 省略了diff渲染和dom更新部分代碼
    ...
    if (!skip) {
        if (component.componentDidUpdate) {
            //componentDidUpdate生命週期函數
            component.componentDidUpdate(previousProps, previousState, previousContext);
        }
    }

    if (component._renderCallbacks != null) {
        // 執行setState的回調
        while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
    }
}
複製代碼

邏輯看代碼註釋就很清晰了。先shouldComponentUpdate生命週期,根據返回值決定是都否更新(經過skip標誌位)。而後將組件的_dirty設置爲true代表已經更新了該組件。而後diff組件更新,執行componentDidUpdate生命週期,最後執行setState傳進的callback。

流程圖以下:

下一步,就是研究setState組件進行更新時的diff算法幹了啥

非組件節點的diff分析

diff的流程,咱們從簡單到複雜進行分析

經過前面幾篇文章的源碼閱讀,咱們也大概清楚了diff函數參數的定義和component各參數的做用

/** * @param dom 初次渲染是undefinde,第二次起是指當前vnode前一次渲染出的真實dom * @param vnode vnode,須要和dom進行比較 * @param context 相似與react的react * @param mountAll * @param parent * @param componentRoot * **/
function diff(dom, vnode, context, mountAll, parent, componentRoot){}
複製代碼
// component
{

    base,// dom
    nextBase,//dom

    _component,//vnode對應的組件
    _parentComponent,// 父vnode對應的component
    _ref,// props.ref 
    _key,// props.key
    _disable,

    prevContext,
    context,

    props,
    prevProps,

    state,
    previousState

    _dirty,// true表示該組件須要被更新
    __preactattr_// 屬性值

    /***生命週期方法**/
    .....
}
複製代碼

diff不一樣類型的vnode也是不一樣的。Preact的diff算法,是將setState後的vnode與前一次的dom進行比較的,邊比較邊更新。diff主要進行了兩步操做(對於非文本節點來講), 先diff內容innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);,再diff屬性diffAttributes(out, vnode.attributes, props);

一、字符串或者布爾型 若是以前也是一個文本節點,則直接修改節點的nodeValue的值;不然,建立一個新節點,並取代舊節點。並調用recollectNodeTree對舊的dom進行臘雞回收。

二、html的標籤類型

  • 若是vnode的標籤對比dom發生了改變(例如原來是span,後來是div),則新建一個div節點,而後把span的子元素都添加到新的div節點上,把新的div節點替換掉舊的span節點,而後回收舊的(回收節點的操做主要是把這個節點從dom中去掉,從vdom中也去掉)
if (!dom || !isNamedNode(dom, vnodeName)) {
         // isNamedNode方法就是比較dom和vnode的標籤類型是否是同樣
        out = createNode(vnodeName, isSvgMode);
        if (dom) {
            while (dom.firstChild) out.appendChild(dom.firstChild);
            if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
            recollectNodeTree(dom, true);//recollectNodeTree
        }
    }
複製代碼
  • 對於子節點的diff

    • Preact對於只含有一個的子字符串節點直接進行特殊處理
    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];
        }
    }
    複製代碼
    • 對於通常狀況
    /****/
    innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);
    複製代碼

    那麼,innerDiffNode函數作了什麼? 首先,先解釋下函數內定義的一些關鍵變量到底幹了啥

    let originalChildren = dom.childNodes,// 舊dom的子node集合
        children = [],// 用來存儲舊dom中,沒有提供key屬性的dom node
        keyed = {},// 用來存舊dom中有key的dom node,
    複製代碼

    首先,第一步的操做就是對舊的dom node進行分類。將含有key的node存進keyed變量有,這是一個鍵值對結構; 將無key的存進children中,這是一個數組結構。

    而後,去循環遍歷vchildren的每一項,用vchild表示每一項。如有key屬性,則取尋找keyed中是否有該key對應的真實dom;若無,則去遍歷children 數據,尋找一個與其類型相同(例如都是div標籤這樣)的節點進行diff(用child這個變量去存儲)。而後執行idiff函數 child = idiff(child, vchild, context, mountAll);。經過前面分析idiff函數,咱們知道若是傳進idiff的child爲空,則會新建一個節點。因此對於普通節點的內容的diff就完成了。而後把這個返回新的dom node去取代舊的就能夠了,代碼以下

    f = originalChildren[i];
            if (child && child !== dom && child !== f) {
                if (f == null) {
                    dom.appendChild(child);
                } else if (child === f.nextSibling) {
                    removeNode(f);
                } else {
                    dom.insertBefore(child, f);
                }
            }
    複製代碼

    當對vchildren遍歷完成diff操做後,把keyedchildren中剩餘的dom節點清除。由於他們在新的vnode結構中已經不存在了

    而後對於屬性進行diff就能夠了。diffAttributes的邏輯就比較簡單了,取出新vnode 的 props和舊dom的props進行比較。新無舊有的去除,新有舊有的替代,新有舊無的添加。setAccessor是對於屬性值設置時一些保留字和特殊狀況進行一層封裝處理

    function diffAttributes(dom, attrs, old) {
    let name;
    for (name in old) {
        if (!(attrs && attrs[name] != null) && old[name] != null) {
            setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
        }
    }
    for (name in attrs) {
        if (name !== 'children' && name !== 'innerHTML' && (!(name in old) || attrs[name] !== (name === 'value' || name === 'checked' ? dom[name] : old[name]))) {
            setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
        }
    }
    }
    複製代碼

    至此,對於非組件節點的內容的diff完成了

相關文章
相關標籤/搜索