前言
以前用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