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 
{state.val}
; } } render(, 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。当然还有很多的细节我也没看懂,所以没有讲的很细致。

你可能感兴趣的:(源码解读)