我們知道在vue3中匯出了一個神奇的createVNode 函數 當前函數它能建立一個vdom,大家不要小看vdom, 我們好好利用它,就能做出意想不到的效果比如我們要實現一個彈窗元件
我們通常的思路是寫一個元件在專案中參照進來,通過v-model來控制他的顯示隱藏,但是這樣有個問題,我們複用的時候的成本需要複製貼上。我們沒有辦法來提高效率,比如封裝成npm 通過呼叫js來使用。【相關推薦:】
然而,有了 createVNode 和render 之後所有問題就迎刃而解了
// 我們先寫一個彈窗元件 const message = { setup() { const num = ref(1) return { num } }, template: `<div> <div>{{num}}</div> <div>這是一個彈窗</div> </div>` }
// 初始化元件生成vdom const vm = createVNode(message) // 建立容器,也可以用已經存在的 const container = document.createElement('div') //render通過patch 變成dom render(vm, container) // 彈窗掛到任何你想去的地方 document.body.appendChild(container.firstElementChild)
經過上面這一通騷操作,我們發現我們可以將他封裝為一個方法,放到任何想放的地方。
檔案上說了,在絕大多數情況下,Vue 推薦使用模板語法來搭建 HTML。然而在某些使用場景下,我們真的需要用到 JavaScript 完全的程式設計能力。這時渲染函數
就派上用場了。
jsx和模板語法的優勢對比
jsx和模板語法都是vue 支援的的書寫範疇,然後他們確有不同的使用場景,和方式,需要我們根據當前元件的實際情況,來酌情使用
什麼是JSX
JSX
是一種 Javascript 的語法擴充套件,JSX = Javascript + XML,即在 Javascript 裡面寫 XML,因為 JSX 的這個特性,所以他即具備了 Javascript 的靈活性,同時又兼具 html 的語意化和直觀性
。
模板語法的優勢
JSX的優勢
1、靈活、靈活、靈活(重要的事情說三遍)
2、一個檔案能寫好多個元件
3、只要JS功底好,就不用記憶那麼多命令,上來就是一通輸出
4、JS和JSX混用,方法即宣告即用,對於懂行的人來說邏輯清晰
對比
由於vue對於JSX的支援,社群裡,也是爭論來爭論去,到底要分個高低,然後本渣認為,他倆本來沒有高低,您覺得哪個適合,就用哪個即可,缺點放在對的地方他就是優勢
要發揚咱們老前輩們傳下來的中庸之道,做集大成者,將兩者結合使用,就能發揮無敵功效,亂軍之中博老闆青睞。
接下來說一下本人的一點粗淺理解,我們知道元件型別,分為容器型元件和展示展示型元件
在一般情況下,容器型元件,他由於可能要對於當前展示型元件做一個標準化或者宰包裝,那麼此時容器型元件中用JSX就再好不過
舉個例子:現在有個需求,我們有兩個按鈕,現在要做一個通過後臺資料來選擇展示哪一個按鈕,我們通常的做法,是通過在一個模板中通過v-if去控制不同的元件
然而有了JSX與函數式元件之後,我們發現邏輯更清晰了,程式碼更簡潔了,品質更高了,也更裝X了
我們來看
先整兩個元件
//btn1.vue <template> <div> 這是btn1{{ num }} <slot></slot> </div> </template> <script> import { ref, defineComponent } from 'vue' export default defineComponent({ name: 'btn1', setup() { const num = ref(1) return { num } } }) </script> //btn2.vue <template> <div> 這是btn2{{ num }} <slot></slot> </div> </template> <script> import { ref, defineComponent } from 'vue' export default defineComponent({ name: 'btn2', setup() { const num = ref(2) return { num } } }) </script>
用JSX配合函數式元件來做一個容器元件
// 容器元件 import btn1 from './btn1.vue' import btn2 from './btn2.vue' export const renderFn = function (props, context) { return props.type == 1 ? <btn1>{context.slots.default()}</btn1> : <btn2>{context.slots.default()}</btn2> }
引入業務元件
//業務元件 <template> <renderFn :type="1">1111111</renderFn> </template> <script> import { renderFn } from './components' console.log(renderFn) export default { components: { renderFn }, setup() { }, }; </script>
在善用依賴注入之前是,我們先來了解一些概念,幫助我們更全面的瞭解依賴注入的前世今生
IOC 和DI 是什麼
控制反轉(Inversion of Control,縮寫為IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫「依賴查詢」(Dependency Lookup)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體,將其所依賴的物件的參照傳遞(注入)給它。
什麼是依賴注入
依賴注入 用大白話來說:就是將範例變數傳入到一個物件中去
vue中的依賴注入
在vue中,我們套用依賴注入的概念,其實就是在父元件中宣告依賴,將他們注入到子孫元件範例中去
,可以說是能夠很大程度上代替全域性狀態管理
的存在
前端(vue)入門到精通課程:進入學習
我們先來看看他的基本用法
父元件中宣告provide
//parent.vue <template> <child @setColor="setColor"></child> <button @click="count++">新增</button> </template> <script > import { defineComponent, provide, ref } from "vue"; import Child from "./child.vue"; export default defineComponent({ components: { Child }, setup() { const count = ref(0); const color = ref('#000') provide('count', count) provide('color', color) function setColor(val) { color.value = val } return { count, setColor } } }) </script>
子元件中注入進來
//child.vue //使用inject 注入 <template> <div>這是注入的內容{{ count }}</div> <child1 v-bind="$attrs"></child1> </template> <script> import { defineComponent, inject } from "vue"; import child1 from './child1.vue' export default defineComponent({ components: { child1 }, setup(props, { attrs }) { const count = inject('count'); console.log(count) console.log(attrs) return { count } } }) </script>
正因為依賴注入的特性,我們很大程度上代替了全域性狀態管理
,相信誰都不想動不動就引入那繁瑣的vuex
吧
接下來我們來舉個例子,現在我麼有個頁面主題色,他貫穿所有元件,並且可以在某一些元件內更改主題色,那我們常規的解決方案中,就是裝個vuex然後通過他的api下發顏色值,這時候如果想改,首先要發起dispatch
到Action ,然後在Action
中觸發Mutation
接著在Mutation中再去改state
,如此一來,你是不是發現有點殺雞用牛刀
了,我就改個顏色而已!
我們來看有了依賴注入 應該怎麼處理
首先我們知道vue是單項資料流,也就是子元件不能修改父元件的內容,於是我們就應該想到使用$attrs
使用它將方法透傳給祖先元件,在元件元件中修改即可。
我們來看程式碼
//子孫元件child1.vue <template> <div :style="`color:${color}`" @click="setColor">這是注入的內容的顏色</div> </template> <script> import { defineComponent, inject } from "vue"; export default defineComponent({ setup(props, { emit }) { const color = inject('color'); function setColor() { console.log(0) emit('setColor', 'red') } return { color, setColor } } }) </script>
將當前子孫元件嵌入到child.vue
中去,就能利用簡潔的方式來修改顏色了
眾所周知,vue3最大的新特性,當屬Composition API
也叫組合api ,用好了他,就是你在行業的競爭力,你也有了不世出
的技能
我們一步步來分析
什麼是Composition API
使用 (data
、computed
、methods
、watch
) 元件選項來組織邏輯通常都很有效。然而,當我們的元件開始變得更大時,邏輯關注點的列表也會增長。尤其對於那些一開始沒有編寫這些元件的人來說,這會導致元件難以閱讀和理解。
於是在vue3中為了解決當前痛點,避免在大型專案中出現程式碼邏輯分散,散落在當前元件的各個角落,從而變得難以維護,Composition API橫空出世
所謂Composition API
就是在元件設定物件中宣告 setup
函數,我們可以將所有的邏輯封裝在setup
函數中,然後在配合vue3中提供的響應式API 勾點函數、計算屬性API等,我們就能達到和常規的選項式
同樣的效果,但是卻擁有更清晰的程式碼以及邏輯層面的複用
基礎使用
<template> <div ref="composition">測試compositionApi</div> </template> <script> import { inject, ref, onMounted, computed, watch } from "vue"; export default { // setup起手 setup(props, { attrs, emit, slots, expose }) { // 獲取頁面元素 const composition = ref(null) // 依賴注入 const count = inject('foo', '1') // 響應式結合 const num = ref(0) //勾點函數 onMounted(() => { console.log('這是個勾點') }) // 計算屬性 computed(() => num.value + 1) // 監聽值的變化 watch(count, (count, prevCount) => { console.log('這個值變了') }) return { num, count } } } </script>
通過以上程式碼我們可以看出,一個setup函數我們幹出了在傳統選項式
中的所有事情,然而這還不是最絕的,通過這些api的組合可以實現邏輯複用,這樣我們就能封裝很多通用邏輯,實現複用,早點下班
舉個例子:大家都用過複製剪貼簿的功能,在通常情況下,利用navigator.clipboard.writeText 方法就能將複製內容寫入剪下板。然而,細心的你會發現,其實賦值剪下板他是一個通用功能,比如:你做b端業務的,管理系統中到處充滿了複製id、複製文案等功能。
於是Composition API
的邏輯複用能力就派上了用場
import { watch, getCurrentScope, onScopeDispose, unref, ref } from "vue" export const isString = (val) => typeof val === 'string' export const noop = () => { } export function unrefElement(elRef) { const plain = unref(elRef)// 拿到本來的值 return (plain).$el ?? plain //前面的值為null、undefined,則取後面的值,否則都取前面的值 } export function tryOnScopeDispose(fn) { // 如果有活躍的effect if (getCurrentScope()) { //在當前活躍的 effect 作用域上註冊一個處理回撥。該回撥會在相關的 effect 作用域結束之後被呼叫 //能代替onUmounted onScopeDispose(fn) return true } return false } //帶有控制元件的setTimeout包裝器。 export function useTimeoutFn( cb,// 回撥 interval,// 時間 options = {}, ) { const { immediate = true, } = options const isPending = ref(false) let timer function clear() { if (timer) { clearTimeout(timer) timer = null } } function stop() { isPending.value = false clear() } function start(...args) { // 清除上一次定時器 clear() // 是否在pending 狀態 isPending.value = true // 重新啟動定時器 timer = setTimeout(() => { // 當定時器執行的時候結束pending狀態 isPending.value = false // 初始化定時器的id timer = null // 執行回撥 cb(...args) }, unref(interval)) } if (immediate) { isPending.value = true start() } tryOnScopeDispose(stop) return { isPending, start, stop, } } //輕鬆使用EventListener。安裝時使用addEventListener註冊,解除安裝時自動移除EventListener。 export function useEventListener(...args) { let target let event let listener let options // 如果第一個引數是否是字串 if (isString(args[0])) { //結構內容 [event, listener, options] = args target = window } else { [target, event, listener, options] = args } let cleanup = noop const stopWatch = watch( () => unrefElement(target),// 監聽dom (el) => { cleanup() // 執行預設函數 if (!el) return // 繫結事件el如果沒有傳入就係結為window el.addEventListener(event, listener, options) // 重寫函數方便改變的時候解除安裝 cleanup = () => { el.removeEventListener(event, listener, options) cleanup = noop } }, //flush: 'post' 模板參照偵聽 { immediate: true, flush: 'post' }, ) // 解除安裝 const stop = () => { stopWatch() cleanup() } tryOnScopeDispose(stop) return stop } export function useClipboard(options = {}) { //獲取設定 const { navigator = window.navigator, read = false, source, copiedDuring = 1500, } = options //事件型別 const events = ['copy', 'cut'] // 判斷當前瀏覽器知否支援clipboard const isSupported = Boolean(navigator && 'clipboard' in navigator) // 匯出的text const text = ref('') //匯出的copied const copied = ref(false) // 使用的的定時器勾點 const timeout = useTimeoutFn(() => copied.value = false, copiedDuring) function updateText() { //解析系統剪貼簿的文字內容返回一個Promise navigator.clipboard.readText().then((value) => { text.value = value }) } if (isSupported && read) { // 繫結事件 for (const event of events) useEventListener(event, updateText) } // 複製剪下板方法 //navigator.clipboard.writeText 方法是非同步的返回一個promise async function copy(value = unref(source)) { if (isSupported && value != null) { await navigator.clipboard.writeText(value) // 響應式的值,方便外部能動態獲取 text.value = value copied.value = true timeout.start()// copied.value = false } } return { isSupported, text, copied, copy, } }
這時我們就複用了複製的邏輯,如下程式碼中直接引入在模板中使用即可
<template> <div v-if="isSupported"> <p> <code>{{ text || '空' }}</code> </p> <input v-model="input" type="text" /> <button @click="copy(input)"> <span v-if="!copied">複製</span> <span v-else>複製中!</span> </button> </div> <p v-else>您的瀏覽器不支援剪貼簿API</p> </template> <script setup> import { ref, getCurrentScope } from 'vue' import { useClipboard } from './copy.js' const input = ref('') const { text, isSupported, copied, copy } = useClipboard() console.log(text)// 複製內容 console.log(isSupported)// 是否支援複製剪下板api console.log(copied)//是否複製完成延遲 console.log(copy) // 複製方法 </script>
以上程式碼參考vue版本的Composition API
庫所有完整版請參考
getCurrentInstance
支援存取內部元件範例, 通常情況下他被放在 setup中獲取元件範例,但是getCurrentInstance
只暴露給高階使用場景,典型的比如在庫中。
強烈反對在應用的程式碼中使用 getCurrentInstance
。請不要把它當作在組合式 API 中獲取 this
的替代方案來使用。
那他的作用是什麼呢?
還是邏輯提取,用來代替Mixin,這是在複雜元件中,為了整個程式碼的可維護性,抽取通用邏輯這是必須要去做的事情,我們可以看element-plus 中table的複用邏輯,在邏輯提取中由於涉及獲取props、proxy、emit
以及能通過當前元件獲取父子元件的關係等,此時getCurrentInstance
的作用無可代替
如下element-plus
程式碼中利用getCurrentInstance 獲取父元件parent
中的資料,分別儲存到不同的變數中,我們只需要呼叫當前useMapState即可拿到資料
// 儲存資料的邏輯封裝 function useMapState<T>() { const instance = getCurrentInstance() const table = instance.parent as Table<T> const store = table.store const leftFixedLeafCount = computed(() => { return store.states.fixedLeafColumnsLength.value }) const rightFixedLeafCount = computed(() => { return store.states.rightFixedColumns.value.length }) const columnsCount = computed(() => { return store.states.columns.value.length }) const leftFixedCount = computed(() => { return store.states.fixedColumns.value.length }) const rightFixedCount = computed(() => { return store.states.rightFixedColumns.value.length }) return { leftFixedLeafCount, rightFixedLeafCount, columnsCount, leftFixedCount, rightFixedCount, columns: store.states.columns, } }
$attrs
現在包含了所有傳遞給元件的 attribute,包括 class
和 style
。
$attrs在我們開發中到底有什麼用呢?
通過他,我們可以做元件的事件以及props
透傳
首先有一個標準化的元件,一般是元件庫的元件等等
//child.vue <template> <div>這是一個標準化元件</div> <input type="text" :value="num" @input="setInput" /> </template> <script> import { defineComponent } from "vue"; export default defineComponent({ props: ['num'], emits: ['edit'], setup(props, { emit }) { function setInput(val) { emit('edit', val.target.value) } return { setInput } } }) </script>
接下來有一個包裝元件,他對當前的標準化元件做修飾,從而使結果變成我們符合我們的預期的元件
//parent.vue <template> <div>這一層要做一個單獨的包裝</div> <child v-bind="$attrs" @edit="edit"></child> </template> <script> import { defineComponent } from "vue"; import child from './child.vue' export default defineComponent({ components: { child }, setup(props, { emit }) { function edit(val) { // 對返回的值做一個包裝 emit('edit', `${val}time`) } return { edit } } }) </script>
我們發現當前包裝元件中使用了$attrs
,通過他透傳給標準化元件,這樣一來,我們就能對比如element UI中的元件做增強以及包裝處理,並且不用改動原元件的邏輯。
vue3的元件通常情況下使用vue提供的component
方法來完成全域性元件的註冊
程式碼如下:
const app = Vue.createApp({}) app.component('component-a', { /* ... */ }) app.component('component-b', { /* ... */ }) app.component('component-c', { /* ... */ }) app.mount('#app')
使用時
<div id="app"> <component-a></component-a> <component-b></component-b> <component-c></component-c> </div>
然而經過大佬的奇技淫巧的開發,我們發現可能使用註冊vue外掛的方式,也能完成元件註冊,並且是優雅的!
vue外掛註冊
外掛的格式
//plugins/index.js export default { install: (app, options) => { // 這是外掛的內容 } }
外掛的使用
import { createApp } from 'vue' import Plugin from './plugins/index.js' const app = createApp(Root) app.use(Plugin) app.mount('#app')
其實外掛的本質,就是在use的方法中呼叫外掛中的install方法
,那麼這樣一來,我們就能在install
方法中註冊元件。
index.js中丟擲一個元件外掛
// index.js import component from './Cmponent.vue' const component = { install:function(Vue){ Vue.component('component-name',component) } //'component-name'這就是後面可以使用的元件的名字,install是預設的一個方法 component-name 是自定義的,我們可以按照具體的需求自己定義名字 } // 匯出該元件 export default component
元件註冊
// 引入元件 import install from './index.js'; // 全域性掛載utils Vue.use(install);
上述案例中,就是一個簡單的優雅的元件註冊方式,大家可以發現包括element-plus、vant
等元件都是用如此方式註冊元件。
<script setup>
是在單檔案元件 (SFC) 中使 的編譯時語法糖。相比於普通的 <script>
語法,它具有更多優勢:
它能代替大多數的setup函數所表達的內容,具體使用方法,大家請看請移步檔案
但是由於setup函數它能返回渲染函數的特性,在當前語法糖中卻無法展示,於是遍尋資料,找到了一個折中的辦法
<script setup> import { ref,h } from 'vue' const msg = ref('Hello World!') const dynode = () => h('div',msg.value); </script> <template> <dynode /> <input v-model="msg"> </template>
如此一來,我們就能在語法糖中返回渲染函數了
我們知道在vue2中想要模擬v-model,必須要子元件要接受一個value props
吐出來一個 叫input的emit
然而在vue3中他升級了
父元件中使用v-model
<template> <child v-model:title="pageTitle"></child> </template> <script> import { defineComponent, ref } from "vue"; import child from './child.vue' export default defineComponent({ components: { child }, setup(props, { emit }) { const pageTitle = ref('這是v-model') return { pageTitle } } }) </script>
子元件中使用 title的props
以及規定吐出update:title的emit
<template> <div>{{ title }}</div> <input type="text" @input="setInput" /> </template> <script> import { defineComponent } from "vue"; export default defineComponent({ props: ['title'], emits: ['update:title'], setup(props, { emit }) { function setInput(val) { emit('update:title', val.target.value) } return { setInput } } }) </script>
有了以上語法糖,我們在封裝元件的時候,就可以隨心所欲了,比如我自己封裝可以控制顯示隱藏的元件我們就能使用v-model:visible
單獨控制元件的顯示隱藏。使用正常的v-model
控制元件內部的其他邏輯,從而擁有使用更簡潔的邏輯,表達相同的功能
目前開發中總結的經驗就分享到這裡了,錯誤之處,請大佬指出!
然後對vue原始碼有興趣的大佬,可以看下這個文章 寫給小白(自己)的vue3原始碼導讀
也可以直接看本渣的原始碼解析github vue-next-analysis
其中包含了vue原始碼執行思維導圖,原始碼中的程式碼註釋,整個原始碼的結構,各個功能的單獨拆解等。錯誤之處請大佬指出!
(學習視訊分享:、)