export function buildComponentFromVNode(dom, vnode, context, mountAll) {
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
isDirectOwner = c && dom._componentConstructor===vnode.nodeName,
isOwner = isDirectOwner,
// props就是vnode的attribute/children/nodename.defaultProps
// 传递最新鲜的porps,常见的子组件更新,都是依赖于props变化
props = getNodeProps(vnode);
while (c && !isOwner && (c=c._parentComponent)) {
isOwner = c.constructor===vnode.nodeName;
}
// 如果vnode 和 dom 是由同类的组件生成则直接 setComponentProps,当然还需要!mountAll || c._component 成立。
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
dom = c.base;
}
else {
// dom由组件生成,而vnode和生成dom的组件实例不是同一构造器生成的。则说明要卸载当前组件,替换上新的。
if (originalComponent && !isDirectOwner) {
unmountComponent(originalComponent);
dom = oldDom = null;
}
// 根据nodeName生成新的组件 c。
c = createComponent(vnode.nodeName, props, context);
// 如果该类组件没有卸载过,而存在dom来diff,则将c.nextBase指向dom,后面做diff用
if (dom && !c.nextBase) {
c.nextBase = dom;
oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
if (oldDom && dom!==oldDom) {
oldDom._component = null;
recollectNodeTree(oldDom, false);
}
}
return dom;
}
buildComponentFromVNode 有几个概念是要分清楚的,如果 dom 是由一个组件生成渲染的,则 dom._component 是指向渲染出 dom 的组件实例。而生成的组件实例的 base 属性又会指向 dom 节点。初次渲染时候会直接是进入下面的 else 语句。对于不同的组件则先卸载之前的组件,让后生成新的组件 c,nextBase 指的是卸载的同类组件的 base 属性,也就是上个该类组件生成的 dom 节点。为什么要这样做呢?答案是提高效率。假设组件替换是这样的 A -> B ->A,在 A 组件卸载的时候,就会将 A 生成的 dom 节点缓存下来,当 B 组件卸载,A 组件再次渲染的时候,这个时候就会用上之前 A 组件生成的 dom 节点,与这次 A 组件渲染出的做 diff对比,这样看是不是很高效?可以看看 createComponnet:
export function createComponent(Ctor, props, context) {
let list = components[Ctor.name],
inst;
if (Ctor.prototype && Ctor.prototype.render) {
inst = new Ctor(props, context);
Component.call(inst, props, context);
}
else {
inst = new Component(props, context);
inst.constructor = Ctor;
inst.render = doRender;
}
if (list) {
for (let i=list.length; i--; ) {
if (list[i].constructor===Ctor) {
inst.nextBase = list[i].nextBase;
list.splice(i, 1);
break;
}
}
}
return inst;
}
export function unmountComponent(component) {
if (options.beforeUnmount) options.beforeUnmount(component);
let base = component.base;
component._disable = true;
if (component.componentWillUnmount) component.componentWillUnmount();
component.base = null;
let inner = component._component;
if (inner) {
unmountComponent(inner);
}
else if (base) {
if (base[ATTR_KEY] && base[ATTR_KEY].ref) base[ATTR_KEY].ref(null);
component.nextBase = base;
removeNode(base);
collectComponent(component);
removeChildren(base);
}
if (component.__ref) component.__ref(null);
}
前言
前面介绍到 diff 方法,但是我们只是从简单的例子开始的,并没有用到组件,而组件才是最重要的部分,毕竟一切的一切可以是组件。
组件 Component
先看看 Preact 输出的 Component 长什么样子:
平时使用组件的时候,大致都是这样
class Clockwarp extends Component
通过extends
来实现继承Component
,有点prototype
的意思。在Component
类里面有state/props/setState/render
,其中setState
方法先判断state
是不是函数,也就是这种写法:this.setSate((preState, props) => {})
这个时候才会会执行state
方法,如果有回调,会被 push 到_renderCallbacks
里面。在看看enqueueRender
:enqueueRender
方法是为了延迟当前的组件的再渲染,采用的是 Promise方法,如果没有就用setTimeout
代替,当然Promise.resolve()
之后调用的then
实现上是要优先于setTimeout
的。组件 diff 机制
上面代码可以看到
setState/forceUpdate
最后都会调用renderComponent
方法,看名字就知道是渲染组件的意思,但是在介绍renderComponent
之前,先看看上篇博客里面介绍的,diff
过程里面,对于组件的处理:buildComponentFromVNode
有几个概念是要分清楚的,如果dom
是由一个组件生成渲染的,则dom._component
是指向渲染出dom
的组件实例。而生成的组件实例的base
属性又会指向dom
节点。初次渲染时候会直接是进入下面的else
语句。对于不同的组件则先卸载之前的组件,让后生成新的组件c
,nextBase
指的是卸载的同类组件的base
属性,也就是上个该类组件生成的 dom 节点。为什么要这样做呢?答案是提高效率。假设组件替换是这样的A -> B ->A
,在 A 组件卸载的时候,就会将 A 生成的 dom 节点缓存下来,当 B 组件卸载,A 组件再次渲染的时候,这个时候就会用上之前 A 组件生成的 dom 节点,与这次 A 组件渲染出的做 diff对比,这样看是不是很高效?可以看看createComponnet
:这里的
components
是缓存的卸载的组件集合。通过简单的判定,将生成的新组件的nextBase
指向卸载的同类组件的nextBase
,其实也是后者 'base'了。在创建组件之后是
setComponentProps
:setComponentProps
里面现实执行组件的componentWillMount/componentWillReceiveProps
方法。将props
传给组件的props
。接着是进行renderComponent
方法,这个时候传参已经是component, opts, mountAll, isChild
,没有vnode
了。renderComponent
方法在setState
也有提到,是更新组件的最重要的步骤,而renderComponent
关键点就是会修改组件的base
也就是 dom,接下来看看renderComponent
方法实现:renderComponent
函数比较长。首先是第一个 if 语句里面,判断是否是isUpdate
,判断依据是有无component.base
,初次加载组件的时候,组件本身是没有base
属性,最多才有 'nextBase' 属性,所以isUpdate
用来区分是否是更新组件,如果是的话,执行组件的shouldComponentUpdate
与componentWillUpdate
方法,并判断是否执行后面一长串的渲染。rendered = component.render(props, state, context)
先是得出要渲染的 VNode,如果rendered
还是组件,则还要进行判断是否前后两次渲染子组件不一样或则是初次渲染。如果不是组件则主要是执行diff
函数,生成新的 dom 节点base
。 最后由于自组件变更或则消失,则卸载子组件。将生成的base
dom 节点指向组件的base
属性,而 dom 节点还要新增_component/_componentConstructor
属性。最后如果是初次加载则将组件放入初次加载组件的队列里面,准备执行componentDidUpdate afterUpdate
。底部的flushMounts
则是,当是最顶部的一次 diff 递归进入尾声了,就执行options.afterMount
和所有初次加载组件的componentDidMount
方法。另外还有component._renderCallbacks
,在组件状态变化,也就是在用setState
的时候,如果存在第二个参数callback
, 则会在这个时候执行callback
。回收机制
之前最早遇到回收问题是出现在 diff 函数上面,经常可以看到
recollectNodeTree(dom, true)
这句话,看看recollectNodeTree
方法:这里可以看到,对于非组件,则可以要先执行节点的 ref,这个 ref是什么?在组件
setComponentProps
最后一行代码还有个component.__ref
。其实这两个都是一样的,都是节点attribute
里面的ref
属性,也就是说组件初次加载的时候,或则回收组件/Dom 的时候都会执行,如果不仅仅是卸载还会在父节点上移除node
。从而实现回收。最后的removeChildren
只是遍历用的。基本上只要涉及到老节点的回收都会用到recollectNodeTree
里面。在innerDiffNode
最后也会对没有用的节点回收。除了节点 diff 的问题外,还有组件卸载的时候,也会调用removeChildren
方法:上面可以看出卸载组件的时候,会调用
componentWillUnmount
方法,接着执行 ref ,将卸载的组件生成的节点转移到nextBase
上面,再执行removeChildren
。最后还有一个贯穿所有 diff 机制的传参
mountAll
,具体作用就算是组件更新,也将其作为初次加载,就是能执行componentWillMount 与 componentDidMount
方法。总结
这次的 Preact 之旅就到这里,代码虽少,但是还是活灵活现的展示了 diff 功能。源码里面有着无数行空行,以及代码解释,然而整体大小才 1000 行多点,min 之后更是只有 10kb 大小,相当袖珍。大家有机会还是去接触一下。