react編寫打字組件

前言

以前用class也封裝過一次打字組件(這裏),最近寫react時想用打字效果,又重新封裝成了react組件,當然原理我是參考的typing.js


效果(動圖在下面)

在這裏插入圖片描述
在這裏插入圖片描述

使用

<Typing>
    //想要打印的內容
<Typing>
參數 說明 類型 默認值
delay 設置打印延時,單位爲毫秒 number 0
frequency 設置打印頻率,單位毫秒 number 30
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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章