从Preact了解一个类React的框架是怎么实现的(三): 组件

前言

  首先欢迎你们关注个人掘金帐号和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,使得_dirtytrue,但由于该属性的存在,只会使得组件仅有一次才会被放入更新队列。
  • 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的逻辑并不复杂,将vnodeattributeschidlren的属性赋值到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算法的同窗来讲,其实整个组件大体渲染的流程咱们已经清楚了,可是若是想要更深层次的了解其中的细节咱们必须去深究函数createComponentsetComponentProps的内部细节。   

createComponent

  关于函数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主要做用就是建立组件实例。参数propscontext分别对应的是组件的中属性和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传入propscontext,那么inst中的propscontext的属性为undefined,经过强制调用Component.call(inst, props, context)能够给instpropscontext进行初始化赋值。
  
  若是组件中不存在render函数,说明该函数是PFC(Pure Function Component)类型,便是纯函数组件。这时直接调用函数Component建立实例,实例的constructor属性设置为传入的函数。因为实例中不存在render函数,则将doRender函数做为实例的render属性,doRender函数会将Ctor的返回的虚拟dom做为结果返回。
  
  而后咱们从组件回收的共享池中那拿到同类型组件的实例,从其中取出该实例以前渲染的实例(nextBase),而后将其赋值到咱们的新建立组件实例的nextBase属性上,其目的就是为了能基于此DOM元素进行渲染,以更少的代价进行相关的渲染。

setComponentProps

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)。函数的参数componentpropscontextmountAll的含义从名字就能够看出来,值得注意地是参数opts,表明的是不一样的刷新模式:

  • NO_RENDER: 不进行渲染
  • SYNC_RENDER: 同步渲染
  • FORCE_RENDER: 强制刷新渲染
  • ASYNC_RENDER: 异步渲染

  首先若是组件component_disable属性为true时则直接退出,不然将属性_disable置为true,其目的至关于一个,保证修改过程的原子性。若是传入组件的属性props中存在refkey,则将其分别缓存在组件的__ref__key,并将其从props将其删除。
  
  组件实例中的base中存放的是以前组件实例对应的真实dom节点,若是不存在该属性,说明是该组件的初次渲染,若是组件中定义了生命周期函数(钩子函数)componentWillMount,则在此处执行。若是不是首次执行,若是存在生命周期函数componentWillReceiveProps,则须要将最新的propscontext做为参数调用componentWillReceiveProps。而后分别将当前的属性contextprops缓存在组件的preContextprevProps属性中,并将contextprops属性更新为最新的contextprops。最后将组件的_disable属性置回false
  
  若是组件更新的模式为NO_RENDER,则不须要进行渲染。若是是同步渲染(SYNC_RENDER)或者是首次渲染(base属性为空),则执行函数renderComponent,其他状况下(例如setState触发的异步渲染ASYNC_RENDER)均执行函数enqueueRender(enqueueRender函数将在setState处分析)。在函数的最后,若是存在ref函数,则将组件实例做为参数调用ref函数。在这里咱们能够显然能够看出在Preact中是不支持React的中字符串类型的ref属性,不过这个也并不重要,由于React自己也不推荐使用字符串类型的ref属性,并表示可能会在未来版本中废除这一属性。
  
  接下来咱们还须要了解renderComponent函数(很是冗长)与enqueueRender函数的做用:   

renderComponent

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实例中的propscontextstate属性表示的是最新的所要渲染的组件实例属性。而对应的prePropspreContextpreState表明的是渲染以前上一个状态组件实例属性。变量isUpdate表明的是当前是处于组件更新的过程仍是组件渲染的过程(mount),咱们经过以前组件实例是否对应存在真实DOM节点来判断,若是存在则认为是更新的过程,不然认为是渲染(mount)过程。nextBase表示能够基于此DOM元素进行修改(可能来源于上一次渲染或者是回收以前同类型的组件实例),以寻求最小的渲染代价。
组件实例中的_component属性表示的组件的子组件,仅仅只有当组件返回的是组件时(也就是当前组件为高阶组件),才会存在。变量skip用来标志是否须要跳过更新的过程(例如: 生命周期函数shouldComponentUpdate返回false)。

  • 第一段代码: 若是存在component.base存在,说明该组件以前对应的真实dom元素,说明组件处于更新的过程。要将propsstatecontext替换成以前的previousPropspreviousStatepreviousContext,这是由于在生命周期函数shouldComponentUpdatecomponentWillUpdate中的this.propsthis.statethis.context仍然是更新前的状态。若是不是强制刷新(FORCE_RENDER)并存在生命周期函数shouldComponentUpdate,则以最新的propsstatecontext做为参数执行shouldComponentUpdate,若是返回的结果为false代表要跳过这次的刷新过程,即置标志位skip为true。不然若是生命周期shouldComponentUpdate返回的不是false(说明若是不返回值或者其余非false的值,都会执行更新),则查看生命周期函数componentWillUpdate是否存在,存在则执行。最后则将组件实例的propsstatecontext替换成最新的状态,并置空组件实例中的prevPropsprevStateprevContext的属性,以及将_dirty属性置为false。须要注意的是只有_dirtyfalse才会被放入更新队列,而后_dirty会被置为true,这样组件实例就不会被屡次放入更新队列。

  • 若是没有跳过更新的过程(即skipfalse),则执行到第二段代码。首先执行组件实例的render函数(相比于React中的render函数,Preact中的render函数执行时传入了参数propsstatecontext),执行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来设置组件的refkey等,以及调用组件的相关生命周期函数(例如: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函数的形参和实参:

  1. cbase对应的是diffdom参数,表示用来渲染的VNode以前的真实dom。能够看到若是以前是组件类型,那么cbase值为undefined,咱们就须要从新开始渲染。不然咱们就能够在以前的渲染基础上更新以寻求最小的更新代价。
    1. rendered对应diff中的vnode参数,表示须要渲染的虚拟dom节点。
    2. context对应diff中的context参数,表示组件的context属性。
    3. mountAll || !isUpdate对应的是diff中的mountAll参数,表示是不是从新渲染DOM节点而不是基于以前的DOM修改,!isUpdate表示的就是非更新状态。
    4. initialBase && initialBase.parentNode对应的是diff中的parent参数,表示的是当前渲染节点的父级节点。
    5. diff函数的第六个参数为componentRoot,实参为true表示的是当前diff是以组件中render函数的渲染内容的形式调用,也能够说当前的渲染内容是属于组件类型的。

  咱们知道idiff函数返回的是虚拟dom对应渲染后的真实dom节点,因此变量base存储的就是本次组件渲染的真实DOM元素。

  • 代码第五部分: 若是组件先后返回的虚拟dom节点对应的真实DOM节点不相同,或者先后返回的虚拟DOM节点对应的先后组件实例不一致时,则在父级的DOM元素中将以前的DOM节点替换成当前对应渲染的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表明自定义组件。你会发现HOC1HOC2compoentbase属性都指向最后的DOM元素,而DOM元素的中的_component是指向HOC1的组价实例的。看懂了这个你就能明白为何会存在下面这个循环语句,其目的就是为了给父组件赋值正确的base属性以及为DOM节点的_component属性赋值正确的组件实例。

  • 在第七段代码中,若是是非更新模式,则须要将当前组件存入mounts(unshift方法存入,pop方法取出,实质上是至关于队列的方式,而且子组件先于父组件存储队列mounts,所以能够保证正确的调用顺序),方便在后期调用组件对应相似于componentDidMount生命周期函数和其余的操做。若是没有跳过更新过程(skip === false),则在此时调用组件对应的生命周期函数componentDidUpdate。而后若是存在组件存在_renderCallbacks属性(存储对应的setState的回调函数,由于setState函数实质也是经过renderComponent实现的),则在此处将其弹出并执行。

  • 在第八段代码中,若是diffLevel0而且isChildfalse时,对应执行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函数调用时确定componentRootfalsediffLevel表示渲染的层次,diffLevel回减到0说明已经要结束diff的调用,因此在使用preact.render渲染的最后确定会使用上面的代码去调用函数flushMounts。可是若是其中某个已经渲染的组件经过setState或者forceUpdate的方式致使了从新渲染而且导致子组件建立了新的实例(好比先后两次返回了不一样的组件类型),这时,就会采用第一种方式在调用flushMounts函数。

setState

  对于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

  接下来咱们看一下神奇的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。其实renderComponentopt参数不传入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类型。

  

forceUpdate

extend(Component.prototype,{
    //.......

    forceUpdate(callback) {
        if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
        renderComponent(this, FORCE_RENDER);
    }

    //......
});复制代码

  执行forceUpdate所须要作的就是将回调函数放入组件实例中的_renderCallbacks属性并调用函数renderComponent强制刷新当前的组件。须要注意的是,咱们渲染的模式是FORCE_RENDER强制刷新,与其余的模式到的区别就是不须要通过生命周期函数shouldComponentUpdate的判断,直接进行刷新。

结语

  至此咱们已经看完了Preact中的组件相关的代码,可能并无对每个场景都进行讲解,可是我也尽可能尝试去覆盖全部相关的部分。代码相对比较长,看起来也常常使人头疼,有时候为了搞清楚某个变量的部分不得不数次回顾。可是你会发现你屡次地、反复性的阅读、仔细地推敲,代码的含义会逐渐清晰。书读百遍其义自见,其实对代码来讲也是同样的。文章如有不正确的地方,欢迎指出,共同窗习。

相关文章
相关标签/搜索