前言上一期我们了解到了Preact渲染普通的节点(VNode的type不是Function类型)时的过程。本期我带大家来了解下当渲染的是一个组件时preact中发生了什么。当然为了降低阅读源码的复杂度, 我们本次只讨论初次渲染组件的情况。暂不考虑setState时组件更新的情况。我们将从官方的Demo入手, 一步步了解渲染的过程import { h, render, Component } from ‘preact’;class Clock extends Component { render() { let time = new Date().toLocaleTimeString(); return <span>{ time }</span>; }}// 将一个时钟渲染到 <body > 标签:render(<Clock />, document.body);src/render.js同渲染普通类型的VNode节点一样, 将VNode包裹一层Fragment后, VNode进入了diffChildren方法中。唯一值得注意的时VNode的type为Clock, 而非普通的字符串类型。我们将在diff方法看到两者的具体的区别。export function render(vnode, parentDom) { // render时_prevVNode还没没有挂载, 此时为null let oldVNode = parentDom._prevVNode; // 使用Fragment包裹VNode // { // type: ‘Fragment’, // props: { // children: [ // { // type: Clock, 这里类型不是字符串 // props: { // } // } // ] // } // } vnode = createElement(Fragment, null, [vnode]); let mounts = []; // 将当前的VNode挂载到parentDom的_prevVNode属性 diffChildren( parentDom, parentDom._prevVNode = vnode, oldVNode, EMPTY_OBJ, parentDom.ownerSVGElement!==undefined, oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), mounts, vnode ); // 执行已挂载组件的componentDidMount生命周期 commitRoot(mounts, vnode);}src/diff/children.js在渲染组件VNode时, 流程大致和渲染普通的VNode一致。都是将通过diff算法返回Dom节点append到parentDom中。export function diffChildren( parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent) { let childVNode, i, j, p, index, oldVNode, newDom, nextDom, sibDom, focus, childDom; // 将扁平的的VNode挂载到_children属性上 // [ { type: Clock, props: { … } } ] let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode); // oldChildren此时为[] let oldChildren = [] let oldChildrenLength = oldChildren.length; // … 省略一部分源码 // 遍历VNode节点 for (i=0; i<newChildren.length; i++) { childVNode = newChildren[i] = coerceToVNode(newChildren[i]); oldVNode = index = null; p = oldChildren[i]; // … 省略一部分源码,这里主要是查询可以复用的DOM节点, // 进入diff算法比较新旧VNode节点 newDom = diff( oldVNode==null ? null : oldVNode._dom, parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, null ); if (childVNode!=null && newDom !=null) { else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) { if (childDom==null || childDom.parentNode!==parentDom) { parentDom.appendChild(newDom); } } } }}src/diff/index.js在diff方法中, 主要会比较三种类型的节点。第一种Fragment类型, 第二种Function类型, 和其他类型的节点。我们的示例会用到两种判断,请仔细阅读我给源码添加的注释。我们在首次diff中, newVNode为Clock组件, 所以我们会进入VNode.type为Functiond的分支。我们接下来会调用组件实例的render函数, 返回VNode( { tpye: ‘span’, props: { //… } } )。接下来, 递归的使用diff算法比较render返回的VNode。我们在递归的时, diff函数将进入其他类型的节点的分支,比较返回的VNode, 并且会调用diffElementNodes函数, 返回创建后dom节点。最后并添加到parentDom中。// 这里每一个参数的含义请参考, 上一期的文章export function diff( dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force) { // 因为是初始化渲染, dom是不能复用, 我们删除整个子树, oldVNode重置为null if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) { dom = null; oldVNode = EMPTY_OBJ; } let c, p, isNew = false, oldProps, oldState, oldContext // 组件的类型(组件的类) let newType = newVNode.type; try { outer: if (oldVNode.type===Fragment || newType===Fragment) { // … 省略一部分源码, 这里是对Fragment类型的处理 } else if (typeof newType===‘function’) { if (oldVNode._component) { // … 省略一部分源码,如果是,不是第一次渲染组件的处理 } else { // 第一次渲染的组件处理 isNew = true; // 如果组件的类拥有render函数, c和_component存储的是组件的实例 if (newType.prototype && newType.prototype.render) { newVNode._component = c = new newType(newVNode.props, cctx); } else { // 如果组件的类没有render函数, 使用基础的Component类构建组件的实例 newVNode._component = c = new Component(newVNode.props, cctx); // 实例c的构造函数等于组件类的构造函数 c.constructor = newType; // render函数直接返回构造函数返回的结果 c.render = doRender; } // _ancestorComponent挂载的是自己父VNode, 目前指的是被Fragment包裹的那一层组件 c._ancestorComponent = ancestorComponent; // 初始化c的props和state c.props = newVNode.props; if (!c.state) c.state = {}; // _dirty属性表明是否更新组件 c._dirty = true; // 用于存储setState回调的数组 c._renderCallbacks = []; } // _vnode挂载组件实例的VNode节点 c._vnode = newVNode; // s变量存储了组件的state状态 let s = c._nextState || c.state; // 调用静态的生命周期方法getDerivedStateFromProps // getDerivedStateFromProps生命周期返回的对象用于更新state if (newType.getDerivedStateFromProps!=null) { oldState = assign({}, c.state); if (s === c.state) { s = assign({}, s); } // 更新state, 如果返回null, 不更新 assign(s, newType.getDerivedStateFromProps(newVNode.props, s)); } if (isNew) { // 如果是第一次渲染, 并且componentWillMount不为null, 执行componentWillMount的生命周期函数 if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) { c.componentWillMount(); } // 将组件的实例push到已经挂载的组件的列表中 if (c.componentDidMount!=null) { mounts.push(c); } } else { // … 省略一部分源码,不是第一次渲染组件的处理 } // 渲染更新前的oldProps,oldState oldProps = c.props; if (!oldState) { oldState = c.state; } // 更新组件的props和state c.props = newVNode.props; c.state = s; // 初次渲染prev为null let prev = c._prevVNode; // 组件实例上挂载的_prevVNode的属性为当前实例调用render函数返回的VNode节点(之前c存储的是组件的实例) let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context)); // 这个组件禁止更新 c._dirty = false; // 调用getSnapshotBeforeUpdate的生命周期函数 // getSnapshotBeforeUpdate()在最新的渲染输出提交给DOM前将会立即调用 if (!isNew && c.getSnapshotBeforeUpdate!=null) { oldContext = c.getSnapshotBeforeUpdate(oldProps, oldState); } // vnode现在存储的是组件实例render返回的VNode // 将render返回VNode,递归的使用diff方法继续比较,返回的dom存储base属性中 c.base = dom = diff( dom, parentDom, vnode, prev, context, isSvg, excessDomChildren, mounts, c, null ); c._parentDom = parentDom; } else { // 当上面的c组件render后, 将render返回VNode节点,使用diff进行递归的比较时 // render返回VNode的type是string类型, 所以进入这个分支中, 进行dom比较, 具体内容可以参考上一篇文章 dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent); } // 将dom挂载到_dom中 newVNode._dom = dom; } catch (e) {} // 返回dom return dom;}结语我们通过上面????精简后的源码可知,如果render中是VNode的组件时渲染的大致流程如下图。