搭建Vue 3.0

217 阅读11分钟

Vue3.0

Vue3中兼容Vue2中定义组件的写法,所以只需要将入口文件main.js中创建Vue实例的代码替换为使用和Vue3中新引入的createApp方法,来创建应用程序实例的方式即可。
Vue 2 中main.js:

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

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

Vue 3 main.js:

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

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

Vue 3.0的新特性:

Composition API(组合式API):

组件变得越来越大时,逻辑关注点的列表也增长了(文件结构复杂)。导致组件难以阅读和理解,(在处理单个逻辑关注点时,需要不断地“跳转”相关代码的选项块)
Composition API:将与同一个逻辑关注点相关的代码配置在一起。
使用Composition API的位置被称为setup(在setup中使用Composition API)

使得组件的大部分内容可以通过setup()方法进行配置

setup组件选项

setup组件选项在创建组件之前执行,一旦props被解析,并充当合成API的入口点。

注:由于在执行setup是尚未创建组件实例,因此在setup选项中没有this。即:除了props之外,将无法访问组件中声明的任何属性————本地状态、计算属性或方法等

// 子组件中
import { APIName } from '@/api'
import { ref, onMounted } from 'vue'

// in our component
export default {
    setup (props) {
      const repositories = ref([]) // 定义一个变量
      const getUserRepositories = async () => { // 定义一个方法
        repositories.value = await APIName(props.user)
      }
      onMounted(getUserRepositories) // 生命周期钩子 当实例mounted后调用getUserRepositories方法

      return {
        repositories, // 返回一个data
        getUserRepositories // 返回一个method
      }
    }
}

单文件组件Composition API语法糖:

当组件可以使用组合API后,setup往往成为了唯一会用到的组件属性,因此利用语法糖简化setup的写法

<script setup>
  import { ref } from 'vue'

  export const count = ref(0)
  export const inc = () => count.value++
</script>

使用场景:

vue 2.0中通过组件data,以及methods的方法来定义一些当前组件的数据;
vue 3.0中通过ref或者reactive(反应性的)创建响应式对象,methods的方法也写在setup中并return;

import {ref,reactive} from 'vue'
setup(){
  const name = ref('test')
  const state = reactive({
    list: []
  })
  const md = () => { console.log('抛出子组件的方法') }
  return {
      name,
      state,
      md
  }
}

ref将给定的值创建一个响应式的数据对象并赋初始值,reactive可以直接定义复杂响应式对象

无法使用EventBus:

vue 2.0中使用EventBus实现组件通信:
vue的四核事件方法 $on、$emit、$off、$once

var EventBus = new Vue()
Vue.prototype.$EventBus = EventBus
this.$EventBus.$on('eventName', (val) => { this.modifyNodeHandle(value); })
this.$EventBus.$emit('eventName', {
    data: result,
    type: 'root'
})
// 或者使用this.$root.$on()使用
this.$root.$off('eventName', cb) // cb可选,移除事件中心的某个事件,cb指移除时触发的方法
this.$root.$once('eventName', cb) // 同$on,但$once只触发一次,一旦触发,监听器就会被移除

注:vue处理边界$root、$parent、$refs、$children
Vue子组件可以通过$root属性访问父组件实例的属性和方法(和$parent的区别:如果存在多级子组件,$parent访问到的是最近一级的父组件,$root访问到的是根父组件),因此使用this.$root.$on可以直接监听vue实例的事件,而不用再创建新的vue实例
setup中使用mitt方案来代替,:

import mitt from 'mitt'
const emitter = mitt()
// 添加监听事件
emitter.on('foo', e => console.log('foo', e) )
// 使用监听事件
emitter.emit('foo', { a: 'b' })

vue 3.0不再支持prototype的方式给Vue绑定静态方法,可以通过app.config.globalProperties.mitt = () => {}方案

setup()中使用props和this:

在vue 2.0中,组件的方法中可以通过this获取到当前组件的实例,并执行data变量的修改,方法的调用,组件通信等,但在3.0中,setup()在beforeCreate和created时机就已经调用,无法使用2.0中的this,但是可以通过接收setup(props, ctx)的方法,获取当前组件的props和实例

  props: {
    name: String,
  },
  setup(props,ctx) {
    console.log(props.name)
    ctx.emit('event')
  },

ctx和2.0的this不完全一样,ctx选择性的暴露了一些property(attrs,emit,slots)

子组件中watch来监听对象改变

2.0中可以采用watch来监听对象属性是否有改动;
3.0中在setup()中,可以使用watch来监听;

import {watch} from 'vue'
setup(){
  let state = reactive({
    name: 'a'
  })
  watch(
    () => state.name,
    (val, oldVal) => {
      console.log(val)
    }
  )
  state.name = 'b'
  return {
      state
  }
}

如果watch的是一个数组对象,调用.push添加数据不会触发watch方法,必须重新给数组赋值

setup中使用computed计算属性:
const storeData = computed(() => store.state.storeData)
return { storeData }

总结

代码依据业务逻辑,完全分散成多个管理类,setup只需要负责加载和整合即可,setup里面也不会有很多代码。
如,在单独的js文件中定义:

import { ref } from 'vue'
 
// 帖子列表的管理类
export function manageArticleForm () {
 const modelForm = ref({
     title: '这是帖子标题',
     content: '帖子内容',
     sendTime: '2020-10-20'
 })
 return {
 articleForm: modelForm,
 }
}

然后在子组件中引用:

import { manageArticleForm } from './bbs-manageArticleForm.js'
setup() {
 // 表单
 const { articleForm } = manageArticleForm()
 // 返回给view
 return {
 articleForm
 }
}

单文件组件状态驱动的CSS变量:

组件状态驱动css变量(style vars)

可以在运行时根据组件状态来动态更新样式(向css传递参数)

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>
<style vars="{ color }">
.text {
  color: var(--color);
}
</style>

单文件组件(style scoped)包含全局规则或只针对插槽内容的规则:

带有scoped属性的style不在只能作用域当前单文件组件,可以通过深度选择器插槽选择器全局选择器拥有了更改其他范围样式的能力。

<style scoped>
/* deep selectors,样式将在子组件中生效 */
::v-deep(.foo) {}
/* shorthand */
:deep(.foo) {}

/* targeting slot content,用于替代那些被放在HTML模板中的元素 */
::v-slotted(.foo) {}
/* shorthand */
:slotted(.foo) {}

/* one-off global rule,全局生效 */
::v-global(.foo) {}
/* shorthand */
:global(.foo) {}
</style>

Vue 3.0的重大改变

引入createApp

vue2中没有“app”的概念,我们定义的应用程序只是通过new Vue()创建的根Vue实例。从同一个Vue构造函数创建的每个根实例共享相同的全局配置,因此全局配置使得在测试期间很容易意外的污染其他测试用例。需要谨慎存储原始全局配置(如:每次测试后恢复,如重置Vue.config.errorHandler)。有些API像Vue.use以及Vue.mixin甚至连恢复效果都没有,使得涉及插件的测试很棘手。

// createApp:
import { createApp } from 'vue'
const app = createApp({})

调用createApp返回一个应用实例,应用程序实例暴露当前全局API的子集,任何全局改变Vue的行为API会移动到应用实例上。其他不全局改变行为的全局API被命名为exports。
全局和内部API已重构为可tree-shakable

Tree shaking是一个通常用于描述移除js上下文中的未引用代码行为的术语
依赖于es6中的import和export语句,用来检测代码模块是否被导出、导入,且被js文件使用
在现代js应用程序中,使用模块打包(webpack或Rollup)将多个js文件打包为单个文件时自动删除未引用的代码。是最终文件结构和最小化大小。
2.0语法

// 全局 API Vue.nextTick() 不能tree-shaking
import Vue from 'vue'
Vue.nextTick(() => {
  // 一些和DOM有关的东西
})

3.0语法
全局API只能作为es模块构建的命名导出进行访问,如果模块绑定器支持tree-shaking,则Vue应用程序中未使用的全局api将从最终捆绑包中消除,从而获得最佳的文件大小。

import { nextTick } from 'vue'
nextTick(() => {
  // 一些和DOM有关的东西
})

受影响的API包括:

- Vue.nextTick
- Vue.observable (用Vue.reactive替换)
- Vue.version
- Vue.compile
- Vue.set
- Vue.delete 

Vue.observable

Vue.observable:进行状态管理。随着组件的细化,出现多组件状态共享的情况,Vuex可以解决,但对于小规模引用,Vuex会导致代码范睢冗余。vue2.6增加的Observable API,通过使用这个api可以应对一些简单的跨组件数据状态共享的问题。
observable()方法,用于设置监控属性,用以监控viewModule中的属性值的变化,从而可以动态改变某个元素的值,监控属性的类型的不变量是一个函数,通过返回一个函数给viewModule对象中的属性,从而来监控该属性。

在src目录下建立store.js,组件中使用提供的store和mutation方法,实现多个组件共享数据状态。

//store.js
import Vue from 'vue';
export let store = Vue.observable({count:0,name:'李四'});
export let mutations={
    setCount(count){
        store.count = count;
    },
    changeName(name){
        store.name = name;
    }
}
// 子组件中
import HomeHeader from '../components/HomeHeader'   
import {store,mutations} from '@/store'
export default {  
    data () {  
        return {  
            name1:'主页的name'
        }  
    },   
    computed:{
        count(){
            return store.count
        },
        name(){
            return store.name
        }
    },
    methods:{
        // 方法的属性表达式写法,无大括号时可直接用':'
        setCount: mutations.setCount, 
        changeName: mutations.changeName    
    }
}  
// HTML中 
<button @click="setCount(count+1)">+1</button> // 调用store中的mutation方法
<button @click="setCount(count-1)">-1</button>
<div>store中count:{{count}}</div> // 显示store中的count值
<button @click="changeName(name1)">父页面修改name</button> //调用store方法修改name
<div>store中name:{{name}}</div> // // 显示store中的name值
// 在其他子组件中可以共同对name或者count进行操作,所有组件共享store中Vue.observable的值

当前项目vue版本低于2.6,要使用Vue.observable(),就必须要升级,升级 vue 和 vue-template-compiler,两者的版本需要同步,如果不同步项目会报错。

`npm update vue -S 或者 yarn add vue -S
npm update vue-template-compiler -D 或者 yarn add vue-template-compiler -D`

Vue.version

提供字符串形式的Vue安装版本号。(对于社区的插件和组件来说很有用,可以根据不同的版本号采取不同策略)

var version = Number(Vue.version.split('.')[0])
 
if (version === 2) {
  // Vue v2.x.x
} else if (version === 1) {
  // Vue v1.x.x
} else {
  // Unsupported versions of Vue
}

Vue.compile

将一个模板字符串变异成render函数。只在full build的时候可用 初始化函数,最后一步进行了vm.$mount(el)的操作,而这个$mount在这两个地方定义过,分别在entry-runtime-with-compiler.js(简称:eMount)和runtime/index.js(简称:rMount)。eMount最后还是调的rMount,不过eMount做了一定的操作, Vue.compile(tempalte) 1、用法:
编译模板字符串并返回包含渲染函数的对象。只在完整版中才有效(并不是所有Vue.js的构建版本都存在Vue.compile)。

var res = Vue.compile('<div><span>{{msg}}</span></div>');
new Vue({
	data:{
		msg:'hello'
	},
	render:res.render
})

2、实现:
Vue.compile方法只需要调用编译器就能实现。Vue.compile = compileToFunctions; compileToFunctions方法可以将模板编译成渲染函数 编译的三个步骤:

  • parse(解析):需要将template转换成抽象语法树(AST)。
  • optimizer(优化器):对这个抽象语法树进行静态节点的标记,就可以优化渲染过程。
  • generateCode(生成代码):根据抽象语法树(AST)生成一个render函数字符串。
解析器

解析器中有个非常重要的概念(AST)
ASTNode分为几种不同类型, image.png 解析以下代码:

<div id="demo">
 <h1>Latest Vue.js Commits</h1>
 <p>{{1 + 1}}</p>
</div>

将生成如下的AST: image.png 在parse函数中,先是定义了非常多的全局属性及函数,然后调用了parseHTML,这个函数会不断解析模板,填充root,最后返回root(AST)。
parseHTML:最重要的是while循环中的代码,在while中,使用html.indexOf('<')去匹配:

  • __等于0: __代表是注释、调换注释、doctype、开始标签,结束标签
  • __大于等于0:__说明是文本、表达式
  • __小于0:__表示html标签解析完了,可能会剩下一些文本、表达式
    parse函数不断重复这个工作,将template转换成AST,解析过程中会对标签之间的空格做优化处理(有些元素之间的空格是没用的)
优化器

优化器的目的是去找出AST中纯静态的子树:
把纯静态的子树提升为常量,每次重新渲染的时候就不需要创建新的节点了,在patch的时候就可以跳过他们。
第一遍遍历:标记静态的节点;第二遍遍历:标记静态根节点(不能是叶子节点,需要有子节点且是静态的——如果一个静态节点只拥有一个半子节点并且这个子节点是文本节点,就不做静态处理,直接渲染)

代码生成器

将AST转换成render函数字符串。
最重要的数genElement这个函数,针对不同的指令、属性,会选择不同的代码生成函数。最后按照AST生成拼接成一个字符串。

vue的核心可分为三个大块:数据处理和双向绑定、模板编译、虚拟dom

vue.use

Vue.use(plugin);
Vue.use(VueLazyLoad, {
    error: './static/error.png',  // 包含install方法的对象
    loading: './static/loading.png'
});

1、用法:安装Vue.js插件,如果插件是一个对象,必须提供install方法。如果插件是一个函数,他会被作为install方法,调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次
2、作用:注册插件,此时只需要调用install方法并将vue作为参数传入即可。
注:插件的类型,可以是install方法,也可以是一个包含install方法的对象;插件只能被安装一次,保证插件列表中不嫩有重复的插件;
3、实现:

Vue.use = function(plugin){ // 在Vue.js上新增了use方法,并接收一个参数plugin。
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1){
        // 先判断插件是否已经被注册过(使用indexOf实现),被注册过则终止方法执行
		return this;
	}
	<!-- 其他参数 -->
        // 使用toArray方法得到arguments。除第一个参数外,其他的都赋值给arg
	const args = toArray(arguments,1); 
        // 然后将Vue添加到args列表的最前面。目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
	args.unshift(this);
        // 由于plugin参数支持对象和函数类型(对象会提供install方法,函数会被作为install方法),所以通过判断plugin.install和plugin那个是函数,可知用户使用哪种方式注册的插件,然后执行用户编写的插件并将args最为参数传入
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
        // 将插件添加到installedPlugins中,保证相同的插件不会被反复注册
	installedPlugins.push(plugin);
	return this;
}

Vue.mixin

Vue.mixin(mixin);
1、用法:

  • 全局注册一个混入(mixin),影响之后创建的每个Vue.js实例
  • 插件作者可以使用混入向组件注入自定义行为(如:监听生命周期钩子) 2、实现:
options

const vm = new Vue(options)。options包含了五类属性
数据: data, props, propsData, computed, Watch;
DOM: el, template, render, renderError;
声明周期钩子: beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed、errorCaptured;
资源: directives、filters、components;
组合: parent、mixins、extends、provide、inject;

// mergeOptions会将用户传入的mixin与this.options合并成一个新对象,将这个生成的新对象覆盖this.options属性,这里的this.options其实就是Vue.options.
// mixin方法修改了Vue.options属性,而之后创建的每个实例都会用该该属性,所以会影响创建的每个实例
import { mergeOptions } from '../util/index'
export function initMixin(Vue){
	Vue.mixin = function(minxin){
		this.options = mergeOptions(this.options,mixin);
		return this;
	}
}

Vue.delete

删除数组用delete和Vue.delete有什么区别?
delete:只是删除数组成员,元素变为empty/undefined,其他元素不变。
Vue.delete:直接删除了数组成员,并改变了数组的键值
用法:对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制

Vue.delete(this.namelist,'name');//vue方法

组件上使用v-model的用法已更改

  • 自定义v-model时,prop和事件默认名称已更改
    • prop:value -> modelValue
    • event:input -> update: modelValue
  • .sync和组件的model选项已移除,可用v-model作为替代
  • 现在可以在同一组件上使用多个v-model进行双向绑定;
  • 可以自定义v-model修饰符,如自定义v-model.capitalize,绑定为字符串第一个字母的大写

<template v-for> 和非 v-for 节点上 key 用法已更改

  • Vue 2.0建议在v-if/v-else/v-else-if的分支中使用key,vue3.0中人能正常工作,但不在建议,因为没有为条件分支提供key时,也会自动生成唯一的key。
  • Vue 2.0中<template>标签不能拥有key,在Vue3.0中key则应该被设置在<template>标签上。

在同一元素上使用的 v-if 和 v-for 优先级已更改

  • Vue3.0中v-if会拥有比v-for更高的优先级(由于语法上存在歧义,建议避免在同一元素上同事使用两者,可以使用计算属性筛选出列表

v-bind="object" 现在排序敏感

  • vue2.0如果一个元素同时定义了v-bind=“object”和一个相同的单独的prototype,那么这个单独的property总是会覆盖object中的绑定。
  • Vue3.0声明绑定的顺序决定了他们如何合并
// 2.x中 id最终为red  3.x中 id为blue
<div id="red" v-bind="{ id: 'blue' }"></div>

v-for 中的 ref 不再注册 ref 数组

  • Vue2.0中,在v-for里使用ref属性时,从$refs中获取的相应属性会是一个ref数组。
  • vue3.0中则将ref绑定到一个更灵活的函数上(ele) => {...// 保存ele的操作}
<div v-for="item in list" :ref="setItemRef"></div>

import { ref, onBeforeUpdate, onUpdated } from 'vue'
export default {
  setup() {
    let itemRefs = []
    const setItemRef = el => {
      itemRefs.push(el)
    }
    onBeforeUpdate(() => {
      itemRefs = []
    })
    onUpdated(() => {
      console.log(itemRefs)
    })
    return {
      itemRefs,
      setItemRef
    }
  }
}