preact管中窺豹

版本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。當然還有很多的細節我也沒看懂,所以沒有講的很細緻。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章