vue3 composition API

composition api出现原因

Vue3中提出的一个新概念, 作用: 聚合代码 & 逻辑重用

vue2组件化时,可以帮助我们更好的拆分代码和约束代码,增加可读性,但是有存在一些问题

  1. 代码逻辑不够聚合, 比较分散, 如果我们的组件代码量一多, 找相同的代码逻辑会显得比较困难
  2. 逻辑重用时只能使用 mixin 来混入逻辑,但 mixin 一旦多处使用时,很难维护
  3. vue的侵入性太强, 导致对typescript这类高阶技术的用法不够自然

基于上面的种种问题, vue推出了composition api的概念

composition api(组合式api)本质上就是vue抽离了一系列方法可以供我们自由引入组合使用, 这里抛出一个问题

在不改变vue模板语法的情况下, vue提供了一个新的函数书写在模板的script标签中, 该函数名叫做 setup

setup

setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口。

执行时机

setup 函数会在 beforeCreate 之后、created 之前执行

接收 props 数据

在 props 中定义当前组件允许外界传递过来的参数名称:

props: {
    p1: String
}

通过 setup 函数的第一个形参,接收 props 数据:

setup(props) {
    console.log(props.p1)
}

context

setup 函数的第二个形参是一个上下文对象,这个上下文对象中包含了一些有用的属性,这些属性在 vue 2.x 中需要通过 this 才能访问到,在 vue 3.x 中,它们的访问方式如下:

const MyComponent = {
  setup(props, context) {
    console.log(context.attrs) // 它是绑定到组件中的 非 props 数据,并且是非响应式的
    console.log(context.slots) // 组件的插槽,同样也不是 响应式的
    console.log(context.parent) 
    console.log(context.root)
    console.log(context.emit) // 一个方法,相当于 vue2 中的 this.$emit 方法
    console.log(context.refs)
  }
}

注意:在 setup() 函数中无法访问到 this

生命周期函数

可以使用导入的onXXX的形式注册生命周期函数,举个例子:

选项 API setup内部的钩子
beforeCreate 不需要
created 不需要
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

onRenderTracked

  • 每次渲染后重新收集响应式依赖
  • 在onMounted前触发,页面更新后也会触发

renderTriggered

  • 每次触发页面重新渲染时自动执行

  • 在onBeforeUpdate之前触发

TIP

由于setup是随着beforeCreate和created这两个生命周期钩子运行的,因此在你无需显式地定义它们。换句话说,任何想写进这两个钩子的代码,都应当直接写在setup方法里面。

setup() {
    onMounted(()=>{
        console.log('mounted被触发')
    })
    ///...其他类似
}

methods

使用普通的函数定义方法,这样可以最大程度的增加复用性。例如:

setup() {
    function add() {
        console.log('add被触发')
    }
    return {
        add//必须将函数return
    }
}

computed

当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性。

setup(props, context) {
    const { reactive, computed } = Vue;
    const countObj = reactive({
        count: 0
    })
    const handleCount = () => {
        countObj.count += 1
    }
    // const countAddFive = computed(() => {
    //   return count.value + 5;
    // })
    // 扩展方法
    const countAddFive = computed({
        get() {
            return countObj.count + 5;
        },
        set(param) {
            countObj.count = param - 5 
        }
    })
    setTimeout(() => {
        countAddFive.value = 100
    }, 3000)
    return {
        countObj,
        countAddFive,
        handleCount
    }
}

这样我们就定义号了一个计算属性countAddFive

侦听器

watch

  • 具有一定的惰性 lazy
  • 参数可以拿到原始和当前值
  • 可以侦听多个数据的变化,用一个侦听器承载
const nameObj = reactive({
    name: 'elf'
})
watch(
    () => nameObj.name, // 监听reactive参数的话,需要使用箭头函数
    (val, oldVal) => {
        console.log(val, oldVal)
    }
)

当需要监听多个参数时,需要将监听参数用数组形式

const nameObj = reactive({
    name: 'elf',
    englishName: 'echo'
})
watch(
    [ () => nameObj.name, () => nameObj.englishName],
    ([curName, curEng], [prevName, prevEng]) => {
        console.log(curName, prevName, '-----', curEng, prevEng)
    }
)

watch第三个参数用法

watch(nameObj, (val, oldVal) => {
    console.log(val, oldVal)
}, {
    immediate: true, // 默认只有数据改变才会监听,第一次不会执行,设置true则第一次也能执行
    deep: true // 可以深度检测到 nameObj 对象的属性值的变化
})

watchEffect

  • 立即执行,没有惰性 immediate
  • 不需要传递你要侦听的内容,自动会感知代码依赖,不需要传递很多参数,只要传递一个回调函数
  • 不能获取之前数据的值

使用场景

异步请求或定时器时可以使用watchEffect

watchEffect(() => {
    console.log(nameObj.name)
    console.log(nameObj.englishName)
})

如果需要停用侦听器,可以使用以下方法,watch也类似

const stop = watchEffect(() => {
    console.log(nameObj.name)
    console.log(nameObj.englishName)
    setTimeout(() => {
        stop()
    }, 5000)
})

provide & inject

常用的父子组件通信方式都是父组件绑定要传递给子组件的数据,子组件通过props属性接收,一旦组件层级变多时,采用这种方式一级一级传递值非常麻烦,而且代码可读性不高,不便后期维护。

vue提供了provideinject帮助我们解决多层次嵌套嵌套通信问题。在provide中指定要传递给子孙组件的数据,子孙组件通过inject注入祖父组件传递过来的数据。

其实,provideinject主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中

注意:子孙层的provide会掩盖祖父层provide中相同key的属性值

// 父组件
provide('name', readonly(name)) // 加readonly 防止子组件修改父组件的值,违反了单向数据流的要求
provide('handleClick', () => {
    name.value = 'echo'
})

// 子组件
const name = inject('name')
const handleClick = inject('handleClick')

获取Vue组件实例或者dom节点

// 父组件
setup(props, context) {
    const { ref, onMounted } = Vue;
    const hello = ref(null) // dom节点对象
    const child = ref(null) // child 组件的实例
    onMounted(() => {
        console.log(hello.value)
        child.value.childClick()
    })
    return {
        hello,
        child
    }
},
template: `
    <div>
        <div ref="hello">hello world</div>
        <child ref="child" />
    </div>
`
// 子组件
setup(props) {
    function childClick() {
        console.log('this is child')
    }
    return {
        childClick
    }
},
template: `
    <div>child</div>
`

这个hello或组件child与下面template的ref的hello、child对应,打印结果如下:

image-20210226142041400

返回值

setup 函数中返回一个对象,可以在模板中直接访问该对象中的属性和方法

const app = Vue.createApp({
  template: `
    <div @click="handleClick">name: {{name}}</div>
  `,
  setup(props, context) {
    console.log(this); // window
    return {
      name: 'elfecho',
      handleClick() {
        console.log(123)
      }
    }
  }
})

疑惑解答

setup函数太长了怎么办

可以使用函数拆分

setup(props, context) {
    // 流程调度
    const { list, addItemTolist } = listRelativeEffect();
    const { inputValue, changeInputValue } = inputRelativeEffect();
    return {
        list, addItemTolist,
        inputValue, changeInputValue
    }
}

return的上下文太长了,我们可以使用vue3的setup script功能,把setup这个配置也优化掉,一个功能export一次

<script setup>
import listRelativeEffect from './listRelativeEffect'
import inputRelativeEffect from './inputRelativeEffect'

let { list, addItemTolist } = listRelativeEffect()
export {list, addItemTolist}

let {count,double,add} = inputRelativeEffect()
export {inputValue, changeInputValue}
</script>

总结

那么我们最后总结一下 setup 函数的所有特性:

  • setup 函数是组合式 API 的入口函数,它在 组件创建之前 被调用
  • 因为在 setup 执行时组件尚未创建,setup 函数中的 this 不是当前组件的实例
  • 函数接收两个参数,props 和 context,context 可以解构为 attrs、slots、emit 函数
  • 函数可以返回一个对象,对象的属性可以直接在模板中进行使用,就像之前使用 data 和 methods 一样。

ref, reactive代替data中的变量

composition-api引入了独立的数据响应式方法reactive,它可以将传入的对象做响应式处理:

const { reactive } = Vue;
const state = reactive({
    name: 'elfecho'
})
setTimeout(() => {
    state.name = 'elf'
}, 2000)

这个方式类似于我们设置的data选项,能够解决我们大部分需求。但是也有以下问题:

  • 当我们直接导出这个state时,我们在模板中就要带上state这个前缀

    setup() {
        const state = reactive({})
        return { state }
    }
    
    <div>{{ state.name }}</div>
    

    为了解决这个前缀的问题,又要引入toRefs

    setup() {
        const state = reactive({})
        // toRefs proxy({name: 'elfecho'}) 会转化为 {
        //    name: proxy({value: 'elfecho'})
        // }
        return { ...toRefs(state) }
    }
    
    <div>{{ name }}</div>
    
  • 单个值时用reactive()显得比较多余

    于是就有了Ref的概念,通过包装单值为Ref对象,这样就可以对其做响应式代理

    setup() {
        const name = ref('name')
        return { name } 
    }
    

    模板中使用还可以省掉前缀,toRefs就是利用这一点将reactive()返回代理对象的每个key对应的值都转换为Ref

    <div>{{ foo }}</div>
    

    但是Ref对象也有副作用:

    在JS中修改这个值要额外加上value

    // proxy, 'elfecho' 变成 proxy({value: 'elfecho'})这样的一个响应式引用
    let name = ref('elfecho')
    setTimeout(() => {
        name.value = 'elf'
    }, 2000)
    return {
        name
    }
    

如何选择ref or reactive

  • 如果是单值,建议ref,哪怕是个单值的对象也可以

    const counterRef = ref(1)
    const usersRef = ref(['tom', 'jerry'])
    

    一个业务关注点有多个值,建议reactive

    const mouse = reactive({
        x: 0,
        y: 0
    })
    
  • 降低Ref负担的方法:利用unref、isRef、isProxy等工具方法,利用一些命名约定。

    setup(props) {
      const foo = unref(props.foo) // foo是我们要的值
      // 等效于
      const foo = isRef(props.foo) ? props.foo.value : props.foo
    }
    

扩展

readonly

如果不想对对象进行变更,作为只读的参数,Vue提供了 readonly 方法

const state = reactive({
    name: 'elfecho'
})
const copyState = readonly(state) // 这个对象不允许被修改
setTimeout(() => {
    state.name = 'elf'
    copyState.name = 'elf' // 此处对这个值进行变更,会出现警告
}, 2000)

toRef

如果对一个未定义的值进行编辑时,需要使用toRef

const data = reactive({
    name: 'elfecho'
})
const { name } = toRefs(data)
const age = toRef(data, 'age')
setTimeout(() => {
    age.value = '20'
}, 2000);
return {
    name,
    age
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容