首先欢迎你们关注个人掘金帐号和Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。
以前分享过几篇关于React的文章:javascript
其实我在阅读React源码的时候,真的很是痛苦。React的代码及其复杂、庞大,阅读起来挑战很是大,可是这却又挡不住咱们的React的原理的好奇。前段时间有人就安利过Preact,千行代码就基本实现了React的绝大部分功能,相比于React动辄几万行的代码,Preact显得别样的简洁,这也就为了咱们学习React开辟了另外一条路。本系列文章将重点分析相似于React的这类框架是如何实现的,欢迎你们关注和讨论。若有不许确的地方,欢迎你们指正。
在前两篇文章java
咱们分别了解了Preact中元素建立以及diff
算法,其中就讲到了组件相关一部份内容。对于一个类React库,组件(Component
)多是最须要着重分析的部分,由于编写类React程序的过程当中,咱们几乎都是在写一个个组件(Component
)并将其组合起来造成咱们所须要的应用。下面咱们就从头开始了解一下Preact中的组件是怎么实现的。 node
首先咱们来了解组件返回的虚拟dom是怎么渲染为真实dom,来看一下Preact的组件是如何构造的: react
//component.js
function Component(props, context) {
this._dirty = true;
this.context = context;
this.props = props;
this.state = this.state || {};
}
extend(Component.prototype, {
setState(state, callback) {
//......
},
forceUpdate(callback) {
//......
},
render() {}
});复制代码
可能咱们会想固然地认为组件Component
的构造函数定义将会及其复杂,事实上偏偏相反,Preact的组件定义代码极少。组件的实例属性仅仅有四个: git
_dirty
: 用来表示存在脏数据(即数据与存在的对应渲染不一致),例如屡次在组件实例调用setState
,使得_dirty
为true
,但由于该属性的存在,只会使得组件仅有一次才会被放入更新队列。context
: 组件的context
属性props
: 组件的props
属性state
: 组件的state
属性 经过extends
方法(原理相似于ES6中的Object.assign
或者Underscore.js中的_.extends
),咱们给组件的构造函数的原型中建立一下几个方法:github
setState
: 与React的setState
相同,用来更新组件的state
forceUpdate
: 与React的forceUpdate
相同,马上同步从新渲染组件render
: 返回组件的渲染内容的虚拟dom,此处函数体为空 因此当咱们编写组件(Component
)类继承preact.Component
时,也就仅仅只能继承上述的方法和属性,这样因此对于用户而言,不只提供了及其简洁的API以供使用,并且最重要的是咱们将组件内部的逻辑封装起来,与用户相隔离,避免用户无心间修改了组件的内部实现,形成没必要要的错误。
对于阅读过从Preact了解一个类React的框架是怎么实现的(二): 元素diff的同窗应该还记的preact
所提供的render
函数调用了内部的diff
函数,而diff
实际会调用idiff
函数(更详细的能够阅读第二篇文章):
算法
从上面的图中能够看到,在idiff
函数内部中在开始若是vnode.nodeName
是函数(function
)类型,则会调用函数buildComponentFromVNode
: 数组
function buildComponentFromVNode(dom, vnode, context, mountAll) {
//block-1
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
isDirectOwner = c && dom._componentConstructor===vnode.nodeName,
isOwner = isDirectOwner,
props = getNodeProps(vnode);
//block-2
while (c && !isOwner && (c=c._parentComponent)) {
isOwner = c.constructor===vnode.nodeName;
}
//block-3
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
dom = c.base;
}
else {
//block-4
if (originalComponent && !isDirectOwner) {
unmountComponent(originalComponent);
dom = oldDom = null;
}
c = createComponent(vnode.nodeName, props, context);
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(VNode)转化成真实dom。参数分别是:浏览器
dom
: 组件对应的真实dom节点vnode
: 组件的虚拟dom节点context
: 组件的中的context
属性mountAll
: 表示组件的内容须要从新渲染而不是基于上一次渲染内容进行修改。为了方便分析,咱们将函数分解成几个部分,依次分析: 缓存
dom
是组件对应的真实dom节点(若是未渲染,则为undefined
),在dom
节点中的_component
属性是组件实例的缓存。isDirectOwner
用来指示用来标识原dom
节点对应的组件类型是否与当前虚拟dom的组件类型相同。而后使用函数getNodeProps
来获取虚拟dom节点的属性值。getNodeProps(vnode) {
let props = extend({}, vnode.attributes);
props.children = vnode.children;
let defaultProps = vnode.nodeName.defaultProps;
if (defaultProps!==undefined) {
for (let i in defaultProps) {
if (props[i]===undefined) {
props[i] = defaultProps[i];
}
}
}
return props;
}复制代码
函数getNodeProps
的逻辑并不复杂,将vnode
的attributes
和chidlren
的属性赋值到props
,而后若是存在组件中存在defaultProps
的话,将defaultProps
存在的属性而且对应props
不存在的属性赋值进入了props
中,并将props
返回。
dom
节点对应的组件类型与当前虚拟dom对应的组件类型不一致时,会向上在父组件中查找到与虚拟dom节点类型相同的组件实例(但也有可能不存在)。其实这个只是针对于高阶组件,假设有高阶组件的顺序:HOC => component => DOM元素复制代码
上面HOC
表明高阶组件,返回组件component
,而后组件component
渲染DOM元素。在Preact,这种高阶组件与返回的子组件之间存在属性标识,即HOC
的组件实例中的_component
指向compoent
的组件实例而组件component
实例的_parentComponent
属性指向HOC
实例。咱们知道,DOM中的属性_component
指向的是对应的组件实例,须要注意的是在上面的例子中DOM对应的_component
指向的是HOC
实例,而不是component
实例。若是理解了上面的部分,就能理解为何会存在这个循环了,其目的就是为了找到最开始渲染该DOM的高阶组件(防止某些状况下dom
对应的_component
属性指代的实例被修改),而后再判断该高阶组件是否与当前的vnode类型一致。
第三段代码: 若是存在当前虚拟dom对应的组件实例存在,则直接调用函数setComponentProps
,至关于基于组件的实例进行修改渲染,而后组件实例中的base
属性即为最新的dom
节点。
第四段代码: 咱们先不具体关心某个函数的具体实现细节,只关注代码逻辑。首先若是以前的dom节点对应存在组件,而且虚拟dom对应的组件类型与其不相同时,则卸载以前的组件(unmountComponent
)。接着咱们经过调用函数createComponent
建立当前虚拟dom对应的组件实例,而后调用函数setComponentProps
去建立组件实例的dom
节点,最后若是当前的dom
与以前的dom
元素不相同时,将以前的dom
回收(recollectNodeTree
函数在diff的文章中已经介绍)。
其实若是以前就阅读过Preact的diff算法的同窗来讲,其实整个组件大体渲染的流程咱们已经清楚了,可是若是想要更深层次的了解其中的细节咱们必须去深究函数createComponent
与setComponentProps
的内部细节。
关于函数createComponent
,咱们看一下component-recycler.js
文件:
import { Component } from '../component';
const components = {};
export function collectComponent(component) {
let name = component.constructor.name;
(components[name] || (components[name] = [])).push(component);
}
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;
}
function doRender(props, state, context) {
return this.constructor(props, context);
}复制代码
变量components
的主要做用就是为了能重用组件渲染的内容而设置的共享池(Share Pool),经过函数collectComponent
就能够实现回收一个组件以供之后重复利用。在函数collectComponent
中经过组件名(component.constructor.name
)分类将可重用的组件缓存在缓存池中。
函数createComponent
主要做用就是建立组件实例。参数props
与context
分别对应的是组件的中属性和context
(与React一致),而Ctor
组件则是须要建立的组件类型(函数或者是类)。咱们知道若是咱们的组件定义用ES6定义以下:
class App extends Component{}复制代码
咱们知道class
仅仅只是一个语法糖,上面的代码使用ES5去实现至关于:
function App(){}
App.prototype = Object.create(Component.prototype, {
constructor: {
value: App,
enumerable: true,
writable: true,
configurable: true
}
});复制代码
若是你对ES5中的Object.create
也不熟悉的话,我简要的介绍一下,Object.create
的做用就是实现原型继承(Prototypal Inheritance)来实现基于已有对象建立新对象。Object.create
的第一个参数就是所要继承的原型对象,第二个参数就是新对象定义额外属性的对象(相似于Object.defineProperty
的参数),若是要我本身实现一个简单的Object.create
函数咱们能够这样写:
function create(prototype, ...obj){
function F(){}
F.prototype = prototype;
return Object.defineProperties(new F(), ...obj);
}复制代码
如今你确定知道了若是你的组件继承了Preact中的Component
的话,在原型中必定存在render
方法,这时候经过new
建立Ctor
的实例inst
(实例中已经含有了你自定义的render
函数),可是若是没有给父级构造函数super
传入props
和context
,那么inst
中的props
和context
的属性为undefined
,经过强制调用Component.call(inst, props, context)
能够给inst
中props
、context
进行初始化赋值。
若是组件中不存在render
函数,说明该函数是PFC(Pure Function Component)类型,便是纯函数组件。这时直接调用函数Component
建立实例,实例的constructor
属性设置为传入的函数。因为实例中不存在render
函数,则将doRender
函数做为实例的render
属性,doRender
函数会将Ctor
的返回的虚拟dom做为结果返回。
而后咱们从组件回收的共享池中那拿到同类型组件的实例,从其中取出该实例以前渲染的实例(nextBase
),而后将其赋值到咱们的新建立组件实例的nextBase
属性上,其目的就是为了能基于此DOM元素进行渲染,以更少的代价进行相关的渲染。
function setComponentProps(component, props, opts, context, mountAll) {
if (component._disable) return;
component._disable = true;
if ((component.__ref = props.ref)) delete props.ref;
if ((component.__key = props.key)) delete props.key;
if (!component.base || mountAll) {
if (component.componentWillMount) component.componentWillMount();
}
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
}
if (context && context!==component.context) {
if (!component.prevContext) component.prevContext = component.context;
component.context = context;
}
if (!component.prevProps) component.prevProps = component.props;
component.props = props;
component._disable = false;
if (opts!==NO_RENDER) {
if (opts===SYNC_RENDER || !component.base) {
renderComponent(component, SYNC_RENDER, mountAll);
}
else {
enqueueRender(component);
}
}
if (component.__ref) component.__ref(component);
}复制代码
函数setComponentProps
的主要做用就是为组件实例设置属性(props),其中props
一般来源于JSX中的属性(attributes
)。函数的参数component
、props
、context
与mountAll
的含义从名字就能够看出来,值得注意地是参数opts
,表明的是不一样的刷新模式:
NO_RENDER
: 不进行渲染SYNC_RENDER
: 同步渲染FORCE_RENDER
: 强制刷新渲染ASYNC_RENDER
: 异步渲染 首先若是组件component
中_disable
属性为true
时则直接退出,不然将属性_disable
置为true
,其目的至关于一个锁,保证修改过程的原子性。若是传入组件的属性props
中存在ref
与key
,则将其分别缓存在组件的__ref
与__key
,并将其从props
将其删除。
组件实例中的base
中存放的是以前组件实例对应的真实dom节点,若是不存在该属性,说明是该组件的初次渲染,若是组件中定义了生命周期函数(钩子函数)componentWillMount
,则在此处执行。若是不是首次执行,若是存在生命周期函数componentWillReceiveProps
,则须要将最新的props
与context
做为参数调用componentWillReceiveProps
。而后分别将当前的属性context
与props
缓存在组件的preContext
与prevProps
属性中,并将context
与props
属性更新为最新的context
与props
。最后将组件的_disable
属性置回false
。
若是组件更新的模式为NO_RENDER
,则不须要进行渲染。若是是同步渲染(SYNC_RENDER
)或者是首次渲染(base
属性为空),则执行函数renderComponent
,其他状况下(例如setState
触发的异步渲染ASYNC_RENDER
)均执行函数enqueueRender
(enqueueRender
函数将在setState
处分析)。在函数的最后,若是存在ref
函数,则将组件实例做为参数调用ref
函数。在这里咱们能够显然能够看出在Preact中是不支持React的中字符串类型的ref
属性,不过这个也并不重要,由于React自己也不推荐使用字符串类型的ref
属性,并表示可能会在未来版本中废除这一属性。
接下来咱们还须要了解renderComponent
函数(很是冗长)与enqueueRender
函数的做用:
renderComponent(component, opts, mountAll, isChild) {
if (component._disable) return;
let props = component.props,
state = component.state,
context = component.context,
previousProps = component.prevProps || props,
previousState = component.prevState || state,
previousContext = component.prevContext || context,
isUpdate = component.base,
nextBase = component.nextBase,
initialBase = isUpdate || nextBase,
initialChildComponent = component._component,
skip = false,
rendered, inst, cbase;
// block-1
if (isUpdate) {
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
if (opts!==FORCE_RENDER
&& component.shouldComponentUpdate
&& component.shouldComponentUpdate(props, state, context) === false) {
skip = true;
}
else if (component.componentWillUpdate) {
component.componentWillUpdate(props, state, context);
}
component.props = props;
component.state = state;
component.context = context;
}
component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
component._dirty = false;
if (!skip) {
// block-2
rendered = component.render(props, state, context);
if (component.getChildContext) {
context = extend(extend({}, context), component.getChildContext());
}
let childComponent = rendered && rendered.nodeName,
toUnmount, base;
//block-3
if (typeof childComponent==='function') {
let childProps = getNodeProps(rendered);
inst = initialChildComponent;
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
}
else {
toUnmount = inst;
component._component = inst = createComponent(childComponent, childProps, context);
inst.nextBase = inst.nextBase || nextBase;
inst._parentComponent = component;
setComponentProps(inst, childProps, NO_RENDER, context, false);
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
base = inst.base;
}
else {
//block-4
cbase = initialBase;
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
if (initialBase || opts===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
}
}
// block-5
if (initialBase && base!==initialBase && inst!==initialChildComponent) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase, false);
}
}
}
if (toUnmount) {
unmountComponent(toUnmount);
}
//block-6
component.base = base;
if (base && !isChild) {
let componentRef = component,
t = component;
while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
base._component = componentRef;
base._componentConstructor = componentRef.constructor;
}
}
//block-7
if (!isUpdate || mountAll) {
mounts.unshift(component);
}
else if (!skip) {
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, previousContext);
}
}
if (component._renderCallbacks!=null) {
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
}
//block-8
if (!diffLevel && !isChild) flushMounts();
}复制代码
为了方便阅读,咱们将代码分红了八个部分,不过为了更方便的阅读代码,咱们首先看一下函数开始处的变量声明:
所要渲染的component
实例中的props
、context
、state
属性表示的是最新的所要渲染的组件实例属性。而对应的preProps
、preContext
、preState
表明的是渲染以前上一个状态组件实例属性。变量isUpdate
表明的是当前是处于组件更新的过程仍是组件渲染的过程(mount),咱们经过以前组件实例是否对应存在真实DOM节点来判断,若是存在则认为是更新的过程,不然认为是渲染(mount)过程。nextBase
表示能够基于此DOM元素进行修改(可能来源于上一次渲染或者是回收以前同类型的组件实例),以寻求最小的渲染代价。
组件实例中的_component
属性表示的组件的子组件,仅仅只有当组件返回的是组件时(也就是当前组件为高阶组件),才会存在。变量skip
用来标志是否须要跳过更新的过程(例如: 生命周期函数shouldComponentUpdate
返回false
)。
第一段代码: 若是存在component.base
存在,说明该组件以前对应的真实dom元素,说明组件处于更新的过程。要将props
、state
、context
替换成以前的previousProps
、previousState
、previousContext
,这是由于在生命周期函数shouldComponentUpdate
、componentWillUpdate
中的this.props
、this.state
、this.context
仍然是更新前的状态。若是不是强制刷新(FORCE_RENDER
)并存在生命周期函数shouldComponentUpdate
,则以最新的props
、state
、context
做为参数执行shouldComponentUpdate
,若是返回的结果为false
代表要跳过这次的刷新过程,即置标志位skip
为true。不然若是生命周期shouldComponentUpdate
返回的不是false
(说明若是不返回值或者其余非false
的值,都会执行更新),则查看生命周期函数componentWillUpdate
是否存在,存在则执行。最后则将组件实例的props
、state
、context
替换成最新的状态,并置空组件实例中的prevProps
、prevState
、prevContext
的属性,以及将_dirty
属性置为false
。须要注意的是只有_dirty
为false
才会被放入更新队列,而后_dirty
会被置为true
,这样组件实例就不会被屡次放入更新队列。
若是没有跳过更新的过程(即skip
为false
),则执行到第二段代码。首先执行组件实例的render
函数(相比于React中的render
函数,Preact中的render
函数执行时传入了参数props
、state
、context
),执行render
函数的返回值rendered
则是组件实例对应的虚拟dom元素(VNode)。若是组件存在函数getChildContext
,则生成当前须要传递给子组件的context
。咱们从代码extend(extend({}, context), component.getChildContext())
能够看出,若是父组件存在某个context
属性而且当前组件实例中getChildContext
函数返回的context
也存在相同的属性时,那么当前组件实例getChildContext
返回的context
中的属性会覆盖父组件的context
中的相同属性。
接下来到第三段代码,childComponent
是组件实例render
函数返回的虚拟dom的类型(rendered.nodeName
),若是childComponent
的类型为函数,说明该组件为高阶组件(High Order Component),若是你不了解高阶组件,能够戳这篇文章。若是是高阶组件的状况下,首先经过getNodeProps
函数得到虚拟dom中子组件的属性。若是组件存在子组件的实例而且子组件实例的构造函数与当前组件返回的子组件虚拟dom类型相同(inst.constructor===childComponent
)并且先后的key
值相同时(childProps.key==inst.__key
),仅须要以同步渲染(SYNC_RENDER
)的模式递归调用函数setComponentProps
来更新子组件的属性props
。之因此这样是由于若是知足前面的条件说明,先后两次渲染的子组件对应的实例不发生改变,仅改变传入子组件的参数(props)。这时子组件仅须要根据当前最新的props
对应渲染真实dom便可。不然若是以前的子组件实例的构造函数与当前组件返回的子组件虚拟dom类型不相同时或者根据key
值标定两个组件实例不相同时,则须要渲染的新的子组件,不只须要调用createComponent
建立子组件的实例(createComponent(childComponent, childProps, context)
)并为当前的子组件和组件设置相关关系(即_component
、_parentComponent
属性)并且用toUnmount
指示待卸载的组件实例。而后经过调用setComponentProps
来设置组件的ref
和key
等,以及调用组件的相关生命周期函数(例如:componentWillMount
),须要注意的是这里的调用模式是NO_RENDER
,不会进行渲染。而在下一句调用renderComponent(inst, SYNC_RENDER, mountAll, true)
去同步地渲染子组件。因此咱们就要注意为何在调用函数setComponentProps
时没有采用SYNC_RENDER
模式,SYNC_RENDER
模式也自己就会触发renderComponent
去渲染组件,其缘由就是为了在调用renerComponent
赋予isChild
值为true
,这个标志量的做用咱们后面能够看到。调用完renderComponent
以后,inst.base
中已是咱们子组件渲染的真实dom节点。
在第四段代码中,处理的是当前组件须要渲染的虚拟dom类型是非组件类型(即普通的DOM元素)。首先赋值cbase = initialBase
,咱们知道initialBase
来自于initialBase = isUpdate || nextBase
,也就是说若是当前是更新的模式,则initialBase
等于isUpdate
,即为上次组件渲染的内容。不然,若是组件实例存在nextBase
(从回收池获得的DOM结构),也能够基于其进行修改,总的目的是为了以更少的代价去渲染。若是以前的组件渲染的是函数类型的元素(即组件),但如今却渲染的是非函数类型的,赋值toUnmount = initialChildComponent
,用来存储以后须要卸载的组件,而且因为cbase
对应的是以前的组件的dom节点,所以就没法使用了,须要赋值cbase = null
以使得从新渲染。而component._component = null
目的就是切断以前组件间的父子关系,毕竟如今返回的都不是组件。若是是同步渲染(SYNC_RENDER
),则会经过调用idiff
函数去渲染组件返回的虚拟dom(详情见第二篇文章diff)。咱们来看看调用idiff
函数的形参和实参:
cbase
对应的是diff
的dom
参数,表示用来渲染的VNode以前的真实dom。能够看到若是以前是组件类型,那么cbase
值为undefined
,咱们就须要从新开始渲染。不然咱们就能够在以前的渲染基础上更新以寻求最小的更新代价。
rendered
对应diff
中的vnode
参数,表示须要渲染的虚拟dom节点。context
对应diff
中的context
参数,表示组件的context
属性。mountAll || !isUpdate
对应的是diff
中的mountAll
参数,表示是不是从新渲染DOM节点而不是基于以前的DOM修改,!isUpdate
表示的就是非更新状态。initialBase && initialBase.parentNode
对应的是diff
中的parent
参数,表示的是当前渲染节点的父级节点。diff
函数的第六个参数为componentRoot
,实参为true
表示的是当前diff
是以组件中render
函数的渲染内容的形式调用,也能够说当前的渲染内容是属于组件类型的。 咱们知道idiff
函数返回的是虚拟dom对应渲染后的真实dom节点,因此变量base
存储的就是本次组件渲染的真实DOM元素。
baseParent.replaceChild(base, initialBase)
),若是没有须要卸载的组件实例,则调用函数recollectNodeTree
回收该DOM节点。不然若是以前组件渲染的是函数类型的元素,但须要废弃,则调用函数unmountComponent
进行卸载(调用相关的生命周期函数)。function unmountComponent(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);
}复制代码
来看unmountComponent
函数的做用,首先将函数实例中的_disable
置为true
表示组件禁用,若是组件存在生命周期函数componentWillUnmount
进行调用。而后递归调用函数unmountComponent
递归卸载组件。若是以前组件渲染的DOM节点,而且最外层节点存在ref
函数,则以参数null
执行(和React保持一致,ref
函数会执行两次,第一次是mount
会以DOM元素或者组件实例回调,第二次是unmount
会回调null
表示卸载)。而后将DOM元素存入nextBase
用以回收。调用removeNode
函数做用是将base
节点的父节点脱离出来。函数removeChildren
的目的是用递归遍历全部的子DOM元素,回收节点(以前的文章已经介绍过,其中就涉及到子元素的ref
调用)。最后若是组件自己存在ref
属性,则直接以null
为参数调用。
component.base = base
用来将当前的组件渲染的dom元素存储在组件实例的base
属性中。下面的代码咱们先举个例子,假若有以下的结构:HOC1 => HOC2 => component => DOM元素复制代码
其中HOC
表明高阶组件,component
表明自定义组件。你会发现HOC1
、HOC2
与compoent
的base
属性都指向最后的DOM元素,而DOM元素的中的_component
是指向HOC1
的组价实例的。看懂了这个你就能明白为何会存在下面这个循环语句,其目的就是为了给父组件赋值正确的base
属性以及为DOM节点的_component
属性赋值正确的组件实例。
在第七段代码中,若是是非更新模式,则须要将当前组件存入mounts
(unshift
方法存入,pop
方法取出,实质上是至关于队列的方式,而且子组件先于父组件存储队列mounts
,所以能够保证正确的调用顺序),方便在后期调用组件对应相似于componentDidMount
生命周期函数和其余的操做。若是没有跳过更新过程(skip === false
),则在此时调用组件对应的生命周期函数componentDidUpdate
。而后若是存在组件存在_renderCallbacks
属性(存储对应的setState
的回调函数,由于setState
函数实质也是经过renderComponent
实现的),则在此处将其弹出并执行。
在第八段代码中,若是diffLevel
为0
而且isChild
为false
时,对应执行flushMounts
函数
function flushMounts() {
let c;
while ((c=mounts.pop())) {
if (c.componentDidMount) c.componentDidMount();
}
}复制代码
其实flushMounts
也是很是的简单,就是将队列mounts
中取出组件实例,而后若是存在生命周期函数componentDidMount
,则对应执行。
其实若是阅读了以前diff的文章的同窗应该记得在diff
函数中有:
function diff(dom, vnode, context, mountAll, parent, componentRoot) {
//......
if (!--diffLevel) {
// ......
if (!componentRoot) flushMounts();
}
}复制代码
上面有两处调用函数flushMounts
,一个是在renderComponent
内部①,一个是在diff
函数②。那么在什么状况下触发上下两段代码呢?首先componentRoot
表示的是当前diff
是否是以组件中渲染内容的形式调用(好比组件中render
函数返回HTML类型的VNode),那么preact.render
函数调用时确定componentRoot
是false
,diffLevel
表示渲染的层次,diffLevel
回减到0说明已经要结束diff的调用,因此在使用preact.render
渲染的最后确定会使用上面的代码去调用函数flushMounts
。可是若是其中某个已经渲染的组件经过setState
或者forceUpdate
的方式致使了从新渲染而且导致子组件建立了新的实例(好比先后两次返回了不一样的组件类型),这时,就会采用第一种方式在调用flushMounts
函数。
对于Preact的组件而言,state
是及其重要的部分。其中涉及到的API为setState
,定义在函数Component
的原型中,这样全部的继承于Component
的自定义组件实例均可以引用到函数setState
。
extend(Component.prototype,{
//.......
setState(state, callback) {
let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof ··==='function' ? state(s, this.props) : state);
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);
}
//......
});复制代码
首先咱们看到setState
接受两个参数: 新的state
以及state
更新后的回调函数,其中state
既能够是对象类型的部分对象,也能够是函数类型。首先使用函数extend
生成当前state
的拷贝prevState
,存储以前的state
的状态。而后若是state
类型为函数时,将函数的生成值覆盖进入state
,不然直接将新的state
覆盖进入state
,此时this.state
已经成为了新的state
。若是setState
存在第二个参数callback
,则将其存入实例属性_renderCallbacks
(若是不存在_renderCallbacks
属性,则须要初始化)。而后执行函数enqueueRender
。
接下来咱们看一下神奇的enqueueRender
函数:
let items = [];
function enqueueRender(component) {
if (!component._dirty && (component._dirty = true) && items.push(component) == 1) {
defer(rerender);
}
}
function rerender() {
let p, list = items;
items = [];
while ((p = list.pop())) {
if (p._dirty) renderComponent(p);
}
}
const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout;复制代码
咱们能够看到当组件实例中的_dirty
属性为false
时,会将属性_dirty
置为true
,并将其放入items
中。当更新队列第一次被items
时,则延迟异步执行函数rerender
。这个延迟异步函数在支持Promise
的浏览器中,会使用Promise.resolve().then
,不然会使用setTimeout
。
rerender
函数就是将items
中待更新的组件,逐个取出,并对其执行renderComponent
。其实renderComponent
的opt
参数不传入ASYNC_RENDER
,而是传入undefined
二者之间并没有区别。惟一要注意的是:
//renderComponent内部
if (initialBase || opts===SYNC_RENDER) {
base = diff(//...;
}复制代码
咱们渲染过程必定是要执行diff
,那就说明initialBase
必定是个非假值,这也是能够保证的。
initialBase = isUpdate || nextBase复制代码
其实由于以前组件已经渲染过,因此是能够保证isUpdate
必定为非假值,由于isUpdate = component.base
而且component.base
是必定存在的而且为上次渲染的内容。你们可能会担忧若是上次组件render
函数返回的是null
该怎么办?其实阅读过第二篇文章的同窗应该知道在idiff
函数内部
if (vnode==null || typeof vnode==='boolean') vnode = '';复制代码
即便render
返回的是null
也会被当作一个空文本去控制,对应会渲染成DOM中的Text
类型。
extend(Component.prototype,{
//.......
forceUpdate(callback) {
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
renderComponent(this, FORCE_RENDER);
}
//......
});复制代码
执行forceUpdate
所须要作的就是将回调函数放入组件实例中的_renderCallbacks
属性并调用函数renderComponent
强制刷新当前的组件。须要注意的是,咱们渲染的模式是FORCE_RENDER
强制刷新,与其余的模式到的区别就是不须要通过生命周期函数shouldComponentUpdate
的判断,直接进行刷新。
至此咱们已经看完了Preact中的组件相关的代码,可能并无对每个场景都进行讲解,可是我也尽可能尝试去覆盖全部相关的部分。代码相对比较长,看起来也常常使人头疼,有时候为了搞清楚某个变量的部分不得不数次回顾。可是你会发现你屡次地、反复性的阅读、仔细地推敲,代码的含义会逐渐清晰。书读百遍其义自见,其实对代码来讲也是同样的。文章如有不正确的地方,欢迎指出,共同窗习。