如何将 vue2 的 Option-API 迁移到 vue3 组合式 API

2,094 阅读4分钟

刚刚分享了一篇关于 Vue3 组合式 API,不过觉得不够全面和具体,没有提及到如何 compute、watch、methods 和 prop 等从 Option-API 迁移到组合式 API。所以今天再来聊一聊,通过 code 帮助大家理解组合式 API 以及如何迁移。

我们先看一看 Vue2 的局限性,然后 Vue3 是解决这个问题,局限性如下

  • 随着你的组件不断扩展,其可读性就会变得 hard,这也就意味着难于维护
  • 组件间共享代码的模式不是那么优雅
  • vue2 存在类型系统

可读性

这里就拿搜索页面,这里先实现搜索功能,搜索功能分布在 data 还有 methods 中,随后可能还添加搜索过滤器和分页功能。逻辑上关注特点通过组件 option 来组织。也就是对于功能他们逻辑散落在 data、computed、 props、methods 和 lifecycle。其实我们希望将功能逻辑都集中在一起。

为什么叫 composition functions 组合函数,所谓组合式 Api 就是可以在 setup 组合这些函数来,其实主要有了 setup 我们就不会将功能拆分到各个 Option,

通过一个实例解释如何将 vue2 的中 Option-API 替换为组合式 API(composition API)。

  • 组合式 API 替换 data(基本类型和复合类型)
  • 组合式 API 替换 methods
  • 组合式 API 替换 compute
  • 组合式 API 替换 watcher
  • 组合式 API 如何范围prop 属性

用于组合式 API 的响应式数据

在 Vue2 我们可以在 data 中定义一个基本类型数据或者一个复合类型数据,例如对象或者数组,然后将数据绑定到界面上,data 中定义数据可以被其他属性 computed 或者 methods 访问到,这些数据是响应式数据

data(){
    return{
        title:"machine leanring"
    }
}

下面是组合式 API(composition API) 实现方式,在 setup 定义个变量后将其作为一个对象的属性返回,便可以通过 {{title}} 显示在界面上

setup(){
  const title = "machine learning"
  return {
  	title
  }
}
<h1 class="title">{{title}}</h1>

响应式的基础类型数据

但是这样定义数据并不是响应式数据,不过 Vue3 提供两种方式在 setup 中定义响应式数据,分别是 refreactive 分别用于定义基本类型数据和复合数据。我们先看基础数据类型响应式方式为 ref(基础数据),例如 String、Number 或者 Boolean

        setup(){
            const title = ref("machine learning")
            return {
                title
            }
        }

响应式的复合类型数据

对于对象可以用 reactive来定义响应式对象,然后再 setup 作为返回对象的一个属性将其返回供调用。

setup(){
  const title = ref("machine learning");
	let count = ref(0)
  const tut = reactive({
		title:'machine learning',
		lesson:12,
		chapters:[
          {id:0,title:"introducion"},
          {id:2,title:"regression"},
          {id:3,title:"classification"},
				]
      })
		return {
      title,
      count,
      tut
		}
}

在 Vue2 通过defineProperty API 提供 getter 和 setter 来监听数据来实现响应式数据,不过这个中实现方式存在一些问题就是如果有大量属性值作为

compute

看一看将 Option-API 的 compute,从 vue 引入 computed 函数接受一个函数,这个函数接受一些基础值的组合(操作)。

            const refTitlContent = computed(function(){
                return `${refTitle.value} ${refContent.value}`
            })
完整代码
<template>
    <h1 class="title">{{refTitlContent}}</h1>
    <div class="card">
        <div class="card-header">
            <div class="card-header-title">{{title}}</div>
        </div>
        <div class="card-content">
            <div class="card-content-body">{{content}}</div>
        </div>
    </div>
    <div class="section">
        <div class="control">
            <input class="input" type="text" v-model="title"/>
        </div>
        <div class="control">
            <input class="input" type="text" v-model="content"/>
        </div>
    </div>
    <div class="section">
        <div class="control">
            <input class="input" type="text" v-model="refTitle"/>
        </div>
        <div class="control">
            <input class="input" type="text" v-model="refContent"/>
        </div>
    </div>
</template>

<script>
import { ref,computed } from 'vue'
    export default {
        name:"ReplaceComputed",
        setup(){
            const refTitle = ref('')
            const refContent = ref('')

            const refTitlContent = computed(function(){
                return `${refTitle.value} ${refContent.value}`
            })

            return {
                refTitle,
                refContent,
                refTitlContent
            }
        },
        data(){
            return {
                title:"",
                content:""
            }
        }
    }
</script>

watcher

watch 接受一个观察值,第一个参数接受可以是一个要观察变量或者由观察变量数组,第二个 handler 回调函数接受 newVal 和 oldVal 的函数。

        setup(){
            const refTitle = ref('');
            const refContent = ref('content');

            watch([refTitle,refContent],(newVals,oldVals)=>{
                console.log('Old refValue',oldVals[0]);
                console.log('New refValue',newVals[0]);
                console.log('New refValue',newVals[1]);
                console.log('New refValue',newVals[1]);
            })
            return {
                refTitle,
                refContent
            }
        },

也可以检测一个响应值,这里第一个参数是接受一个函数,这个函数返回的值是要监测的对象。

				const tut = reactive({
                tutTitle:'',
                tutContent:''
            });

            watch(()=>{return {...tut}}, function(newVal,oldVal){
                console.log('tut title',oldVal.tutTitle)
                console.log('tut title',newVal.tutTitle)
                // console.log('new tut title',newVal.tutContent)
                // console.log('new tut Content',newVal.tutContent)
            })
            watch(()=>tut.tutTitle,function(newVal,oldVal){
                console.log('old tut title',newVal);
                console.log('new tut title',oldVal);
            })

如果要想要深度检测对象就可以在第二参数中指定deep:true

            const tut = reactive({
                tutTitle:'',
                tutContent:'',
                category:{
                    name:''
                }
            });
            const tut = reactive({
                tutTitle:'',
                tutContent:'',
                category:{
                    name:''
                }
            });

            watch(()=>tut.category,function(newVal,oldVal){
                console.log('new tut category',newVal)
                console.log('old tut category',oldVal)
            },{
                deep:true
            })

provide 和 inject

首先创建 3 个 Component 分别为 ChildA、ChildB 和 ChildC,依赖关系如下图

provide_inject_001.png

在 App.vue 文件中

  • 引入 ChildA 组件
  • 然后从 vue 引入 provide 函数,函数接受两个参数,第一个参数为提供变量名称,第二个参数为该变量的值
<template>
<div class="container">
  <ChildA/>
</div>

</template>

<script>
import ChildA from './components/Option/ChildA.vue'
import {provide} from 'vue';
export default {
  
  name: 'App',
  
  setup(){
    provide('refProvideTitle','machine learning 2');
  },
  
  data(){
    return {
      title:"machine learning",
    }
  },
  components:{
    ChildA
  },
  provide(){
    return{
      title:this.title
    }
  }
}
</script>

在 ChildC 文件中

  • 从 vue 中引入 inject
  • 然后函数 inject 中,第一个参数接受变量名为 refProvideTitle 第二个参数为该参数的默认值
  • inject 返回接受到 injectedTitle 一个响应式数据,然后将 setup 返回供模板使用
<template>
    <div>
        <h1 class="title">Child C</h1>
        <h1 class="title">{{title}}</h1>
        <h1 class="title"> injected Title:{{injectedTitle}}</h1>
    </div>
</template>

<script>
    import {inject} from 'vue';

    export default {

        name:"ChildC",
        setup(){
            const injectedTitle = inject('refProvideTitle','default machine leaerning2');
            return {
                injectedTitle
            }
        },
        inject:['title']
    }
</script>

上面我们完成如何通过组合式 API 在不同组件之间进行传值,

    const count = ref('0');
    const refTut = reactive({
      title:"machine learning title",
      content:"machine learning content"
    });

    provide('count',count);
    provide('tut',refTut);
<template>
<div class="container">
  <div class="control">
    <button class="button" @click="incrementCount" > increment </button>
  </div>
  <ChildA/>
</div>

</template>

<script>
import ChildA from './components/Option/ChildA.vue'
import {provide, ref, reactive} from 'vue';
export default {
  
  name: 'App',
  
  setup(){
    provide('refProvideTitle','machine learning 2');
    const count = ref('10');
    const refTut = reactive({
      refTitle: "machine learning title",
      refContent:"machine learning content"
    });

    function incrementCount(){
      count.value ++;
    }

    provide('count',count);
    provide('refTut',refTut);

    return {
      count,
      incrementCount,
    }
  },
  
  data(){
    return {
      title:"machine learning",
    }
  },
  components:{
    ChildA
  },
  provide(){
    return{
      title:this.title
    }
  }
}
</script>
完整代码
<template>
<div class="container">
  <div class="control">
    <button class="button" @click="incrementCount" > increment </button>
  </div>
  <ChildA/>
</div>

</template>

<script>
import ChildA from './components/Option/ChildA.vue'
import {provide, ref, reactive} from 'vue';
export default {
  
  name: 'App',
  
  setup(){
    provide('refProvideTitle','machine learning 2');
    const count = ref('10');
    const refTut = reactive({
      refTitle: "machine learning title",
      refContent:"machine learning content"
    });

    function incrementCount(){
      count.value ++;
    }

    provide('count',count);
    provide('refTut',refTut);
    provide('incrementCount',incrementCount)

    return {
      count,
      incrementCount,
    }
  },
  
  data(){
    return {
      title:"machine learning",
    }
  },
  components:{
    ChildA
  },
  provide(){
    return{
      title:this.title
    }
  }
}
</script>


不但可以通过 Provide 和 Injection 来跨组件层级传递数据还可以传递方法,不过这个还是从上层至下进行传递。

<template>
    <div>
        <h1 class="title">Child C</h1>
        <h1 class="title">{{title}}</h1>
        <h1 class="title"> injected Title:{{injectedTitle}}</h1>
        <h1 class="title"> injected count:{{childCount}}</h1>
        <h1 class="title"> injected refTitle:{{refTitle}}</h1>
        <h1 class="title"> injected refContent:{{refContent}}</h1>
        <div class="control">
            <button class="button" @click="incrementCount" > increment </button>
        </div>
    </div>
</template>

<script>
    import {inject, toRefs} from 'vue';

    export default {

        name:"ChildC",
        setup(){
            const injectedTitle = inject('refProvideTitle','default machine leaerning2');
            const childCount = inject('count',0);
            const childTut = inject('refTut',{});
            const incrementCount = inject('incrementCount')
            console.log(childTut)
            return {
                injectedTitle,
                childCount,
                ... toRefs(childTut),
                incrementCount
            }
        },
        inject:['title']
    }
</script>

生命周期对比

Option APIHook inside(setup)
beforeCreate无需
created无需
beforeMountonBeforemount
mountedonMounted
beforeUpdateonBeforeUpdate
updateonUpdated
beforeUnmountonBeforeUnmount
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTtriggeredonRenderTtriggered

template 引用

<template>
    <div class="section">
        <div class="control">
            <input type="text" class="input" ref="inputRef">
        </div>
    </div>
</template>

<script>
    export default {
        name:"ReplaceTemplateRef",
        mounted(){
            this.$refs.inputRef.focus()
        }
    }
</script>

这个大家都比较熟悉在 vue2 通过为 dom 元素指定一个 ref 便可以在 dom 元素绑定到 DOM tree 之后通过引用 $refs.inputRef来获取这个 DOM 元素的引用了。有点类似 jQuery 来操作 DOM 的方法。

接下来我们组合式 API 来实现一下,注意这里需要在 setup 函数经该 inputRef 返回

<script>
    import {ref, onMounted} from 'vue';

    export default {
        name:"ReplaceTemplateRef",
        setup(){
            const inputRef = ref(null);
            onMounted(()=>{
                console.log(inputRef.value)
                inputRef.value.focus();
            })
            return {
                inputRef
            }
        },
        // mounted(){
        //     this.$refs.inputRef.focus()
        // }
    }
</script>

prop

接下来聊一聊如何在 setup 函数访问 prop 属性值,其实 setup 第一个参数是 prop 子组件的自定义属性可以通过这个参数进行访问。

<template>
<div class="container">
  <Person/>
</div>

</template>

<script>
import Person from './components/Option/Person.vue';
     
export default {
  
  name: 'App',
  components:{
      Person
  }
}
</script>


<template>
    <div class="container">
        <div class="control">
            <input class="input" type="text" placeholder="tut title" v-model="tutTitle">
        </div>
        <div class="control">
            <input class="input" type="text" placeholder="tut content" v-model="tutContent">
        </div>
        <PersonGreeting :title="tutTitle" :content="tutContent" />
    </div>
</template>

<script>
    import {ref} from 'vue';
    import PersonGreeting from './PersonGreeting.vue'
    export default {
        name:"Person",
        components:{
            PersonGreeting
        },
        setup(){
            const tutTitle = ref('');
            const tutContent = ref('');
            return {
                tutTitle,
                tutContent,
            }
        }

    }
</script>

<template>
    <div class="section">
        <h1 class="title">{{title}}</h1>
        <h1 class="subtitle">{{content}}</h1>
    </div>
</template>

<script>
    export default {
        name:'PersonGreeting',
        props:['title','content']
        
    }
</script>
<template>
    <div class="section">
        <h1 class="title">{{title}}</h1>
        <h1 class="subtitle">{{content}}</h1>
        <h1 class="title"> description {{tutDescription}}</h1>
    </div>
</template>

<script>
import { computed } from 'vue'
    export default {
        name:'PersonGreeting',
        setup(props){
            const tutDescription = computed(()=>{
                return `${props.title} ${props.content}`
            })
            return {
                tutDescription 
            }
        },
        props:['title','content']
        
    }
</script>

<style scoped>

</style>

context

在 setup 接受两个参数第一个参数已经介绍过了 props,从这个参数 props 可以获取从父组件传入的参数,第二个参数 context 是对象有 attrs、emit 和 slots。可以通过 emit 从子组件向父组件传递数据。在父组件 Person 接受子组件发出事件 sendDescription 处理函数。

<template>
    <div class="container">
        <div class="control">
            <input class="input" type="text" placeholder="tut title" v-model="tutTitle">
        </div>
        <div class="control">
            <input class="input" type="text" placeholder="tut content" v-model="tutContent">
        </div>
        <PersonGreeting :title="tutTitle" :content="tutContent" @sendDescription="descriptionHandler" />

    </div>
</template>

<script>
    import {ref} from 'vue';
    import PersonGreeting from './PersonGreeting.vue'
    export default {
        name:"Person",
        components:{
            PersonGreeting
        },
        setup(){
            const tutTitle = ref('');
            const tutContent = ref('');
            function descriptionHandler(decription){
                alert(decription)
            }
            return {
                tutTitle,
                tutContent,
                descriptionHandler,
            }
        }

    }
</script>

通过 context 的 emit 方法发送事件,第一个参数是事件名,第二个参数是要通过事件传递的值。

<template>
    <div class="section">
        <h1 class="title">{{title}}</h1>
        <h1 class="subtitle">{{content}}</h1>
        <h1 class="title"> description {{tutDescription}}</h1>
        <button @click="sendEvent">send Description</button>
    </div>
</template>

<script>
import { computed } from 'vue'
    export default {
        name:'PersonGreeting',
        setup(props,context){
            const tutDescription = computed(()=>{
                return `${props.title} ${props.content}`
            })

            function sendEvent(){
                context.emit('sendDescription',tutDescription.value)
            }
            return {
                tutDescription,
                sendEvent
            }
        },
        props:['title','content']
        
    }
</script>