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,首先通過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函數會根據子組件的不一樣類型進行封裝,具體以下
最後賦值給child變量並存進childdren數組中,再封裝成下面的vnode結構並返回
{
nodeName:"div",//標籤名
children:[],//子組件組成的數組,每一項也是一個vnode
key:"",//key
attributes:{}//jsx的屬性
}
複製代碼
// 一個簡單的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主要進行了如下操做
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的過程當中最重要的函數就是createComponent
和setComponentProps
。咱們能夠發現,在前後執行了createComponent
和setComponentProps
後,真實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仍是子組件的話。則再次調用setComponentProps
、renderComponent
去進一步生成真實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(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函數參數的定義和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的標籤類型
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
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操做後,把keyed
和children
中剩餘的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完成了