看下虚拟 dom 的结构,其中,nodeName === HelloDOM 时,那它的类型就是一个 function,那我们就来看下这个 buildComponentFromVNode 方法的具体实现吧!
buildComponentFromVNode
/**
*
* @param {*} dom 旧真实dom
* @param {*} vnode 虚拟dom
* @param {*} context 上下文
* @param {*} mountAll
*/
export function buildComponentFromVNode(dom, vnode, context, mountAll) {
// 取得附上真实DOM上的组件实例,注意:dom._component 属性缓存的是这个真实dom是由哪个虚拟dom渲染的。
// 变量c就是原真实dom由哪个虚拟dom渲染的
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
// 判断原dom节点对应的组件类型与虚拟dom的元素类型是否相同
isDirectOwner = c && dom._componentConstructor === vnode.nodeName,
isOwner = isDirectOwner,
// 获取虚拟dom节点的属性值
props = getNodeProps(vnode);
/**
* while 循环 c 这个组件,首先将c上的 _parentComponent 属性赋值给c,条件不达,就再次赋值,一直往上找
* _parentComponent属性缓存的就是该组件父类组件实例
* {
* _parentComponent: {
* _parentComponent: {
* _parentComponent: {
* _parentComponent: {}
* }
* }
* }
* }
*/
while (c && !isOwner && (c = c._parentComponent)) {
// 如果组件类型变了,一直向上遍历;看类型是否相同,直到找到与之同类型的组件实例,让isOwner为true为止
isOwner = c.constructor === vnode.nodeName;
}
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
dom = c.base;
} else {
// 当虚拟dom和原dom节点的元素类型不一致
if (originalComponent && !isDirectOwner) {
// 移除旧的dom元素
unmountComponent(originalComponent);
dom = oldDom = null;
}
/**
* 创建新的组件实例(typeof vnode.nodeName === function)
* 这个组件实例结构:
* {
* _dirty: true,
context: {},
props: {},
state: {},
_renderCallbacks: [],
constructor: [Function: Ctor],
render: [Function: doRender] }
nextBase?: dom
* }
*/
c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
// 对这个实例对象的 nextBase 重新赋值,将原dom赋值给这个属性
// 这个属性就是为了能基于此DOM元素进行渲染,从缓存中读取
c.nextBase = dom;
// passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
// 将 oldDom 销毁
oldDom = null;
}
// 为这个刚生成的组件实例对象 c 添加 props 属性
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
// 将这个组件实例对象的缓存 base 替换 dom 这个值
dom = c.base;
// 如果oldDom 和 dom 不是同一个,则对 oldDom 进行销毁和回收
if (oldDom && dom !== oldDom) {
oldDom._component = null;
recollectNodeTree(oldDom, false);
}
}
return dom;
}
buildComponentFromVNode 这个方法的实际含义就是将一个虚拟 dom 创建成一个真实的 dom,上面代码的注释已经很清楚了,流程下来就能理清楚这个方法的意思。
let c = dom && dom._component 首先从这个原真实 dom 的缓存中获取原组件实例信息。
isDirectOwner = c && dom._componentConstructor === vnode.nodeName ,这个变量判断原 dom 节点对应的组件类型与虚拟 dom 的元素类型是否相同。
理解这个循环之前,要注意注意 typeof vnode.nodeName === 'function',那么 vnode.nodeName 即是一个 function ,也是一个 HOC :
HOC => component => DOM;
HOC 返回一个组件 component ,然后 在通过 component 渲染成真实 dom,dom._component 指向的是渲染该 dom 的原组件实例,如果,这个组件的实例的构造器指向 vnode.nodeName ,那说明原 dom 节点的类型与虚拟 dom 是一致的,如果没有指向的情况:
component 组件
在
diff
代码中有一段:什么情况下才会触发这个分支呢?
babel 转义后:
可以看下
babel
转义后的代码,HelloDOM
,直接被当作参数传入到h
函数中,对应的虚拟 dom 就是:看下虚拟 dom 的结构,其中,
nodeName === HelloDOM
时,那它的类型就是一个function
,那我们就来看下这个buildComponentFromVNode
方法的具体实现吧!buildComponentFromVNode
buildComponentFromVNode
这个方法的实际含义就是将一个虚拟 dom 创建成一个真实的dom
,上面代码的注释已经很清楚了,流程下来就能理清楚这个方法的意思。let c = dom && dom._component
首先从这个原真实 dom 的缓存中获取原组件实例信息。isDirectOwner = c && dom._componentConstructor === vnode.nodeName
,这个变量判断原 dom 节点对应的组件类型与虚拟 dom 的元素类型是否相同。接着是一段循环:
如果原组件存在,
isOwner
和现在的虚拟 dom 的nodeName
不一致,则获取这个原组件的缓存上的_parentComponent
属性,这是缓存的父组件数据。循环体是对isOwner
重新赋值,依据是 原组件的父组件的构造器指向vnode.nodeName
一致时,isOwner
就为ture
,跳出循环,可以看出,这是一个寻找vnode.nodeName
实例的故事。理解这个循环之前,要注意注意
typeof vnode.nodeName === 'function'
,那么vnode.nodeName
即是一个function
,也是一个HOC
:HOC
返回一个组件component
,然后 在通过component
渲染成真实dom
,dom._component
指向的是渲染该 dom 的原组件实例,如果,这个组件的实例的构造器指向vnode.nodeName
,那说明原dom
节点的类型与虚拟 dom 是一致的,如果没有指向的情况:上面的方程不成立时,那就有出现了这个循环的必要了,在这个原组件属性
_parentComponent
上,寻找vnode.nodeName
的实例。vnode.nodeName
实例是否存在的条件分支:vnode.nodeName
实例存在那就直接给这个实例添加
props
属性,调用setComponentProps
方法,最后生成的这个 dom 就是这个实例c
上缓存的属性base
值。这是原 dom 的类型和虚拟 dom 的类型没有变化的情况。vnode.nodeName
实例不存在,说明修改过或者不存在如果原 dom 实例对象存在且组件实例的类型与虚拟 dom 不一致,则调用
unmountComponent
移除旧的 dom 并回收这个组件。创建了一个新的组件实例,如果旧 dom 存在,则赋值
c.nextBase = dom
,这个属性就是为了能基于此 DOM 元素进行渲染,从缓存中读取。为之前生成的组件实例添加属性,并执行
renderComponent
方法渲染组件。dom = c.base
给 dom 赋值,组件实例c
的属性base
值,这个值会在renderComponent
方法里设置,后面会讲,这个属性值就是为了缓存这个组件的真实 dom。createComponent
这个
createComponent
方法接收三个参数:vnode.nodeName
,是一个函数(PFC 无状态组件)或者类什么情况下
Ctor
的原型上存在render
方法呢?如上,继承
Preact.Component
这个父类,那这个 app 类的实例就存在了render
方法。定义
inst
为Ctor
组件实例,传入参数props
,context
,inst
实例对象是存在render
方法的,但是不一定存在props
和context
的属性, 如果没有给父级构造函数 super 传入props
和context
,那么 inst 中的props
和context
的属性为 undefined,通过强制调用Component.call(inst, props, context)
可以给inst
中props
、context
进行初始化赋值。如果
Ctor
不存在render
方法,那就要给他添加render
方法,怎么处理呢?首先初始化一个空组件实例对象
inst = new Component(props, context)
,这个实例对象虽然有render
方法,但是和我们传入的无状态组件没有任何联系,处理方法是执行这个语句:inst.constructor = Ctor
。如果没有这句话,那inst.constructor
指向的是Component
这个类,对它重新赋值,就是改变了这个实例对象inst
构造器的值指向了无状态组件Ctor
,即,一个高阶函数。 最后重新实现render
方法:这个
this
指向实例对象inst
,执行this.constructor
方法,其实就是执行Ctor
这个函数方法,这个方法传入两个参数props
和context
, 这个方法返回的就是一个虚拟 dom,到此为止,也解释了无状态组件如何生成组件实例,也同时拥有render
方法。组件实例
inst
已经生成了,接着从组件回收池recyclerComponents
中是否存在这个组件实例:recyclerComponents
这个队列中存放的都是一些回收的组件实例,其实就是上面生成的inst
。上面的if
语句就是判断回收池中有没有组件Ctor
的实例。 如果存在这样的实例:给组件实例对象
inst
的nextBase
(nextBase 属性记录的是该组件之前渲染的真实 dom)属性赋值,从回收池中的实例对象中取,然后移除回收池中的元素。setComponentProps
生成了组件实例之后,就考虑给这个实例添加传入的
props
属性了。setComponentProps
方法接收 5 个参数:inst
对象;component._disable
属性是判断组件是否可用,状态锁,在设置组件属性之前,设置为不可用,组件设置之后,再将这个状态锁设置为可用。执行生命周期componentWillMount
这个钩子函数。每个组件实例都有四个状态属性:
context
属性状态context
属性状态props
属性状态props
属性状态判断每个组件是否有前一个属性状态字段,有则更新为新的对应的属性值,没有则追加字段,设置的值就是传入的属性值。
属性值已经更新完毕了,接下来就是将这个组件渲染出来了:
渲染模式
renderMode
有四种:NO_RENDER
(不渲染),SYNC_RENDER
(同步渲染),ASYNC_RENDER
(异步渲染),FORCE_RENDER
(强制渲染)。上面代码中判断了
renderMode
是否是SYNC_RENDER
,如果是同步渲染,则直接调用了renderComponent
方法,如果是异步渲染,则会调用enqueueRender
这个方法。首先来看下
enqueueRender
是如何实现异步渲染的:异步渲染其实就是延时渲染,在代码内部,做了一个缓冲区
items
,它是一个数组,其中就是一个个待渲染的组件队列。只要调用了enqueueRender
这个方法的组件实例,都会将这个组件实例push
到这个队列中进行排队,然后逐一取出,依次调用renderComponent
方法。在将组件
push
到队列之前,系统做了依次判断,就是判断component._dirty
这个组件实例对象属性_dirty
是否为false
,不成立则立即将这个属性赋值为true
,这样做就是为了防止一个组件多次render
而设置的一把锁,这种写法还是非常值得学习的。renderComponent
unmountComponent
移除并回收组件,
preact
之所以快速,就是因为大量使用了缓存空间,这个方法就缓存了移除组件的实例对象,这是一个双刃剑,如果组件特别多特别大的情况下,内存消耗将是一个很大的问题。在上文
createComponent
方法中就使用了recyclerComponents
这个数组内容,它缓存了销毁的组件实例,那为什么要缓存它呢,因为在这个组件实例对象上,追加了很多缓存属性:这个组件在 preact 中会有被处理为以下的数据结构: