react编写打字组件

前言

以前用class也封装过一次打字组件(这里),最近写react时想用打字效果,又重新封装成了react组件,当然原理我是参考的typing.js


效果(动图在下面)

在这里插入图片描述
在这里插入图片描述

使用
<Typing>
    //想要打印的内容
<Typing>
参数说明类型默认值
delay设置打印延时,单位为毫秒number0
frequency设置打印频率,单位毫秒number30
done打印完成后的回调function()=>{}

暂不支持打印自定义组件和事件

原理

加上注释,这个组件也才137行代码,所以不算复杂,基本就是围绕两件事情做(如果不想了解原理,可以直接看下面的源码,复制了就可以直接用)。

  1. 将需要打印的dom转换为要打印的数组
  2. 打印数组

以下面的例子进行分析

<Typing delay={500} frequency={100}>
	测试打印
	<p>123</p>
	测试打印
</Typing>

在这里插入图片描述
流程

  1. 遇见字符就打印
  2. 遇见dom节点,就先创建,然后去打印此dom节点下的所有后代内容
  3. dom打印完成后继续往下打印,直至结束

通过this.props.children可以获取打印的内容
在这里插入图片描述
我们将this.props.children转换成这种类型的数据
在这里插入图片描述
this.props.children的类型不一定是数组,所以这里我们要判断,并且这里需要递归去转换子节点的内容

  /**
     * children转换为符合打印的数组
     * @param {*} children  Object、Array、String、undefined、Null
     * @param {array} arr 保存打印的数组
     */
    _convert(children, arr = []) {
        let list = arr.slice()
        if(Array.isArray(children)){
            for(let item of children){
                list = list.concat(this._convert(item))
            }
        }
        if(isObject(children)){
            const dom = this._createDom({
                ...children.props,
                type:children.type
            })
            const val = this._convert(children.props.children,[])
            list.push({
                dom,
                val
            })
        }
        if(typeof children === 'string'){
            list = list.concat(children.split(''))
        }
        return list
    }
    
     /**
     * 根据信息生成dom节点
     * @param {object} info 
     */
    _createDom(info) {
        info = { ...info }   //因为要删除info的属相,所以这里深拷贝对象,避免影响外面
        let dom = document.createElement(info.type)

        delete info.children

        for (let [key, value] of Object.entries(info)) {
            if (key === 'className') {
                key = 'class'
            }
            dom.setAttribute(key, value)
        }
        if (info.style) {
            let cssText = ''
            for (let [key, value] of Object.entries(info.style)) {
                cssText += `${key}:${value};`
            }
            dom.style.cssText = cssText
            dom.onclick = info.onClick
        }

        return dom
    }

之所以不支持打印自定义组件和事件,就是不好将自定义组件转换为原生dom,react事件不好绑定到原生dom上。

将内容转换为符合打印的数组后就可以开始打印了

  1. 遇见字符就打印
  2. 遇见dom1就先创建,然后递归打印它的后代内容
  3. dom1的所有后代内容打印结束后,接着打印dom1下面的内容
  4. 打印结束删除当前内容
  5. 重复上述过程

这里我们打印dom1的后代内容时需要保存dom1,这样在dom1后代内容打印完成后可以回到dom1继续往下打印

组件源码
import React from 'react'
import PropTypes from 'prop-types'

//暂不支持打印自定义组件
// 如何将react节点转换为dom,比如拥有className、style、onClick的react元素,我们生成的dom如何保留这些。
// 本来想直接用React.createElement来代替document.createElement。但是ReactDOM.render()方法插入的位置节点必须是dom所以此方法不行
function isObject(obj){
    return Object.prototype.toString.call(obj) === '[object Object]'
}

class Typing extends React.Component {
    static propTypes = {
        delay: PropTypes.number,   //设置打印延时,单位为毫秒
        frequency: PropTypes.number,   //设置打印频率
        done: PropTypes.func    //打印结束的函数
    }
    static defaultProps = {
        delay: 0,
        frequency: 30,
        done: () => { }
    }

    componentDidMount() {
        this.chain = {          //此变量就是将要打印的对象
            parent: null,
            dom: this.wrapper,
            val: []
        };
        this.chain.val = this._convert(this.props.children, this.chain.val)
        setTimeout(() => {
            this._play(this.chain)
        }, this.props.delay)
    }
    /**
     * children转换为符合打印的数组
     * @param {*} children  Object、Array、String、undefined、Null
     * @param {array} arr 保存打印的数组
     */
    _convert(children, arr = []) {
        let list = arr.slice()
        if(Array.isArray(children)){
            for(let item of children){
                list = list.concat(this._convert(item))
            }
        }
        if(isObject(children)){
            const dom = this._createDom({
                ...children.props,
                type:children.type
            })
            const val = this._convert(children.props.children,[])
            list.push({
                dom,
                val
            })
        }
        if(typeof children === 'string'){
            list = list.concat(children.split(''))
        }
        return list
    }
    /**
     * 打印字符
     * @param {*} dom 父节点
     * @param {*} val 打印内容
     * @param {*} callback 打印完成的回调
     */
    _print(dom, val, callback) {
        setTimeout(function () {
            dom.appendChild(document.createTextNode(val));
            callback();
        }, this.props.frequency);
    }
    /**
     * 打印节点
     * @param {*} node 
     */
    _play = (node) => {
        //当打印最后一个字符时,动画完毕,执行done
        if (!node.val.length) {
            if (node.parent) this._play(node.parent);
            else this.props.done();
            return;
        }
        let current = node.val.shift()    //获取第一个元素,并从打印列表中删除
        if (typeof current === 'string') {
            this._print(node.dom, current, () => {
                this._play(node)
            })
        } else {
            let dom = current.dom
            node.dom.appendChild(dom)
            this._play({
                parent: node,
                dom,
                val: current.val
            })
        }
    }
    /**
     * 根据信息生成dom节点
     * @param {object} info 
     */
    _createDom(info) {
        info = { ...info }   //因为要删除info的属相,所以这里深拷贝对象,避免影响外面
        let dom = document.createElement(info.type)

        delete info.children

        for (let [key, value] of Object.entries(info)) {
            if (key === 'className') {
                key = 'class'
            }
            dom.setAttribute(key, value)
        }
        if (info.style) {
            let cssText = ''
            for (let [key, value] of Object.entries(info.style)) {
                cssText += `${key}:${value};`
            }
            dom.style.cssText = cssText
        }

        return dom
    }

    render() {
        const { className = '', style = {} } = this.props
        return (
            <div ref={el => this.wrapper = el} className={className} style={style}>

            </div>
        )
    }
}

export default Typing
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值