Vue3寫一個倒計時組件
vue3 beta版本發布已有一段時間了,文檔也大概看了一下,不過對於學一門技術,最好的方法還是實戰,於是找了一個比較簡單的組件用vue3來實現,參考的是vant的countdown組件。
Vue Composition API文檔:
如果對vue3語法還不熟悉的,可以先看一下語法
目前文檔還是英文的,這里簡單翻譯下幾個比較核心的東西:
setup
setup函數是一個新的組件option選項,作為在組件內使用Composition API的入口。
1. 調用時機
setup會在組件實例創建並且初始props解析后立即調用。對於生命周期這一層面,會在beforeCreate鈎子之前調用。
2. 配合template使用
如果setup返回一個對象,那么對象的屬性會被合並到當前組件的render上下文。
從setup中返回的refs在template中獲取值時會被自動unwrapped(猜測可能是get值時用了unref。unref也是一個新的api,val = isRef(val) ? val.value : val的語法糖),因此在模板中取值時無需加上.value。
3. 配合render函數使用
setup里不僅可以返回一個對象,也可以直接返回一個render函數。不過要注意的是,跟template會自己unwrapped不同,在render中使用refs的值時,需要加上.value。
4. 參數
setup接收兩個參數,第一個是用的最多的props,第二是ctx上下文,不過是精簡版,只提供了三個屬性attrs,slots,emit,這三個都是寫組件必不可少的。
- emit: 同父組件通信用
- slots: 插槽分發內容
- attrs: 可以用於封裝高階組件, 配合v-bind進行屬性透傳
5. this在setup中不可用
在vue2,你可以在每一個生命周期或者方法中通過this獲取當前實例,不過在setup方法中是無法獲取到this的。但是,可以通過getCurrentInstance獲取到當前實例。
reactive
傳入一個對象並返回對目標源的響應式代理結果,等同於2版本的Vue.observable()。
ref
類似reactive,但是傳入的是基本值,取值時需要加上.vaule去獲取,而reactive包裹的對象可以直接像對象那樣去獲取值。
不過當數據結構是數組或者Map時,即使數組已經被reactive包裹了,如果數組里面的某一項是ref,依然需要通過.value去獲取值。
//ref
const count = ref(0)
console.log(count.value);
//reactive
const state = reactive({
count
})
console.log(state.count);
//reactive with array
const arr = reactive([ref(0),3,5])
// need .value here
console.log(arr[0].value)
computed
接收一個回調函數作為getter,或者傳入帶有getter和setter對象 。一個computed會返回一個對象,有多個computed時就需要調用多次
const plusOne = computed(() => count.value + 1)
const plusTwo = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
toRef, toRefs
toRef可用於為源響應對象上的某個屬性創建ref。
export default {
//由於Javascript函數參數是按值傳遞,所以如果傳遞的是基本類型,傳參可以理解為復制變量值。基本類型復制后倆個變量完全獨立,之后任何一方改變都不會影響另一方。如果直接傳遞props.numner也就是10進去,函數內部跟外部是獨立的,函數里面的操作無法影響到外部變量,除非你傳遞的是一個對象比如整個props,才能保持引用。但是如果你只需要某個屬性,傳整個進去也是沒必要的。此時toRef就顯的很有用了。toRef返回的就是一個對象,通過這個對象.value可以獲取到值。
setup(props) {
// {number:10}
useSomeFeature(toRef(props, 'number'))
}
}
toRefs可以將響應式對象轉換為普通對象,其中結果對象上的每個屬性都是指向原始對象中相應屬性的引用。可以用於解構的時候防止丟失響應。
生命周期
生命周期大部分都改了名字,寫法上也稍有不同。
- 需要自己import;
- 在setup中調用;
- beforeCreate -> use setup()
- created -> use setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
- errorCaptured -> onErrorCaptured
語法就大概介紹這些,具體的內容還是要看看官方文檔。下面看一下組件的具體代碼
<script>
import { h, reactive, onMounted, onBeforeUnmount, toRef } from "vue";
import { formatTime } from "./utils";
export default {
props: {
time: {
type: Number,
default: 0
},
millisecond: {
type: Boolean,
default: false
},
autoStart: {
type: Boolean,
default: true
}
},
setup(props, { emit }) {
const interval = props.millisecond ? 16 : 1000;
let leftTime = toRef(props, "time").value;
let autoStart = toRef(props, "autoStart").value;
let ticker = null;
let timeData = reactive(formatTime(leftTime));
const updateTime = (timeData, leftTime) => {
const data = formatTime(leftTime);
Object.keys(timeData).forEach(k => {
timeData[k] = data[k];
});
};
const start = () => {
if (!ticker && leftTime > 0) {
ticker = setInterval(() => {
leftTime -= interval;
if (leftTime <= 0) {
leftTime = 0;
clearInterval(ticker);
emit("finish");
} else {
emit("change", leftTime);
}
updateTime(timeData, leftTime);
}, interval);
}
};
const stop = () => {
clearInterval(ticker);
ticker = null;
};
const restart = () => {
clearInterval(ticker);
ticker = null;
leftTime = props.time;
emit("change", leftTime);
updateTime(timeData, leftTime);
start();
};
onMounted(() => {
autoStart && start();
});
onBeforeUnmount(() => {
stop();
});
return {
timeData,
start,
stop,
restart
};
},
render({ $slots, timeData, millisecond }) {
const time = ["hours", "minutes", "seconds", "millisecond"]
.filter(v => v != "millisecond" || millisecond)
.map(v => timeData[v])
.join(":");
const defaultContent = h(
"span",
{
style: { fontSize: "14px", color: "#333" }
},
time
);
return h(
"div",
($slots.default && $slots.default(timeData)) || defaultContent
);
}
};
</script>
主要的變化還是在setup,沒有data,也沒有methods了,都需在setup里面返回才可以使用。基本上絕大部門的代碼都寫在setup里面,包括事件,生命周期等, 當然這也很變量的作用域有關。也可以考慮把邏輯抽取出去,不過傳參的時候,需要使用toRef或者toRefs,不能傳基本值。
高階組件
主要是用attrs來實現屬性的綁定,但是具體是不是這樣寫,我還不太確定。
<template>
<div>
<p>{{ word }}</p>
<countdown v-bind="attrs"></countdown>
</div>
</template>
<script>
import Countdown from "../components/countdown";
export default {
components: {
Countdown
},
props: {
word: {
type: String
}
},
setup(props, { attrs }) {
return {
attrs
};
}
};
</script>
DEMO
<template>
<div class="home">
<p>基本使用</p>
<countdown :time="66000"></countdown>
<p>毫秒數</p>
<countdown :time="44444" millisecond></countdown>
<p>獲取組件實例以及方法</p>
<countdown :time="66000" ref="count" :auto-start="false"></countdown>
<button @click="count.start()">開始</button>
<button @click="count.stop()">關閉</button>
<button @click="count.restart()">重啟</button>
<p>使用插槽自定義樣式</p>
<countdown :time="66000" @change="change">
<template v-slot="timeData">
<span class="block">{{ timeData.hours }}</span>
<span class="colon">:</span>
<span class="block">{{ timeData.minutes }}</span>
<span class="colon">:</span>
<span class="block">{{ timeData.seconds }}</span>
</template>
</countdown>
<p>高階組件</p>
<higher :time="8888" word="測試higher" />
</div>
</template>
<script>
import Countdown from "../components/countdown";
import Higher from "../components/higher";
import { ref, onMounted } from "vue";
export default {
name: "Home",
components: {
Countdown,
Higher
},
setup() {
const change = () => {};
const count = ref(null);
onMounted(() => {
console.dir(count.value);
});
return {
change,
count
};
}
};
</script>