Preact源碼分析

本文同步在我的博客shymean.com上,歡迎關注html

最近打算學習React源碼,發現了一個簡易版的框架Preact,且與React的API比較類似,所以決定先看看它的代碼。node

本文使用源碼版本preact 10.0.0-beta.3,複製了部分核心源碼,刪除了一些邏輯分支並增長了註釋。react

開發環境

克隆整個項目,安裝依賴,而後進行斷點調試webpack

git clone git@github.com:preactjs/preact.git
# 安裝項目依賴
cd preact
npm i

# 進入demo項目,安裝webpack、babel等相關依賴
cd demo 
npm i

# 啓動demo項目,開始進行斷點調試
npm run start
複製代碼

修改demo/index.js中的代碼,構建一個最基本的應用git

import { createElement, render, Component } from 'preact';
class Home extends Component {
	constructor(props) {
		super(props);
		this.state = {
			count: 1
		};
	}
	render() {
		let { count } = this.state;
		let { msg } = this.props;
		return (
			<div> <h1>{msg}</h1> <p>count:{count}</p> </div>
		);
	}
}
let vnode = (
	<Home msg="hello msg"/> ); console.log(vnode); let app = document.createElement('div'); document.body.appendChild(app); render(vnode, app); 複製代碼

咱們構建了一個叫Home的組件,而後將它掛載到一個DOM節點上上,從上面代碼能夠看出,咱們首先須要瞭解Component類和render函數github

渲染流程

Component

下面是Component類的源碼,web

// src/component.js
export function Component(props, context) {
	this.props = props;
	this.context = context;
}
Component.prototype.setState = function(update, callback) {}
Component.prototype.forceUpdate = function(callback) {}
Component.prototype.render = Fragment

// src/create-element.js
export function Fragment(props) {
	return props.children;
}
複製代碼

能夠把Component看作是一個類,咱們暫時不須要關心其方法的做用和實現。chrome

vnode

再回過頭來看看<Home />究竟是啥東西npm

// demo/index.js
let vnode = (
	<Home msg="hello msg"/> ); // 被babel轉化成下面代碼,chrome調試模式->network面板->main.js中可查看babel編譯後的代碼 var vnode = Object(preact__WEBPACK_IMPORTED_MODULE_0__["createElement"])(Home, { msg: "hello msg" }); // 打印vnode, 控制檯輸出下面內容,可見標籤上的屬性轉換成了props {"props":{"msg":"hello msg"},"_children":null,"_parent":null,"_depth":0,"_dom":null,"_lastDomChild":null,"_component":null} 複製代碼

babel編譯JSX,將其轉換成createElement方法調用,這也是爲何demo/index.js文件頭部須要手動引入一個createElement方法的緣由,查看createElement相關源碼redux

// src/create-element.js
export function createElement(type, props, children) {
	props = assign({}, props);

	if (arguments.length>3) {
        // children後傳入多個參數時轉換爲數組
        // ...
	}
	if (type!=null && type.defaultProps!=null) {
        // 處理type.defaultProps,並將其合併到props上
        // ...
	}
    // 處理key和ref
	let ref = props.ref;
	let key = props.key;
	if (ref!=null) delete props.ref;
	if (key!=null) delete props.key;
    // 調用createVNode,所以createElement 返回的是一個vnode
	return createVNode(type, props, key, ref);
}
複製代碼

注意propschildren參數都是有babel編譯JSX時,經過解析模板替咱們傳入的參數。順藤摸瓜,咱們來看看createNode

// src/create-element.js
export function createVNode(type, props, key, ref) {
    // 已經把vnode簡化成一個對象字面量了,能夠看到這跟上面打印的<Home />基本一致
	const vnode = {
		type,
		props,
		key,
		ref,
		_children: null,
		_parent: null,
		_depth: 0,
		_dom: null,
		_lastDomChild: null,
		_component: null,
		constructor: undefined
	};

	return vnode;
}
複製代碼

render

如今咱們知道了<Home />實際上就是一個vnode,接下來再看看render(<Home />, document.body)中的邏輯

// src/render.js
export function render(vnode, parentDom, replaceNode) {
	if (options._root) options._root(vnode, parentDom);
	let oldVNode = parentDom._children;
	vnode = createElement(Fragment, null, [vnode]); // 使用Fragment包裹了實際的vnode

	let mounts = [];
	diff(
		parentDom, // document.body
		replaceNode ? vnode : (parentDom._children = vnode), // parentDom._children = vnode
		oldVNode || EMPTY_OBJ, // {}
		EMPTY_OBJ, // {}
		parentDom.ownerSVGElement !== undefined, // false
		replaceNode
			? [replaceNode]
			: oldVNode
				? null
				: EMPTY_ARR.slice.call(parentDom.childNodes), // document.body全部DOM子節點
		mounts, // []
		false,
		replaceNode || EMPTY_OBJ, // {}
	);
	commitRoot(mounts, vnode);
}
複製代碼

經過斷點發現,在diff方法結束以後頁面進行了渲染,那麼在該方法內,確定實現了從vnode到實際DOM節點的轉變。至此,整個渲染流程分析基本完畢。

小結

大體流程以下

  • 建立了一個組件類Home,而後構造了一個vnode,最後調用render方法將該vnode掛載到了頁面DOM節點上
  • render函數內部,調用了diff方法,將遞歸遍歷以該vnode構造的AST,並將全部vnode轉換成DOM節點,完成頁面渲染

preact把渲染相關的操做一併放在了diff代碼中,所以看起來涉及到的流程仍是比較多的。初始化時能夠當作新vnode與舊的空節點作比較,所以第一次渲染也使用與頁面更新時相同的diff邏輯來完成渲染。

那麼,diff方法內部的流程究竟是如何實現的呢?

diff三部曲

該函數有點長,咱們如今暫時只須要關注初始化時頁面的渲染流程,所以下面源碼刪除了與初始化無關的條件分支。記住,咱們如今把初識化的過程當作一個全新的vnode與空節點之間的對比。

// src/diff/index.js
/* parentDom: 父DOM節點 newVNode: 新的AST根節點 oldVNode: 舊的AST根節點 context: 當前context isSvg: 是不是svg節點 excessDomChildren: 父節點下其他的DOM節點 mounts: 一個表示須要觸發掛載成功的組件列表,從根節點一直透傳到全部葉子節點,並收集全部須要出發的節點 force: 是否強制更新 oldDom: 當前DOM節點 */
export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, force, oldDom) {
	let tmp, newType = newVNode.type;

	try {
		outer: if (typeof newType==='function') {
      // 根據oldVNode是否存在判斷是更新仍是新增節點,初始化相關數組和組件實例
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;
			let cctx =  context;
			// 若是是一個註冊的Component組件,則調用構造函數獲取組件實例,所以Home組件就是在此處實例化的
      if (newType.prototype && newType.prototype.render) {
        // vnode經過_component維持了對於組件實例的引用,所以能夠newVNode._component.setState()等方式調用組件方法
        newVNode._component = c = new newType(newProps, cctx); // eslint-disable-line new-cap
      }else {
        // 根節點由Fragment組件包裹,無render方法,所以直接調用Component
        newVNode._component = c = new Component(newProps, cctx);
        c.constructor = newType;
        c.render = doRender; // (props, state, context) => this.constructor(props, context)
      }

      c.props = newProps;
      if (!c.state) c.state = {}; // 設置組件默認的state
      c.context = cctx;
      c._context = context;
      isNew = c._dirty = true;
      c._renderCallbacks = [];

			if (c._nextState==null) {
				c._nextState = c.state;
			}
      // 調用組件的getDerivedStateFromProps生命週期,該鉤子函數是組件的一個靜態方法
      if (newType.getDerivedStateFromProps!=null) {
				assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newProps, c._nextState));
			}
      // 調用componentWillMount聲明周期函數,可見父組件的componentWillMount先於子組件調用
      // 將註冊了componentDidMount聲明周期函數的組件放在mounts數組中,等待全部子節點都掛載完畢後在render的commitRoot方法中統一調用
			if (isNew) {
				if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) c.componentWillMount();
				if (c.componentDidMount!=null) mounts.push(c);
			}

			oldProps = c.props;
			oldState = c.state;

			c.context = cctx;
			c.props = newProps;
      // 設置_nextState的初始值爲state
      if (c._nextState==null) {
				c._nextState = c.state;
			}

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

      tmp = c.render(c.props, c.state, c.context); // 調用組件render方法

      // 將tmp子節點轉換爲一個一維數組, 並存放在newVNode._children中
      // 其中coerceToVNode接收一個vnode做爲參數,若是vnode已經有了_dom屬性,則返回一個克隆後的vnode;不然返回當前vnode
      toChildArray(tmp, newVNode._children=[], coerceToVNode, true); 

      // 開始對比子節點,其內部遞歸調用了diff方法,經過diffElementNodes獲取子節點的真實dom
      // 而後調用parentDom.appendChild(newDom)或parentDom.insertBefore(newDom, oldDom),將dom插入頁面
			diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom);

			c.base = newVNode._dom;
			while (tmp=c._renderCallbacks.pop()) tmp.call(c);
		}else {
      // 一個封裝組件的最底層都是用html標籤構造的,當newType不是Component時,表示渲染的是元素DOM,其內部調用了document.createElement方法渲染真正的dom
			newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts);
		}
	}catch (e) {
		catchErrorInComponent(e, newVNode._parent);
	}

	return newVNode._dom;
}
複製代碼

可見在diff中調用了diffChildren方法來比較兩個vNode的全部子節點的差別,讓咱們緊隨其後,一探究竟

// src/diff/children.js
export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, oldDom) {
    let childVNode, i, j, oldVNode, newDom, sibDom, firstChildDom, refs;
    // 在上一層的diff方法中已經調用了toChildArray將其組件的render函數返回值轉換成了_children屬性,
    // 若是render函數未返回數據,則再次調用toChildArray將其props.children屬性轉換成_children屬性
    // 這就是爲何在無render函數的時候還能夠染組件內標籤的緣由
    let newChildren =newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode, true); 
    // 獲取舊的子節點列表
    let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
    for (i=0; i<newChildren.length; i++) {
        // 若是vnode已被使用且關聯了一個_dom元素,則克隆出一個新的vnode
        childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
        // 跳過爲null的子節點
        if (childVNode!=null) {
            childVNode._parent = newParentVNode;
            childVNode._depth = newParentVNode._depth + 1;

            oldVNode = oldChildren[i];
            // 處理oldChildren[i],若是存在某些未與childVNode作比較的子節點,則再後面會調用unmount進行移除
            if (oldVNode===null || (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type)) {
				oldChildren[i] = undefined;
			}else {
				for (j=0; j<oldChildrenLength; j++) {
					oldVNode = oldChildren[j];
					if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
						oldChildren[j] = undefined;
						break;
					}
					oldVNode = null;
				}
			}
      oldVNode = oldVNode || EMPTY_OBJ;

      // 開始比較每一個子節點的區別,遞歸調用diff方法內的diffChildren方法,
      // 此時咱們將跳轉會diff方法,並最終跳轉到diffElementNodes方法,獲取到一個真實的dom節點,所以這裏先閱讀下面的 diffElementNodes 源碼部分
      newDom = diff(parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, null, oldDom);

      // 閱讀完diffElementNodes源碼,咱們知道了diff方法返回的是一個oldVNode._dom通過初始化、diffChildren和diffProps後的DOM節點
      // 此時 newVNode._dom = newDom
          if (newDom!=null) {
                if (firstChildDom == null) {
                    firstChildDom = newDom;
                }
                if (childVNode._lastDomChild != null) {
                    // 咱們知道一個組件只能包含一個最外層的子節點,
                    // 若是childVNode.type是一個組件,那麼將childVNode保存的_lastDomChild屬性賦值給newDom,無需進行下面分支的判斷比較
                    newDom = childVNode._lastDomChild;
                    childVNode._lastDomChild = null;
                }else if (excessDomChildren==oldVNode || newDom!=oldDom || newDom.parentNode==null) {
                    outer: if (oldDom==null || oldDom.parentNode!==parentDom) {
                        // 若是父節點都已經修改,則直接向新的parentDom中追加newDom便可
                        parentDom.appendChild(newDom);
                    }
                    else {
                        // 若是父節點相同,則判斷newDom是否已經存在parentDom中,不存在則調用insertBefore插入newDom
                        // todo 這裏爲何調用的是insertBefore
                        // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2`
                        for (sibDom=oldDom, j=0; (sibDom=sibDom.nextSibling) && j<oldChildrenLength; j+=2) {
                            if (sibDom==newDom) {
                                break outer;
                            }
                        }
                        parentDom.insertBefore(newDom, oldDom);
                    }
                }

                oldDom = newDom.nextSibling;
                // 若是childVNode.type是一個組件,保存newDom到其_lastDomChild屬性
                if (typeof newParentVNode.type == 'function') {
                    newParentVNode._lastDomChild = newDom;
                }
            }
        }
    }
    newParentVNode._dom = firstChildDom;
    // 若是還存在未被設置爲undefined的舊節點,如oldChildrenLength > newChildren.length 的狀況,則須要移除舊節點
	for (i=oldChildrenLength; i--; ) if (oldChildren[i]!=null) unmount(oldChildren[i], newParentVNode);
}
複製代碼

接下來看看diffElementNodes是何方神聖

// src/diff/index.js
function diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts) {
	let i;
	let oldProps = oldVNode.props;
	let newProps = newVNode.props;

	isSvg = newVNode.type==='svg' || isSvg;
    // 在diff方法中傳入的是oldVNode._dom,第一次調用時會初始化,生成真實的dom節點
	if (dom==null) {
        // 無type類型,返回純文本節點
		if (newVNode.type===null) {
			return document.createTextNode(newProps);
		}
        // 有類型,如div、h一、p標籤等,則返回實際dom節點
		dom = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type) : document.createElement(newVNode.type);
	}
    // 若是節點未發生變化,則能夠以該節點爲根的子AST未發生變化,即全部子節點均無變化,diff到此爲止
    // 只有當新節點發生變化時,才進行該條件判斷的邏輯,其內部會繼續調用diffChildren判斷相關子節點
	if (newVNode!==oldVNode) {
		oldProps = oldVNode.props || EMPTY_OBJ;

        // 替換dom節點html內容爲dangerouslySetInnerHTML屬性傳遞的內容
		let oldHtml = oldProps.dangerouslySetInnerHTML;
		let newHtml = newProps.dangerouslySetInnerHTML;
		if ((newHtml || oldHtml) && excessDomChildren==null) {
			// Avoid re-applying the same '__html' if it did not changed between re-render
			if (!newHtml || !oldHtml || newHtml.__html!=oldHtml.__html) {
				dom.innerHTML = newHtml && newHtml.__html || '';
			}
		}
        // 處理multiple屬性
		if (newProps.multiple) {
			dom.multiple = newProps.multiple;
		}
        // 將dom做爲parentDom,並開始對比newVNode和oldVNode的子節點列表
		diffChildren(dom, newVNode, oldVNode, context, newVNode.type==='foreignObject' ? false : isSvg, excessDomChildren, mounts, EMPTY_OBJ);
        
        // 將新舊屬性的變化複製到新的dom節點上,如style、value、checked等屬性
		diffProps(dom, newProps, oldProps, isSvg);
	}
    // 返回新的dom節點,此時跳回diffChildren調用diff方法的地方,調用diff方法獲得的就是這個dom節點
	return dom;
}
複製代碼

diff方法須要結合diffChildrendiffElementNodes這兩個方法一塊兒閱讀,他們內部互相嵌套調用,直至遍歷完整個vnode組成的AST。

  • 首先調用diff,根據newType的類型判斷調用diffChildren仍是diffElementNodes
  • diffChildren中,獲取新舊節點的子節點列表,依次遞歸調用diff方法;
  • diffElementNodes,經過判斷newVNode和oldVNode是否相同,若是不相同,則遞歸調用diffChildren,若是相同,則表示無變化,遞歸出棧。

在render函數中調用diff方法進行初始化時,oldVnode爲空,oldVnode._dom也爲null,所以就會進入上面相關代碼的初始化化流程。

咱們知道diff主要是用來比較新舊兩個VNode樹,用於減小真實DOM操做的性能消耗,在狀態更新引發的頁面從新渲染時,咱們須要繼續關注diff函數的其餘工做,在此以前,咱們只須要關注vnode是如何轉換成DOM便可。

setState

在diff代碼中能夠看見,初始化時,vnode經過vnode._component屬性維持了組件實例的引用。而在調用setState更新狀態以後,頁面會從新渲染組件,接下來讓咱們看看狀態更新時發生了什麼。

渲染流程

修改demo內的代碼

// demo/index.js
setTimeout(() => {
	vnode._component.setState({
		count: 2
	});
}, 1000);
複製代碼

通過1s的延遲以後,會從新渲染文本內容爲count:2,如今咱們從_component.setState入手,看看調用setState以後的執行流程

// src/component.js
Component.prototype.setState = function(update, callback) {
    // 在diff中初始化時,將_nextState初始化爲state,須要注意assign返回的是它的第一個參數
	let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));
	if (typeof update!=='function' || (update = update(s, this.props))) {
        // 合併this._nextState和須要更新的數據update,update上的屬性會覆蓋this._nextState的值
        // 注意此處並不會修改當前this.state的值,setState()方法是異步的!!
		assign(s, update);
	}
	if (update==null) return;
	if (this._vnode) {
        // 收集更新後的回調,在渲染完成以後將執行該回調
		if (callback) this._renderCallbacks.push(callback);
		enqueueRender(this); // 將這次更新入隊列
	}
};
複製代碼

而後咱們來看看這個渲染隊列enqueueRender的實現

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

export function enqueueRender(c) {
	if (!c._dirty && (c._dirty = true) && q.push(c) === 1) {
		(options.debounceRendering || defer)(process); // 異步執行process,這裏能夠說明setState方法是異步的
	}
}
function process() {
	let p;
    // 將節點按深度進行排序,深度越大,排位越靠前,可見子組件先觸發forceUpdate
	q.sort((a, b) => b._vnode._depth - a._vnode._depth);
    // 逐步調用forceUpdate,最後清空q
	while ((p=q.pop())) {
		// forceUpdate's callback argument is reused here to indicate a non-forced update.
		if (p._dirty) p.forceUpdate(false);
	}
}
複製代碼

從上面的代碼咱們知道了setState方法是異步的緣由,可知調用setState以後,preact會把組件更新後的數據放在_nextState上,而後將該組件放入渲染隊列中,等待全部的setState調用完畢,瀏覽器進入異步事件隊列時,根據組件對應vnode的深度進行排序,依次調用組件的forceUpdate方法,接下來看看forceUpdate這個方法

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

		let mounts = [];
        // 調用diff方法,從新渲染頁面
		let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom);

		commitRoot(mounts, vnode);

		if (newDom != oldDom) {
			updateParentDomPointers(vnode);
		}
	}
	if (callback) callback();
};

複製代碼

diff

能夠發現,在forceUpdate中,調用的仍舊是diff方法,經過對比新的節點vnode和舊的節點assign({}, vnode)從新渲染頁面,所以咱們如今須要回到diff方法,查看當更新節點state時是如何從新渲染的。一樣地,爲了簡化流程,移除了大部分與setState相關流程無關的代碼。

export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, force, oldDom) {
	let tmp, newType = newVNode.type;

	try {
		outer: if (typeof newType==='function') {
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;
			let cctx =  context;

            // 以前已經調用過組件構造函數,所以此處直接賦值
			if (oldVNode._component) {
				c = newVNode._component = oldVNode._component;
				clearProcessingException = c._processingException = c._pendingError;
			}
            // 調用相關聲明周期函數
            if (newType.getDerivedStateFromProps!=null) {
				assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newProps, c._nextState));
			}
            if (newType.getDerivedStateFromProps==null && force==null && c.componentWillReceiveProps!=null) {
                c.componentWillReceiveProps(newProps, cctx);
            }
            // 若是組件的shouldComponentUpdate方法返回false,則不更新組件,跳出最外層outer處的if循環
            if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newProps, c._nextState, cctx)===false) {
                c.props = newProps;
                c.state = c._nextState;
                c._dirty = false;
                c._vnode = newVNode;
                newVNode._dom = oldVNode._dom;
                newVNode._children = oldVNode._children;
                break outer;
            }
            if (c.componentWillUpdate!=null) {
                c.componentWillUpdate(newProps, c._nextState, cctx);
            }
            
            // 獲取新舊節點的props和state
			oldProps = c.props;
			oldState = c.state;

			c.context = cctx;
			c.props = newProps; // 設置新的props
			c.state = c._nextState; // 此時纔開始將組件的state設置爲調用setState方法傳入的新值,牢記setState方法是異步的!!

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

			try {
				tmp = c.render(c.props, c.state, c.context); // 從新調用render函數,生成新的vnode,新的vnode會渲染新的dom節點
				toChildArray(tmp, newVNode._children=[], coerceToVNode, true);
			}catch (e) {
				if ((tmp = options._catchRender) && tmp(e, newVNode, oldVNode)) break outer;
				throw e;
			}
            // 調用getSnapshotBeforeUpdate生命週期函數
			if (!isNew && c.getSnapshotBeforeUpdate!=null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

            // 開始對比子節點,在內部修改newVNode._dom實際DOM節點並掛載到parentDom上,這裏與上面在初識化渲染時候分析基本一致
            // 遞歸diffChildren、diff和diffElementNodes,獲取新的newVNode._dom
			diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom);

			c.base = newVNode._dom;
            // 此時渲染完畢,開始執行setState時傳入的回調函數
			while (tmp=c._renderCallbacks.pop()) tmp.call(c);

            // 調用componentDidUpdate生命週期函數
			if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
				c.componentDidUpdate(oldProps, oldState, snapshot);
			}
		}
		else {
			newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts);
		}
	}
	catch (e) {
		catchErrorInComponent(e, newVNode._parent);
	}

	return newVNode._dom;
}
複製代碼

剩下的diffChildren方法與diffElementNodes在上面render流程中已基本理清,這裏再也不贅述。

小結

總結一下上面的執行流程

  • 首先調用setState,其內部把須要更新的屬性掛載到c._nextState屬性上,而後將組件放入enqueueRender隊列中
  • 當瀏覽器進行異步事件循環階段時,會調用根據enqueueRender中每一個組件的深度,從大到小依次調用組件的forceUpdate方法
  • 在組件的forceUpdate中,會調用diff方法從新渲染c._vnode._domDOM節點
    • 在diff方法中,會將c._nextState賦值給c.state,而後從新調用c.render方法,獲取新的vnode節點,並經過toChildArray將新的vnode.children賦值爲newVNode._children
    • 在diffChildren中,會依次比較newVNode._childrenoldVNode._children全部子節點,若是節點不相同,則返回新的DOM節點;最後刪除多餘的舊節點
    • 將更新後的DOM節點掛載到parentDom上,並移除多餘的舊DOM節點,完成頁面渲染的更新

props

咱們知道,babel會把JSX中標籤上的屬性轉換成props屬性,而後傳入createElement函數,在上面的diffElementNodes中咱們知道,當vnode節點發生改變時,會遞歸調用diffChildren比較子節點,此外,還會調用diffProps更新當前DOM節點的屬性

// src/diff/index.js
function diffElementNodes(dom, newVNode, oldVNode, ...) {
	if (newVNode!==oldVNode) {
		diffChildren(dom, newVNode, oldVNode, ...);
		diffProps(dom, newProps, oldProps, isSvg);
	}
}
複製代碼

前面咱們只關注了diffChildren,接下來咱們看看props是如何傳遞給子組件的,下面是diffProps的源碼

export function diffProps(dom, newProps, oldProps, isSvg) {
	let i;

	const keys = Object.keys(newProps).sort();
	for (i = 0; i < keys.length; i++) {
		const k = keys[i];
		// 跳過一些特殊的屬性名
		if (k!=='children' && k!=='key' && (!oldProps || ((k==='value' || k==='checked') ? dom : oldProps)[k]!==newProps[k])) {
			setProperty(dom, k, newProps[k], oldProps[k], isSvg);
		}
	}

	for (i in oldProps) {
		if (i!=='children' && i!=='key' && !(i in newProps)) {
			setProperty(dom, i, null, oldProps[i], isSvg);
		}
	}
}
複製代碼

可見其內部是經過setProperty更新DOM屬性的,針對於屬性,咱們須要着重關注一下DOM事件是如何綁定的

// src/diff/props.js
function setProperty(dom, name, value, oldValue, isSvg) {
	name = isSvg ? (name==='className' ? 'class' : name) : (name==='class' ? 'className' : name);
	if (name==='style') {
		// 修改樣式...
	}
	// Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
	else if (name[0]==='o' && name[1]==='n') {
		// 處理onClick等事件
		let useCapture = name !== (name=name.replace(/Capture$/, ''));
		let nameLower = name.toLowerCase();
		name = (nameLower in dom ? nameLower : name).slice(2);

		// 註冊事件函數
		if (value) {
			if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
			(dom._listeners || (dom._listeners = {}))[name] = value;
		}
		else {
			dom.removeEventListener(name, eventProxy, useCapture);
		}
	}
	else if (name!=='list' && name!=='tagName' && !isSvg && (name in dom)) {
		// ...特殊處理select和options
	}
	else if (typeof value!=='function' && name!=='dangerouslySetInnerHTML') {
		// 調用setAttribute和removeAttribute...
	}
}
複製代碼

在組件更新時,若是相關的vnode上props屬性發生了變化,則會進入diffProps的操做,DOM節點的屬性就會隨之更新。

小結

本文從一段簡易的demo代碼觸發,分析了preact幾段比較核心的代碼,包括

  • Component組件系統,包括組件的初始化,生命週期以及render函數的調用時機
  • createElement方法,以及vnode的做用,可見在diff操做中,基本的思路是比較新舊兩個vnode
  • setState方法調用時,將組件放在隊列中並調用forceUpdate來觸發頁面的更新
  • diff操做的流程,包括diffdiffChildrendiffElementNodesdiffProps幾個方法,瞭解頁面是如何從vnode組成的AST轉換成一顆真實的DOM樹,能夠看見diff過程當中對於vnode._dom的複用
  • 分析了props系統以及事件註冊,preact的事件是註冊在對應的單個DOM節點上的,貌似存在事件委託的邏輯

總體來講,Preact仍是比較簡單的,經過閱讀源碼,咱們能夠大體瞭解React的實現原理,接下來能夠去了解一下preact-routerpreact-redux,這樣對於學習React來講應該是有必定幫助的。最後,就應該去試試閱讀React的源碼了,畢竟只是當一個API使用者是遠遠不夠的。

相關文章
相關標籤/搜索