react實現數據驅動動畫的難點和痛點
- 如果只是單純的實現動畫,不需要與數據交互,css可以很好實現一些簡單的不需要重複執行的動畫(解釋一下這裏所說的不用重複執行的動畫是指不需要更改dom節點在html文檔重的結構順序),並且react也提供了很多動畫庫,比如react-motion,animejs等,問題是如果動畫的驅動需要由數據驅動,並且不同的數據要採用不同的動畫,動畫執行完畢之後需要調整dom節點在html文檔中的結構順序,也就是說要在componentWillReceiveProps這個生命週期裏處理所有的邏輯操作。
- 數據推送過快會出現“供不應求“,處理不好動畫的執行和數據的改變會出現不同步的問題。編程模式裏,有生產者和消費者模式可以解決這個問題。
讀者可能會覺得哪有這樣的業務需求呢,且看下文細說。
業務需求
業務需求是這樣的,如下圖在網頁左側有7個div,後臺會每隔幾分鐘推送一次數字,當推送的數字在左側的div裏時,就會一次向上面/下面的div交換位置,直至交換到中間位置。例如,當後臺推送數據1時,1就會與2這個div交換位置,此時1就會到達2的位置,2到達了原來1的位置,然後1再與3交換位置,然後再與4交換位置。如果推送的是7就一次往上交換位置,直至到4的位置。如果後臺推送的數字已經在中間的位置了,就什麼不用處理。但是,如果後臺推送的數據不在左側的任何一個div中,就從後臺獲取新的7條數據替換左側的7個div裏的數據。
具體的實現細節
defaultData是一個數組,存放7個div中數據,div都有各自的
<div id='pagParent' className={styles.parent} style={{ width: '12.5vw', height: "100vh", }}>
{
defaultData.length > 0 ? defaultData.map((e, index) =>
{
return <div id={`div${index}`} key={index} style={{ color: index === 3 ? "rgb(228,94,35)" : "rgb(255,255,255)" }} >
<Icon type="pie-chart" />
<p style={{ fontSize: "1.5em" }}>{e.logicEntityGroupName}</p></div>
}) : <></>
}
</div>
這是控制7個div樣式的,這個設計也是有技巧的,一開始的時候我是把7個div每個div都單獨設置一個class,然後在數據遍歷的時候將一次賦予class,這樣就出現了一個嚴重的問題,react需要首先引入css或者less文件,在通過遍歷數據生成div的同時需要將對應的class放在div裏,當時我是這樣解決的,我在less樣式文件裏定義了7個div的樣式,分別是div1,div2,div3,div4,div5,div6,div7這樣就可以通過ES6模版字符串拼接生成樣式類名了,${styles.div}${index}
,這樣寫有問題嗎?直到在github上的一個大神給我了提示說:**styles是從less注入進去的,這麼寫babel支持不了。最好不要這樣用css,沒法維護的如果非要這麼寫可以用:global。className = { chanxian${i}}**爲了給這個7個div設置不同的的樣式,我想到了css僞類選擇器,雖然這樣寫執行效率很低,但是當時我似乎沒有找到更好的解決方法。以下是css樣式代碼
.parent{
position: absolute;
width: 12.5vw;
height: 100vh;
div{
width: 200px;
height: 100px;
background-image: url(../../assets//title1.jpg);
background-position: center;
position: fixed;
background-size: 100% 60%;
zoom:0.9;
background-repeat: no-repeat;
display: flex;
color:@divColor;
i{
font-size: 3em;
color: rgb(9,109,217);
padding-top: 17% ;
padding-left:6%;
}
p{
font-size: 20px;
margin-top: 18%;
margin-left: 7%;
color: inherit;
}
}
div:nth-child(1){
left:0px;
top:190px;
}
div:nth-child(2){
left:14px;
top:268px;
}
div:nth-child(3){
left:25px;
top:346px;
}
div:nth-child(4){
left:40px;
top:424px;
}
div:nth-child(5){
left:34px;
top:502px;
}
div:nth-child(6){
left:22px;
top:580px;
}
div:nth-child(7){
left:0px;
top:660px;
}
}
動畫實現部分
由於每一個div都不是相同的動畫機制,所以需要爲每一個div書寫不同的動畫執行函數,這裏我引入了animejs這個動畫庫,我敢保證有更好的辦法來找到這6個動畫的共性,抽離出共性然後在根據不同的參數執行函數,我覺得這樣更容易維護。出於技術點和沒有找到共性的原因,我採用了下面愚笨的方法,如果讀者有更好的方法實現,請一定告訴我
// 觸發第一個div
onClickTo0 = (tempstyle) => anime({
targets: '#div0',
top: tempstyle[1].top,//1
left: tempstyle[1].left,
easing: 'easeInOutCirc',
begin: function (anim) {
anime({
targets: "#div1",
top: tempstyle[0].top,//0
left: tempstyle[0].left,
easing: 'easeInOutCirc',
duration: 450,
})
},
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[0].id = "div1";
father[1].id = "div0";
let existingnode1 = father[1];
let existingnode0 = father[0];
p.insertBefore(existingnode1, existingnode0);
anime({
targets: "#div1",
top: tempstyle[2].top,//2
left: tempstyle[2].left,
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div2",
top: tempstyle[1].top,//1
left: tempstyle[1].left,
easing: 'easeInOutCirc',
duration: 450,
})
},
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[1].id = "div2";
father[2].id = "div1";
let existingnode2 = father[2];
let existingnode1 = father[1];
p.insertBefore(existingnode2, existingnode1);
anime({
targets: "#div2",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[2].top,//2
left: tempstyle[2].left,
color: "rgb(255,255,255)",
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[2].id = "div3";
father[3].id = "div2";
let existingnode2 = father[3];
let existingnode1 = father[2];
p.insertBefore(existingnode2, existingnode1);
}
})
}
})
}
})
}
})
// 觸發第二個div
onClickTo1 = (tempstyle) => anime({
targets: '#div1',
color: 'red',
top: tempstyle[2].top,//2
left: tempstyle[2].left,
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div2",
top: tempstyle[1].top,//1
left: tempstyle[1].left,
easing: 'easeInOutCirc',
duration: 450,
})
},
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[1].id = "div2";
father[2].id = "div1";
let existingnode1 = father[2];
let existingnode0 = father[1];
p.insertBefore(existingnode1, existingnode0);
anime({
targets: "#div2",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[2].top,//2
left: tempstyle[2].left,
color: "rgb(255,255,255)",
easing: 'easeInOutCirc',
duration: 450,
})
},
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[2].id = "div3";
father[3].id = "div2";
let existingnode2 = father[3];
let existingnode1 = father[2];
p.insertBefore(existingnode2, existingnode1);
}
})
}
})
// 觸發第三個div
onClickTo2 = (tempstyle) => anime({
targets: "#div2",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[2].top,//2
left: tempstyle[2].left,
color: "rgb(255,255,255)",
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[2].id = "div3";
father[3].id = "div2";
let existingnode1 = father[3];
let existingnode0 = father[2];
p.insertBefore(existingnode1, existingnode0);
}
})
},
})
// 觸發第五個div
onClickTo4 = (tempstyle) => anime({
targets: "#div4",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[4].top,//4
left: tempstyle[4].left,
color: "rgb(255,255,255)",
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[4].id = "div3";
father[3].id = "div4";
let existingnode1 = father[4];
let existingnode0 = father[3];
p.insertBefore(existingnode1, existingnode0);
}
})
},
})
// 觸發第六個div
onClickTo5 = (tempstyle) => anime({
targets: "#div5",
top: tempstyle[4].top,//4
left: tempstyle[4].left,
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div4",
top: tempstyle[5].top,//5
left: tempstyle[5].left,
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[5].id = "div4";
father[4].id = "div5";
let existingnode1 = father[5];
let existingnode0 = father[4];
p.insertBefore(existingnode1, existingnode0);
anime({
targets: "#div4",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: "easeInOutCirc",
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[4].top,//4
left: tempstyle[4].left,
color: "rgb(255,255,255)",
easing: "easeInOutCirc",
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[4].id = "div3";
father[3].id = "div4";
let existingnode1 = father[4];
let existingnode0 = father[3];
p.insertBefore(existingnode1, existingnode0);
}
})
}
})
}
})
}
})
// 觸發第七個div
onClickTo6 = (tempstyle) => anime({
targets: "#div6",
top: tempstyle[5].top,//5
left: tempstyle[5].left,
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div5",
top: tempstyle[6].top,//6
left: tempstyle[6].left,
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[6].id = "div5";
father[5].id = "div6";
let existingnode1 = father[6];
let existingnode0 = father[5];
p.insertBefore(existingnode1, existingnode0);
anime({
targets: "#div5",
top: tempstyle[4].top,//4
left: tempstyle[4].left,
easing: 'easeInOutCirc',
duration: 450,
begin: function (anim) {
anime({
targets: "#div4",
top: tempstyle[5].top,//5
left: tempstyle[5].left,
easing: 'easeInOutCirc',
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[5].id = "div4";
father[4].id = "div5";
let existingnode1 = father[5];
let existingnode0 = father[4];
p.insertBefore(existingnode1, existingnode0);
anime({
targets: "#div4",
top: tempstyle[3].top,//3
left: tempstyle[3].left,
color: "rgb(228,94,35)",
easing: "easeInOutCirc",
duration: 450,
begin: function (anim) {
anime({
targets: "#div3",
top: tempstyle[4].top,//4
left: tempstyle[4].left,
color: "rgb(255,255,255)",
easing: "easeInOutCirc",
duration: 450,
complete: () => {
var father = document.getElementById("pagParent").children;
let p = document.getElementById("pagParent");
father[4].id = "div3";
father[3].id = "div4";
let existingnode1 = father[4];
let existingnode0 = father[3];
p.insertBefore(existingnode1, existingnode0);
}
})
}
});
}
})
}
})
}
})
}
})
當新的數據來了,我們得處理新的數據,和調用上面六個動畫函數的主函數,於是有了下面的主函數
ch = (i) => {
switch (i) {
case 0: this.onClickTo0(this.props.leftStyleData)
break;
case 1: this.onClickTo1(this.props.leftStyleData)
break;
case 2: this.onClickTo2(this.props.leftStyleData)
break;
case 4: this.onClickTo4(this.props.leftStyleData)
break;
case 5: this.onClickTo5(this.props.leftStyleData)
break;
case 6: this.onClickTo6(this.props.leftStyleData)
break;
}
}
當動畫執行過後,div的在網頁中的位置以及dom節點在html文檔中的順序已經調整一致後,需要將div中的數據同時替換成最新的順序,爲此需要將後臺傳來的數據臨時保存,並且進行數據的深拷貝
todoIndexSmallThree = (index, data) => {
let arr = [].concat(JSON.parse(JSON.stringify(data)));
let temp = arr.splice(0, index + 1);
let temp1 = arr.splice(0, 6 - index);
let arrLast = temp.concat(temp1);
[arrLast[3], arrLast[index]] = [arrLast[index], arrLast[3]];
return arrLast;
}
todoIndexMiddle = (index, data) => {
let arr = [].concat(JSON.parse(JSON.stringify(data)));
let temp = arr.splice(index - 3, 4);
let temp1 = arr.splice(index + 1 - 4, 3);
let arrLast = temp.concat(temp1);
return arrLast;
}
todoIndexSmallLast = (index, data) => {
let arr = [].concat(JSON.parse(JSON.stringify(data)));
let temp = arr.splice(index, data.length - index);
let temp1 = arr.splice(arr.length - (7 - temp.length), arr.length - 1);
let arrLast = temp.concat(temp1);
[arrLast[0], arrLast[3]] = [arrLast[3], arrLast[0]];
return arrLast;
}
todoData = (index, arr) => {
var newData = [];
switch (index) {
case 0: {
// 深拷貝一個數組
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[1];
let tag1 = newArr[2];
let tag2 = newArr[3];
let tag3 = newArr[0];
let tag4 = newArr[4];
let tag5 = newArr[5];
let tag6 = newArr[6];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
case 1: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[0];
let tag1 = newArr[2];
let tag2 = newArr[3];
let tag3 = newArr[1];
let tag4 = newArr[4];
let tag5 = newArr[5];
let tag6 = newArr[6];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
case 2: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[0];
let tag1 = newArr[1];
let tag2 = newArr[3];
let tag3 = newArr[2];
let tag4 = newArr[4];
let tag5 = newArr[5];
let tag6 = newArr[6];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
case 3: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
newData = newArr;
} break
case 4: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[0];
let tag1 = newArr[1];
let tag2 = newArr[2];
let tag3 = newArr[4];
let tag4 = newArr[3];
let tag5 = newArr[5];
let tag6 = newArr[6];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
case 5: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[0];
let tag1 = newArr[1];
let tag2 = newArr[2];
let tag3 = newArr[5];
let tag4 = newArr[3];
let tag5 = newArr[4];
let tag6 = newArr[6];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
case 6: {
var newArr = [].concat(JSON.parse(JSON.stringify(arr)));
let tag0 = newArr[0];
let tag1 = newArr[1];
let tag2 = newArr[2];
let tag3 = newArr[6];
let tag4 = newArr[3];
let tag5 = newArr[4];
let tag6 = newArr[5];
newData = [tag0, tag1, tag2, tag3, tag4, tag5, tag6];
} break;
}
return newData;
}
當後臺傳來的數據不在現有的數據集中,也就是不在左側的div中,我們不單要更換數據,還要從html結構中刪除這7個div,因爲只有生成新的div順序關係,才能確定下一波數據來臨之後執行什麼樣的動畫。這裏由於採用了iconfont,還需要利用js的生成iconfont圖標,這是一個麻煩的問題。
for (let i = childs.length - 1; i >= 0; i--) {
if (n === 3) {
parent.removeChild(childs[i]);
let div = document.createElement("div");
let icon = document.createElement("i");
let ptitle = document.createElement("p");
parent.appendChild(div);
div.appendChild(icon);
div.appendChild(ptitle);
div.style.color = "rgb(228,94,35)";
icon.className = `${styles.iconfont}`;
icon.style.fontSize = this.props.IconfontStyle.size;
icon.innerHTML = "";
icon.style.paddingTop = this.props.IconfontStyle.paddingTop;
icon.style.paddingLeft = "6%";
ptitle.innerHTML = defaultData[n].logicEntityGroupName;
div.id = `div${n}`;
} else {
parent.removeChild(childs[i]);
let div = document.createElement("div");
let icon = document.createElement("i");
let ptitle = document.createElement("p");
parent.appendChild(div);
div.appendChild(icon);
div.appendChild(ptitle);
div.style.color = "rgb(255,255,255)";
icon.className = `${styles.iconfont}`;
icon.style.fontSize = this.props.IconfontStyle.size;
icon.innerHTML = "";
icon.style.paddingTop = this.props.IconfontStyle.paddingTop;
icon.style.paddingLeft = "6%";
ptitle.innerHTML = defaultData[n].logicEntityGroupName;
div.id = `div${n}`;
}
n++;
}