Object.defineProperty => Proxy
重構了虛擬DOM
OptionApi => Composition API
setup
是干啥的?
setup
實際上是一個組件的入口,它運行在組件被實例化時候,props
屬性被定義之后,實際上等價于 2 版本的beforeCreate
和 Created
這兩個生命周期。
setup
接受兩個參數,第一個參數是props
, 另一個參數是context
,
setup(props, ctx) {
console.log(props, ctx)
}
let Child = {
template: `<div>{{title}}</div>`,
setup(props, context) {
console.log(props)
}
}
let App = {
template: `
<div class="container">
<Child title="test props"/>
</div>`,
components: { Child }
}
Vue.createApp().mount(App, '#app')
reactive
const { reactive, toRefs } = Vue
let App = {
template: `
<div class="container">
count: {{count}}
<button @click="handlerCountAdd"> Click ++ </button>
</div>`,
setup() {
const state = reactive({ count: 0 })
const handlerCountAdd = () => {
state.count++
}
return { ...toRefs(state), handlerCountAdd }
}
}
Vue.createApp().mount(App, '#app')
toRefs
vue3提供的ref讓我們有機會創建單個的響應式的對象,在setup函數中return出去之后,在模板中可直接訪問
const App = {
template: `
<div class="container">
{{value}}
</div>`,
setup() {
const value = ref(1)
return { value }
}
}
Vue.createApp().mount(App, '#app')
const App = {
template: `
<div class="container">
{{state.value}}
</div>`,
setup() {
const state = reactive({ value: 'reactive' })
return { state }
}
}
Vue.createApp().mount(App, '#app')
const App = {
template: `
<div class="container">
{{value}}
</div>`,
setup() {
const state = reactive({ value: 'reactive' })
return toRefs(state)
}
}
Vue.createApp().mount(App, '#app')
反轉字符串 demo
let App = {
template: `
<div class="container">
value: <input v-model="value"/>
<br/>
rvalue: {{rvalue}}
</div>`,
setup() {
const state = reactive({
value: '',
rvalue: computed(() =>
state.value
.split('')
.reverse()
.join('')
)
})
return toRefs(state)
}
}
Vue.createApp().mount(App, '#app')
數據響應式
在Vue3中實現數據響應式的方案由Vue2中的Object.defineProperty
換成了 Proxy
,關于數據響應式的Api上邊說到了一些,還剩下effect
和watch
沒有提及到,effect
是數據響應式中重要的一部分,watch
和computed
都是基于 effect
的.
let App = {
template: `
<div class="container">
count: {{count}}
<button @click="handlerCountAdd"> Click ++ </button>
</div>`,
setup() {
const state = reactive({ count: 0, value: 1 })
const handlerCountAdd = () => {
state.count++
}
watch(
() => state.count,
val => {
console.log('watch', state.count)
console.log('watch', state.value)
}
)
effect(() => {
console.log('effect', state.count)
console.log('effect', state.value)
})
return { ...toRefs(state), handlerCountAdd }
}
}
Vue.createApp().mount(App, '#app')
effect
在響應式數據變化的時候就會執行,執行次數根據響應式數據的個數來決定
let App = {
template: `
<div class="container">
<button @click="handlerCountAdd"> Click ++ </button>
</div>`,
setup() {
const r = ref(1)
const s = ref(1)
const t = ref(1)
const handlerCountAdd = () => {
r.value *= 1
s.value *= 2
t.value *= 3
}
effect(() => {
console.log('effect', [r.value, s.value, t.value])
})
return { handlerCountAdd }
}
}
Vue.createApp().mount(App, '#app')
而watch
則點擊一次 ,只會觸發執行一次
let App = {
template: `
<div class="container">
<button @click="handlerCountAdd"> Click ++ </button>
</div>`,
setup() {
const state = reactive({ count: 0, value: 1 })
const r = ref(1)
const s = ref(1)
const t = ref(1)
const handlerCountAdd = () => {
r.value *= 1
s.value *= 2
t.value *= 3
}
watch([r, s, t], val => {
console.log('watch', val)
})
return { handlerCountAdd }
}
}
Vue.createApp().mount(App, '#app')
生命周期
beforeCreate => setup(替代)
created => setup(替代)
beforeMount => onBeforeMount
mounted => onMounted
beforeUpdate => onBeforeUpdate
updated => onUpdated
beforeDestroy => onBeforeUnmount
destroyed => onUnmounted
errorCaptured => onErrorCaptured
全局配置
Vue2.x
創建實例并且掛載DOM
上
import Vue from "vue";
import App from './App.vue'
new Vue({
render: (h) => h(App)
}).$mount("#app");
Vue3新增api===>createApp
創建實例
createApp 會產生一個 app 實例,該實例擁有全局的可配置上下文
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
component
Vue2.x
【注冊或獲取全局組件。注冊還會自動使用給定的 id 設置組件的名稱】
// 注冊組件,傳入一個選項對象 (自動調用 Vue.extend)
Vue.component('my-component', { /* ... */ })
// 獲取注冊的組件 (始終返回構造器)
var MyComponent = Vue.component('my-component')
Vue3
【注冊或獲取全局組件注冊還會自動使用給定的name
組件 設置組件的名稱】全局組件
基本vue2寫法一致
import { createApp } from 'vue'
const app = createApp({})
// 注冊組件,傳入一個選項對象
app.component('my-component', {
/* ... */
})
// 獲取注冊的組件 (始終返回構造器)
const MyComponent = app.component('my-component', {})
globalProperties 【新增屬性】
app.config.globalProperties.foo = 'bar'
app.component('child-component', {
mounted() {
console.log(this.foo) // 'bar'
}
})
添加可在程序內的任何組件實例中訪問的全局屬性。當存在鍵沖突時,組件屬性將優先
替代掉Vue2.x
的 Vue.prototype
屬性放到原型上的寫法
// Vue2.x
Vue.prototype.$http = () => {}
// Vue3
const app = Vue.createApp({})
app.config.globalProperties.$http = () => {}
isCustomElement
【新增屬性】
替代掉Vue2.x
的ignoredElements
- Vue.config.ignoredElements = [
// 用一個 `RegExp` 忽略所有“ion-”開頭的元素
// 僅在 2.5+ 支持
/^ion-/
]
// 一些組件以'ion-'
開頭將會被解析為自定義組件
+ app.config.isCustomElement = tag => tag.startsWith('ion-')
指定一個方法來識別在Vue之外定義的自定義組件(例如,使用Web Component API
)。如果組件符合這個條件,它就不需要本地或全局注冊,Vue也不會拋出關于Unknown custom element
的警告
注意,這個函數中不需要匹配所有原生HTML和SVG標記—Vue
解析器會自動執行此檢查
optionMergeStrategies
const app = Vue.createApp({
mounted() {
console.log(this.$options.hello)
}
})
app.config.optionMergeStrategies.hello = (parent, child, vm) => {
return `Hello, ${child}`
}
app.mixin({
hello: 'Vue'
})
// 'Hello, Vue
定義自定義選項的合并策略。
合并策略接收在父實例options
和??
子實例??options
和子實例options和??子實例??options,分別作為第一個和第二個參數。上下文Vue實例作為第三個參數傳遞
【自定義選項合并策略】mixin
const app = Vue.createApp({
custom: 'hello!'
})
app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
console.log(fromVal, toVal)
// => "goodbye!", undefined
// => "hello!", "goodbye!"
return fromVal || toVal
}
app.mixin({
custom: 'goodbye!',
created() {
console.log(this.$options.custom) // => "hello!"
}
})
optionMergeStrategies
先獲取到子實例的$options
的mixin而沒有父實例
【custom第一次改變從undefined
到goodbye--->
打印"goodbye!", undefined
】
父實例的options替換掉子實例的options替換掉子實例的options替換掉子實例的options
【custom第二次從goodbye到hello!--->打印了"hello", "goodbye!"】
最后在打印`app.config.optionMergeStrategies.custom`返回的父實例的`$options`
無論如何this.options.custom最后會返回合并策略的return的值【使用場景利用父子組件的options.custom最后會返回合并策略的return的值【使用場景利用父子組件的options.custom最后會返回合并策略的return的值【使用場景利用父子組件的options,然后返回計算等操作得到所需要的值】optionMergeStrategies合并$options變化
directive
import { createApp } from 'vue'
const app = createApp({})
// 注冊
app.directive('my-directive', {
// 指令的生命周期
// 在綁定元素的父組件被掛載之前調用
beforeMount(el, binding, vnode) {},
// 在掛載綁定元素的父組件時調用
mounted(el, binding, vnode) {},
// 在更新包含組件的VNode之前調用
beforeUpdate(el, binding, vnode, prevNode) {},
// 組件的VNode及其子組件的VNode更新之后調用
updated(el, binding, vnode, prevNode) {},
// 在卸載綁定元素的父組件之前調用
beforeUnmount(el, binding, vnode) {},
// 在卸載綁定元素的父組件時調用
unmounted(el, binding, vnode) {}
})
// 注冊 (指令函數)
app.directive('my-directive', (el, binding, vnode, prevNode) => {
// 這里將會被 `mounted` 和 `updated` 調用
})
// getter,返回已注冊的指令
const myDirective = app.directive('my-directive')
el
指令綁定到的元素。這可以用來直接操作DOM。
binding【包含下列屬性的對象】
instance:使用指令的組件的實例
value:指令的綁定值,例如:v-my-directive="1 + 1"中,綁定值為 2
oldValue:指令綁定的前一個值,僅在 beforeUpdate 和 updated 鉤子中可用。無論值是否改變都可用
arg:傳給指令的參數,可選。例如 v-my-directive:foo 中,參數為 "foo"
modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象為 { foo: true, bar: true }
dir:一個對象,在注冊指令時作為參數傳遞; 舉個例子,看下面指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
dir就是下面的對象
{
mounted(el) {
el.focus()
}
}
vnode
編譯生成的虛擬節點
prevNode
前一個虛擬節點,僅在beforeUpdate和updated鉤子中可用
tips:除了 el 之外,其它參數都應該是只讀的,切勿進行修改。如果需要在鉤子之間共享數據,建議通過元素的 dataset 來進行
mount
【類似Vue2.x】
在所提供的DOM
元素上掛載應用程序實例的根組件
import { createApp } from 'vue'
const app = createApp({})
// 做一些準備
app.mount('#my-app')
provide/inject
【Vue2.x一致】
該選項與inject一起使用,允許一個祖先組件作為其所有后代的依賴注入器,無論組件層次結構有多深,只要它們位于同一父鏈中就可以
provide 選項應該是一個對象或返回一個對象的函數。該對象包含可注入其子孫的 property。在該對象中你可以使用 ES2015 Symbols 作為 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的環境下可工作。
如果在組件中兩者都只能在當前活動組件實例的 setup() 中調用,詳細請看依賴注入部分
import { createApp } from 'vue'
const app = createApp({
provide: {
user: 'John Doe'
}
})
app.component('user-card', {
inject: ['user'],
template: `
<div>
{{ user }}
</div>
`
})
unmount【新增屬性】
在所提供的DOM元素上卸載應用程序實例的根組件
import { createApp } from 'vue'
const app = createApp({})
// 做一些必要的準備
app.mount('#my-app')
// 應用程序將在掛載后5秒被卸載
setTimeout(() => app.unmount('#my-app'), 5000)
use【Vue2.x一致】
安裝 Vue.js 插件。如果插件是一個對象,必須提供 install 方法。如果插件是一個函數,它會被作為 install 方法。install 方法調用時,會將 Vue 作為參數傳入。
當 install 方法被同一個插件多次調用,插件將只會被安裝一次。
setup
setup 函數是一個新的組件選項。作為在組件內使用 Composition API 的入口點
注意 setup 返回的 ref 在模板中會自動解開,不需要寫 .value【setup 內部需要.value】
調用時機
創建組件實例,然后初始化 props ,緊接著就調用setup 函數。從生命周期鉤子的視角來看,它會在 beforeCreate 鉤子之前被調用
如果 setup 返回一個對象,則對象的屬性將會被合并到組件模板的渲染上下文
參數
props 作為其第一個參數
注意 props 對象是響應式的,watchEffect 或 watch 會觀察和響應 props 的更新
不要解構 props 對象,那樣會使其失去響應性
```js
```js
export default {
props: {
name: String,
},
setup(props) {
console.log(props.name)
watchEffect(() => {
console.log(`name is: ` + props.name)
})
},
}
第二個參數提供了一個上下文對象【從原來 2.x 中 this 選擇性地暴露了一些 property(attrs/emit/slots)】
attrs 和 slots 都是內部組件實例上對應項的代理,可以確保在更新后仍然是最新值。所以可以解構,無需擔心后面訪問到過期的值
為什么props作為第一個參數?
組件使用 props 的場景更多,有時候甚至只使用 props
將 props 獨立出來作為第一個參數,可以讓 TypeScript 對 props 單獨做類型推導,不會和上下文中的其他屬性相混淆。這也使得 setup 、 render 和其他使用了 TSX 的函數式組件的簽名保持一致
this 在 setup() 中不可用。由于 setup() 在解析 2.x 選項前被調用,setup() 中的 this 將與 2.x 選項中的 this 完全不同。同時在 setup() 和 2.x 選項中使用 this 時將造成混亂
setup(props, { attrs }) {
// 一個可能之后回調用的簽名
function onClick() {
console.log(attrs.foo) // 一定是最新的引用,沒有丟失響應性
}
}
響應式系統 API
reactive
desc: 接收一個普通對象然后返回該普通對象的響應式代理【等同于 2.x 的 Vue.observable()】
tips:Proxy對象是目標對象的一個代理器,任何對目標對象的操作(實例化,添加/刪除/修改屬性等等),都必須通過該代理器。因此我們可以把來自外界的所有操作進行攔截和過濾或者修改等操作
響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性?;?ES2015 的 Proxy 實現,返回的代理對象不等于原始對象。建議僅使用代理對象而避免依賴原始對象
reactive 類的 api 主要提供了將復雜類型的數據處理成響應式數據的能力,其實這個復雜類型是要在object array map set weakmap weakset 這五種之中【如下源碼,他會判斷是否是五類以及是否被凍結】
因為是組合函數【對象】,所以必須始終保持對這個所返回對象的引用以保持響應性【不能解構該對象或者展開】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
return pos
}
toRefs API 用來提供解決此約束的辦法——它將響應式對象的每個 property 都轉成了相應的 ref【把對象轉成了ref】。
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
return toRefs(pos)
}
// x & y 現在是 ref 形式了!
const { x, y } = useMousePosition()
ref
接受一個參數值并返回一個響應式且可改變的 ref 對象。ref 對象擁有一個指向內部值的單一屬性 .value
const count = ref(0)
console.log(count.value) // 0
如果傳入 ref 的是一個對象,將調用 reactive 方法進行深層響應轉換
陷阱
setup 中return返回會自動解套【在模板中不需要.value】
ref 作為 reactive 對象的 property 被訪問或修改時,也將自動解套 .value
const count = ref(0)
/*當做reactive的對象屬性----解套*/
const state = reactive({
count,
})
/* 不需要.value*/
console.log(state.count) // 0
/*修改reactive的值*/
state.count = 1
/*修改了ref的值*/
console.log(count.value) // 1
注意如果將一個新的 ref 分配給現有的 ref, 將替換舊的 ref
/*創建一個新的ref*/
const otherCount = ref(2)
/*賦值給reactive的舊的ref,舊的會被替換掉*/
state.count = otherCount
/*修改reactive會修改otherCount*/
console.log(state.count) // 2
/*修改reactive會count沒有被修改 */
console.log(count.value) // 1
嵌套在 reactive Object 中時,ref 才會解套。從 Array 或者 Map 等原生集合類中訪問 ref 時,不會自動解套【自由數據類型是Object才會解套,array map set weakmap weakset集合類 訪問 ref 時,不會自動解套】
const arr = reactive([ref(0)])
// 這里需要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 這里需要 .value
console.log(map.get('foo').value)
心智負擔上 ref vs reactive
在普通 JavaScript 中區別聲明基礎類型變量與對象變量時一樣區別使用 ref 和 reactive
所有的地方都用 reactive,然后記得在組合函數返回響應式對象時使用 toRefs。這降低了一些關于 ref 的心智負擔
readonly
傳入一個對象(響應式或普通)或 ref,返回一個原始對象的只讀代理。一個只讀的代理是“深層的”,對象內部任何嵌套的屬性也都是只讀的【返回一個永遠不會變的只讀代理】【場景可以參數比對等】
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 依賴追蹤
console.log(copy.count)
})
// original 上的修改會觸發 copy 上的偵聽
original.count++
// 無法修改 copy 并會被警告
copy.count++ // warning!
reactive響應式系統工具集
isProxy
檢查一個對象是否是由 reactive 或者 readonly 方法創建的代理
isReactive
檢查一個對象是否是由 reactive 創建的響應式代理
import { reactive, isReactive } from 'vue'
const state = reactive({
name: 'John'
})
console.log(isReactive(state)) // -> true
如果這個代理是由 readonly 創建的,但是又被 reactive 創建的另一個代理包裹了一層,那么同樣也會返回 true
import { reactive, isReactive, readonly } from 'vue'
const state = reactive({
name: 'John'
})
// 用readonly創建一個只讀響應式對象plain
const plain = readonly({
name: 'Mary'
})
//readonly創建的,所以isReactive為false
console.log(isReactive(plain)) // -> false
// reactive創建的響應式代理對象包裹一層readonly,isReactive也是true,isReadonly也是true
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
isReadonly
檢查一個對象是否是由 readonly 創建的只讀代理
reactive高級響應式系統API
toRaw
返回由 reactive 或 readonly 方法轉換成響應式代理的普通對象。這是一個還原方法,可用于臨時讀取,訪問不會被代理/跟蹤,寫入時也不會觸發更改。不建議一直持有原始對象的引用【**不建議賦值給任何變量**】。請謹慎使用
被toRaw之后的對象是沒有被代理/跟蹤的的普通對象
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
console.log(toRaw(reactiveFoo) !== reactiveFoo) // true
markRaw
顯式標記一個對象為“永遠不會轉為響應式代理”,函數返回這個對象本身。
【markRaw傳入對象,返回的值是永遠不會被轉為響應式代理的】
const foo = markRaw({
name: 'Mary'
})
console.log(isReactive(reactive(foo))) // false
被 markRaw 標記了,即使在響應式對象中作屬性,也依然不是響應式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
markRaw注意點
markRaw和 shallowXXX 一族的 API允許選擇性的覆蓋reactive或者readonly 默認創建的 "深層的" 特性【響應式】/或者使用無代理的普通對象
設計這種「淺層讀取」有很多原因
一些值的實際上的用法非常簡單,并沒有必要轉為響應式【例如三方庫的實例/省市區json/Vue組件對象】
當渲染一個元素數量龐大,但是數據是不可變的,跳過 Proxy 的轉換可以帶來性能提升
這些 API 被認為是高級的,是因為這種特性僅停留在根級別,所以如果你將一個嵌套的,沒有 markRaw 的對象設置為 reactive 對象的屬性,在重新訪問時,你又會得到一個 Proxy 的版本,在使用中最終會導致標識混淆的嚴重問題:執行某個操作同時依賴于某個對象的原始版本和代理版本(標識混淆在一般使用當中應該是非常罕見的,但是要想完全避免這樣的問題,必須要對整個響應式系統的工作原理有一個相當清晰的認知)。
const foo = markRaw({
nested: {},
})
const bar = reactive({
// 盡管 `foo` 己經被標記為 raw 了, 但 foo.nested 并沒有
nested: foo.nested,
})
console.log(foo.nested === bar.nested) // false
foo.nested沒有被標記為(永遠不會轉為響應式代理),導致最后的值一個reactive
shallowReactive
只為某個對象的私有(第一層)屬性創建淺層的響應式代理,不會對“屬性的屬性”做深層次、遞歸地響應式代理,而只是保留原樣【第一層是響應式代理,深層次只保留原樣(不具備響應式代理)】
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
})
// 變更 state 的自有屬性是響應式的【第一層次響應式】
state.foo++
// ...但不會深層代理【深層次不是響應式】(渲染性能)
isReactive(state.nested) // false
state.nested.bar++ // 非響應式
shallowReadonly
類似于shallowReactive,區別是:
第一層將會是響應式代理【第一層修改屬性會失敗】,屬性為響應式
深層次的對象屬性可以修改,屬性不是響應式
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2,
},
})
// 變更 state 的自有屬性會失敗
state.foo++
// ...但是嵌套的對象是可以變更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套屬性依然可修改
ref 響應式系統工具集
unref
unref是val = isRef(val) ? val.value : val 的語法糖
unref(ref(0))===unref(0)===0 返回number
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x) // unwrapped 一定是 number 類型
}
toRef
toRef 可以用來為一個 reactive 對象的屬性【某個屬性區別toRefs每一個屬性】創建一個 ref。這個 ref 可以被傳遞并且能夠保持響應性
const state = reactive({
foo: 1,
bar: 2,
})
//reactive獲取單個屬性轉為ref【fooRef只是一個代理】
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs
把一個響應式對象轉換成普通對象,該普通對象的每個 property 都是一個 ref ,和響應式對象 property 一一對應
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的類型如下:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
// ref 對象 與 原屬性的引用是 "鏈接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
可以通過toRefs返回可解構的reactive,因為toRefs包裹之后返回一一對應的ref屬性
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2,
})
// 對 state 的邏輯操作
// 返回時將屬性都轉為 ref
return toRefs(state)
}
export default {
setup() {
// 可以解構,不會丟失響應性
const { foo, bar } = useFeatureX()
return {
foo,
bar,
}
},
}
isRef
檢查一個值是否為一個 ref 對象
ref 高級響應式系統API
customRef
用于自定義一個 ref,可以顯式地控制依賴追蹤和觸發響應,接受一個工廠函數,兩個參數分別是用于追蹤的 track 與用于觸發響應的 trigger,并返回一個一個帶有 get 和 set 屬性的對象【實際上就是手動 track追蹤 和 trigger觸發響應】
以下代碼可以使得v-model防抖
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
/*初始化手動追蹤依賴講究什么時候去觸發依賴收集*/
track()
return value
},
set(newValue) {
/*修改數據的時候會把上一次的定時器清除【防抖】*/
clearTimeout(timeout)
timeout = setTimeout(() => {
/*把新設置的數據給到ref數據源*/
value = newValue
/*再有依賴追蹤的前提下觸發響應式*/
trigger()
}, delay)
},
}
})
}
setup() {
return {
/*暴露返回的數據加防抖*/
text: useDebouncedRef('hello'),
}
}
shallowRef
創建一個 ref ,將會追蹤它的 .value 更改操作,但是并不會對變更后的 .value 做響應式代理轉換(即變更不會調用 reactive)
前面我們說過如果傳入 ref 的是一個對象,將調用 reactive 方法進行深層響應轉換,通過shallowRef創建的ref,將不會調用reactive【對象不會是響應式的】
const refOne = shallowRef({});
refOne.value = { id: 1 };
refOne.id == 20;
console.log(isReactive(refOne.value),refOne.value);//false { id: 1 }
triggerRef 【與shallowRef配合】
手動執行與shallowRef相關的任何效果
const shallow = shallowRef({
greet: 'Hello, world'
})
// 第一次運行打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 這不會觸發效果,因為ref是shallow
shallow.value.greet = 'Hello, universe'
// 打印 "Hello, universe"
triggerRef(shallow)
Computed and watch【監控變化】
computed
傳入一個 getter 函數,返回一個默認不可手動修改的 ref 對象【默認傳入的是get函數的對象】
傳入一個擁有 get 和 set 函數的對象,創建一個可手動修改的計算狀態
const count = ref(1)
/*不支持修改【只讀的】 */
const plusOne = computed(() => count.value + 1)
plusOne.value++ // 錯誤!
/*【可更改的】 */
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
watchEffect
立即執行傳入的一個函數,并響應式追蹤其依賴,并在其依賴變更時重新運行該函數
computed與watchEffect區別:
computed計算屬性可通過setup return,再模板中使用,watchEffect不能;
computed可以使用多個,并且對多個屬性進行不同的響應計算,watchEffect會存在副作用
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
停止觀察
當在組件的setup()函數或生命周期鉤子期間調用watchEffect時,監視程序會鏈接到組件的生命周期,并在卸載組件時自動停止
一般情況下watchEffect返回可以stop 操作,停止監聽程序
const stop = watchEffect(() => {
/* ... */
})
// 停止監聽程序
stop()
副作用(函數式編程)
一個帶有副作用的函數不僅只是簡單的返回一個值,還干了一些其他的事情,比如:
修改一個變量
直接修改數據結構
設置一個對象的成員
拋出一個異?;蛞砸粋€錯誤終止
打印到終端或讀取用戶的輸入
讀取或寫入一個文件
在屏幕上繪畫
buyCoffee的例子(p3):函數只不過是需要返回一杯咖啡,可是卻對費用進行了持久化操作(產生副作用),我們可以在buyCoffee方法返回咖啡時也把費用作為值一并返回,將費用這條記錄交給其他程序來做持久化,以此來去除副作用 ====》通過把這些副作用推到程序的外層,來轉換任何帶有副作用的函數(純的內核和一層很薄的外圍來處理副作用)
如果一個函數內外有依賴于外部變量或者環境時,常常我們稱之為其有副作用,如果我們僅通過函數簽名不打開內部代碼檢查并不能知道該函數在干什么,作為一個獨立函數我們期望有明確的輸入和輸出,副作用是bug的發源地,作為程序員開發者應盡量少的開發有副作用的函數或方法,副作用也使得方法通用性下降不適合擴展和可重用性
清除副作用
[^]: watchEffect函數都是副作用
在一些時候監聽函數將執行異步副作用【一個響應式依賴被修改了,會做其他事情】,這些響應需要在其失效時清除(例如在效果完成前狀態改變)。effect函數接收一個onInvalidate 函數作入參, 用來注冊清理失效時的回調。這個 invalidation函數 在什么時候會被調用:
監聽函數重新被執行的時候【響應式依賴的數據被修改】
監聽停止的時候(如果watchEffect在setup()或者生命周期函數中被使用的時候組件會被卸載)【停止觀察】
watchEffect(onInvalidate => {
/*這是個異步操作*/
const token = performAsyncOperation(id.value)//id依賴
onInvalidate(() => {
// id被修改了或者監聽停止了會觸發token.cancel()事件【這塊區域的代碼】.
// 這里是異步事件的話,前面的peding的異步操作無效【這里的異步事件只執行一次】
token.cancel()/*異步操作*/
console.log('onInvalidate')
})
})
從上面看:我們之所以是通過傳入一個函數去注冊失效回調,而不是從回調返回它(如 React useEffect
中的方式),是因為返回值對于異步錯誤處理很重要
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {...}) // 我們在Promise的resolves之前注冊清理函數(cleanup function)
data.value = await fetchData(props.id)
})
我們知道異步函數都會隱式地返回一個 Promise,但是清理副作用的函數必須要在 Promise 被 resolve 之前被注冊。另外,Vue 依賴這個返回的 Promise 來自動處理 Promise 鏈上的潛在錯誤
副作用刷新時機
Vue 的響應式系統會緩存副作用函數,并異步地刷新它們,這樣可以避免同一個 tick 中多個狀態改變導致的不必要的重復調用。在核心的具體實現中, 組件的更新函數也是一個被偵聽的副作用。當一個用戶定義的副作用函數進入隊列時, 會在所有的組件更新后執行
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
return {
count,
}
},
}
</script>
count 會在初始運行時同步打印出來
更改 count 時,將在組件更新后執行副作用
初始化運行是在組件 mounted 之前執行的【你希望在編寫副作用函數時訪問 DOM(或模板 ref),請在 onMounted 鉤子中進行】
onMounted(() => {
watchEffect(() => {
// 在這里可以訪問到 DOM 或者 template refs
})
})
如果副作用需要同步或在組件更新之前重新運行,我們可以傳遞一個擁有 flush 屬性的對象作為選項(默認為 'post')
// 同步運行
watchEffect(
() => {
/* ... */
},
{
flush: 'sync',
}
)
// 組件更新前執行
watchEffect(
() => {
/* ... */
},
{
flush: 'pre',
}
)
偵聽器調試【響應式調試用的】
onTrack 和 onTrigger 選項可用于調試一個偵聽器的行為。
當一個 reactive 對象屬性或一個 ref 作為依賴被追蹤時,將調用 onTrack【調用次數為被追蹤的數量】
依賴項變更會導致重新追蹤依賴,從而onTrack被調用【調用次數為被追蹤的數量】
依賴項變更導致副作用被觸發時,將調用 onTrigger
這兩個回調都將接收到一個包含有關所依賴項信息的調試器事件。建議在以下回調中編寫 debugger 語句來檢查依賴關系:【onTrack 和 onTrigger 僅在開發模式下生效】
watchEffect(
() => {
/* 副作用的內容 */
},
{
onTrigger(e) {
/*副作用依賴修改*/
debugger
},
onTrack(e) {
/*副作用依賴修改*/
debugger
},
}
)
watch
watch API 完全等效于 2.x watch 中相應的選項。watch 需要偵聽特定的數據源,并在回調函數中執行副作用【默認情況是懶執行的,也就是說僅在偵聽的源變更時才執行回調】
watch允許我們:
懶執行副作用
更明確哪些狀態的改變會觸發偵聽器重新運行副作用
訪問偵聽狀態變化前后的值
偵聽單個數據源
偵聽器的數據源可以是一個擁有返回值的 getter 函數,也可以是 ref:
// 偵聽一個 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接偵聽一個 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
偵聽多個數據源
watcher 也可以使用數組來同時偵聽多個源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
與 watchEffect 共享的行為
watch 和 watchEffect 在停止偵聽, 清除副作用 (相應地 onInvalidate 會作為回調的第三個參數傳入),副作用刷新時機 和 偵聽器調試 等方面行為一致
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar],onInvalidate) => {
/* ... */
onInvalidate(() => {...})
},
{
onTrigger(e) {
/*副作用依賴修改*/
debugger
},
onTrack(e) {
/*副作用依賴修改*/
debugger
},
})
生命周期鉤子函數
與 2.x 版本生命周期相對應的組合式 API
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured
import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
這些生命周期鉤子注冊函數只能在 setup() 期間同步使用, 因為它們依賴于內部的全局狀態來定位當前組件實例(正在調用 setup() 的組件實例), 不在當前組件下調用這些函數會拋出一個錯誤。
組件實例上下文也是在生命周期鉤子同步執行期間設置的,因此,在卸載組件時,在生命周期鉤子內部同步創建的偵聽器和計算狀態也將自動刪除。
新增的鉤子函數
除了和 2.x 生命周期等效項之外,組合式 API 還提供了以下調試鉤子函數:
onRenderTracked
onRenderTriggered
兩個鉤子函數都接收一個 DebuggerEvent,與 watchEffect 參數選項中的 onTrack 和 onTrigger 類似:
export default {
onRenderTracked(e){
debugger
// 檢查有響應和追蹤的依賴性
},
onRenderTriggered(e) {
debugger
// 檢查哪個依賴性導致組件重新渲染
},
}
Vue提供的內置組件
component 與Vue2.x一致
渲染一個“元組件”為動態組件。依 is 的值,來決定哪個組件被渲染。
<!-- 動態組件由 vm 實例的 `componentId` property 控制 -->
<component :is="componentId"></component>
<!-- 也能夠渲染注冊過的組件或 prop 傳入的組件 -->
<component :is="$options.components.child"></component>
transition 與 Vue2.x 【基本】 一致有差異
Props新增:
persisted - boolean 如果為true,則表示這是一個轉換,實際上不會插入/刪除元素,而是切換顯示/隱藏狀態。 transition 過渡掛鉤被注入,但會被渲染器跳過。 相反,自定義指令可以通過調用注入的鉤子(例如v-show)來控制過渡
enter-class----->enter-from-class
leave-class----->leave-from-class
事件
before-appear
transition-group 與 Vue2.x 一致
slot 與 Vue2.x 一致
teleport 【新增組件】
Props
to - string 必填屬性,必須是一個有效的query選擇器,或者是元素(如果在瀏覽器環境中使用)。中的內容將會被放置到指定的目標元素中
<!-- 正確的 -->
<teleport to="#some-id" />
<teleport to=".some-class" />
/*元素*/
<teleport to="[data-teleport]" />
<!-- 錯誤的 -->
<teleport to="h1" />
<teleport to="some-string" />
disabled - boolean 這是一個可選項 ,做一個是可以用來禁用的功能,這意味著它的插槽內容不會移動到任何地方,而是按沒有teleport組件一般來呈現【默認為false】
<teleport to="#popup" :disabled="displayVideoInline">
<h1>999999</h1>
</teleport>
注意,這將移動實際的DOM節點,而不是銷毀和重新創建,并且還將保持任何組件實例是活動的。所有有狀態HTML元素(比如一個正在播放的視頻)將保持它們的狀態。【控制displayVideoInline并不是銷毀重建,它保持實例是存在的,不會被注銷】
關于Teleport 其他內容
Vue鼓勵我們通過將UI和相關行為封裝到組件中來構建UI。我們可以將它們彼此嵌套在一起,以構建構成應用程序UI的樹
但是,有時組件模板的一部分邏輯上屬于這個組件,而從技術角度來看,最好將這一部分模板移到DOM中的其他地方,放到Vue應用程序之外
一個常見的場景是創建一個包含全屏模態的組件。在大多數情況下,您希望模態的邏輯駐留在組件中,但是模態框的定位問題很快就很難通過CSS解決,或者需要更改組件的組成
考慮下面的HTML結構:
<body>
<div style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<modal-button></modal-button>
</div>
</div>
</body>
讓我們看看 mode -button
該組件將有一個button元素來觸發模態的打開,還有一個div元素,其類為.modal,它將包含模態的內容和一個自關閉按鈕
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal!
</button>
<div v-if="modalOpen" class="modal">
<div>
I'm a modal!
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
`,
data() {
return {
modalOpen: false
}
}
})
當在初始HTML結構中使用這個組件時,我們可以看到一個問題——模態被呈現在深嵌套的div中,模態的絕對位置以父div相對位置作為參考。
Teleport提供了一種干凈的方式,允許我們控制DOM中希望在哪個父節點下呈現HTML片段,而不必訴諸全局狀態或將其拆分為兩個組件。
讓我們修改我們的modal-button來使用并告訴Vue "teleport this HTML to the "body"標簽"。
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
與Vue組件一起使用
如果包含一個Vue組件,它將仍然是的父組件的邏輯子組件
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})
app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})
在這種情況下,即使在不同的地方呈現child-component,它仍將是parent-componen的子組件【而不是爺爺組件】,并將從其父組件接收一個name 的props
這也意味著來自父組件的注入如預期的那樣工作,并且子組件將嵌套在Vue Devtools的父組件之下,而不是放在實際內容移動到的地方
對同一目標使用多次teleports
一個常見的用例場景是一個可重用的組件,該組件可能同時有多個活動實例。對于這種場景,多個組件可以將它們的內容掛載到相同的目標元素。這個順序將是一個簡單的附加—稍后的掛載將位于目標元素中較早的掛載之后。
<teleport to="#modals">
<div>A</div>
</teleport>
<teleport to="#modals">
<div>B</div>
</teleport>
<!-- result-->
<div id="modals">
<div>A</div>
<div>B</div>
</div>
依賴注入Provide / Inject
provide 和 inject 提供依賴注入,功能類似 2.x 的 provide/inject。兩者都只能在當前活動組件實例的 setup() 中調用
例如,如果我們想在根組件上提供一個book name,并將其inject到子組件上
import { provide, inject } from 'vue'
const RootComponent = {
setup() {
provide('book', 'Vue 3 guide')
}
}
const MyBook = {
setup() {
const book = inject(
'book',
'Eloquent Javascript' /* 選項的默認值,假如父組件不提供值就返回默認 */
)
return {
book
}
}
}
inject 接受一個可選的的默認值作為第二個參數。如果未提供默認值,并且在 provide 上下文中未找到該屬性,則 inject 返回 undefined。
如果我們需要提供或注入多個值,我們可以通過隨后分別調用provide或inject來實現【多次調用】
import { provide, inject } from 'vue'
const RootComponent = {
setup() {
provide('book', 'Vue 3 guide')
provide('year', '2020')
}
}
const MyBook = {
setup() {
const book = inject(
'book',
'Eloquent Javascript' /* 選項的默認值,假如父組件不提供值就返回默認 */
)
const year = inject('year')
return {
book,
year
}
}
}
注入的響應性
可以使用 ref 或 reactive 來保證 provided 和 injected 之間值的響應
import { ref, reactive } from 'vue'
// 提供者
setup() {
const book = reactive({
title: 'Vue 3 Guide',
author: 'Vue Team'
})
const year = ref('2020')
/*提供reactive響應式*/
provide('book', book)
/*提供ref響應式*/
provide('year', year)
}
// 消費者
setup() {
const book = inject('book')
const year = inject('year')
/*響應式*/
return { book, year }
}
現在,當提供者組件上的book或year發生變化時,我們可以觀察到它們在注入的組件上的變化。
警告:我們不建議改變一個被注入的反應性屬性【子組件去修改數據流】,因為它會破壞Vue的單向數據流。相反,嘗試在提供值【父組件去修改】的地方改變值,或者提供一個方法來改變值
import { ref, reactive } from 'vue'
// in provider
setup() {
const book = reactive({
title: 'Vue 3 Guide',
author: 'Vue Team'
})
function changeBookName() {
book.title = 'Vue 3 Advanced Guide'
}
provide('book', book)
provide('changeBookName', changeBookName)
}
// in consumer
setup() {
const book = inject('book')
const changeBookName = inject('changeBookName')
return { book, changeBookName }
}
指令
v-text 【Vue2.x一致】
v-html【Vue2.x一致】
v-show【Vue2.x一致】
v-if【Vue2.x一致】
v-else【Vue2.x一致】
v-else-if【Vue2.x一致】
v-for【Vue2.x一致】
v-on【Vue2.x一致】
v-bind 【Vue2.x 修飾符差異】
修飾符
.prop 去除
.sync 去除
.camel 將 kebab-case attribute 名轉換為 camelCase
v-model【Vue2.x一致】
v-slot【Vue2.x一致】
v-cloak【Vue2.x一致】
v-once 【Vue2.x一致】
v-pre【Vue2.x一致】
v-is【新增】
注意:本節只影響在頁面的HTML中直接編寫Vue模板的情況
限制:原生html元素
使用:
使用in-DOM模板時,該模板應遵守本機HTML解析規則。 某些HTML元素(例如,,和)對可以在其中顯示哪些元素有限制,而某些元素(例如,和)只能 出現在某些其他元素內。 解決方法是,我們可以在這些元素上使用v-is指令【作用就是轉成組件的名字】
警告v-is 功能 像一個動態2.x :is 綁定 所以要根據注冊的名稱渲染組件,它的值應該是一個JavaScript字符串
<!-- 不正確的, 不會出現任何渲染 -->
<tr v-is="blog-post-row"></tr>
<!-- 正確 -->
<tr v-is="'blog-post-row'"></tr>
全局API
createApp
返回一個應用程序實例,提供了一個應用程序上下文。應用程序實例掛載的整個組件樹共享相同的上下文
const app = Vue.createApp({})
參數
該函數接收一個根組件選項對象作為第一個參數
const app = Vue.createApp({
data() {
return {
...
}
},
methods: {...},
computed: {...}
setup(){...}
...
})
使用第二個參數,我們可以將根組件props 傳遞給應用
<div id="app">
<!-- 這里將會顯示 'Evan' -->
{{ username }}
</div>
const app = Vue.createApp(
{
props: ['username']
},
{ username: 'Evan' }
)
h
返回“虛擬節點”,通??s寫為VNode:一個簡單的對象,它包含描述Vue應該在頁面上渲染何種類型的節點的信息,包括對任何子節點的描述。你可以手動閱讀render functions
render() {
return Vue.h('h1', {}, 'Some title')
}
參數
接受三個參數tag, props and children
tag:
類型:String | Object | Function | null
詳情:一個HTML標簽名,一個組件,一個異步組件或null。使用null將渲染成注釋。此參數是必需的
props
類型:Object
詳情:模板中使用的attributes、props 和events 對應的對象??蛇x
children
類型: String | Array | Object
詳情:
Children VNodes,使用h()構建,或使用字符串來獲取“text VNodes”或帶有槽的對象??蛇x
const aaa = {
props: {
someProp: String
},
setup(props) {
console.log(props, "dsadasdasddasds");
},
render() {
return h(
"h2",
// {Object}props
//與props,attributes和events相對應的對象
//我們將在template中使用。
// 可選的。
{style: {"font-size": "20px",
color: "#136"}},
[this.someProp,this.$slots.default()]);
}
};
app.component("anchored-heading", {
render() {
return h(
/*
// {String | Object | Function | null}標簽
// HTML標記名稱,組件,異步組件或null。
//使用null將渲染注釋。
//必填
*/
"h" + this.level, // tag name
// {Object}props
//與props,attributes和events相對應的對象
//我們將在template中使用。
// 可選的。
{},
// {String | Array | Object} children
//使用`h()`構建的子級VNode,
//或使用字符串獲取“文本VNodes”或
//具有插槽的對象。
// 可選的。
[
"Some text comes first.",
h("h1", "A headline"),
h(aaa, {
someProp: "foobar"
})
] );},
});
Vue.h(
'a',
{
name: headingId,
href: '#' + headingId
},
this.$slots.default()
)
])
限制
VNodes 必須獨一無二
組件樹中的所有vnode必須是唯一的。這意味著下面的渲染函數是無效的
render() {
const myParagraphVNode = Vue.h('p', 'hi')
return Vue.h('div', [
// 表示驚訝 - 副本復制 VNodes!
myParagraphVNode, myParagraphVNode
])
}
如果您確實想多次復制相同的元素/組件,則可以使用工廠函數進行復制。 例如,以下呈現函數是呈現20個相同段落的完美有效方法:
render() {
return Vue.h('div',
Array.apply(null, { length: 20 }).map(() => {
return Vue.h('p', 'hi')
})
)
}
用普通的JavaScript替換模板特性
v-if and v-for
在任何地方都可以用普通JavaScript輕松完成,Vue渲染functions 都不提供專有的替代方案。例如,在使用v-if和v-for的模板中
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
==>
props: ['items'],
render() {
if (this.items.length) {
return Vue.h('ul', this.items.map((item) => {
return Vue.h('li', item.name)
}))
} else {
return Vue.h('p', 'No items found.')
}
}
v-model
v-model指令被擴展到modelValue和onUpdate:modelValue道具在模板編譯期間,我們將不得不自己提供這些props
props: ['modelValue'],
render() {
return Vue.h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
v-on
我們必須為事件處理程序提供一個適當的prop名稱,例如,為了處理click事件,prop名稱應該是onClick
render() {
return Vue.h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}
事件修飾符
對于.passive、.capture和.once事件修飾符,Vue提供了處理程序的對象語法
render() {
return Vue.h('input', {
onClick: {
handler: this.doThisInCapturingMode,
capture: true
},
onKeyUp: {
handler: this.doThisOnce,
once: true
},
onMouseOver: {
handler: this.doThisOnceInCapturingMode, //事件
once: true, //是否觸發一次
capture: true
},
})
}
對于所有其他事件和鍵修飾符,不需要特殊的API,因為我們可以在處理程序中使用事件方法
render() {
return Vue.h('input', {
onKeyUp: event => {
// 如果發出事件的元素不存在,則中止事件綁定到的元素
if (event.target !== event.currentTarget) return
// 同時如果按下的鍵不是enter鍵key (13)以及shift鍵沒有按下
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件傳播
event.stopPropagation()
// 阻止此元素的默認keyup處理程序
event.preventDefault()
// ...
}
})
}
Slots
你可以訪問插槽內容this.$slots在VNodes數組的
render() {
// `<div><slot></slot></div>`
return Vue.h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
// `<div><slot :text="message"></slot></div>`
return Vue.h('div', {}, this.$slots.default({
text: this.message
}))
}
使用render函數將槽傳遞給子組件
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return Vue.h('div', [
Vue.h('child', {}, {
// 通過`slots'作為子對象
// in the form of { name: props => VNode | Array<VNode> }
default: (props) => Vue.h('span', props.text)
})
])
}
JSX
如果我們要編寫大量的渲染函數,編寫這樣的東西可能會讓人感到痛苦
Vue.h(
'anchored-heading',
{
level: 1
},
[Vue.h('span', 'Hello'), ' world!']
)
特別是當模板版本相比之下如此簡潔的時候
<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
這就是為什么有一個Babel插件可以在Vue中使用JSX,讓我們回到更接近模板的語法
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
defineComponent【組件】
在實現方面,defineComponent只會執行返回傳遞給它的對象的操作。 但是,就類型而言,返回的值具有人工渲染功能,TSX和IDE工具支持的構造函數的綜合類型
參數
具有組件選項的對象
import { defineComponent } from 'vue'
const MyComponent = defineComponent({
data() {
return { count: 1 }
},
methods: {
increment() {
this.count++
}
}
})
defineAsyncComponent 【異步組件】
創建只在必要時加載的異步組件
參數
對于基本用法,defineAsyncComponent可以接受返回Promise的工廠函數。當您從serve檢索到組件定義時,應該調用Promise的解析回調。您還可以調用reject(reason)來指示加載失敗。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
/*或者*/
import('./components/AsyncComponent.vue')
/*或者*/
new Promise((resolve, reject) => {
/*可以reject*/
resolve({
template: '<div>I am async!</div>'
})
})
)
app.component('async-component', AsyncComp)
在使用本地注冊時,還可以直接提供返回Promise的函數
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
對于高級用法,defineAsyncComponent可以接受一個對象
const AsyncComp = defineAsyncComponent({
// 工廠函數
loader: () => import('./Foo.vue')
// 加載異步組件時使用的組件
loadingComponent: LoadingComponent,
//加載失敗的時候使用的組件
errorComponent: ErrorComponent,
// 在顯示加載組件之前延遲。默認值:200 ms。
delay: 200,
// 如果超時,將顯示錯誤組件
// 存在timeout并且超過這個時間. 默認值:無窮
timeout: 3000,
// 返回布爾值的函數,指示當加載器promise rejects時異步組件是否應該重試
retryWhen: error => error.code !== 404,
// 允許的最大重試次數
maxRetries: 3,
// 定義組件是否可掛載
suspensible: false
})
resolveComponent
警告resolveComponent只能在render或setup函數中使用。
允許通過名稱解析組件,如果它在當前應用程序實例中可用。如果找不到組件,返回組件或未定義組件
如果找不到組件,返回組件或未定義組件【組件】
app.component('MyComponent', {
/* ... */
})
const MyComponent = resolveComponent('MyComponent')
resolveDynamicComponent【解析活動的組件active】
resolveDynamicComponent只能在render或setup函數中使用。
允許使用與<component:is="">相同的機制來解析組件。
返回解析的組件或一個新創建的VNode以組件名稱作為節點標記的。
如果沒有找到組件,會發出警告
resolveDirective
警告resolveDirective只能在render或setup函數中使用。
允許通過名稱解析指令,如果它在當前應用程序實例中可用。
返回一個Directive或 當沒有找到的時候,返回undefined。
app.directive('highlight', {})
render(){
const highlightDirective = resolveDirective('highlight')
}
withDirectives
警告withDirectives只能在render或setup函數中使用。
:::允許應用指令到VNode。返回一個帶有應用指令的VNode。
const bar = resolveDirective('bar')
return withDirectives(h('div'), [
[bar, this.y]
])
createRenderer *【待】
nextTick
將回調延遲到下一個DOM更新周期之后執行。在更改了一些數據以等待DOM更新之后立即使用它
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
/*等待DOM更新*/
await nextTick()
console.log('Now DOM is updated')
}
}
實例方法methods
$watch
參數
{string | Function} source
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate
用法
觀察組件實例上的響應式屬性或computed函數的更改。使用回調獲取到給定屬性的新值和舊值。我們只能通過頂級data、prop或computed的屬性名作為字符串的形式傳遞。對于更復雜的表達式或嵌套屬性,使用函數代替。
例子
const app = Vue.createApp({
data() {
return {
a: 1,
b: 2,
c: {
d: 3,
e: 4
}
}
},
created() {
// 頂級屬性名a
this.$watch('a', (newVal, oldVal) => {
// 做一些事
})
// 觀察監視單個嵌套屬性
this.$watch(
() => this.c.d,
(newVal, oldVal) => {
// 做一些事
}
)
// 監控復雜表達式
this.$watch(
// 每當表達式`this.a + this.b`產生不同的結果時
// 處理程序將被調用。這就好像我們在看computed屬性
// 而不定義計算屬性本身
() => this.a + this.b,
(newVal, oldVal) => {
// 做一些事
}
)
}
})
當監視的值是對象或數組時,對其屬性或元素的任何更改都不會觸發監視程序,因為它們引用相同的對象/數組
const app = Vue.createApp({
data() {
return {
article: {
text: 'Vue is awesome!'
},
comments: ['Indeed!', 'I agree']
}
},
created() {
this.$watch('article', () => {
console.log('Article changed!')
})
this.$watch('comments', () => {
console.log('Comments changed!')
})
},
methods: {
// 這些方法不會觸發觀察者,因為我們僅更改了對象/數組的屬性,
// 并不是 Object/Array 本身
changeArticleText() {
this.article.text = 'Vue 3 is awesome'
},
addComment() {
this.comments.push('New comment')
},
// 這些方法會觸發觀察者,因為我們完整替換了對象/數組
changeWholeArticle() {
this.article = { text: 'Vue 3 is awesome' }
},
clearComments() {
this.comments = []
}
}
})
$watch返回一個取消監視的函數,該函數停止觸發回調
const unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
Option: deep
檢測對象內部嵌套的值更改,需要在options參數中傳入deep: true。注意,偵聽數組突變并不需要這樣做。
vm.$watch('someObject', callback, {
deep: true
})
vm.someObject.nestedValue = 123
// 觸發回調
Option: immediate
在選項中傳入immediate: true將立即用表達式的當前值觸發回調
vm.$watch('a', callback, {
immediate: true
})
// “callback”被立即觸發,當前值為“a”
請注意,使用immediate選項,您將無法在第一個回調調用中取消監視給定的屬性。
//這個例子是錯誤的
const unwatch = vm.$watch(
'value',
function() {
doSomething()
unwatch()
},
{ immediate: true }
)
如果你仍然想在回調中調用一個unwatch函數,你應該首先檢查它的可用性
const unwatch = vm.$watch(
'value',
function() {
doSomething()
if (unwatch) {
unwatch()
}
},
{ immediate: true }
)
$emit 【一致】
$forceUpdate【一致】
$nextTick【一致】
實例 property
vm.$data 【一致】
vm.$props 【一致】
vm.$el 【一致】
vm.$options 【一致】
vm.$parent 【一致】
vm.$root【一致】
vm.$slots 【一致】
vm.$refs 【一致】
vm.$attrs 【一致】
廢棄:
vm.$children
vm.$slots
vm.$scopedSlots
vm.$isServer
vm.$listeners
選項 / 組合
mixins 【一致】
extends【一致】
provide / inject【一致】
parent【廢棄】
setup【新增】
詳情見上
選項 / 資源
directives【一致】
components【一致】
filters【廢棄】
選項 / 數據
data【一致】
props【一致】
computed【一致】
methods【一致】
watch【一致】
emits【新增】
詳情
可以從組件發出的自定義事件的list/hash。 它具有基于數組的簡單語法和允許配置事件驗證的替代的基于對象的語法。
在基于對象的語法中,每個屬性的值可以為null或驗證函數。 驗證函數將接收傳遞給emit調用的其他參數。例如,如果調用this.emit調用的其他參數。 例如,如果調用this.emit調用的其他參數。例如,如果調用this.emit('foo',1),則foo的相應驗證器將接收參數1。驗證器函數應返回一個布爾值,以指示事件參數是否有效。
const app = Vue.createApp({})
// 數組語法
app.component('todo-item', {
emits: ['check'],
created() {
this.$emit('check')
}
})
// 對象語法
app.component('reply-form', {
emits: {
// 無效
click: null,
// 有效
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
})
提示 在emit選項中列出的事件將不會被組件的根元素繼承。
vue都是函數
createApp
const app = createApp(App)
app.use(store)
app.use(router)
app.mount('#app')
傳了兩個屬性
v-model:selectKeys = "selectKeys"
import {reactive,toRef } from 'vue
export default{
setup(props,ctx){
//默認執行一次
//頁面使用 state.selectKeys
const state = reactive({ //attr slots emit
selectKeys:0
})
//1.直接使用
return {
selectKeys:state.selectKeys
}
//2.導出,頁面上直接使用,數據響應式還帶解構
return {
...toRefs(state)
}
onMounted(()=>{
})
}
}
監控路由變化
import {reactive,toRef,watch } from 'vue
import {useRoute} from 'vue-router'
export default{
setup(props,ctx){
const state = reactive({ //attr slots emit
selectKeys:0
})
//1.watch監控路由變化
watch(()=>route.path,(newValue)=>{
state.selectKeys = [newValue]
})
//2.computed監控路由變化
const selectKeys = computed(()=>{
return [route.path]
})
return {
selectKeys
}
}
}
vuex
import {reactive,toRef,watch ,computed} from 'vue'
import {useRoute} from 'vue-router'
export default{
setup(props,ctx){
const route = userRoute()
const store = useStore()
const state = reactive({ //attr slots emit
selectKeys:0
})
//1.watch監控路由變化
watch(()=>route.path,(newValue)=>{
state.selectKeys = [newValue]
})
//2.computed監控路由變化
const selectKeys = computed(()=>{
return [route.path]
})
//ref 把普通值變成包裝后的結構,將屬性變成響應式
// ref(store.getters.allTime)
return {
selectKeys,
allTime:ref(store.getters.allTime)
}
}
}
//store.js
import {createStore} from 'vuex
export default {
state:{
},
getters:{
allTime:()=>{
return 0
}
},
mutations:{
},
actions:{
},
modules:{
}
}
組件通信
import {reactive,toRef,watch ,computed} from 'vue'
import {useRoute} from 'vue-router'
import moment from 'moment'
export default{
setup(props,ctx){
const state = reactive({
form:{
date:moment(Date.now()).format('YYYY-MM-DD')
}
})
//方法函數
const onSubmit =()=>{
//傳給父組件
this.$emit('handlePlan',state.form)
}
return {
...toRefs(state),
onSubmit
}
}
}
//父組件
<Child @handlePlan="handlePlan" />
import {reactive,toRef,watch ,computed} from 'vue'
import {useRoute} from 'vue-router'
import moment from 'moment'
export default{
setup(props,ctx){
const state = reactive({
form:{
date:moment(Date.now()).format('YYYY-MM-DD')
}
})
const handlePlan = (plan)=>{
console.log(plan)
}
return {
handlePlan
}
}
}
環境變量
VUE_APP_URL = 'http://www.xxx.com:3000'
封裝api
import axios from 'axios
const instance = axios.create({
baseURL:process.env.VUE_APP_URL,
timeout:3000
})
instance.interceptors.request.use((config)=>{
return config
})
instance.interceptors.response.use((res)=>{
return res.data.data
},err=>{
return Promise.reject(err)
})
export function request(opts){
return instance(opts)
}
//request.js
import {request } from '../utils/axios'
export function getPlanList(){
return request({url:'/plan',method:'get'})
}
export function addPlan(data){
return request({url:'/plan',method:'post',data})
}
export function deletePlan(){
return request({url:'/plan',method:'delete',params:{id}})
}
//action_type.js
export const SET_PLAN_LIST = 'SET_PLAN_LIST'
export const ADD_PLAN = 'ADD_PLAN'
export const DELETE_PLAN = 'DELETE_PLAN'
//store.js
import {createStore} from 'vuex'
export * as types from './action_type'
import * as api from './request'
export default {
state:{
},
getters:{
allTime:()=>{
return 0
}
},
mutations:{
[type.ADD_PLAN](state,payload){
state.planList = [...state.planList,payload]
},
[type.DELETE_PLAN](state,payload){
state.planList.filter(item=>{
return item._id !=payload._id
})
},
[type.SET_PLAN_LIST](state,payload){
},
},
actions:{
//restful api根據不同方法返回不同的資源
async [type.ADD_PLAN]({commit},payload){
let plan = await api.addPlan(payload)
commit(type.ADD_PLAN,plan)
},
async [type.DELETE_PLAN]({commit},payload){
let plan = await api.deletePlan(payload)
commit(type.DELETE_PLAN,plan)
},
async [type.SET_PLAN_LIST]({commit},payload){
let plan = await api.getPlanList(payload)
commit(type.SET_PLAN_LIST,plan)
},
},
modules:{
}
}
使用數據
import {reactive,toRef,watch ,onMounted,onUpdated,compile,computed} from 'vue'
import {useStore} from 'vuex'
import moment from 'moment'
import * as types from '@/store/action_types'
export default{
setup(props,ctx){
const store = useStore()
// const state = reactive({
// planList:store.state.planList //這樣取的是默認值
// })
onMounted(()){
store.dispatch(types.SET_PLAN_LIST)
}
//時間格式化方法
const formatDate = (value)=>{
return moment(value).format('YYYY-MM-DD')
}
return {
...toRefs(state.store),
formatDate
}
}
}
簡版vue
//1.創建虛擬節點,將虛擬節點轉化為真實節點
//2.組件的實現 setup
//3.reactive api實現effect
//4.diff算法
//5.vite
let { render} = Vue
const state = {
count:0
}
const vnode = {
tag:'div',
props:{color:'red'},
children:[
{
tag:'p',
props:{color:'blue},
children:[
'vue@3-計數器'
]
},
{
tag:'p',
props:{
onClick:()=>{
alert(state.count)
}
}
children:[
'vue@3-計數器'
]
}
]
}
render(vnode,app)
export function render(vnode,container){
// 渲染頁面的方法叫patch
//1.第一次渲染 2.dom-diff
patch(null,vnode,container)
}
/**
* n1 老的虛擬節點
* n2 新的虛擬節點
* container 容器
*/
function patch(n1,n2,container){
//組件的虛擬節點是一個對象,tag是一個對象
//如果是組件,tag可能是個對象
//后續diff可以執行這個方法
if(typeof n2.tag ==='string'){
//標簽
mountElement(n2,container)
}else if(typeof n2.tag==='object'){
}
}
function mountElement(vnode,container){
const { tag,children,props } = vnode
//虛擬節點和真實節點做映射關系
let el = (vnode.el = nodeOps.createElement(tag))
if(Array.isArray(children)){
mountChild(children,el)
}else{
nodeOps.hostSetElementText(el,children)
}
container.insert(el,container)
}
function mountChild(children,container){
for(var i=0;i<children.length;i++){
let child = children[i]
patch(null,child,container)
}
}
//節點操作方法
exoprt const nodeOps = {
//插入元素節點
insert(child,parent,anchor){
if(anchor){
parent.insertBefore(child,anchor)
}else{
parent.appendChild(child)
}
},
//移除節點
remove(child){
const parent = child.parentNode;
parent && parent.removeChild(child)
},
//創建節點
createElement(tag){
return document.createElement(tag)
},
//設置文本內容
hostSetElementText(el,text){
el.textContent = text
}
}