vue3开发使用体验,来一波技术性总结。

477 阅读5分钟

2020年9月18日,Vue.js发布了3.0版本。时隔一年多,首次体验使用vue3进行项目开发。主要因为此时vue2仍是主流,虽然知道vue3将是未来的趋势,但一个好的产品是需要经过市场的不断打磨,才能以最完美的状态呈现在大众的面前。

一、vu3安装

1、使用 vue-cli 创建工程

官方提供的@vue/cli在4.5.0版本之后提供了vue3脚手架的快速安装。官方文档

image.png

## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue3-demo
## 启动
cd vue3-demo
npm run serve

2、使用 vite 创建工程

vite由vue团队打造,号称下一代前端开发与构建工具,比传统的构建工具更快更轻量。官方文档

image.png

image.png

## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev

二、组合式 API (Composition API)

vue3相较于vue2是属于一个大的版本升级,源码进全面重写,除了性能上的提升,还带来了一些新的特性(组合式api、新的内置组件)。最重要的是vue3还向下兼容,vue2中配置项(OptionsAPI)的写法仍然支持。

1、Setup 函数

setup是vue3中新增的配置项,为一个函数。它是所有组合式api的基础。

setup函数有两种返回值:

  • 返回一个对象,对象中的属性、方法, 在模板中均可以直接使用。
  • 返回一个渲染函数,可以自定义渲染内容。
<template>
   <div>{{name}}</div>
   <button @click="printInfo">打印信息</button>
</template>

<script>
export default {
  setup() {
    let name = 'bandianjuse';
  
    let printInfo = () => {
       console.log('hello vue3!')
    }
    
    // 返回一个对象,对象中的属性、方法, 在模板中均可以直接使用。
    return {
      name,
      printInfo
    }
  },
}
</script>
import { h } from 'vue'

export default {
  name: 'App',
  setup() {
    // 返回一个渲染函数,可以自定义渲染内容。
    return () => h('div', ['hello vue3!'])
  },

}

setup函数接收两个参数:

  • props 组件传入的属性。
  • context 上下文对象,包含了一些有用的属性,如attrs、slots、emit等。
export default {
  props: {
      title: String
  },
  setup(props) {
    console.log(props.title)
  },
}
export default {
  setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    console.log(context.attrs)

    // 插槽 (非响应式对象,等同于 $slots)
    console.log(context.slots)

    // 触发事件 (方法,等同于 $emit)
    console.log(context.emit)

    // 暴露公共 property (函数)
    console.log(context.expose)
  }
}

注意

  • 使用配置项(data、methos、computed...)可以访问setup中的返回的属性、方法,但setup不能访问到配置项(data、methos、computed...)

  • setup函数内部this不是该活跃实例的引用,无法同配置项一样使用this。

2、ref、reactive、toRef、toRefs函数

  • ref函数用来定义一个响应式数据。
<template>
   <!-- 模板中读取数据不需要.value -->
   <div>{{counter}}</div>
   <button @click="add">点击增加</button>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const counter = ref(0); // 接收的数据可以是:基本类型、也可以是对象类型。

    console.log(counter) // { value: 0 }
    console.log(counter.value) // 0

    const add = () => {
        counter.value++;  // 操作数据需要 counter.value
    }

    return {
        counter,
        add
    }
  },

}
</script>
  • reactive函数定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)。
<template>
   <div>名称:{{person.name}}</div>
   <div>年龄:{{person.age}}</div>
   <button @click="addAge">点击增加年龄</button>
</template>

<script>
import { reactive } from 'vue'

export default {
  setup() {
    // reactive接收一个对象(或数组),返回一个proxy对象
    const person = reactive({
        name: 'bandianjuse',
        age: 18
    });

    const addAge = () => {
        person.age++;  
    }

    return {
        person,
        addAge
    }
  },

}
</script>
  • toRef函数创建一个 ref 对象,其value值指向另一个对象中的某个属性。主要应用在将响应式对象中的某个属性单独提供给外部使用时。
  • toRefs函数与toRef函数行为一致,可以批量创建多个 ref 对象
<template>
   <div>名称:{{name}}</div>
   <div>年龄:{{age}}</div>
   <div>工作岗位:{{post}}</div>
   <div>工作年限:{{workingYears}}</div>
   <button @click="addAge">点击增加年龄</button>
</template>

<script>
import { reactive, toRef, toRefs } from 'vue'

export default {
  setup() {
    const person = reactive({
        name: 'bandianjuse',
        age: 18,
        job: {
            post: '前端工程师',
            workingYears: 10
        }
    });

    const addAge = () => {
        person.age++;  
    }

    return {
        name: toRef(person, 'name'),
        age: toRef(person, 'age'),
        //  age: person.age, // 这种写法将会失去对数据的响应,数据更新了无法反馈到页面
        ...toRefs(person.job), 
        addAge
    }
  },

}
</script>

3、vue3响应式原理

vue2中对象的响应式是通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。 而数组是通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})

这种方式存在一些问题

  • 新增属性、删除属性, 界面不会更新。
  • 直接通过下标修改数组, 界面不会自动更新。

当然vue2也提供了一些解决方案:使用用内置$set、$delete 方式来处理响应式数据。

vue3中通过Proxy(代理)拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。然后通过Reflect(反射)对源对象的属性进行操作。

new Proxy(data, {
    // 拦截读取属性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

proxy.name = 'bandianjuse'   

4、computed与watch函数

  • computed函数与vue2中计算属性一样,根据依赖的值变化创建新的值。
<template>
  <div>姓:{{ firstName }}</div>
  <div>名:{{ lastName }}</div>
  <div>全名:{{ fullName }}</div>
  <button @click="updateName">改名换性</button>
</template>

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

export default {
  setup() {
    const person = reactive({
        firstName: '张',
        lastName: '三'
    });

    //计算属性——简写
    let fullName = computed(() => {
      return person.firstName + '-' + person.lastName;
    });

    //计算属性——完整
    /* let fullName = computed({
      get() {
        return person.firstName + '-'+ person.lastName;
      },
      set(value) {
        const nameArr = value.split('-');
        person.firstName = nameArr[0];
        person.lastName = nameArr[1];
      },
    }); */

    const updateName = () => {
        person.firstName = '李';
        person.lastName = '四'
    }

    return {
        ...toRefs(person),
        fullName,
        updateName
    }
  },
};
</script>
  • watch函数与vue2中监听属性一样,用来响应数据的变化。
<template>
  <div>{{ sum }}</div>
  <div>{{ name }}</div>
  <div>{{ person.job }}</div>
  <button @click="updateData">更新数据</button>
</template>

<script>
import { ref, watch, reactive } from "vue";

export default {
  setup() {
    const sum = ref(0);
    const name = ref("张三");
    const person = reactive({
        job: '前端'
    })

    // 监视ref定义的响应式数据
    watch(
      sum,
      (newValue, oldValue) => {
        console.log("sum变化了", newValue, oldValue);
      },
      { immediate: true }
    );

    // 监视多个ref定义的响应式数据
    watch([sum, name], (newValue, oldValue) => {
      console.log("sum或msg变化了", newValue, oldValue);
    });

    // 监视reactive定义的响应式数据中的某些属性
    watch(
      () => person.job,
      (newValue, oldValue) => {
        console.log("person的job变化了", newValue, oldValue);
      },
      { deep: true }
    );

    const updateData = () => {
      sum.value++;
      name.value = "李四";
      person.job = "后端"
    };

    return {
      sum,
      name,
      person,
      updateData,
    };
  },
};
</script>
  • watchEffect函数跟watch类似,但比watch更智能,它不需要指定监视哪个属性,只要回调中用到哪个属性,它就会监视哪个属性。
// watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
    const s = sum.value
   
    console.log('watchEffect配置的回调执行了', s)
})

5、vue3的生命周期钩子

vue3中的生命周期钩子基本上保持跟vue2一致,只是将beforeDestroy改名为beforeUnmountdestroyed改名为unmounted

v3.cn.vuejs.org_images_lifecycle.svg.png

vue3也提供了 Composition API 形式的生命周期钩子,与vue2中钩子对应关系如下:

OptionsAPIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

计时器案例

计时器案例.gif

// App.vue
<template>
   <Child v-if="isUnmount"></Child>
   <button @click="handleTimer">{{timeBtnStr}}</button>
</template>

<script>
import { computed, ref } from 'vue';
import Child from './components/Child'

export default {
  name: 'App',
  components: { Child },
  setup() {
    let isUnmount = ref(true);
  
    let handleTimer = () => {
       isUnmount.value = !isUnmount.value
    }

    const timeBtnStr = computed(() => {
      return isUnmount.value ? '卸载计时器' : '启动计时器'
    })

    return {
      isUnmount,
      timeBtnStr,
      handleTimer
    }
  },

}
</script>


// Child.vue
<template>
  <div>计数器:{{ num }}</div>
</template>

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

export default {
  setup() {
    let num = ref(0);
    let timer = null;

    onMounted(() => {
        console.log('onMounted');
        timer = setInterval(() => {
            num.value++
        }, 1000)
    })

    onBeforeUnmount(() => {
        console.log('onBeforeUnmount');
        clearInterval(timer);
    })

    return {
      num
    };
  },
};
</script>

6、自定义Hook函数

Hook本质是一个函数,把setup函数中使用的Composition API进行了封装,提高了代码的复用性。类似于vue2的mixin

hook使用.gif

// App.vue
<template>
   <div>总数:{{current}}</div>
   <button @click="inc">加数</button>
   <button @click="dec">减数</button>
   <button @click="set(10)">设值</button>
   <button @click="reset">重置</button>   
</template>

<script>
import  useCount from './hooks/useCount'

export default {
  name: 'App',
 
  setup() {
     const { current, inc, dec, set, reset } = useCount(1, {
      min: 1,
      max: 15
    })
 
    return {
      current,
      inc,
      dec,
      set,
      reset
    }
  },
}
</script>

// ------------------
// useCount.js
import { ref,  watch } from 'vue'
 
/**
  * 计数器
  * @param {number} initialVal 初始值
  * @param {Object} range { min: number, max: number }限制范围
  * @returns 
  */ 
export default function useCount(initialVal, range) {
  const current = ref(initialVal);

  // 数值加
  const inc = (data) => {
    if (typeof data === 'number') {
      current.value += data
    } else {
      current.value += 1
    }
  }

  // 数值减
  const dec = (data) => {
    if (typeof data === 'number') {
      current.value -= data
    } else {
      current.value -= 1
    }
  }

  // 设置值
  const set = (value) => {
    current.value = value
  }

  // 重置值
  const reset = () => {
    current.value = initialVal
  }
  
  // 监听值变化,限定在范围内
  watch(current, (newVal, oldVal) => {
    if (newVal === oldVal) return
    if (range && range.min && newVal < range.min) {
      current.value = range.min
    } else if (range && range.max && newVal > range.max) {
      current.value = range.max
    }
  })
 
  return {
    current,
    inc,
    dec,
    set,
    reset
  }
}


7、组合式 API 的优势

使用传统Options API中,新增或者修改一个需求,就需要分别在data,methods,computed里修改。而使用Composition API我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。

Options API 演示图

Composition API 演示图

从演示图中可以看出Composition API结合Hook函数,将单一功能进行拆分,进一步提高了代码的复用率,使我们可以写出更加清楚优雅的代码。这就是vue3核心思想所在。

三、vue3新增组件

1、Teleport

Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。 借用这种技术,我们可以解决多层级组件嵌套的定位问题,例如封装modals,toast 类型的组件,使用teleport非常方便。

<!-- to: 移动的位置 -->
<teleport to="body">
    <div v-if="isShow" class="mask">
        <div class="dialog">
            <h3>我是一个弹窗</h3>
	    <button @click="isShow = false">关闭弹窗</button>
	</div>
    </div>
</teleport>

2、Fragment

在发vue2中,组件是必须有一个根标签的。但在vue3中,组件可以没有根标签,因为其内部会将多个标签包含在一个Fragment虚拟元素中。Fragment在真实DOM中是不存在的,这样的做法可以有效的减少标签的层级,减少内在占用。

<template>
  <Fragment>
    <div>总数:{{ current }}</div>
    <button @click="inc">加数</button>
    <button @click="dec">减数</button>
    <button @click="set(10)">设值</button>
    <button @click="reset">重置</button>
  </Fragment>
</template>

3、Suspense

Suspense组件用来包裹一个异步组件,在等待异步组件时可以渲染一些额外内容,让应用有更好的用户体验。

// 引入异步组件
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))

// 使用Suspense组件包裹,并配置好`default` 与 `fallback`插槽
<template>
    <div class="app">
        <h3>我是App组件</h3>
        <Suspense>
            <template v-slot:default>
                <Child/>
            </template>
            <template v-slot:fallback>
                <h3>加载中.....</h3>
            </template>
	</Suspense>
    </div>
</template>

四、一些全局api和配置的调整

vue2中有许多全局 API 和配置在vue3中做了调整,将Vue.xxx调整到应用实例(app)上:

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

const app = createApp(App).mount('#app');

调整对照表:

vue2 全局 API(Vuevue3 实例 API (app)
Vue.config.xxxxapp.config.xxxx
Vue.config.productionTip移除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties