前文再續:【🚨萬字警告】了不得的Vue3(上)javascript
一塊兒看看Vue3給咱們帶來了哪些值得關注的新東西。css
首先固然是萬衆矚目的Composition API。html
爲此,我搬運瞭然叔的一晚上動畫~前端
咱們先回顧一下在Vue2中OptionsAPI是怎麼寫的: vue
隨着產品迭代,產品經理不斷提出了新的需求: java
因爲相關業務的代碼須要遵循option的配置寫到特定的區域,致使後續維護很是的複雜,代碼可複用性也不高。最難受的是敲代碼的時候不得不上下反覆橫跳,晃得眼瞎...node
用了CompositionAPI會變成什麼樣呢? react
咱們能夠看到,功能相關的代碼都聚合起來了,代碼變得井井有理,再也不頻繁地上下反覆橫跳。但還差點意思,事實上,咱們不少邏輯相關的操做是不須要體現出來的,真正須要使用到的可能只是其中的一些變量、方法,而Composition API帶來的出色代碼組織和複用能力,讓你能夠把功能相關的代碼抽離出去成爲一個可複用的函數JS、TS文件,在.vue文件中經過函數的調用把剛剛這些函數的返回值組合起來,最後返回模板真正須要使用到的東西: webpack
巴適得很~git
Composition API爲什麼這麼好用,得益於它的兩個核心組成:
響應式系統暴露了更多底層的API出來,從而讓咱們很輕鬆地去建立使用響應式變量。而後結合暴露出來的生命週期鉤子,基本就能夠完成整個組件的邏輯運做。固然還能夠結合更多的api完成更復雜的工做,社區也有不少關於CompositionAPI的使用技巧和方法,這一塊就不去細化了,點到爲止。
對比Class API:
更好的 TypeScript 類型推導支持
function對於類型系統是很是友好的,尤爲是函數的參數和返回值。
代碼更容易被壓縮
代碼在壓縮的時候,好比對象的key是不會進行壓縮的,這一點能夠從咱們剛剛對於Three shaking demo構建出來的包就能夠看得出來:
而composition API聲明的一些響應式變量,就能夠很安全地對變量名進行壓縮。
Tree-shaking 友好
CompositionAPI這種引用調用的方式,構建工具能夠很輕鬆地利用Tree shaking去消除咱們實際未使用到 「死代碼「
更靈活的邏輯複用能力
在Vue2中,咱們一直缺乏一種很乾淨方便的邏輯複用方法。
以往咱們要想作到邏輯複用,主要有三種方式:
爲了更好地體會這三種方法的噁心之處,我用一個簡單的demo去分別演示這三種方法。
案例:鼠標位置偵聽:
先看看Mixins的方式:
MouseMixin.js:
import {throttle} from "lodash"
let throttleUpdate;
export default {
data:()=>({
x:0,
y:0
}),
methods:{
update(e){
console.log('still on listening')
this.x = e.pageX
this.y = e.pageY
}
},
beforeMount() {
throttleUpdate = throttle(this.update,200).bind(this)
},
mounted() {
window.addEventListener('mousemove',throttleUpdate)
},
unmounted() {
window.removeEventListener('mousemove',throttleUpdate)
}
}
複製代碼
使用:
<template>
<header> <h1>獲取鼠標位置——Mixins</h1> </header>
<main> <span>(</span> <transition name="text" mode="out-in"> <div :key="x" class="position">{{ x }}</div> </transition> <span>,</span> <transition name="text" mode="out-in"> <div :key="y" class="position">{{ y }}</div> </transition> <span>)</span> </main>
</template>
<script> import {defineComponent} from "vue" import MouseMixin from "@/components/Mouse/MouseMixin.js"; export default defineComponent({ mixins: [MouseMixin], components: {} }) </script>
複製代碼
當大量使用mixin時:
HOC在React使用得比較多,它是用來替代mixin的方案。事實上Vue也能夠寫HOC。
其原理就是在組件外面再包一層父組件,複用的邏輯在父組件中,經過props傳入到子組件中。
看看這個帶有可複用邏輯的MouseHOC怎麼寫:
import Mouse2 from "@/views/Mouse/Mouse2.vue";
import { defineComponent } from "vue";
import { throttle } from "lodash";
let throttleUpdate;
export default defineComponent({
render() {
return (
<Mouse2 x={this.x} y={this.y}/>
);
},
data: () => ({
x: 0,
y: 0,
}),
methods: {
update(e) {
this.x = e.pageX;
this.y = e.pageY;
},
},
beforeMount() {
throttleUpdate = throttle(this.update, 200).bind(this);
},
mounted() {
window.addEventListener("mousemove", throttleUpdate);
},
unmounted() {
window.removeEventListener("mousemove", throttleUpdate);
},
});
複製代碼
HOC內部的子組件——Mouse2.vue:
<template>
<header> <h1>獲取鼠標位置——HOC</h1> </header>
<main> <span>(</span> <transition name="text" mode="out-in"> <div :key="x" class="position">{{ x }}</div> </transition> <span>,</span> <transition name="text" mode="out-in"> <div :key="y" class="position">{{ y }}</div> </transition> <span>)</span> </main>
</template>
<script lang="ts"> import {defineComponent} from "vue" export default defineComponent({ props:['x','y'] }) </script>
複製代碼
一樣,在大量使用HOC的時候的問題:
原理就是經過一個無需渲染的組件——renderless component,經過做用域插槽的方式把可複用邏輯輸出的內容放到slot-scope
中。
看看這個無渲染組件怎麼寫:
<template>
<slot :x="x" :y="y"></slot>
</template>
<script> import {throttle} from "lodash"; let throttleUpdate; export default { data:()=>({ x:0, y:0 }), methods:{ update(e){ console.log('still on listening') this.x = e.pageX this.y = e.pageY } }, beforeMount() { throttleUpdate = throttle(this.update,200).bind(this) }, mounted() { window.addEventListener('mousemove',throttleUpdate) }, unmounted() { window.removeEventListener('mousemove',throttleUpdate) } } </script>
複製代碼
在頁面組件Mouse3.vue
中使用:
<template>
<header> <h1>獲取鼠標位置——slot</h1> </header>
<main> <span>(</span> <MouseSlot v-slot="{x,y}"> <transition name="text" mode="out-in"> <div :key="x" class="position">{{ x }}</div> </transition> <span>,</span> <transition name="text" mode="out-in"> <div :key="y" class="position">{{ y }}</div> </transition> </MouseSlot> <span>)</span> </main>
</template>
<script lang="ts"> import {defineComponent} from "vue" import MouseSlot from "@/components/Mouse/MouseSlot.vue" export default defineComponent({ components: { MouseSlot } }) </script>
複製代碼
當大量使用時:
雖然無渲染組件已是一種比較好的方式了,但寫起來仍然蠻噁心的。
因此,在Composition API中,怎麼作到邏輯複用呢?
暴露一個可複用函數的文件:useMousePosition.ts
,這個命名只是讓他看起來更像react hooks一些,一眼就能看出來這個文件這個函數是幹什麼的,實際上你定義爲其餘也不是不能夠。
import {ref, onMounted, onUnmounted} from "vue"
import {throttle} from "lodash"
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = throttle((e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}, 200)
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
複製代碼
頁面組件Mouse4.vue
中使用:
<template>
<header> <h1>獲取鼠標位置——Composition API</h1> </header>
<main> <span>(</span> <transition name="text" mode="out-in"> <div :key="x" class="position">{{ x }}</div> </transition> <span>,</span> <transition name="text" mode="out-in"> <div :key="y" class="position">{{ y }}</div> </transition> <span>)</span> </main>
</template>
<script lang="ts"> import {defineComponent} from "vue" import useMousePosition from "@/components/Mouse/useMousePosition"; export default defineComponent({ setup() { const { x, y } = useMousePosition() return { x, y } } }) </script>
複製代碼
即便在大量使用時:
乾淨、清晰。
除此以外,這種函數式也給予了優秀的代碼組織能力。
爲了演示這一點,我把Vue2示例中的todoMVC
項目搬下來用CompositionAPI重構了一下。
todoMVC
就是一個待辦事項的小應用,功能有:
(刁鑽的朋友可能發現我把編輯功能閹割掉了,這裏確實偷了個懶,當時寫得比較着急,又由於一些兼容性的緣由,編輯狀態點不出來,一氣之下把編輯閹了....其實有沒有也不太影響我想要說明的東西)
來碼,整個代辦事項組件:TodoMVC.vue
import {defineComponent} from "vue"
import useTodoState from "@/views/TodoMVC/useTodoState";
import useFilterTodos from "@/views/TodoMVC/useFilterTodos";
import useHashChange from "@/views/TodoMVC/useHashChange";
export default defineComponent({
setup() {
/*響應式變量、新增和刪除代辦事項的方法*/
const {
todos,
newTodo,
visibility,
addTodo,
removeTodo
} = useTodoState()
// 篩選數據、一鍵所有完成/未完成、清空所有已完成事項
const {
filteredTodos,
remaining,
allDone,
filters,
removeCompleted
} = useFilterTodos(todos, visibility)
// 監聽路由哈希變化
useHashChange(filters, visibility)
return {
todos,
newTodo,
filteredTodos,
remaining,
allDone,
visibility,
removeCompleted,
addTodo,
removeTodo,
}
},
})
複製代碼
useTodoState
中又調用了一個本地存儲
邏輯相關的composition function:useTodoStorage.ts
useTodoState.ts
:
import { Todo, Visibility } from "@/Types/TodoMVC";
import { ref, watchEffect, } from "vue"
import useTodoStorage from "@/views/TodoMVC/useTodoStorage";
export default function useTodoState() {
const { fetch, save, uid } = useTodoStorage()
// 所有事項
const todos = ref(fetch())
// 即將新增事項的內容
const newTodo = ref("")
// 新增代辦事項
const addTodo = () => {
const value = newTodo.value && newTodo.value.trim()
if (!value) {
return;
}
todos.value.push({
id: uid.value,
title: value,
completed: false
})
uid.value += 1
newTodo.value = ""
}
// 刪除代辦事項
const removeTodo = (todo: Todo) => {
todos.value.splice(todos.value.indexOf(todo), 1)
}
// 使用todos.value的反作用去動態保存代辦事項到本地緩存中
watchEffect(() => {
save(todos.value)
})
// 當前篩選的類型(url的hash值與此值一致)
const visibility = ref<Visibility>("all")
return {
todos,
newTodo,
visibility,
addTodo,
removeTodo
}
}
複製代碼
用於本地緩存的useTodoStorage.ts
:
import {Todo} from "@/Types/TodoMVC";
import {ref, watchEffect} from "vue"
export default function useTodoStorage() {
const STORAGE_KEY = 'TodoMVC——Vue3.0'
// 獲取LocalStorage中的數據
const fetch = (): Todo[] => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
// 數據存儲到LocalStorage中
const save = (todos: Todo[]) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
// 用於新增代辦事項的id自動生成
const uid = ref(~~(localStorage.getItem('uid') || 0));
watchEffect(() => localStorage.setItem('uid', uid.value.toString()))
return {
fetch,
save,
uid
}
}
複製代碼
其餘就不一一展現了,代碼最終都放在文末的連接中的github倉庫裏了,感興趣的能夠細品。這個demo由於寫得比較倉促,自我感受寫得不咋滴,邏輯的組織有待商榷,這也從側面展現了composition API給咱們帶來的高靈活組織和複用能力,至於如何把代碼組織得更漂亮就是開發者本身的事了,我也在試圖慢慢摸索出寫得更舒服的最佳實踐。
不可置否,Composition API的誕生確實受到了React Hooks的啓發,若是所以就貼上抄襲的標籤就未免太流於表面了,也不想在此去引戰。框架都是好框架,前端圈內要以和爲貴,互相借鑑學習難道很差嗎,不要搞窩裏鬥。
事實上,Composition API的實現與使用方式也都是大相徑庭的,懂得天然懂。
與React Hooks的對比也已經有很多文章說得挺詳細了,這裏就再也不進行贅述。
簡單來講就是得益於響應式系統,Composition API 使用的心智負擔相比之下實在是小太多了。
這個新特性比較簡單,就是在模板中能夠寫多個根節點。至於它的意義:
第二個意義比較重要,利用這個新特性,好比能夠寫一個騷氣的快速排序組件。
QuickSort.vue
:
<template>
<quick-sort :list="left" v-if="left.length"></quick-sort>
<span class="item">{{ flag }}</span>
<quick-sort :list="right" v-if="right.length"></quick-sort>
</template>
<script lang="ts"> import {defineComponent, ref} from "vue" export default defineComponent({ name: 'quick-sort', props: ["list"], setup(props) { // eslint-disable-next-line vue/no-setup-props-destructure const flag: number = props.list[0] const left = ref<number[]>([]) const right = ref<number[]>([]) setTimeout(() => { props.list.slice(1).forEach((item: number) => { item > flag ? right.value.push(item) : left.value.push(item) }) }, 100) return { flag, left, right } } }) </script>
複製代碼
在頁面組件Fragment.vue
中使用:
<template>
<h1>快速排序</h1>
<h2> {{ list }} </h2>
<div> <button @click="ok = !ok">SORT</button> </div>
<hr>
<template v-if="ok"> <QuickSort :list="list"></QuickSort> </template>
</template>
<script lang="ts"> import QuickSort from "@/components/QuickSort.vue"; import {defineComponent, ref} from "vue" import {shuffle} from "lodash" export default defineComponent({ components: { QuickSort }, setup() { const ok = ref(false) const list = ref<number[]>([]) for (let i = 1; i < 20; i++){ list.value.push(i) } list.value = shuffle(list.value) return {list, ok} } }) </script> 複製代碼
向QuickSort
中傳入一個長度爲20被打亂順序的數組:
能夠看到,每一個遞歸的組件都是平級的:
而在Vue2中的遞歸組件每每是層層嵌套的,由於它只能存在一個根元素,一樣的寫法在Vue2中將會報錯。
利用這一特性,咱們就能夠寫一個乾淨的樹組件等等了。
能夠理解爲異步組件的爹。用於方便地控制異步組件的一個掛起和完成狀態。
直接上代碼,
首先是一個異步組件,AsyncComponent.vue
:
<template>
<h2>AsyncComponent</h2>
</template>
<script lang="ts"> import {defineComponent} from "vue" export default defineComponent({ props: { timeout:{ type: Number, required: true } }, async setup(props) { const sleep = (timeout: number) => { return new Promise(resolve => { setTimeout(resolve, timeout) }) } await sleep(props.timeout) } }) </script>
複製代碼
在頁面組件Suspense.vue
中:
<template>
<h1>Suspense</h1>
<Suspense> <template #default> <AsyncComponent :timeout="5000"/> </template> <template #fallback> <p class="loading">loading {{ loadingStr }}</p> </template> </Suspense>
</template>
<script lang="ts"> import {defineComponent} from "vue" import AsyncComponent from "@/components/AsyncComponent.vue" import useLoading from "@/composables/useLoading"; export default defineComponent({ components: { AsyncComponent }, setup() { const {loading: loadingStr} = useLoading() return {loadingStr} } }) </script>
複製代碼
簡單來講,就是用Vue3提供的內置組件:Suspense
將異步組件包起來,template #default
中展現加載完成的異步組件,template #fallback
中則展現異步組件掛起狀態時須要顯示的內容。
看看效果:
理解爲組件任意門,讓你的組件能夠任意地丟到html中的任一個DOM下。在react中也有相同功能的組件——Portal,之因此更名叫Teleport是因爲html也準備提供一個原生的protal標籤,爲了不重名就叫作Teleprot了。
利用這個特性,咱們能夠作的事情就比較有想象空間了。例如,寫一個Ball
組件,讓它在不一樣的父組件中呈現不同的樣式甚至是邏輯,這些樣式和邏輯能夠寫在父組件中,這樣當這個Ball組件被傳送到某個父組件中,就能夠將父組件對其定義的樣式和邏輯應用到Ball組件中了。再例如,能夠在任意層級的組件中寫一個須要掛載到外面去的子組件,好比一個Modal彈窗,雖然掛載在當前組件下也能夠達到效果,可是有時候當前組件的根節點的樣式可能會與之發生一些干擾或者衝突。
這裏,我寫了一個Modal彈窗的demo:
<template>
<h1>Teleport——任意門</h1>
<div class="customButton" @click="handleToggle">偷襲</div>
<teleport to="body"> <TeleportModal v-if="isOpen" @click="handleToggle"></TeleportModal> </teleport>
</template>
<script lang="ts"> import {defineComponent, ref} from "vue" import TeleportModal from "@/components/TeleportModal.vue" export default defineComponent({ components: { TeleportModal }, setup() { const isOpen = ref(false) const handleToggle = () => { isOpen.value = !isOpen.value } return { isOpen, handleToggle } } }) </script>
複製代碼
用Vue3內置的Teleport
組件將須要被傳送的Modal組件包起來,寫好要被傳送到的元素選擇器。(有點像寄快遞,用快遞盒打包好,寫上收貨地址,起飛)
看看這個demo的效果:
能夠看到,馬保國確實被踢到body下面去了(🐶)。
利用這個API,在Vue3中咱們能夠自由方便地去構建Web(瀏覽器)平臺或非Web平臺的自定義渲染器。
原理大概就是:將Virtual DOM和平臺相關的渲染分離,經過createRendererAPI咱們能夠自定義Virtual DOM渲染到某一平臺中時的全部操做,好比新增、修改、刪除一個「元素」,咱們能夠這些方法中替換或修改成咱們自定義的邏輯,從而打造一個咱們自定義的渲染器。
固然,在web平臺下是相對比較簡單的,由於能夠利用Vue的runtime-dom
給咱們提供的一個上層的抽象層,它幫咱們完成了Virtual DOM渲染到Web DOM中的復雜瀏覽器接口編程操做,咱們只須要在createRenderer的參數中傳入一些自定義的邏輯操做便可自動完成整合,好比你能夠在createElement
方法中加一段本身的邏輯: 這樣在每次建立新元素的時候都會跟你「打招呼」。
調用createRenderer之後的返回值是一個renderer,createApp這個方法就是這個renderer的一個屬性方法,用它替代原生的createApp方法就可使用咱們本身的自定義渲染器了~
爲此,我準備了一個用Three.js和自定義渲染器實現的3D方塊demo,而且用composition API將咱們以前寫的偵聽鼠標位置的邏輯複用過來,讓這個3D方塊跟着咱們的鼠標旋轉。
首先,寫一個自定義渲染器:renderer.js
:
import { createRenderer } from '@vue/runtime-dom'
import * as THREE from 'three'
let webGLRenderer
// Three.js相關
function draw(obj) {
const {camera,cameraPos, scene, geometry,geometryArg,material,mesh,meshY,meshX} = obj
if([camera,cameraPos, scene, geometry,geometryArg,material,mesh,meshY,meshX].filter(v=>v).length<9){
return
}
let cameraObj = new THREE[camera]( 40, window.innerWidth / window.innerHeight, 0.1, 10 )
Object.assign(cameraObj.position,cameraPos)
let sceneObj = new THREE[scene]()
let geometryObj = new THREE[geometry]( ...geometryArg)
let materialObj = new THREE[material]()
let meshObj = new THREE[mesh]( geometryObj, materialObj )
meshObj.rotation.x = meshX
meshObj.rotation.y = meshY
sceneObj.add( meshObj )
webGLRenderer.render( sceneObj, cameraObj );
}
const { createApp } = createRenderer({
insert: (child, parent, anchor) => {
if(parent.domElement){
draw(child)
}
},
createElement:(type, isSVG, isCustom) => {
alert('hi Channing~')
return {
type
}
},
setElementText(node, text) {},
patchProp(el, key, prev, next) {
el[key] = next
draw(el)
},
parentNode: node => node,
nextSibling: node => node,
createText: text => text,
remove:node=>node
});
// 封裝一個自定義的createApp方法
export function customCreateApp(component) {
const app = createApp(component)
return {
mount(selector) {
webGLRenderer = new THREE.WebGLRenderer( { antialias: true } );
webGLRenderer.setSize( window.innerWidth, window.innerHeight );
const parentElement = document.querySelector(selector) || document.body
parentElement.appendChild( webGLRenderer.domElement );
app.mount(webGLRenderer)
}
}
}
複製代碼
App.vue
,這裏寫一些對真實DOM的操做邏輯,好比我把meshX
和meshY
設置爲了獲取鼠標位置這個composition function 返回的鼠標x和y的計算屬性值(爲了減少旋轉的靈敏度)。
<template>
<div camera="PerspectiveCamera" :cameraPos={z:1} scene="Scene" geometry="BoxGeometry" :geometryArg="[0.2,0.2,0.2]" material="MeshNormalMaterial" mesh="Mesh" :meshY="y" :meshX="x" > </div>
</template>
<script> import {computed} from 'vue' import useMousePosition from "./useMousePosition"; export default { setup() { const {x: mouseX, y: mouseY} = useMousePosition() const x = computed(() => (mouseY.value)/200) const y = computed(() => (mouseX.value)/200) return {x,y} } } </script>
<style> body { padding: 0; margin: 0; overflow: hidden; } </style>
複製代碼
最後,在main.js中使用咱們剛剛在renderer.js
中封裝的帶有自定義渲染器的customCreateApp
方法替換普通的createApp方法,便可:
import { customCreateApp } from './renderer';
import App from "./App.vue";
customCreateApp(App).mount("#app")
複製代碼
咱們看看最終的效果:
因缺思廳!
最後,號稱面向將來的構建工具Vite。
yarn dev
啪地一下應用就起來了,很快啊。
它的原理就是一個基於瀏覽器原生 ES imports 的開發服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,徹底跳過了打包這個概念,服務器隨起隨用。支持 .vue文件 和熱更新,而且熱更新的速度不會隨着模塊增多而變慢。
固然,生產環境的構建仍是使用的rollup進行打包。它的香是在於開發環境的調試速度。
爲了更好地理解它的工做原理,我找了蝸牛老溼畫的一張圖:
而後,我建立了一個vite的演示demo,用來看看Vite是怎麼處理咱們的文件的。
yarn create vite-app vite-demo
cd vite-demo && yarn && yarn dev
複製代碼
看到localhost的請求結果,依然是保留了ES Module類型的代碼
而後Vite的服務器攔截到你對main.js
的請求,而後返回main.js的內容給你,裏面依然是ES Module的類型,
又攔截到vue.js
、App.vue
,繼續返回相應的內容給你,如此類推……
因此Vite應用啓動的過程徹底跳過了打包編譯,讓你的應用秒起。文件的熱更新也是如此,好比當你修改了App.vue的內容,它又攔截給你返回一個新的編譯事後的App.vue文件:
對於大型的項目來講,這種毫秒級的響應實在是太舒服了。去年參與過一個內部組件庫的開發工做,當時是修改的webpack插件,每次修改都得重啓項目,每次重啓就是四五分鐘往上,簡直感受人要裂開。
固然,也不至於到能夠徹底取代Webpack的誇張地步,由於Vite仍是在開發階段,許多工程化的需求仍是難以知足的,好比Webpack豐富的周邊插件等等。
感謝你們能夠耐心地讀到這裏。
固然,文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也很是歡迎你們在評論交流。
文中所用的全部Demo都已放在:GitHub傳送門。
最後,但願朋友們能夠點贊評論關注三連,由於這些就是我分享的所有動力來源🙏