vue3 实用

415 阅读7分钟

1.为啥重写vue2.x

肯定有原因的... 尤雨溪的回答是两个关键因素:

  • 主流浏览器对新的JavaScript语言特性的普遍支持。
  • 当前Vue代码库随着时间的推移而暴露出来的设计和体系架构问题。
vue2.xvue3
beforeCreate使用 setup()
created使用 setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

组件实例初始化速度提高 100%,提速一倍/内存使用降低一半,更多编译时 (compile-time)提醒以减少 runtime 开销

2.为什么要换掉defineProperty,使用proxy代替defineProperty

重点:vue为什么对数组对象的深层监听无法实现,因为组件每次渲染都是将data里的数据通过defineProperty进行响应式或者双向绑定上,之前没有后加的属性是不会被绑定上,也就不会触发更新渲染,基于 Proxy 的观察者,它会提供全语言覆盖的响应式跟踪。

  • 可以检测属性的新增和删除

  • 可以检测数组索引的变化和 length 的变化

  • 支持 Map、Set、WeakMap 和 WeakSet

Object.defineProperty( Obj, 'name', {
    enumerable: true, //可枚举
    configurable: true, //可配置
    // writable:true, //跟可配置不能同时存在
    // value:'name',  //可写死直
    get: function () {
        return def
    },
    set: function ( val ) {
        def = val
    }
} )

proxy:

let obj = { a : 1 }
//两个参数,对象,13个配置项
const handler = {
    get: function(target, prop) {
        return prop in target ? target[prop] : 0;
    },
    set : function (target,prop,value) {
        target[prop] = 888; 
    }
    ...13个配置项
};
const p = new Proxy(obj, handler);
console.log(p.a); // 1 
console.log(p.b); // 0 
p.a = 666; 
console.log(p.a) // 888

defineProperty只能响应首次渲染时候的属性,Proxy需要的是整体,不需要关心里面有什么属性,而且Proxy的配置项有13种,可以做更细致的事情,这是之前的defineProperty无法达到的

3.diff算法的提升

vue2.x 提供类似于HTML的模板语法,将模板编译成渲染函数来返回虚拟DOM树,通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性,来确定实际DOM的哪些部分需要更新。但是dom的更新会涉及不必要的CPU工作

尤雨溪: 为了实现这一点,编译和运行时需要协同工作:编译器分析模板并生成带有优化提示的代码,而运行时尽可能获取提示并采用快速路径。

  • 在没有动态改变节点结构的模板指令(例如v-if和v-for)的情况下,节点结构保持完全静态,如果我们将一个模板分成由这些结构指令分隔的嵌套“块”,则每个块中的节点结构将再次完全静态。当我们更新块中的节点时,我们不再需要递归遍历DOM树 - 该块内的动态绑定可以在一个平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级来规避虚拟DOM的大部分开销。
  • 编译器积极地检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提升到渲染函数之外。这样可以避免在每次渲染时重新创建这些对象,从而大大提高内存使用率并减少垃圾回收的频率
  • 编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。运行时将获取这些提示并采用专用的快速路径。

Vue 3有时占用的CPU时间不到Vue 2的十分之一

4.使用ts

vue2是支持类型的,用的是Facebook的Flow做类型检查,但是因为某些情况下推断有问题,所以改为支持ts。一个是为了更好的类型检查,另一个是拥抱ts

vue3借鉴了react hook实现了更自由的编程方式,提出了Composition APIComposition API不需要通过指定一长串选项来定义组件,而是允许用户像编写函数一样自由地表达、组合和重用有状态的组件逻辑,同时提供出色的TypeScript支持。

5.打包

vue2官方说的运行时打包23k,但这只是没安装依赖的时候,随着依赖包和框架特性的增多,有时候不必要的,未使用的代码文件都被打包了进去, 尤雨溪:

  • Vue 3中,我们通过将大多数全局API和内部帮助程序移动到Javascriptmodule.exports属性上实现这一点。这允许现代模式下的module bundler能够静态地分析模块依赖关系,并删除与未使用的module.exports属性相关的代码。模板编译器还生成了对树抖动友好的代码,只有在模板中实际使用某个特性时,该代码才导入该特性的帮助程序。
  • 尽管增加了许多新特性,但Vue 3被压缩后的基线大小约为10 KB,不到Vue 2的一半

6.安装

vite脚手架安装

npm init vite-app hello-vue3  |  yarn create vite-app hello-vue3

7.组件基本结构

//dom 里的东西基本上都是没有变的
<template>
  <h1>{{ msg }}</h1>
  <button @click="increment">
    count: {{ state.count }}, double: {{ state.double }},three:{{ three }},refnum:{{refnum}}
  </button>
</template>

<script>
//这里就是Vue3的组合Api了,这里跟react的 import { useState ,useEffect } from 'react' 有些类似,需要用啥引啥
import {ref, reactive, computed ,watchEffect,watch} from "vue";
export default {
  name: "HelloWorld",
  props: {
    msg: String,
  },
  //上面对比的时候说过,setup相当于beforeCreate 和created,简单理解就是初始化
  setup() { 
  	//这里通过reactive使state成为相应状态(后面会详细介绍)
    const state = reactive({
      count: 0,
      //计算属性computed的使用更灵活了
      double: computed(() => state.count * 2),
    });
    //computed也可以单独拿出来使用
    const three = computed(() => state.count * 3)
    // 计算多个属性,可以通过返回一个对象的方式来实现
    const computeCount = computed(() => {
        return{
            '100x': state.count*100,
            '200x': state.count*200
        }
     })
    //ref跟reactive作用一样都是用来数据相应的,ref的颗粒度更小(后面详细对比)
    const refnum = ref()
   //这里的watchEffect只要里面的变量发生了改变就会执行,并且第一次渲染会立即执行,没有变化前后返回参数,无法监听整个reactive
    watchEffect(() => {
      refnum.value = state.count;
      console.log(state, "watchEffect");
    });
    //watch里第一个参数是监听需要的变量,第二个是执行的回调函数,
    watch(refnum,(a,b)=>{
      console.log(a,b,'watch,a,b')
    })
    //所有的方法里再也不需要用this了
    function increment() {
      state.count++;
    }
   	//组中模板中需要的变量,都要通过return给暴露出去,就像当初data({return { } }) 是一样的
    return {
      state,
      increment,
      three,
      refnum,
      computeCount
    };
  },
};
</script>

8.生命周期

<script>
import {
  reactive,
  computed,
  onMounted,
  onBeforeMount,
  onBeforeUpdate,
  onUpdated,
  onUnmounted,
  onBeforeUnmount,
} from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2),
    });
    function increment() {
      state.count++;
    }
    onUpdated(() => {
      console.log("onUpdated");
    });
    onUnmounted(() => {
      console.log("onUnmounted");
    });
    onBeforeUnmount(() => {
      console.log("onBeforeUnmount");
    });
    onBeforeUpdate(() => {
      console.log("onBeforeUpdate1");
    });
    onMounted(() => {
      console.log("onMounted");
    });
    onBeforeMount(() => {
      console.log("onBeforeMount");
    });
    console.log("setup");
    return {
      state,
      increment,
    };
  },
};
</script>

执行顺序: setup onBeforeMount onMounted onBeforeUpdate onUpdated

9.api使用

1.setup(props,context)

setup 会比 options API 的生命周期函数 beforeCreate 更先执行

父组件
<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld :activeId="12222" />
  //这里传参给子组件
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import { provide } from "vue";
export default {
  name: "App",
  components: {
    HelloWorld,
  }
};
</script>

子组件
//props 接收的父组件传的参数,这就有点像react的props了
//context 这个参数表示的当前对象实例,也就个是变相的this
setup(props,ctx){
 console.log(props.msg, context, "app-setup");
}

image.png

2.ref、toRef、toRefs

在生命周期函数setup中使用

ref接受一个内部值并返回一个响应式且可变 的 ref 对象

使用 ref.value = xx 进行改变值


<script>
import { ref } from 'vue'
export default {
  setup() {
    const nameRef = ref('haha')
    // 1.5秒后 nameRef的值从 haha 变为 haha2
    setTimeout(() => {
      nameRef.value = 'haha2'
    }, 1500)
    // 返回的值可以在 template 中使用
    return {
      nameRef
    }
  }
}
</script>

reactive 引用类型变成响应式

{{ person.age }}
export default { 
    setup() { 
        const person = reactive({ name: 'haha', age: 50 })
        // 1.5 秒后 person.age 的值从 50 变为 51 
        setTimeout(() => { person.age = 51 }, 1500) 
        return { 
            person 
        } 
     } 
  }

toRef 对定义的响应对象的某个属性进行引用,这其实就是对响应对象的一种“延续”

const nameRef = toRef(person, 'name')

使用nameRef和 person.name 进行赋值都会触发响应式

使用一个函数返回一个响应式对象,可以使用的 toRef 或是 toRefs 保持他的响应式

toRef

data.js
import { reactive } from 'vue'

export default function () {
    return reactive({
        name: 'haha',
        age: 50
    })
}
App.vue

<template>
  <div>
    {{ nameRef }}
  </div>
</template>
<script>
import { toRef } from 'vue'
import data from './data'
export default {
  setup() {
    const nameRef = toRef(data(), 'name')
    
    setTimeout(() => {
      nameRef.value = 'haha2'
    }, 1000)
    
    return {
      nameRef
    }
  }
}
</script>

toRefs

// data.js
import { reactive, toRefs } from 'vue'

export default function () {
    const person = reactive({
        name: 'haha',
        age: 50
    })
    return toRefs(person)
}
App.vue
<template>
  <div>
    {{ name }}
    <br />
    {{ age }}
  </div>
</template>
<script>
import data from './data'
export default {
  setup() {
    const person = data()
    setTimeout(() => {
      person.name.value = 'haha2'
      person.age.value = 51
    }, 1000)
    
    // 直接写 return person 也行
    return {
      ...person
    }
  }
}
</script>

总结

ref 是对值类型创造响应式的方法

toRef、toRefs 是延续引用类型响应式对象的方法

只不过 toRef 延续单个响应式对象的属性,

而 toRefs 延续响应式对象的全部属性

10. 父子组件传值方法

父组件 openPacket.vue image.png 子组件 packetMes.vue image.png 结果: image.png

11.vue3.0中路由进行跳转、传参、取值

vue2.x
this.$router.push({
    path: "./xxx"
    query: {
        name: "小可爱是我"
    }
})
this.$route.query.name

vue3.0取消了vue2.0的部分api,新增的两个API useRouter,useRoute。

image.png

image.png

12.main.js 与 main.ts

// Vue2中实例化Vue 
new Vue({ 
    router, 
    store,
    render: h => h(App) 
}).$mount('#app')
// 导入根组件App
import App from './App.vue'
// 支持前端路由
import router from './router/index'
// 支持vuex状态管理
import store from './store'
// 就是相当于创建出了一个实例,链式编程的方式去.use()store和router
// 最后再挂载到index.html中ID为app的div标签中,只是$mount被换成了mount
const app = createApp(App)
app.use(store).use(router).mount('#app')

13.vue2.x与vue3 router

vue2.x

image.png vue 3

image.png

VUE3.0 createWebHistory和createWebHashHistory

  • createWebHistory路由模式路径不带#号
    http://localhost:8080/
    
    生产环境下不能直接访问,需要nginx转发
  • createWebHashHistory路由模式路径带#号
http://localhost:8080/#/
const router = createRouter({
  history: createWebHashHistory()/createWebHistory(),
  routes
});

11.watch 与 watchEffect 监听

//这里的watchEffect只要里面的变量发生了改变就会执行,并且第一次渲染会立即执行,没有变化前后返回参数,无法监听整个reactive
   watchEffect(() => {
     refnum.value = state.count;
     console.log(state, "watchEffect");
   });
   //watch里第一个参数是监听需要的变量,第二个是执行的回调函数,
   watch(refnum,(a,b)=>{
     console.log(a,b,'watch,a,b')
   })

  • watch 需要具体监听参数,watchEffect 不需要传入监听参数

  • watch 的回调函数跟以前一样有前后对比的参数,watchEffect 啥都没有

  • watch 只有监听属性变化才执行,watchEffect 第一次会立即执行

  • watch watchEffect 都无法监听未被绑定的属性

  • watch 可以直接监听 refreactive 绑定的对象,watchEffect 不可以(ref的值要.value,reactive的值要具体到内部属性),只会执行第一次

11.函数组件

所有的函数式组件都是普通函数创建的,有两个参数 props和 context

import { h } from 'vue'
const Fun = (props, context) => {
//这里h的用法跟以前还是一样的
  return h(p, context.attrs, context.slots)
}
export default Fun

区别:

  • 以前是在 render 函数中隐式提供 creatElement,现在是组合Api里引入h 引用大佬的模版写法
import { ref, computed, watch, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = ref(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}

全局注册组件


// 1.引入封装好的全局组件地址
import 组件名 from './地址' //一般放置在./src/components下

// 2.导出
exports default{
    install (app) {
    // 此处形参为main.js文件中use()方法自动传进来的Vue实例
        app.component('自定义组件名,最好与组件内的name一致', 组件名)
    }
}

3.main.js中挂载到Vue实例中

import { createApp } from 'vue'
import App from './App.vue' //vue3中引入Vue实例方式

import component from './components'// 引入公共组件
// 链式添加一项 .use(component) 来引入配置好的公共组件
createApp(App).use(component).mount('#app')
4.使用
<自定义组件名 />

自定义全局参数

toast.vue
<template>
  <div v-show="showWrap" class="mb-toast">
    <div class="mb-mask"></div>
    <div class="mb-toast-container">
      <p class="mb-toast-msg">{{ wrapText }}</p>
    </div>
  </div>
</template>
<script lang="ts">
import { reactive, toRefs } from "vue";
interface DataProps {
  wrapText: string;
  showWrap: boolean;
}
export default {
  name: "App",
  components: {},
  setup() {
    const data: DataProps = reactive({
      wrapText: "",
      showWrap: true,
    });
    const refData = toRefs(data);
    const setLoading = (type: boolean, val: string, duration = 2000) => {
      data.wrapText = val;
      data.showWrap = type;
      setTimeout(() => {
        data.showWrap = false;
      }, duration);
    };
    return {
      ...refData,
      setLoading,
    };
  },
};
</script>
<style scoped lang="less">
.mb-toast {
  width: 100%;
  height: 100%;
  position: fixed;
  left: 0;
  top: 0;
  z-index: 9999;
  .mb-mask {
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
  }
  .mb-toast-container {
    width: auto;
    height: auto;
    overflow: hidden;
    border-radius: 5px;
    padding: 10px;
    position: absolute;
    left: 50%;
    top: 50%;
    -webkit-transform: translateX(-50%) translateY(-50%);
    transform: translateX(-50%) translateY(-50%);
    background: rgba(17, 17, 17, 0.7);
    .mb-toast-msg {
      font-size: 16px;
      text-align: center;
      color: #ffffff;
    }
  }
}
</style>

toast.ts
import { createApp } from "vue";
import toast from "./toast.vue";

const app = createApp(toast);
const dom = document.createElement("div");
const instant: any = app.mount(dom);
document.body.appendChild(dom)
class Toast {
  constructor(type: boolean, mes: string) {
    this.info(type, mes)
  }
  info(type: boolean, mes: string) {
    console.log("toast", instant);
    instant.setLoading(type, mes)
  }
}

export default new Toast();
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import 'amfe-flexible/index'
import Toast from "./components/toast/toast";
const app = createApp(App);
app.config.globalProperties.$Toast = Toast;
app.use(router).use(Toast).mount('#app')

image.png

ts文件下使用全局组件

image.png

vue3获取dom

<template>
  <div ref="root">This is a root element</div>
</template>

<script>
import { ref, onMounted, getCurrentInstance } from 'vue';
export default {
	setup() {
		const { proxy } = getCurrentInstance();
		const root = ref(null);

		onMounted(() => {
			// DOM元素将在初始渲染后分配给ref
			console.log(root.value); // <div>This is a root element</div>
		});

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