手写虚拟DOM(五)—— 自定义组件

本文为系列文章:

手写虚拟DOM(一)—— VirtualDOM介绍
手写虚拟DOM(二)—— VirtualDOM Diff
手写虚拟DOM(三)—— Diff算法优化
手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key
手写虚拟DOM(五)—— 自定义组件
手写虚拟DOM(六)—— 事件处理
手写虚拟DOM(七)—— 异步更新

一、前言

今天,我们继续在之前项目的基础上扩展功能。
现在流行的前端框架都支持自定义组件,组件化开发已经成为提高前端开发效率的银弹。
下面我们就将自定义组件功能加到项目中去,目标是正确的渲染和更新自定义组件。

二、JSX语法

要想正确的渲染组件,第一步就是要告诉JSX某个标签是自定义组件:只要标签名的首字母大写就可以了。
下面的例子里,DemoComp就是一个自定义组件:

<div>
    <div>Header</div>
    <DemoComp/>
</div>

经过Babel jsx语法转换之后如下:

v(
    "div", 
    null, 
    v(
        "div", 
        null, 
        "Header"
    ), 
    v(
        DemoComp, 
        null
    )
)

当首字母大写当时候,JSX会将标签名当作变量处理,而不是像普通标签一样当字符串处理。
解决了识别自定义标签的问题,下一步就是定义标签了。

三、定义基类Component

在React中,所有自定义组件都要继承Component基类,它为我们提供了一系列生命周期方法和修改组件的方法。
我们也对应的定义一个自己的Component类:

/*****************************************************************************************************************
 * Define a basic Component
 *****************************************************************************************************************/
class Component {
    constructor(props) {
        this.props = props;
        this.state = {};
    }

    setState(newState) {
        this.state = {...this.state, ...newState};
        diff(this._dom, this.render());
    }

    render() {
        throw new Error('Component should define its own render');
    }
}

如果用一句话描述Component,那就是属性和状态的UI表达
我们先不考虑生命周期函数,先定义一个最精简版的Component。
首先在初始化的时候,需要传入props属性,然后提供一个setState方法来改变组件的状态,最后就是子类必须要实现的render函数。
如果子类没有实现,就会沿着原型链查找到Component类,然后会抛出一个错误。

四、继承基类,实现自定义组件

class DemoComp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name: 'chris'
        };
        this.interval();
    }

    interval = () => {
        setInterval(() => {
            this.setState({name: 'chris-' + Math.floor(Math.random() * 100)});
        }, 2000);
    };

    render() {
        return (
            <div>
                <div>This is DemoComp....props = {this.props.value}</div>
                <div>DemoComp.state.name = {this.state.name}</div>
            </div>
        );
    }
}

其中,方法 interval 是 class 的属性(ES6),在ES2015中需要转成 function,
所以,还需要安装babel插件:@babel/plugin-proposal-class-properties
当然,也可以直接写成 function,这样就不用安装插件。

五、组件渲染逻辑

一切渲染,都是通过 diff 来完成,不论是首次,还是之后的更新、删除、替换等。
因此,我们要对tag为函数类型(自定义组件)的节点做特殊处理,同时对新建的节点,也要加入一些额外的逻辑:

function diff(srcDOM, destDOM, parent, _component) {
    if (typeof destDOM === 'object' && typeof destDOM.tag === 'function') {
        buildComponent(srcDOM, destDOM, parent);
        return false;
    }

    // 原dom没有,新vdom有,则表明是新增节点
    if (srcDOM === undefined) {
        srcDOM = createElement(destDOM);
        if (_component) {
            srcDOM._component = _component;
            srcDOM._componentConstructor = _component.constructor;
            _component._dom = srcDOM;
        }
        parent.appendChild(srcDOM);
        return false;
    }
    ......
}


function buildComponent(dom, component, parent) {
    const {tag, props, children} = component;
    props.children = children;

    let _component = dom && dom._component;
    if (_component === undefined) {
        _component = new tag(props)
    } else {
        _component.props = props;
    }
    diff(dom, _component.render(), parent, _component);
}

如果是自定义组件,会调用buildComponent方法。先获取vdom最新的属性,包括children
如果dom对象有_component属性,说明是组件更新的过程,否则为组件创建的过程。

  • 如果是创建过程则直接实例化一个对象;
  • 如果是更新过程,则传入最新的props
  • 最后通过组件的render方法得到最新的vdom后,再进行diff操作;

diff多了一个_component的参数,在新建dom节点的时候,如果有这个参数,说明是自定义组件创建的节点,需要用_component_componentConstructor做一下标识。
其中_component上面就用到了,用来判断是组件更新过程还是组件创建过程;_componentConstructor用在组件更新过程中判断组件的类型是否相同。

function isEqual(vdom, element) {
    ......
    // 自定义组件 tag 判断
    if (typeof vdom.tag === 'function') {
        return element._componentConstructor === vdom.tag;
    }
    ......
}

到此为止,自定义组件的被动更新过程已经完成了。

六、setState

class Component {
    ......
    setState(newState) {
        this.state = {...this.state, ...newState};
        diff(this._dom, this.render());
    }
    ......
}

该方法定义在基类中,合并当前状态和最新状态(如果有相同key,则后者覆盖前者)。
然后,diff 当前真实 dom 和 子组件的 virtual dom。

七、示例

刚开始如下图:

image.png

2秒之后:
image1.png

可以看到propsstate都得到了正确都渲染。

八、总结

本文基于上一个版本的代码,加入了对自定义组件的支持,大大提高代码的复用性。

项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-05

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容