讓我們一起愉快地學習vue3.0吧


Object.defineProperty => Proxy
重構(gòu)了虛擬DOM
OptionApi => Composition API

setup是干啥的?
setup實際上是一個組件的入口,它運行在組件被實例化時候,props 屬性被定義之后,實際上等價于 2 版本的beforeCreateCreated 這兩個生命周期。
setup接受兩個參數(shù),第一個參數(shù)是props, 另一個參數(shù)是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讓我們有機會創(chuàng)建單個的響應式的對象,在setup函數(shù)中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')

反轉(zhuǎn)字符串 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')

數(shù)據(jù)響應式

在Vue3中實現(xiàn)數(shù)據(jù)響應式的方案由Vue2中的Object.defineProperty 換成了 Proxy,關(guān)于數(shù)據(jù)響應式的Api上邊說到了一些,還剩下effectwatch沒有提及到,effect是數(shù)據(jù)響應式中重要的一部分,watchcomputed都是基于 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 在響應式數(shù)據(jù)變化的時候就會執(zhí)行,執(zhí)行次數(shù)根據(jù)響應式數(shù)據(jù)的個數(shù)來決定

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則點擊一次 ,只會觸發(fā)執(zhí)行一次

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創(chuàng)建實例并且掛載DOM

import Vue from "vue";
import App from './App.vue'

new Vue({
  render: (h) => h(App)
}).$mount("#app");

Vue3新增api===>createApp 創(chuàng)建實例

createApp 會產(chǎn)生一個 app 實例,該實例擁有全局的可配置上下文

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

component

Vue2.x【注冊或獲取全局組件。注冊還會自動使用給定的 id 設(shè)置組件的名稱】

// 注冊組件,傳入一個選項對象 (自動調(diào)用 Vue.extend) 
Vue.component('my-component', { /* ... */ }) 

// 獲取注冊的組件 (始終返回構(gòu)造器) 
var MyComponent = Vue.component('my-component')

Vue3【注冊或獲取全局組件注冊還會自動使用給定的name組件 設(shè)置組件的名稱】全局組件
基本vue2寫法一致

import { createApp } from 'vue'

const app = createApp({})

// 注冊組件,傳入一個選項對象
app.component('my-component', {
  /* ... */
})

// 獲取注冊的組件 (始終返回構(gòu)造器) 
const MyComponent = app.component('my-component', {})

globalProperties 【新增屬性】

app.config.globalProperties.foo = 'bar'

app.component('child-component', {
  mounted() {
    console.log(this.foo) // 'bar'
  }
})

添加可在程序內(nèi)的任何組件實例中訪問的全局屬性。當存在鍵沖突時,組件屬性將優(yōu)先
替代掉Vue2.xVue.prototype屬性放到原型上的寫法

// Vue2.x
Vue.prototype.$http = () => {}

// Vue3
const app = Vue.createApp({})
app.config.globalProperties.$http = () => {}

isCustomElement 【新增屬性】

替代掉Vue2.xignoredElements

- Vue.config.ignoredElements = [
  // 用一個 `RegExp` 忽略所有“ion-”開頭的元素
  // 僅在 2.5+ 支持
  /^ion-/
]

// 一些組件以'ion-'開頭將會被解析為自定義組件

+ app.config.isCustomElement = tag => tag.startsWith('ion-')

指定一個方法來識別在Vue之外定義的自定義組件(例如,使用Web Component API)。如果組件符合這個條件,它就不需要本地或全局注冊,Vue也不會拋出關(guān)于Unknown custom element的警告

注意,這個函數(shù)中不需要匹配所有原生HTML和SVG標記—Vue解析器會自動執(zhí)行此檢查

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,分別作為第一個和第二個參數(shù)。上下文Vue實例作為第三個參數(shù)傳遞

【自定義選項合并策略】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第一次改變從undefinedgoodbye--->打印"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', {
  // 指令的生命周期
  // 在綁定元素的父組件被掛載之前調(diào)用
  beforeMount(el, binding, vnode) {},
  // 在掛載綁定元素的父組件時調(diào)用
  mounted(el, binding, vnode) {},
  // 在更新包含組件的VNode之前調(diào)用
  beforeUpdate(el, binding, vnode, prevNode) {},
  // 組件的VNode及其子組件的VNode更新之后調(diào)用
  updated(el, binding, vnode, prevNode) {},
  // 在卸載綁定元素的父組件之前調(diào)用
  beforeUnmount(el, binding, vnode) {},
  // 在卸載綁定元素的父組件時調(diào)用
  unmounted(el, binding, vnode) {}
})

// 注冊 (指令函數(shù))
app.directive('my-directive', (el, binding, vnode, prevNode) => {
  // 這里將會被 `mounted` 和 `updated` 調(diào)用
})

// getter,返回已注冊的指令
const myDirective = app.directive('my-directive')
el
指令綁定到的元素。這可以用來直接操作DOM。

binding【包含下列屬性的對象】

instance:使用指令的組件的實例

value:指令的綁定值,例如:v-my-directive="1 + 1"中,綁定值為 2

oldValue:指令綁定的前一個值,僅在 beforeUpdate 和 updated 鉤子中可用。無論值是否改變都可用

arg:傳給指令的參數(shù),可選。例如 v-my-directive:foo 中,參數(shù)為 "foo"

modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象為 { foo: true, bar: true }

dir:一個對象,在注冊指令時作為參數(shù)傳遞;  舉個例子,看下面指令
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})
dir就是下面的對象
{
  mounted(el) {
    el.focus()
  }
}
vnode
編譯生成的虛擬節(jié)點

prevNode
前一個虛擬節(jié)點,僅在beforeUpdate和updated鉤子中可用

tips:除了 el 之外,其它參數(shù)都應該是只讀的,切勿進行修改。如果需要在鉤子之間共享數(shù)據(jù),建議通過元素的 dataset 來進行

mount【類似Vue2.x】

在所提供的DOM元素上掛載應用程序?qū)嵗母M件

import { createApp } from 'vue'

const app = createApp({})
// 做一些準備
app.mount('#my-app')

provide/inject【Vue2.x一致】

該選項與inject一起使用,允許一個祖先組件作為其所有后代的依賴注入器,無論組件層次結(jié)構(gòu)有多深,只要它們位于同一父鏈中就可以


provide 選項應該是一個對象或返回一個對象的函數(shù)。該對象包含可注入其子孫的 property。在該對象中你可以使用 ES2015 Symbols 作為 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的環(huán)境下可工作。


如果在組件中兩者都只能在當前活動組件實例的 setup() 中調(diào)用,詳細請看依賴注入部分
import { createApp } from 'vue'

const app = createApp({
  provide: {
    user: 'John Doe'
  }
})
app.component('user-card', {
  inject: ['user'],
  template: `
    <div>
      {{ user }}
    </div>
  `
})

unmount【新增屬性】

在所提供的DOM元素上卸載應用程序?qū)嵗母M件
import { createApp } from 'vue'

const app = createApp({})
// 做一些必要的準備
app.mount('#my-app')

// 應用程序?qū)⒃趻燧d后5秒被卸載
setTimeout(() => app.unmount('#my-app'), 5000)

use【Vue2.x一致】

安裝 Vue.js 插件。如果插件是一個對象,必須提供 install 方法。如果插件是一個函數(shù),它會被作為 install 方法。install 方法調(diào)用時,會將 Vue 作為參數(shù)傳入。
當 install 方法被同一個插件多次調(diào)用,插件將只會被安裝一次。
setup
setup 函數(shù)是一個新的組件選項。作為在組件內(nèi)使用 Composition API 的入口點

注意 setup 返回的 ref 在模板中會自動解開,不需要寫 .value【setup 內(nèi)部需要.value】

調(diào)用時機

創(chuàng)建組件實例,然后初始化 props ,緊接著就調(diào)用setup 函數(shù)。從生命周期鉤子的視角來看,它會在 beforeCreate 鉤子之前被調(diào)用

如果 setup 返回一個對象,則對象的屬性將會被合并到組件模板的渲染上下文

參數(shù)
props 作為其第一個參數(shù)

注意 props 對象是響應式的,watchEffect 或 watch 會觀察和響應 props 的更新
不要解構(gòu) props 對象,那樣會使其失去響應性
```js

```js
export default {
  props: {
    name: String,
  },
  setup(props) {
    console.log(props.name)
     watchEffect(() => {
      console.log(`name is: ` + props.name)
    })
  },
}
第二個參數(shù)提供了一個上下文對象【從原來 2.x 中 this 選擇性地暴露了一些 property(attrs/emit/slots)】

attrs 和 slots 都是內(nèi)部組件實例上對應項的代理,可以確保在更新后仍然是最新值。所以可以解構(gòu),無需擔心后面訪問到過期的值

為什么props作為第一個參數(shù)?

組件使用 props 的場景更多,有時候甚至只使用 props
將 props 獨立出來作為第一個參數(shù),可以讓 TypeScript 對 props 單獨做類型推導,不會和上下文中的其他屬性相混淆。這也使得 setup 、 render 和其他使用了 TSX 的函數(shù)式組件的簽名保持一致

this 在 setup() 中不可用。由于 setup() 在解析 2.x 選項前被調(diào)用,setup() 中的 this 將與 2.x 選項中的 this 完全不同。同時在 setup() 和 2.x 選項中使用 this 時將造成混亂

setup(props, { attrs }) {
    // 一個可能之后回調(diào)用的簽名
    function onClick() {
      console.log(attrs.foo) // 一定是最新的引用,沒有丟失響應性
    }
  }

響應式系統(tǒng) API

reactive
desc: 接收一個普通對象然后返回該普通對象的響應式代理【等同于 2.x 的 Vue.observable()】

tips:Proxy對象是目標對象的一個代理器,任何對目標對象的操作(實例化,添加/刪除/修改屬性等等),都必須通過該代理器。因此我們可以把來自外界的所有操作進行攔截和過濾或者修改等操作

響應式轉(zhuǎn)換是“深層的”:會影響對象內(nèi)部所有嵌套的屬性。基于 ES2015 的 Proxy 實現(xiàn),返回的代理對象不等于原始對象。建議僅使用代理對象而避免依賴原始對象

reactive 類的 api 主要提供了將復雜類型的數(shù)據(jù)處理成響應式數(shù)據(jù)的能力,其實這個復雜類型是要在object array map set weakmap weakset 這五種之中【如下源碼,他會判斷是否是五類以及是否被凍結(jié)】
因為是組合函數(shù)【對象】,所以必須始終保持對這個所返回對象的引用以保持響應性【不能解構(gòu)該對象或者展開】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }
function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return pos
}
toRefs API 用來提供解決此約束的辦法——它將響應式對象的每個 property 都轉(zhuǎn)成了相應的 ref【把對象轉(zhuǎn)成了ref】。
 function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return toRefs(pos)
}
// x & y 現(xiàn)在是 ref 形式了!
const { x, y } = useMousePosition()

ref

接受一個參數(shù)值并返回一個響應式且可改變的 ref 對象。ref 對象擁有一個指向內(nèi)部值的單一屬性 .value
const count = ref(0)
console.log(count.value) // 0
如果傳入 ref 的是一個對象,將調(diào)用 reactive 方法進行深層響應轉(zhuǎn)換
陷阱
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 分配給現(xiàn)有的 ref, 將替換舊的 ref
/*創(chuàng)建一個新的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 時,不會自動解套【自由數(shù)據(jù)類型是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 中區(qū)別聲明基礎(chǔ)類型變量與對象變量時一樣區(qū)別使用 ref 和 reactive
所有的地方都用 reactive,然后記得在組合函數(shù)返回響應式對象時使用 toRefs。這降低了一些關(guān)于 ref 的心智負擔

readonly
傳入一個對象(響應式或普通)或 ref,返回一個原始對象的只讀代理。一個只讀的代理是“深層的”,對象內(nèi)部任何嵌套的屬性也都是只讀的【返回一個永遠不會變的只讀代理】【場景可以參數(shù)比對等】
const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 依賴追蹤
  console.log(copy.count)
})

// original 上的修改會觸發(fā) copy 上的偵聽
original.count++

// 無法修改 copy 并會被警告
copy.count++ // warning!
reactive響應式系統(tǒng)工具集
isProxy

 檢查一個對象是否是由 reactive 或者 readonly 方法創(chuàng)建的代理

isReactive

 檢查一個對象是否是由 reactive 創(chuàng)建的響應式代理
import { reactive, isReactive } from 'vue'
const state = reactive({
      name: 'John'
    })
console.log(isReactive(state)) // -> true
如果這個代理是由 readonly 創(chuàng)建的,但是又被 reactive 創(chuàng)建的另一個代理包裹了一層,那么同樣也會返回 true
import { reactive, isReactive, readonly } from 'vue'
const state = reactive({
      name: 'John'
    })
// 用readonly創(chuàng)建一個只讀響應式對象plain
const plain = readonly({
    name: 'Mary'
})
//readonly創(chuàng)建的,所以isReactive為false
console.log(isReactive(plain)) // -> false  

// reactive創(chuàng)建的響應式代理對象包裹一層readonly,isReactive也是true,isReadonly也是true
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
isReadonly

檢查一個對象是否是由 readonly 創(chuàng)建的只讀代理

reactive高級響應式系統(tǒng)API
toRaw

返回由 reactive 或 readonly 方法轉(zhuǎn)換成響應式代理的普通對象。這是一個還原方法,可用于臨時讀取,訪問不會被代理/跟蹤,寫入時也不會觸發(fā)更改。不建議一直持有原始對象的引用【**不建議賦值給任何變量**】。請謹慎使用

被toRaw之后的對象是沒有被代理/跟蹤的的普通對象
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true
console.log(toRaw(reactiveFoo) !== reactiveFoo) // true
markRaw
顯式標記一個對象為“永遠不會轉(zhuǎn)為響應式代理”,函數(shù)返回這個對象本身。

【markRaw傳入對象,返回的值是永遠不會被轉(zhuǎn)為響應式代理的】
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 默認創(chuàng)建的 "深層的" 特性【響應式】/或者使用無代理的普通對象


設(shè)計這種「淺層讀取」有很多原因

一些值的實際上的用法非常簡單,并沒有必要轉(zhuǎn)為響應式【例如三方庫的實例/省市區(qū)json/Vue組件對象】
當渲染一個元素數(shù)量龐大,但是數(shù)據(jù)是不可變的,跳過 Proxy 的轉(zhuǎn)換可以帶來性能提升



這些 API 被認為是高級的,是因為這種特性僅停留在根級別,所以如果你將一個嵌套的,沒有 markRaw 的對象設(shè)置為 reactive 對象的屬性,在重新訪問時,你又會得到一個 Proxy 的版本,在使用中最終會導致標識混淆的嚴重問題:執(zhí)行某個操作同時依賴于某個對象的原始版本和代理版本(標識混淆在一般使用當中應該是非常罕見的,但是要想完全避免這樣的問題,必須要對整個響應式系統(tǒng)的工作原理有一個相當清晰的認知)。
const foo = markRaw({
  nested: {},
})

const bar = reactive({
  // 盡管 `foo` 己經(jīng)被標記為 raw 了, 但 foo.nested 并沒有
  nested: foo.nested,
})

console.log(foo.nested === bar.nested) // false
foo.nested沒有被標記為(永遠不會轉(zhuǎn)為響應式代理),導致最后的值一個reactive




shallowReactive
只為某個對象的私有(第一層)屬性創(chuàng)建淺層的響應式代理,不會對“屬性的屬性”做深層次、遞歸地響應式代理,而只是保留原樣【第一層是響應式代理,深層次只保留原樣(不具備響應式代理)】
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 變更 state 的自有屬性是響應式的【第一層次響應式】
state.foo++
// ...但不會深層代理【深層次不是響應式】(渲染性能)
isReactive(state.nested) // false
state.nested.bar++ // 非響應式
shallowReadonly
類似于shallowReactive,區(qū)別是:

第一層將會是響應式代理【第一層修改屬性會失敗】,屬性為響應式
深層次的對象屬性可以修改,屬性不是響應式

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 變更 state 的自有屬性會失敗
state.foo++
// ...但是嵌套的對象是可以變更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套屬性依然可修改
ref 響應式系統(tǒng)工具集
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 對象的屬性【某個屬性區(qū)別toRefs每一個屬性】創(chuàng)建一個 ref。這個 ref 可以被傳遞并且能夠保持響應性
const state = reactive({
  foo: 1,
  bar: 2,
})

//reactive獲取單個屬性轉(zhuǎn)為ref【fooRef只是一個代理】
const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

把一個響應式對象轉(zhuǎn)換成普通對象,該普通對象的每個 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返回可解構(gòu)的reactive,因為toRefs包裹之后返回一一對應的ref屬性   
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  })

  // 對 state 的邏輯操作

  // 返回時將屬性都轉(zhuǎn)為 ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以解構(gòu),不會丟失響應性
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar,
    }
  },
}

isRef

檢查一個值是否為一個 ref 對象
ref 高級響應式系統(tǒng)API
customRef
用于自定義一個 ref,可以顯式地控制依賴追蹤和觸發(fā)響應,接受一個工廠函數(shù),兩個參數(shù)分別是用于追蹤的 track 與用于觸發(fā)響應的 trigger,并返回一個一個帶有 get 和 set 屬性的對象【實際上就是手動 track追蹤 和 trigger觸發(fā)響應】

以下代碼可以使得v-model防抖

function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
          /*初始化手動追蹤依賴講究什么時候去觸發(fā)依賴收集*/
        track()
        return value
      },
      set(newValue) {
          /*修改數(shù)據(jù)的時候會把上一次的定時器清除【防抖】*/
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            /*把新設(shè)置的數(shù)據(jù)給到ref數(shù)據(jù)源*/
          value = newValue
            /*再有依賴追蹤的前提下觸發(fā)響應式*/
          trigger()
        }, delay)
      },
    }
  })
}

setup() {
    return {
        /*暴露返回的數(shù)據(jù)加防抖*/
      text: useDebouncedRef('hello'),
    }
  }

shallowRef

創(chuàng)建一個 ref ,將會追蹤它的 .value 更改操作,但是并不會對變更后的 .value 做響應式代理轉(zhuǎn)換(即變更不會調(diào)用 reactive)

前面我們說過如果傳入 ref 的是一個對象,將調(diào)用 reactive 方法進行深層響應轉(zhuǎn)換,通過shallowRef創(chuàng)建的ref,將不會調(diào)用reactive【對象不會是響應式的】
const refOne = shallowRef({});
refOne.value = { id: 1 };
refOne.id == 20;
console.log(isReactive(refOne.value),refOne.value);//false  { id: 1 }
triggerRef 【與shallowRef配合】
手動執(zhí)行與shallowRef相關(guān)的任何效果
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次運行打印 "Hello, world" 
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 這不會觸發(fā)效果,因為ref是shallow
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)

Computed and watch【監(jiān)控變化】

computed


傳入一個 getter 函數(shù),返回一個默認不可手動修改的 ref 對象【默認傳入的是get函數(shù)的對象】


傳入一個擁有 get 和 set 函數(shù)的對象,創(chuàng)建一個可手動修改的計算狀態(tài)



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

立即執(zhí)行傳入的一個函數(shù),并響應式追蹤其依賴,并在其依賴變更時重新運行該函數(shù)
computed與watchEffect區(qū)別:

computed計算屬性可通過setup return,再模板中使用,watchEffect不能;
computed可以使用多個,并且對多個屬性進行不同的響應計算,watchEffect會存在副作用

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 打印出 0

setTimeout(() => {
  count.value++
  // -> 打印出 1
}, 100)

停止觀察


當在組件的setup()函數(shù)或生命周期鉤子期間調(diào)用watchEffect時,監(jiān)視程序會鏈接到組件的生命周期,并在卸載組件時自動停止
一般情況下watchEffect返回可以stop 操作,停止監(jiān)聽程序
const stop = watchEffect(() => {
  /* ... */
})

// 停止監(jiān)聽程序
stop()

副作用(函數(shù)式編程)

一個帶有副作用的函數(shù)不僅只是簡單的返回一個值,還干了一些其他的事情,比如:

修改一個變量
直接修改數(shù)據(jù)結(jié)構(gòu)
設(shè)置一個對象的成員
拋出一個異常或以一個錯誤終止
打印到終端或讀取用戶的輸入
讀取或?qū)懭胍粋€文件
在屏幕上繪畫


buyCoffee的例子(p3):函數(shù)只不過是需要返回一杯咖啡,可是卻對費用進行了持久化操作(產(chǎn)生副作用),我們可以在buyCoffee方法返回咖啡時也把費用作為值一并返回,將費用這條記錄交給其他程序來做持久化,以此來去除副作用 ====》通過把這些副作用推到程序的外層,來轉(zhuǎn)換任何帶有副作用的函數(shù)(純的內(nèi)核和一層很薄的外圍來處理副作用)


如果一個函數(shù)內(nèi)外有依賴于外部變量或者環(huán)境時,常常我們稱之為其有副作用,如果我們僅通過函數(shù)簽名不打開內(nèi)部代碼檢查并不能知道該函數(shù)在干什么,作為一個獨立函數(shù)我們期望有明確的輸入和輸出,副作用是bug的發(fā)源地,作為程序員開發(fā)者應盡量少的開發(fā)有副作用的函數(shù)或方法,副作用也使得方法通用性下降不適合擴展和可重用性

清除副作用
[^]: watchEffect函數(shù)都是副作用
在一些時候監(jiān)聽函數(shù)將執(zhí)行異步副作用【一個響應式依賴被修改了,會做其他事情】,這些響應需要在其失效時清除(例如在效果完成前狀態(tài)改變)。effect函數(shù)接收一個onInvalidate 函數(shù)作入?yún)ⅲ?用來注冊清理失效時的回調(diào)。這個 invalidation函數(shù) 在什么時候會被調(diào)用:


監(jiān)聽函數(shù)重新被執(zhí)行的時候【響應式依賴的數(shù)據(jù)被修改】


監(jiān)聽停止的時候(如果watchEffect在setup()或者生命周期函數(shù)中被使用的時候組件會被卸載)【停止觀察】
watchEffect(onInvalidate => {
  /*這是個異步操作*/
  const token = performAsyncOperation(id.value)//id依賴
  onInvalidate(() => {
    // id被修改了或者監(jiān)聽停止了會觸發(fā)token.cancel()事件【這塊區(qū)域的代碼】.
    // 這里是異步事件的話,前面的peding的異步操作無效【這里的異步事件只執(zhí)行一次】
     token.cancel()/*異步操作*/
    console.log('onInvalidate')
  })
})

從上面看:我們之所以是通過傳入一個函數(shù)去注冊失效回調(diào),而不是從回調(diào)返回它(如 React useEffect 中的方式),是因為返回值對于異步錯誤處理很重要

  const data = ref(null)
  watchEffect(async onInvalidate => {
    onInvalidate(() => {...}) // 我們在Promise的resolves之前注冊清理函數(shù)(cleanup function)
    data.value = await fetchData(props.id)
  })

  我們知道異步函數(shù)都會隱式地返回一個 Promise,但是清理副作用的函數(shù)必須要在 Promise 被 resolve 之前被注冊。另外,Vue 依賴這個返回的 Promise 來自動處理 Promise 鏈上的潛在錯誤
副作用刷新時機
Vue 的響應式系統(tǒng)會緩存副作用函數(shù),并異步地刷新它們,這樣可以避免同一個 tick 中多個狀態(tài)改變導致的不必要的重復調(diào)用。在核心的具體實現(xiàn)中, 組件的更新函數(shù)也是一個被偵聽的副作用。當一個用戶定義的副作用函數(shù)進入隊列時, 會在所有的組件更新后執(zhí)行
<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      const count = ref(0)

      watchEffect(() => {
        console.log(count.value)
      })

      return {
        count,
      }
    },
  }
</script>
count 會在初始運行時同步打印出來


更改 count 時,將在組件更新后執(zhí)行副作用

初始化運行是在組件 mounted 之前執(zhí)行的【你希望在編寫副作用函數(shù)時訪問 DOM(或模板 ref),請在 onMounted 鉤子中進行】
onMounted(() => {
  watchEffect(() => {
    // 在這里可以訪問到 DOM 或者 template refs
  })
})

如果副作用需要同步或在組件更新之前重新運行,我們可以傳遞一個擁有 flush 屬性的對象作為選項(默認為 'post')
// 同步運行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'sync',
  }
)

// 組件更新前執(zhí)行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'pre',
  }
)

偵聽器調(diào)試【響應式調(diào)試用的】

onTrack 和 onTrigger 選項可用于調(diào)試一個偵聽器的行為。

當一個 reactive 對象屬性或一個 ref 作為依賴被追蹤時,將調(diào)用 onTrack【調(diào)用次數(shù)為被追蹤的數(shù)量】
依賴項變更會導致重新追蹤依賴,從而onTrack被調(diào)用【調(diào)用次數(shù)為被追蹤的數(shù)量】
依賴項變更導致副作用被觸發(fā)時,將調(diào)用 onTrigger

這兩個回調(diào)都將接收到一個包含有關(guān)所依賴項信息的調(diào)試器事件。建議在以下回調(diào)中編寫 debugger 語句來檢查依賴關(guān)系:【onTrack 和 onTrigger 僅在開發(fā)模式下生效】
watchEffect(
  () => {
    /* 副作用的內(nèi)容 */
  },
  {
    onTrigger(e) {
      /*副作用依賴修改*/
      debugger
    },
    onTrack(e) {
      /*副作用依賴修改*/
      debugger
    },
  }
)

watch

watch API 完全等效于 2.x watch 中相應的選項。watch 需要偵聽特定的數(shù)據(jù)源,并在回調(diào)函數(shù)中執(zhí)行副作用【默認情況是懶執(zhí)行的,也就是說僅在偵聽的源變更時才執(zhí)行回調(diào)】
watch允許我們:

懶執(zhí)行副作用
更明確哪些狀態(tài)的改變會觸發(fā)偵聽器重新運行副作用
訪問偵聽狀態(tài)變化前后的值

偵聽單個數(shù)據(jù)源
偵聽器的數(shù)據(jù)源可以是一個擁有返回值的 getter 函數(shù),也可以是 ref:
// 偵聽一個 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接偵聽一個 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

偵聽多個數(shù)據(jù)源


watcher 也可以使用數(shù)組來同時偵聽多個源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

與 watchEffect 共享的行為

watch 和 watchEffect 在停止偵聽, 清除副作用 (相應地 onInvalidate 會作為回調(diào)的第三個參數(shù)傳入),副作用刷新時機 和 偵聽器調(diào)試 等方面行為一致
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar],onInvalidate) => {
  /* ... */
  onInvalidate(() => {...})
},
  {
    onTrigger(e) {
      /*副作用依賴修改*/
      debugger
    },
    onTrack(e) {
      /*副作用依賴修改*/
      debugger
    },
  })

生命周期鉤子函數(shù)

與 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!')
    })
  }
這些生命周期鉤子注冊函數(shù)只能在 setup() 期間同步使用, 因為它們依賴于內(nèi)部的全局狀態(tài)來定位當前組件實例(正在調(diào)用 setup() 的組件實例), 不在當前組件下調(diào)用這些函數(shù)會拋出一個錯誤。
組件實例上下文也是在生命周期鉤子同步執(zhí)行期間設(shè)置的,因此,在卸載組件時,在生命周期鉤子內(nèi)部同步創(chuàng)建的偵聽器和計算狀態(tài)也將自動刪除。
新增的鉤子函數(shù)
除了和 2.x 生命周期等效項之外,組合式 API 還提供了以下調(diào)試鉤子函數(shù):

onRenderTracked
onRenderTriggered

兩個鉤子函數(shù)都接收一個 DebuggerEvent,與 watchEffect 參數(shù)選項中的 onTrack 和 onTrigger 類似:
export default {
  onRenderTracked(e){
      debugger
    // 檢查有響應和追蹤的依賴性
 },
  onRenderTriggered(e) {
    debugger
    // 檢查哪個依賴性導致組件重新渲染
  },
}

Vue提供的內(nèi)置組件

component 與Vue2.x一致
渲染一個“元組件”為動態(tài)組件。依 is 的值,來決定哪個組件被渲染。
<!-- 動態(tài)組件由 vm 實例的 `componentId` property 控制 -->
<component :is="componentId"></component>

<!-- 也能夠渲染注冊過的組件或 prop 傳入的組件 -->
<component :is="$options.components.child"></component>

transition 與 Vue2.x 【基本】 一致有差異

Props新增:
persisted - boolean 如果為true,則表示這是一個轉(zhuǎn)換,實際上不會插入/刪除元素,而是切換顯示/隱藏狀態(tài)。 transition 過渡掛鉤被注入,但會被渲染器跳過。 相反,自定義指令可以通過調(diào)用注入的鉤子(例如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選擇器,或者是元素(如果在瀏覽器環(huán)境中使用)。中的內(nèi)容將會被放置到指定的目標元素中
<!-- 正確的 -->
<teleport to="#some-id" />
<teleport to=".some-class" />
 /*元素*/
<teleport to="[data-teleport]" />

<!-- 錯誤的 -->
<teleport to="h1" />
<teleport to="some-string" />
disabled  - boolean 這是一個可選項 ,做一個是可以用來禁用的功能,這意味著它的插槽內(nèi)容不會移動到任何地方,而是按沒有teleport組件一般來呈現(xiàn)【默認為false】
<teleport to="#popup" :disabled="displayVideoInline">
  <h1>999999</h1>
</teleport>

注意,這將移動實際的DOM節(jié)點,而不是銷毀和重新創(chuàng)建,并且還將保持任何組件實例是活動的。所有有狀態(tài)HTML元素(比如一個正在播放的視頻)將保持它們的狀態(tài)?!究刂芼isplayVideoInline并不是銷毀重建,它保持實例是存在的,不會被注銷】


關(guān)于Teleport 其他內(nèi)容
Vue鼓勵我們通過將UI和相關(guān)行為封裝到組件中來構(gòu)建UI。我們可以將它們彼此嵌套在一起,以構(gòu)建構(gòu)成應用程序UI的樹
但是,有時組件模板的一部分邏輯上屬于這個組件,而從技術(shù)角度來看,最好將這一部分模板移到DOM中的其他地方,放到Vue應用程序之外
一個常見的場景是創(chuàng)建一個包含全屏模態(tài)的組件。在大多數(shù)情況下,您希望模態(tài)的邏輯駐留在組件中,但是模態(tài)框的定位問題很快就很難通過CSS解決,或者需要更改組件的組成
考慮下面的HTML結(jié)構(gòu):
<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>
讓我們看看 mode -button
該組件將有一個button元素來觸發(fā)模態(tài)的打開,還有一個div元素,其類為.modal,它將包含模態(tài)的內(nèi)容和一個自關(guān)閉按鈕
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結(jié)構(gòu)中使用這個組件時,我們可以看到一個問題——模態(tài)被呈現(xiàn)在深嵌套的div中,模態(tài)的絕對位置以父div相對位置作為參考。


Teleport提供了一種干凈的方式,允許我們控制DOM中希望在哪個父節(jié)點下呈現(xiàn)HTML片段,而不必訴諸全局狀態(tài)或?qū)⑵洳鸱譃閮蓚€組件。
讓我們修改我們的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>
  `
})
在這種情況下,即使在不同的地方呈現(xiàn)child-component,它仍將是parent-componen的子組件【而不是爺爺組件】,并將從其父組件接收一個name 的props
這也意味著來自父組件的注入如預期的那樣工作,并且子組件將嵌套在Vue Devtools的父組件之下,而不是放在實際內(nèi)容移動到的地方
對同一目標使用多次teleports
一個常見的用例場景是一個可重用的組件,該組件可能同時有多個活動實例。對于這種場景,多個組件可以將它們的內(nèi)容掛載到相同的目標元素。這個順序?qū)⑹且粋€簡單的附加—稍后的掛載將位于目標元素中較早的掛載之后。
<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() 中調(diào)用
例如,如果我們想在根組件上提供一個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 接受一個可選的的默認值作為第二個參數(shù)。如果未提供默認值,并且在 provide 上下文中未找到該屬性,則 inject 返回 undefined。
如果我們需要提供或注入多個值,我們可以通過隨后分別調(diào)用provide或inject來實現(xiàn)【多次調(diào)用】
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 }
}
現(xiàn)在,當提供者組件上的book或year發(fā)生變化時,我們可以觀察到它們在注入的組件上的變化。

警告:我們不建議改變一個被注入的反應性屬性【子組件去修改數(shù)據(jù)流】,因為它會破壞Vue的單向數(shù)據(jù)流。相反,嘗試在提供值【父組件去修改】的地方改變值,或者提供一個方法來改變值
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 名轉(zhuǎn)換為 camelCase

v-model【Vue2.x一致】
v-slot【Vue2.x一致】
v-cloak【Vue2.x一致】
v-once 【Vue2.x一致】
v-pre【Vue2.x一致】
v-is【新增】

注意:本節(jié)只影響在頁面的HTML中直接編寫Vue模板的情況



限制:原生html元素


使用:
 使用in-DOM模板時,該模板應遵守本機HTML解析規(guī)則。 某些HTML元素(例如,,和)對可以在其中顯示哪些元素有限制,而某些元素(例如,和)只能 出現(xiàn)在某些其他元素內(nèi)。 解決方法是,我們可以在這些元素上使用v-is指令【作用就是轉(zhuǎn)成組件的名字】



警告v-is 功能 像一個動態(tài)2.x :is 綁定 所以要根據(jù)注冊的名稱渲染組件,它的值應該是一個JavaScript字符串
<!-- 不正確的, 不會出現(xiàn)任何渲染 -->
<tr v-is="blog-post-row"></tr>

<!-- 正確 -->
<tr v-is="'blog-post-row'"></tr>
全局API
createApp
返回一個應用程序?qū)嵗?提供了一個應用程序上下文。應用程序?qū)嵗龗燧d的整個組件樹共享相同的上下文
const app = Vue.createApp({})

參數(shù)

該函數(shù)接收一個根組件選項對象作為第一個參數(shù)

const app = Vue.createApp({
  data() {
    return {
      ...
    }
  },
  methods: {...},
  computed: {...}
  setup(){...}
  ...
})
使用第二個參數(shù),我們可以將根組件props 傳遞給應用

<div id="app">
  <!-- 這里將會顯示 'Evan' -->
  {{ username }}
</div>

const app = Vue.createApp(
  {
    props: ['username']
  },
  { username: 'Evan' }
)
h
返回“虛擬節(jié)點”,通常縮寫為VNode:一個簡單的對象,它包含描述Vue應該在頁面上渲染何種類型的節(jié)點的信息,包括對任何子節(jié)點的描述。你可以手動閱讀render functions
render() {
  return Vue.h('h1', {}, 'Some title')
}

參數(shù)
接受三個參數(shù)tag, props and children

tag: 

類型:String | Object | Function | null
詳情:一個HTML標簽名,一個組件,一個異步組件或null。使用null將渲染成注釋。此參數(shù)是必需的

props
類型:Object
詳情:模板中使用的attributes、props 和events 對應的對象??蛇x

children
類型: String | Array | Object

詳情:
Children VNodes,使用h()構(gòu)建,或使用字符串來獲取“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()`構(gòu)建的子級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必須是唯一的。這意味著下面的渲染函數(shù)是無效的
 render() {
     const myParagraphVNode = Vue.h('p', 'hi')
      return Vue.h('div', [
        // 表示驚訝 - 副本復制 VNodes!
        myParagraphVNode, myParagraphVNode
      ])
  }

如果您確實想多次復制相同的元素/組件,則可以使用工廠函數(shù)進行復制。 例如,以下呈現(xiàn)函數(shù)是呈現(xiàn)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
我們必須為事件處理程序提供一個適當?shù)膒rop名稱,例如,為了處理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, //是否觸發(fā)一次
      capture: true 
    },
  })
}
對于所有其他事件和鍵修飾符,不需要特殊的API,因為我們可以在處理程序中使用事件方法

render() {
  return Vue.h('input', {
    onKeyUp: event => {
      // 如果發(fā)出事件的元素不存在,則中止事件綁定到的元素
      if (event.target !== event.currentTarget) return
      // 同時如果按下的鍵不是enter鍵key (13)以及shift鍵沒有按下
      if (!event.shiftKey || event.keyCode !== 13) return
      // 停止事件傳播
      event.stopPropagation()
      // 阻止此元素的默認keyup處理程序
      event.preventDefault()
      // ...
    }
  })
}
Slots
你可以訪問插槽內(nèi)容this.$slots在VNodes數(shù)組的
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函數(shù)將槽傳遞給子組件
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
如果我們要編寫大量的渲染函數(shù),編寫這樣的東西可能會讓人感到痛苦
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【組件】
在實現(xiàn)方面,defineComponent只會執(zhí)行返回傳遞給它的對象的操作。 但是,就類型而言,返回的值具有人工渲染功能,TSX和IDE工具支持的構(gòu)造函數(shù)的綜合類型
參數(shù)
具有組件選項的對象
import { defineComponent } from 'vue'

const MyComponent = defineComponent({
  data() {
    return { count: 1 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
})
defineAsyncComponent 【異步組件】
創(chuàng)建只在必要時加載的異步組件
參數(shù)
對于基本用法,defineAsyncComponent可以接受返回Promise的工廠函數(shù)。當您從serve檢索到組件定義時,應該調(diào)用Promise的解析回調(diào)。您還可以調(diào)用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的函數(shù)
import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.vue')
    )
  }
})
對于高級用法,defineAsyncComponent可以接受一個對象
const AsyncComp = defineAsyncComponent({
  // 工廠函數(shù)
  loader: () => import('./Foo.vue')
  // 加載異步組件時使用的組件
  loadingComponent: LoadingComponent,
  //加載失敗的時候使用的組件
  errorComponent: ErrorComponent,
  // 在顯示加載組件之前延遲。默認值:200 ms。
  delay: 200,
  // 如果超時,將顯示錯誤組件
  // 存在timeout并且超過這個時間. 默認值:無窮
  timeout: 3000,
  // 返回布爾值的函數(shù),指示當加載器promise rejects時異步組件是否應該重試
  retryWhen: error => error.code !== 404,
  // 允許的最大重試次數(shù)
  maxRetries: 3,
  // 定義組件是否可掛載
  suspensible: false
})

resolveComponent

警告resolveComponent只能在render或setup函數(shù)中使用。
允許通過名稱解析組件,如果它在當前應用程序?qū)嵗锌捎?。如果找不到組件,返回組件或未定義組件
如果找不到組件,返回組件或未定義組件【組件】
app.component('MyComponent', {
  /* ... */
})
const MyComponent = resolveComponent('MyComponent')
resolveDynamicComponent【解析活動的組件active】

resolveDynamicComponent只能在render或setup函數(shù)中使用。
允許使用與<component:is="">相同的機制來解析組件。
返回解析的組件或一個新創(chuàng)建的VNode以組件名稱作為節(jié)點標記的。
如果沒有找到組件,會發(fā)出警告

resolveDirective

警告resolveDirective只能在render或setup函數(shù)中使用。
允許通過名稱解析指令,如果它在當前應用程序?qū)嵗锌捎谩?返回一個Directive或 當沒有找到的時候,返回undefined。
app.directive('highlight', {})
render(){
    const highlightDirective = resolveDirective('highlight')
}
withDirectives

警告withDirectives只能在render或setup函數(shù)中使用。
:::允許應用指令到VNode。返回一個帶有應用指令的VNode。
const bar = resolveDirective('bar')

return withDirectives(h('div'), [
  [bar, this.y]
])
createRenderer *【待】
nextTick
將回調(diào)延遲到下一個DOM更新周期之后執(zhí)行。在更改了一些數(shù)據(jù)以等待DOM更新之后立即使用它
setup() {
    const message = ref('Hello!')
    const changeMessage = async newMessage => {
      message.value = newMessage
      /*等待DOM更新*/
      await nextTick()
      console.log('Now DOM is updated')
    }
  }
實例方法methods
$watch
參數(shù)

{string | Function} source
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate

用法
觀察組件實例上的響應式屬性或computed函數(shù)的更改。使用回調(diào)獲取到給定屬性的新值和舊值。我們只能通過頂級data、prop或computed的屬性名作為字符串的形式傳遞。對于更復雜的表達式或嵌套屬性,使用函數(shù)代替。
例子
const app = Vue.createApp({
  data() {
    return {
      a: 1,
      b: 2,
      c: {
        d: 3,
        e: 4
      }
    }
  },
  created() {
    // 頂級屬性名a
    this.$watch('a', (newVal, oldVal) => {
      // 做一些事
    })

    // 觀察監(jiān)視單個嵌套屬性
    this.$watch(
      () => this.c.d,
      (newVal, oldVal) => {
        // 做一些事
      }
    )

    // 監(jiān)控復雜表達式
    this.$watch(
      // 每當表達式`this.a + this.b`產(chǎn)生不同的結(jié)果時
      // 處理程序?qū)⒈徽{(diào)用。這就好像我們在看computed屬性
      // 而不定義計算屬性本身
      () => this.a + this.b,
      (newVal, oldVal) => {
        // 做一些事
      }
    )
  }
})

當監(jiān)視的值是對象或數(shù)組時,對其屬性或元素的任何更改都不會觸發(fā)監(jiān)視程序,因為它們引用相同的對象/數(shù)組

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: {
    // 這些方法不會觸發(fā)觀察者,因為我們僅更改了對象/數(shù)組的屬性,
    // 并不是 Object/Array 本身
    changeArticleText() {
      this.article.text = 'Vue 3 is awesome'
    },
    addComment() {
      this.comments.push('New comment')
    },

    // 這些方法會觸發(fā)觀察者,因為我們完整替換了對象/數(shù)組
    changeWholeArticle() {
      this.article = { text: 'Vue 3 is awesome' }
    },
    clearComments() {
      this.comments = []
    }
  }
})
$watch返回一個取消監(jiān)視的函數(shù),該函數(shù)停止觸發(fā)回調(diào)

const unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
Option: deep
檢測對象內(nèi)部嵌套的值更改,需要在options參數(shù)中傳入deep: true。注意,偵聽數(shù)組突變并不需要這樣做。
vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// 觸發(fā)回調(diào)
Option: immediate
在選項中傳入immediate: true將立即用表達式的當前值觸發(fā)回調(diào)
vm.$watch('a', callback, {
  immediate: true
})
// “callback”被立即觸發(fā),當前值為“a”
 請注意,使用immediate選項,您將無法在第一個回調(diào)調(diào)用中取消監(jiān)視給定的屬性。
//這個例子是錯誤的
const unwatch = vm.$watch(
  'value',
  function() {
    doSomething()
    unwatch()
  },
  { immediate: true }
)
如果你仍然想在回調(diào)中調(diào)用一個unwatch函數(shù),你應該首先檢查它的可用性
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【廢棄】
選項 / 數(shù)據(jù)
data【一致】
props【一致】
computed【一致】
methods【一致】
watch【一致】
emits【新增】
詳情
可以從組件發(fā)出的自定義事件的list/hash。 它具有基于數(shù)組的簡單語法和允許配置事件驗證的替代的基于對象的語法。
在基于對象的語法中,每個屬性的值可以為null或驗證函數(shù)。 驗證函數(shù)將接收傳遞給emit調(diào)用的其他參數(shù)。例如,如果調(diào)用this.emit調(diào)用的其他參數(shù)。 例如,如果調(diào)用this.emit調(diào)用的其他參數(shù)。例如,如果調(diào)用this.emit('foo',1),則foo的相應驗證器將接收參數(shù)1。驗證器函數(shù)應返回一個布爾值,以指示事件參數(shù)是否有效。
const app = Vue.createApp({})

// 數(shù)組語法
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都是函數(shù)

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){
    //默認執(zhí)行一次

    //頁面使用 state.selectKeys
        const state  = reactive({ //attr slots emit
            selectKeys:0
        })
    //1.直接使用
        return {
            selectKeys:state.selectKeys
        }

    //2.導出,頁面上直接使用,數(shù)據(jù)響應式還帶解構(gòu)
        return {
            ...toRefs(state) 
        }

        onMounted(()=>{

        })
    }
}

監(jiān)控路由變化

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監(jiān)控路由變化
        watch(()=>route.path,(newValue)=>{
            state.selectKeys = [newValue]
        })
        //2.computed監(jiān)控路由變化
        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監(jiān)控路由變化
        watch(()=>route.path,(newValue)=>{
            state.selectKeys = [newValue]
        })
        //2.computed監(jiān)控路由變化
        const selectKeys = computed(()=>{
            return [route.path]
        })

        //ref 把普通值變成包裝后的結(jié)構(gòu),將屬性變成響應式
        // 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')
            }
        })

        //方法函數(shù)
        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
        }
    }
}

環(huán)境變量

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根據(jù)不同方法返回不同的資源
        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:{

    }

}

使用數(shù)據(jù)

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.創(chuàng)建虛擬節(jié)點,將虛擬節(jié)點轉(zhuǎn)化為真實節(jié)點
//2.組件的實現(xiàn) setup
//3.reactive api實現(xiàn)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-計數(shù)器'
                ]
            },
            {
                tag:'p',
                props:{
                    onClick:()=>{
                        alert(state.count)
                    }
                }
                children:[
                    'vue@3-計數(shù)器'
                ]
            }
        ]
    }
    render(vnode,app)

export function render(vnode,container){
    // 渲染頁面的方法叫patch

    //1.第一次渲染 2.dom-diff
    patch(null,vnode,container)
}
/**
* n1 老的虛擬節(jié)點
* n2 新的虛擬節(jié)點
* container 容器
*/
function patch(n1,n2,container){
    //組件的虛擬節(jié)點是一個對象,tag是一個對象
    //如果是組件,tag可能是個對象
    //后續(xù)diff可以執(zhí)行這個方法
    if(typeof n2.tag ==='string'){
        //標簽
        mountElement(n2,container)
    }else if(typeof n2.tag==='object'){
        
    }
}

function mountElement(vnode,container){
    const  { tag,children,props }  = vnode
    //虛擬節(jié)點和真實節(jié)點做映射關(guān)系
    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)
    }
}


//節(jié)點操作方法
exoprt const nodeOps = {
    //插入元素節(jié)點
    insert(child,parent,anchor){
        if(anchor){
            parent.insertBefore(child,anchor)
        }else{
            parent.appendChild(child)
        }
    },
    //移除節(jié)點
    remove(child){
        const parent  = child.parentNode;
        parent && parent.removeChild(child)
    },
    //創(chuàng)建節(jié)點
    createElement(tag){
        return document.createElement(tag)
    },
    //設(shè)置文本內(nèi)容
    hostSetElementText(el,text){
        el.textContent = text
    }
}

1.Vue3 嘗鮮
1.Vue3文檔【Vue2遷移Vue3】

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容