版本10.3.1
本文不多描述已經說得很多了的一些東西,比如setState什麼的,在preact裏面很容易找到相關代碼(在component.js裏),實現也不是特別複雜,這裏主要看看一個組件如何執行第一次渲染,以及preact的diff在其中扮演的角色
先看例子
import { h, render, Component } from "preact";
class App extends Component {
constructor(props) {
super(props);
}
state = {
val: 123
};
render(props, state) {
return <div>{state.val}</div>;
}
}
render(<App />, document.getElementById('root'));
這一段代碼經過編譯後是這樣
const preact = __webpack_require__("./node_modules/preact/dist/preact.module.js");
var App = function (_Component) {
// 省略一些代碼
_createClass(App, [{
key: "render",
value: function render(props, state) {
// 這裏下面會提到
return preact["h"]("div", null, state.val);
}
}]);
}(preact["Component"]);
// 入口是這裏
preact["render"](preact["h"](App, null), document.getElementById('root'));
顯然入口是render函數,而render的參數是preact的h函數創建的組件和掛載的dom節點,h函數是createElement的別名,render裏比較重要的是調用了createElement,diff和commitRoot函數,我們先來看createElement
createElement在create-element.js文件中,它只是處理了下參數然後調用了createVNode,createVNode構造了一個vnode對象,這個是vnode初始化時候的樣子,diff就拿這個vnode來使用
const vnode = {
type,
props,
key,
ref,
_children: null,
_parent: null,
_depth: 0,
_dom: null,
_nextDom: undefined,
_component: null,
constructor: undefined
}
diff可以分三部分來看,diff函數(diff/index.js),diffChildren函數(diff/children.js)和diffElementNodes函數(diff/index.js),diff函數是render中調用的,setState也調用它,所以有些邏輯揉在一起不太好懂。
我們先來看diff函數,首先區分當前處理的是自定義組件還是dom元素,如果是dom元素,執行diffElementNodes,這裏可以參考編譯出來的代碼的結果,觀察h函數的第一個參數,dom元素的type和組件是不同的
outer: if (typeof newType === 'function') {
// 省略很多代碼
} else {
newVNode._dom = diffElementNodes(
oldVNode._dom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
commitQueue,
isHydrating
);
}
如果是自定義組件,分兩種情況,一種是Fragment包裹的,一種不是,render函數進來的diff其實就是經過Fragment包裹的
// diff不光要給render用,還要給setState用,這裏這個_component就是用來標識當前是否是第一次創建
if (oldVNode._component) {
// 省略一些代碼
} else {
// 滿足下面這個if條件的是沒有Fragment包裹的組件,直接通過構造函數newType創建一個實例
if ('prototype' in newType && newType.prototype.render) {
newVNode._component = c = new newType(newProps, cctx);
} else {
// Fragment包裹的組件用Component實例化
newVNode._component = c = new Component(newProps, cctx);
c.constructor = newType;
// render也不一樣
c.render = doRender;
}
// 省略一些代碼
// isNew用來區分組件第一次渲染和setState時執行的不同的生命週期
isNew = c._dirty = true;
// 省略一些代碼
// 調用diffChildren
diffChildren(
parentDom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating
);
}
diffChildren的主要邏輯在toChildArray函數,它在遍歷子節點的過程中執行傳入的回調函數參數,這個回調函數定義在diffChildren內部,並且其中調用了diff函數,相當於對每個子節點遞歸調用了diff
let i = 0;
newParentVNode._children = toChildArray(
newParentVNode._children,
// childVNode是每次遍歷的子節點的vnode
childVNode => {
if (childVNode != null) {
childVNode._parent = newParentVNode;
// _depth用於標識節點深度,在setState中會用到,根據_depth排序後進行更新操作
childVNode._depth = newParentVNode._depth + 1;
// 省略新舊vnode對比代碼,在第一次渲染時不會進行對比,因爲沒有舊的
// 調用diff,相當於遞歸diff了子節點
newDom = diff(
parentDom,
childVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating
);
// 省略很多還沒看懂的代碼🤣
}
i++;
return childVNode;
}
);
diffElementNodes中主要邏輯是調用了diffProps和diffChildren(因爲dom元素內還是可以繼續嵌套dom元素或者自定義組件的)。
diffProps比較容易懂,先遍歷老的props,如果新的porps裏面沒有,那麼就添加到新的props裏面去,然後遍歷新的props,如果新的數據和老的數據不一致,用新的。這裏設置數據用到一個setProperty函數,比較長,處理了className、style、on開頭的屬性(其實就是事件)等等。preact沒有合成事件,就是直接addEventListener。在真實dom上進行的操作都可以通過搜索dom.和parentDom.來搜索到
commitRoot函數,只是把整個diff過程中保存到_renderCallbacks裏的生命週期執行了一下
總結
preact的初次渲染過程可以抽象爲,createElement元素,render,render中調用了diff,diff遞歸了每一個子節點;調用setState也一樣,經過enqueueRender,defer(process),process最終調用了diff。當然還有很多的細節我也沒看懂,所以沒有講的很細緻。