前言
以前用class也封裝過一次打字組件(這裏),最近寫react時想用打字效果,又重新封裝成了react組件,當然原理我是參考的typing.js。
效果(動圖在下面)
使用
<Typing>
//想要打印的內容
<Typing>
參數 | 說明 | 類型 | 默認值 |
---|---|---|---|
delay | 設置打印延時,單位爲毫秒 | number | 0 |
frequency | 設置打印頻率,單位毫秒 | number | 30 |
done | 打印完成後的回調 | function | ()=>{} |
暫不支持打印自定義組件和事件
原理
加上註釋,這個組件也才137行代碼,所以不算複雜,基本就是圍繞兩件事情做(如果不想了解原理,可以直接看下面的源碼,複製了就可以直接用)。
- 將需要打印的dom轉換爲要打印的數組
- 打印數組
以下面的例子進行分析
<Typing delay={500} frequency={100}>
測試打印
<p>123</p>
測試打印
</Typing>
流程
- 遇見字符就打印
- 遇見dom節點,就先創建,然後去打印此dom節點下的所有後代內容
- 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上。
將內容轉換爲符合打印的數組後就可以開始打印了
- 遇見字符就打印
- 遇見dom1就先創建,然後遞歸打印它的後代內容
- dom1的所有後代內容打印結束後,接着打印dom1下面的內容
- 打印結束刪除當前內容
- 重複上述過程
這裏我們打印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