前言

  • @ToPeasIssue来描述下 preact 的流程。
  • 这里决定使用我自己改过的zreact的 flow 分支,这个分支的代码不会再变。

代码例子

这次用个简单的例子, 这里为了简化就不使用 jsx 转换了,手写 jsx。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { h, Component, render } from "zreact";
class App extends Component {
    constructor(props, context) {
        super(props, context);
        this.state = {
            num: 0
        };
    }
    click() {
        this.setState({ num: this.state.num + 1 });
    }
    render(props, state) {
        return h("div", { onClick: this.click.bind(this) }, state.num);
    }
}
render(h(App), document.body);

例子代码分解

  • App 类没什么好说的。
  • render(h(App), document.body),这里的h(App)作为参数会先执行得到
1
2
3
4
5
6
VNode:{
    nodeName: App,
    children: [],
    attributes: undefined,
    key: undefined
}
  • 最后render接受到两个参数,一个上面的VNode实例,一个要挂载的父节点。

render 执行流程

1
2
3
4
5
const child = {};
function render(vnode, parent, merge, domChild) {
    const pchild = domChild || child;
    return diff(merge, vnode, {}, false, parent, false, pchild);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function diff(
    dom: Element | undefined,
    vnode: VNode | void,
    context: IKeyValue,
    mountAll: boolean,
    parent: any,
    componentRoot: boolean,
    child: any
) {
    // 因为是第一次调用设置isSvgMode,hydrating
    const ret = idiff(dom, vnode, context, mountAll, componentRoot, child);
    // 生成dom后的操作
    return ret;
}
  • idiff通过分支判断出vnode.vnodeName是一个类,调用buildComponentFromVNode diff.ts#L158
1
2
3
4
if (typeof vnodeName === "function") {
    // 是一个组件,创建或复用组件实例,返回dom
    return buildComponentFromVNode(dom, vnode, context, mountAll, child);
}
1
2
3
4
// 通过缓存组件的方式创建组件实例
c = createComponent(vnode.nodeName, props, context);
// 设置props,并同步执行render
setComponentProps(c, props, SYNC_RENDER, context, mountALL);
  • setComponentProps设置一堆属性,触发生命周期componentWillMount component.ts#L84
1
2
// 同步执行render
renderComponent(component, SYNC_RENDER, mountAll);
  • renderComponent判断 render 返回的为 html 元素,递归调用diff,递归层数 2 component.ts#L237
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 当前组件的render函数返回的VNode
const rendered: VNode | void = component.render(props, state, context);
// rendered {
//     vnodeName: "div",
//     children: [0],
//     attributes: {onClick: fun},
//     key: undefined
// }
// ...
// 渲染原生组件
base = diff(
    // 原dom
    cbase,
    // VNode
    rendered,
    context,
    // 父级或者该原生组件,原dom不存在说明必须触发生命周期
    mountALL || !isUpdate,
    // 把组件挂载到缓存dom的父级
    initialBase && initialBase.parentNode,
    // 以原生组件这里执行说明是自定义组件的第一个原生组件
    true,
    // dom上下文
    component.child
);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (!dom || !isNamedNode(dom, vnodeName)) {
    // 没有原dom或者原dom与vnode里的不同,新建一个
    out = createNode(vnodeName, isSvgMode);
    // ...
}
// ...
const vchildren = vnode.children;
else if (vchildren && vchildren.length || fc != null) {
    if (!child.children) {
        child.children = [];
    }
    // vnode子元素需要渲染或者为空但dom子元素需要清空
    diffChildren(
        out,
        vchildren,
        context,
        mountAll,
        hydrating || props.dangerouslySetInnerHTML != null,
        child,
    );
}
  • diffChildren只有一个子节点需要渲染,为 1 调用idiff渲染 diff.ts#L360
1
2
// vchild=1
child = idiff(child && child.base, vchild, context, mountAll, false, tchild);
  • idiff创建 Text
1
2
out = document.createTextNode(data);
return out;
  • 回到diffChildren挂载到父 dom 上结束这个方法回到idiff diff.ts#L367
1
dom.appendChild(child);
  • idiff创建完 dom 与子 dom 后,通过diffAttributes设置事件和原生属性,然后返回到diff diff.ts#L233 diff.ts#L238
1
2
3
4
// 设置dom属性
diffAttributes(out, vnode.attributes, props, child);

return out;
  • diff因为是递归层数 2 且不需要挂载,直接返回到renderComponent diff.ts#L96
1
return ret;
  • renderComponent把 dom 挂载到 Component,一大堆操作后继续返回到setComponentProps component.ts#L287
1
componentRef.base = base;
1
2
3
// 获取dom
dom = c.base;
return dom;
1
2
parent.appendChild(ret);
return ret;
  • render返回到用户代码,到此结束。

render 流程图

render-flow

setState 流程