版本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。当然还有很多的细节我也没看懂,所以没有讲的很细致。