vue3学习笔记

主要的编程逻辑

通过在根目录创建一个 composition文件夹

里面放置的是关于各个模块的辅助函数(用于封装函数,给页面上要对数据进行操作的标签使用)

通过函数的返回值(函数),在App.vue组件setup函数的return中调用获取到需要用到的函数,如:

<button
          @click="removeCompleted"
          class="clear-completed"
          v-show="completedRef > 0"
>
//上面用到的函数或数据都是在辅助函数中封装好的,只不过是在这边setup的return中调用函数获取,从而能够使用

setup() {
    const { todosRef } = useTodoList();
    return {
      todosRef,
      ...useNewTodo(todosRef),//展开对象拿到里面封装的函数,然后在页面上就能够使用了
      ...useFilter(todosRef),
      ...useEditTodo(todosRef),
      ...useRemoveTodo(todosRef),
    };
  },

setup函数返回对象的属性,能够在组件中使用(相当于vue2中data的return)

入门------------------------------------------

搭建工程

使用vue-cli构建

前提是安装了 vue-cli

在终端输入: vue create vue3-app-cli

启动工程: npm run serve

使用vite 构建(推荐使用)

对比vue-cli的优点:运行速度快

vite是一个构建工具,可以构建多种框架,如:vue、react等待

使用方式: npm init vite-app 工程名

工程构建完成后,需要进入到工程文件夹

安装需要的依赖: npm i

启动工程: npm run dev

**注意:**在vite搭建的工程中,导入文件的位置,必须加上文件后缀名(node_modules里面的模块除外)

与vue-cli工程的一点小区别:页面文件不在public目录下,而是在工程的根目录中

打包: vite build

Vue3与Vue2的不同

main.js文件的不同

在vue2中的main.js,是通过Vue构造函数实现挂载元素

//导入 Vue构造函数
import Vue from "vue";
new Vue(options).$mount("#app");

在vue3中的main.js,vue3已经没有了Vue构造函数,使用的是 vue3具名导出createApp 函数(也就是说vue3中,是export 函数名 导出的)

//导出 createApp函数
import { createApp } from "vue";
import App from "App.vue";
createApp(App).mount("#app");	
插件使用方式的不同

vue2中,使用插件:通过构造函数调用use()

import Vue from "vue";
Vue.use("插件");

vue3中,使用插件:通过实例调用use()

import { createApp } from "vue";
import App from "App.vue";
const app = createApp(App);
app.mount("#app");
app.use("插件");
this指向不同
vue3的组件实例代理.jpg

vue2的this指向:Vue实例对象

vue3的this指向:组件代理对象(但是对于操作数据等功能,使用this效果依然相同)

compositionAPI(重点)和optionAPI

这些API其实就是我们使用vue时自带的API,如:data、mothods、computed等等

vue2使用的是optionAPI vue3使用的是compositionAPI

image-20210122144120494 image-20210122144341728

颜色表示功能模块代码区域

可以看出,compositionAPI高内聚的把相同功能模块的代码都聚在一起,使用compositionAPI维护和编写起来比较高效

vue3的compositionAPI(初了解)

setup函数

实际上可以这么理解:vue3使用setup,把所用模块需要使用的代码都进行了集中处理,最后返回模块需要用到的数据(通过调用函数传参的方式)

并且可以简单理解为:vue3中用 setup函数代替了 vue2中的data,并且setup更加强大

使用:

**setup函数接收两个参数,参数1:一个对象,里面是所有的属性值,参数2:上下文,里面有以前this的函数,如 ctx.emit(手动触发函数) **

//导入ref函数,目的是实现数据响应式
import {ref} from "vue";
export default = {
	setup(props,ctx){
        let count = ref(0);  //把变量的默认值传进ref,他就会返回一个具有响应式的数据,相当于count = 0
        const say = ()=>{
            	console.log(count.value);
            	count.value++;
        }
		return {
            count,
            say
        }
	}
}

特点:

  • setup函数会自动执行
  • 并且会在所有生命周期钩子函数执行之前调用
  • this为undefined(也就是指:不能用this)
  • 通常return一个对象,对象中的属性会被放到实例
  • setup中定义的数据,如果没有使用 refAPI,则不具有响应式(也即是数据改变了页面依然没反应)
ref函数

使用: let 变量 = ref( 数据的默认值 );

ref的作用就是将数据编程响应式数据,

实际的过程:其实它是返回了一个对象,对象中有属性value,是以访问器的形式存在着

数据执行任何操作的时候,他都会提交给代理进行处理

注意:实例上这些数据的值类型依然是原本的数据类型,如 : typeof count 依然返回 Number

主要记住的一点:

  • 在setup中,count是一个对象
  • 在实例代理中,count是一个 count.value(也就是指:如果是在setup中操作count值,则使用 count.value
  • 如果变量使用ref定义的,最好使用 变量名Ref 命名方式
vue3对ref的特殊处理.jpg
watchEffect

翻译:监听副作用

**主要作用:**用于监听响应式数据的改变而出发函数

特点:如果watchEffect接收的函数中用到了响应式数据,那么该函数就会监听这些响应式数据,数据一旦发生改变就会被调用。

使用方式:(使用场景:个个函数中都可以)

import {ref,watchEffect} from "vue";
export default = {
    setup(){
        let count = ref(0);
        watchEffect(()=>{
            //该函数内的响应式数据发生变化,则就会调用该函数
            console.log(count);
        })
    }
}
生命周期函数

在vue3中,钩子函数已经变成普通的函数导出了

所以可以通过导入的方式获取使用:

import { onMounted , onUnmounted }from "vue";
//onMounted:组件挂载完毕的钩子函数
onMounted(()=>{
    ...
})

//onUnmounted:组件销毁后的钩子函数
onUnmounted(()=>{
    ...
})
computed

计算属性函数

使用

import {computed} from "vue";
//语法1:传一个对象,里面写get、set
computed({
    get(){...},
    set(data){...}
})
//语法2:传一个函数,该函数就相当于vue2中的computed使用,也就是如果里面用到的响应式数据,它会自动监听这些数据
computed(()=>{
    return ...
})

关于网页地址hash的一些操作

监听事件

onhashchange事件

window.addEventListener("hashchange",function(){
	...
})
获取hash值
location.hash
//一般获取后用正则截取需要的字符
http://127.0.0.1/#/123
const hash = location.hash.replace(/#\/?/,"");  //123

就业-----------------------------------------

vite原理

在现实工作中,因为vite 还是属于提案阶段,可能还有很多问题,所以很多公司仍然在使用 vue-cli 构建

vite: https://github.com/vitejs/vite

面试题:谈谈你对vite的理解,最好对比webpack说明?

webpack 原理图

image-20200929144416064

webpack是:

  • 入口文件中读取依赖

  • 然后针对依赖进行加载

  • 生成chunk

  • 打包生成压缩后的文件

  • 再放到服务器。

    **特点:**先压缩打包,再上传到服务器(热更新:如果更改了某部分内容,那么跟这部分内容相关的都需要重新加载,速度慢)

vite 原理图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXmvTz01-1627093459784)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20210126134548040.png)]

vite是:

  • 一开始先开发一个服务器

  • 然后根据页面加载文件(如:index.js)(注意:vite页面文件引入js的方式是 ES6模块化方式 ,也就是 type = “module”)

  • 然后读取index.js的依赖

  • 服务器根据依赖找到文件,将文件解析后返回给页面

    **特点:**vite并不是先打包再上传服务器,而是一开始先打开服务器,之后服务器根据页面的内容和依赖进行请求文件

面试题答案:
首先,vite的优势都体现在开发环境中

webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果

vite直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。
vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。
由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。
当浏览器请求某个模块时,再根据需要对模块内容进行编译

这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。
当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段

  另外,由于vite利用的是ES Module,因此在**代码中不可以使用CommonJS(因为这些代码直接发送到浏览器,浏览器压根不认识CommonJS的代码)**

效率提升

客户端渲染效率比vue2提升了1.3~2倍

SSR渲染(服务器渲染)效率比vue2提升了2~3倍

面试题:vue3的效率提升主要表现在哪些方面?

下面五个方面

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
// vue2 的静态节点
render(){
  createVNode("h1", null, "Hello World")
  // ...
}

// vue3 的静态节点
const hoisted = createVNode("h1", null, "Hello World") //直接在外面创建好,那么他就只会创建一次
function render(){
  // 直接使用 hoisted 即可
}

静态属性会被提升

<div class="user">
  {{user.name}}
</div>
//静态属性提升
const hoisted = { class: "user" }

function render(){
  createVNode("div", hoisted, user.name)
  // ...
}
预字符串化
<div class="menu-bar-container">
  <div class="logo">
    <h1>logo</h1>
  </div>
  <ul class="nav">
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
  </ul>
  <div class="user">
    <span>{{ user.name }}</span>
  </div>
</div>

当编译器遇到大量连续的静态内容(静态内容:指的是没有使用动态数据的标签,写死的),会直接将其编译为一个普通字符串节点

const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")

vue2中,它不管是不是静态节点,都会渲染成虚拟节点树

image-20200929170205828

vue3中,它会监测到哪些是静态节点,哪些是动态节点,然后根据不同节点进行渲染(静态节点生成字符串,动态节点生成虚拟节点树)

image-20200929170304873
缓存事件处理函数

对处理事件函数进行缓存(因为很多时候,事件处理函数都是写完就不会改变的)

<button @click="count++">plus</button>
// vue2
render(ctx){
  return createVNode("button", {
    onClick: function($event){
      ctx.count++;
    }
  })
}

// vue3
render(ctx, _cache){
  return createVNode("button", {
    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//它会通过cache进行对事件函数缓存
  })
}
Block Tree

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

vue3中,BlockTree会对节点进行标记,标记哪些是动态节点,哪些是静态节点,那么到时候就知道哪些是静态节点(不需要对比)

<form>
  <div>
    <label>账号:</label>
    <input v-model="user.loginId" />
  </div>
  <div>
    <label>密码:</label>
    <input v-model="user.loginPwd" />
  </div>
</form>

vue2的对比新旧树:

image-20200929172002761

vue3的BlockTree处理:

image-20210126143720114
PatchFlag

vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

vue3中,PatchFlage能够辨别出哪些信息是动态的内容,哪些是静态的内容,并且做上标记,供对比的时候处理。

<div class="user" data-id="1" title="user name">
  {{user.name}}
</div>

image-20200929172805674

API和数据响应式的变化

面试题1:为什么vue3中去掉了vue构造函数?

面试题2(常考):谈谈你对vue3数据响应式的理解?

去掉了Vue构造函数

在过去,如果遇到一个页面有多个vue应用时,往往会遇到一些问题

<!-- vue2 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
  Vue.use(...); // 此代码会影响所有的vue应用
  Vue.mixin(...); // 此代码会影响所有的vue应用
  Vue.component(...); // 此代码会影响所有的vue应用
                
	new Vue({
    // 配置
  }).$mount("#app1")
  
  new Vue({
    // 配置
  }).$mount("#app2")
</script>

vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用

<!-- vue3 -->
<div id="app1"></div>
<div id="app2"></div>
<script>  
	createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
  createApp(根组件).mount("#app2")
</script>

更多vue应用的api:https://v3.vuejs.org/api/application-api.html

组件实例中的API

vue3中,组件实例是一个Proxy,它仅提供了下列成员,功能和vue2一样

属性:https://v3.vuejs.org/api/instance-properties.html

方法:https://v3.vuejs.org/api/instance-methods.html

对比数据响应式

vue2和vue3均在相同的生命周期(beforeCreate之后,created之前)完成数据响应式,但做法不一样

vue2中,它会将所有数据都遍历一遍,并且如果后面的数据需要新增加一些属性,还得需要$set等方法的辅助(显得比较麻烦)

vue3中,它把这些操作都交给了代理(proxy),并且proxy是动态返回的,也就是你需要用到哪些数据,他只会返回给你对应的数据,其他数据并不做遍历。

image-20201014155433311

面试题参考答案

面试题1:为什么vue3中去掉了vue构造函数?

vue2的全局构造函数带来了诸多问题:
1. 调用构造函数的静态方法会对所有vue应用生效,不利于隔离不同应用
2. vue2的构造函数集成了太多功能,不利于tree shaking,vue3把这些功能使用普通函数导出,能够充分利用tree shaking优化打包体积
3. vue2没有把组件实例和vue应用两个概念区分开,在vue2中,通过new Vue创建的对象,既是一个vue应用,同时又是一个特殊的vue组件。vue3中,把两个概念区别开来,通过createApp创建的对象,是一个vue应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。

面试题2(常考):谈谈你对vue3数据响应式的理解

vue3不再使用Object.defineProperty的方式定义完成数据响应式,而是使用Proxy。
除了Proxy本身效率比Object.defineProperty更高之外,由于不必递归遍历所有属性,而是直接得到一个Proxy。所以在vue3中,对数据的访问是动态的,当访问某个属性的时候,再动态的获取和设置,这就极大的提升了在组件初始阶段的效率。
同时,由于Proxy可以监控到成员的新增和删除,因此,在vue3中新增成员、删除成员、索引访问等均可以触发重新渲染,而这些在vue2中是难以做到的。

模板中的变化

v-model

组件之间的数据传递

vue2 比较让人诟病的一点就是提供了两种双向绑定v-model.sync

在**vue3中**,去掉了.sync修饰符只需要使用v-model进行双向绑定即可

为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改:

  • 对自定义组件使用v-model指令并且没有传参数时绑定的默认属性名由原来的**value变为modelValue
    默认事件名由原来的
    input变为update:modelValue**

    <!-- App.vue组件中 -->
    
    <!-- vue2 -->
    <ChildComponent :value="pageTitle" @input="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" />
    
    <!-- vue3 -->
    <ChildComponent
      :modelValue="pageTitle"
      @update:modelValue="pageTitle = $event"
    />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" /> <!-- 这里把pageTitle的值传递给了modelValue -->
    <script>
    	export default = {
            data(){
                return{
                    pageTitle:false
                }
            }
        }
    </script>
    
    <!-- ChildComponent组件内的接收 -->
    <template>
        <div :class = "{cheaked:modelValue}">
            123
        </div>
    	<button @click = handleCheaked>
            点击更改modelValue
        </button>
    </template>
    <script>
    export default = {
    	props:{
    		modelValue:Boolean, //接收App.vue组件传递过来的值(pageTitle的值),此时cheaked类名还不能加上去
    	},
        setup(props,ctx){
            const handleCheaked = ()=>{
           //触发 ChildComponent 的 update:modelValue 事件,并传参,修改了pageTitle从而修改App.vue的modelValue的值
           //App.vue的modelValue改变,组件中的modelValue也改变,div的cheaked类名就加上去了
                ctx.emit("update:modelValue",!props.modelValue)
    		}
        }
    }
    </script>
    
  • 去掉了.sync修饰符,它原本的功能由v-model的参数替代
    如下: v-model : title = “pageTitle”
    那么组件props中接收时,就不是默认的 modelValue了,而是参数 title

    <!-- vue2 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent :title.sync="pageTitle" />
    
    <!-- vue3 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model:title="pageTitle" />
    
  • model配置被移除

  • 允许自定义v-model修饰符

    主要作用就是:监听组件是否使用了自定义修饰符,通过这个自定义修饰符就能对数据做出想要的处理

    vue2 无此功能

    如下图,表示当使用这种修饰符的方式时,他就会自动向组件传递一个属性

    默认情况下为 : modelModifiers : { cap : true}

    如果传递了参数则是: 参数Modifiers : { cap : true }

    组件内需要通过 props接收,拿到后,里面的值就是修饰符,如下:值就为 {cap : true}

    有这个值后,我们就可以在 ctx.emit()调用事件函数前,对数据做一些处理

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lSFuQNfJ-1627093459788)(https://camo.githubusercontent.com/216697fc3cd13be5fede2b53e45f0fd1fba8ce9b0add09ed09a3324f53bc0ef6/687474703a2f2f6d6472732e7975616e6a696e2e746563682f696d672f32303230313030383136333032322e706e67)]

v-if v-for

v-if 的优先级 现在高于 v-for

所以,以下代码会报错:

<div v-for = "item in arr" v-if = "item.show">
    <!-- 因为vue3中,v-if的优先级比 v-for的高,就导致它无法读取到item.show ,所以会报错-->
	<span>{{ item.msg }}</span>
</div>
key
  • 当使用<template>进行v-for循环时(也就是有多个根节点时),需要把key值放到<template>中,而不是它的子元素中
    如:

    <!-- vue2 -->
    <template v-for = "item in arr">
    	<div key=1></div>
        <span key=2></span>
    </template>
    <!-- vue3 -->
    <template v-for = "item in arr" :key="item.id">
    	<div></div>
        <span></span>
    </template>
    
  • 当使用v-if v-else-if v-else分支的时候,不再需要指定key(vue2时为了防止被复用,所以需要加key值来辨别),因为vue3会自动给予每个分支一个唯一的key

    即便要手工给予key值,也必须给予每个分支唯一的key不能因为要重用分支而给予相同的 key

Fragment

vue3现在允许组件出现多个根节点

组件的变化

路由

与vue2不同的是,安装的版本是 next 版本

使用前需要安装插件 : npm i vue-router@next

使用

在vue3中,去除的东西:

  • 去除了 Router 构造函数,使用的是里面导出的函数 createRouter
  • 去除了 model (设置模式 hash / history)、baseURL(设置初始重定向位置)
//通常做法是,创建router.js文件,里面导出一个路由供main.js使用
import { createRouter,createWebHistory } from "vue-router";
import Home from "./views/Home.vue";
export default createRouter({
    history:createWebHistory("/home"), //model:history,baseURL:"/home"
    routes:[
        {path:"/home",component:Home},
        {...}
    ]
})
        
//main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from "./router.js";
//实例.use(插件): 使用插件
createApp(App).use(router).mount('#app')
异步组件

通过 vue 中的 defineAsyncComponent 函数创建异步组件

使用:

  • 传递一个函数,函数返回一个promise,一般通过 **import( )**返回异步组件promise对象
  • 传递一个对象,对象中的 loader 属性放的值就是像传递函数时的那个函数
    对象中的loadingComponent属性可以配置promise对象处于pendding时显示的组件(指:组件还在加载中)
    对象中的errorComponent属性可以配置promise对象出错时(rejected状态)显示的组件
<template>
    <async-com /> //使用异步组件
</template
<script>
import { defineAsyncComponent,h } from "vue";
import Loading from "./components/Loading.vue";
import Error from "./components/Error.vue"; //组件内没有放内容,而是设置了插槽 slot,用于接收render传递的内容

//传递函数
const asyncCom = defineAsyncComponent(()=>import("./asyncCom"));

//传递对象
const asyncCom = defineAsyncComponent({
    loader:()=>import("./asyncCom"),
    loadingComponent: Loading, //当promise状态处于 pendding时,自动显示Loading组件
    errorComponent:{           //当promise状态处于 rejected时,显示Error组件,这里使用了render方式,也可以直接放组件对象
    	render(){
    		return h(Error,"出错了"); //把 "出错了" 插到Error组件的插槽(solt)中
		}
	}
})

export default = {
    components:{
        asyncCom,
    }
}
</script>

可以封装一个生成异步组件函数

import { defineAsyncComponent,h } from "vue";
import Loading from "./components/Loading.vue";
import Error from "./components/Error.vue"; //Error组件内没有放内容,而是设置了插槽 slot,用于接收render传递的内容

//传入位置(path)返回一个异步组件
function getAsyncComponent(path){
    return defineAsyncComponent({
    loader:()=>import(path),
    loadingComponent: Loading, //当promise状态处于 pendding时,自动显示Loading组件
    errorComponent:{           //当promise状态处于 rejected时,显示Error组件,这里使用了render方式,也可以直接放组件对象
    	render(){
    		return h(Error,"出错了"); //把 "出错了" 插到Error组件的插槽(solt)中
		}
	}
})
}
render函数的改动

与vue2不同的是,render函数的需要使用的渲染函数 h, 不再是从函数中接收

而是从vue 中导出

import { h } from "vue";
Teleport组件

vue3中新增的组件,能够直接使用

作用:将组件内的元素插到 某个标签里的最后面

使用

<Teleport to = "body"> <!-- to的值填 css选择器,表示插入到哪个标签内 -->
	<div v-if = "show">
        <button @click = "show = !show">
            关闭
        </button>
    </div>
</Teleport>

进度条插件

安装:npm i nprogress

使用

//index.js
import "nprogress/nprogress.css";//获取样式
import NProgress from "nprogress";
NProgress.configure({
    trickSpeed:50,//配置进度条的速度
    showSpinner:false//禁用显示加载指针(也就是那个加载的圆圈)
})

NProgress.start();//进度条启动
NProgress.done();//直接让进度条完成

异步组件中的使用

const AsycnPage = defineAsyncComponent({ //创建一个异步页面组件(其实跟异步组件一样)
	loader:async ()=>{
		NProgress.start(); //开始进度条
		const comp = await import("./Home");
		NProgress.done(); //结束进度条
		return comp;
	}
})

reactivity API(最复杂,笔试最难)

获取响应式数据
API传入返回备注
reactive普通对象对象代理 (获取时直接点获取)深度代理对象中的所有成员
readonly普通对象 或对象代理对象代理只能读取代理对象中的成员,不可修改
refany{ value: ... }对value的访问是响应式的
如果给value的值是一个对象, 则会通过reactive函数进行代理
如果已经是对象代理,则直接使用代理
computedfunction{ value: ... }当读取value值时,会根据情况决定是否要运行函数 (查看是否有缓存 / 数据改变后再次读取value值时

返回值决定了获取方式

注意:

readonly 和 reactive 同时使用时

//使用 readonly ,里面参数放的是一个代理对象时
const state = reactive({a:1});
const readonlyState = readonly(state);
console.log(readonly === state); //false

//					    代理		 代理
//因为实际过程是: readonly --> state ---> {a : 1} ,也即是套了两层代理

ref 和 reactive 同时使用时

const state = reactive({a:1});
const stateRef = ref(state);
console.log(stateRef === state); //true

//因为stateRef认为state本身就是响应式数据的,所以stateRef 直接就等于state

应用:

  • 如果想要让一个对象变为响应式数据,可以使用**reactiveref**
  • 如果想要让一个对象的所有属性只读,使用**readonly **
  • 如果想要让一个非对象数据变为响应式数据,使用**ref**
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用**computed**

笔试题1:下面的代码输出结果是什么?

import { reactive, readonly, ref, computed } from "vue";

const state = reactive({
  firstName: "Xu Ming",
  lastName: "Deng",
});
const fullName = computed(() => {
  console.log("changed");
  return `${state.lastName}, ${state.firstName}`;
});
console.log("state ready"); //state ready
console.log("fullname is", fullName.value); //changed fullname is Deng,Xu Ming
console.log("fullname is", fullName.value); //fullname is Deng,Xu Ming
const imState = readonly(state);
console.log(imState === state); //false

const stateRef = ref(state);
console.log(stateRef.value === state); //true

state.firstName = "Cheng";  //此时并不会运行computed,需要读到 fullName.value才会运行
state.lastName = "Ji"; 

console.log(imState.firstName, imState.lastName); //Cheng  Ji
console.log("fullname is", fullName.value); //changed fullname is Ji,Cheng
console.log("fullname is", fullName.value); //fullname is Ji,Cheng

const imState2 = readonly(stateRef); 
console.log(imState2.value === stateRef.value); //false

笔试题2:按照下面的要求完成函数

import {reactive,readonly} from "vue";
function useUser(){
  // 在这里补全函数
//    const userOrigin = reactive({});
//    const user = readonly(userOrigin);
//    const setUserName = name =>{
//        userOrigin.name = name;
//    }
//    const setUserAge = age =>{
//        userOrigin.age = age;
//    }
  return {
    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
  }
}

笔试题3:按照下面的要求完成函数

import {ref,readonly} from "vue";
function useDebounce(obj, duration){
  // 在这里补全函数
    let timer = null;
    const valueOrigin = ref(obj);
    const value = readonly(obj);
    const setValue = obj =>{
        clearTimeout(timer);
        timer = setTimeout(()=>{
            valueOrigin.value = {...valueOrigin.value , ...obj};
        },duration)
    }
  return {
    value, // 这里是一个只读对象,响应式数据,默认值为参数值
    setValue // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
  }
}
监听数据变化

watchEffect

相对watch的特点就是:

  • 首先会立即执行一次
  • 会自动监听函数中用到的响应式数据(监听不到没用到的数据)
const stop = watchEffect(() => {
  // 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})

// 通过调用stop函数,会停止监听
stop(); // 停止监听

watch

使用: watch( 需要监听的数据 , 回调);

  • watch的第一个参数:需要通过函数返回数据的方式进行传递
  • watch的第一个参数:如果是 ref 的响应式数据,则可以直接传递数据
  • watch的第二个参数:是一个回调函数,监听的数据改变时触发,有两个参数,分变表示 新的值 , 旧的值
// 等效于vue2的$watch

// 监听单个数据的变化
import { watch,reactive,ref } from "vue";
const state = reactive({ count: 0 })
watch(() => state.count, (newValue, oldValue) => {
  // ...
}, options)

//如果是ref数据,则可以直接传递数据
const countRef = ref(0);
watch(countRef, (newValue, oldValue) => {
  // ...
}, options)

// 监听多个数据的变化,使用数组方式
//那么回调得到的参数也会对应的接收两个数组
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
  // ...
});

注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)

应用:除非遇到下面的场景,否则均建议选择watchEffect

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据

笔试题: 下面的代码输出结果是什么?

import { reactive, watchEffect, watch } from "vue";
const state = reactive({
  count: 0,
});
watchEffect(() => {
  console.log("watchEffect", state.count);
});
watch(
  () => state.count,
  (count, oldCount) => {
    console.log("watch", count, oldCount);
  }
);
console.log("start");
setTimeout(() => {
  console.log("time out");
  state.count++;
  state.count++;
});
//连续两次改变值的时候,由于执行速度很快,它会认为是改变了一次,最开始的一次到最后一次  0 -> 2
state.count++;
state.count++;

console.log("end");
//watchEffect 0
//start
//end
//watchEffect 2
//watch 2 0
//time out
//watchEffect 4
//watch 4 2
判断
API含义
isProxy判断某个数据是否是由reactivereadonly
isReactive判断某个数据是否是通过reactive创建的 详细:https://v3.vuejs.org/api/basic-reactivity.html#isreactive
isReadonly判断某个数据是否是通过readonly创建的
isRef判断某个数据是否是一个ref对象

使用:

import { isProxy,reactive } from "vue";
const state = reactive({a:1});
console.log(isProxy(state));//true
转换

unref

等同于:isRef(val) ? val.value : val

应用:

function useNewTodo(todos){
  todos = unref(todos);
  // ...
}

toRef

得到一个响应式对象某个属性的ref格式

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo'); // fooRef: {value: ...}

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object中返回

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs: not a proxy
{
  foo: { value: ... },
  bar: { value: ... }
}
*/

应用:(主要目的就是:方便展开使用)

setup(){
  const state1 = reactive({a:1, b:2});
  const state2 = reactive({c:3, d:4});
  return {
    ...state1, // lost reactivity
    ...state2 // lost reactivity
  }
}

setup(){
  const state1 = reactive({a:1, b:2});
  const state2 = reactive({c:3, d:4});
  return {
    ...toRefs(state1), // reactivity
    ...toRefs(state2) // reactivity
  }
}
// composition function
function usePos(){
  const pos = reactive({x:0, y:0});
  return pos;
}

setup(){
  const {x, y} = usePos(); // lost reactivity
  const {x, y} = toRefs(usePos()); // reactivity
}
降低心智负担

所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactivereadonly直接产生的数据

function usePos(){
  const pos = reactive({ x:0, y:0 });
  return toRefs(pos); //  {x: refObj, y: refObj}
}
function useBooks(){
  const books = ref([]);
  return {
    books // books is refObj
  }
}
function useLoginUser(){
  const user = readonly({
    isLogin: false,
    loginId: null
  });
  return toRefs(user); // { isLogin: refObj, loginId: refObj }  all ref is readonly
}

setup(){
  // 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
  return {
    ...usePos(),
    ...useBooks(),
    ...useLoginUser()
  }
}

Composition API

面试题:composition api相比于option api有哪些优势?

不同于reactivity api,composition api提供的函数很多是与组件深度绑定的,不能脱离组件而存在。

setup
// component
export default {
  setup(props, context){
    // 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数
    // props 是一个对象,包含了所有的组件属性值
    // context 是一个对象,提供了组件所需的上下文信息
  }
}

context对象的成员

成员类型说明
attrs对象vue2this.$attrs
slots对象vue2this.$slots
emit方法vue2this.$emit
生命周期函数
vue2 option apivue3 option apivue 3 composition api
beforeCreatebeforeCreate不再需要,代码可直接置于setup中
createdcreated不再需要,代码可直接置于setup中
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroy beforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
-renderTrackedonRenderTracked
-renderTriggeredonRenderTriggered

新增钩子函数说明:

钩子函数参数(接收的事件源)执行时机
renderTracked(适合调试)DebuggerEvent渲染vdom收集到的每一次依赖时
renderTriggeredDebuggerEvent某个依赖变化导致组件重新渲染时

DebuggerEvent上的属性:

  • target: 跟踪或触发渲染的对象
  • key: 跟踪或触发渲染的属性
  • type: 跟踪或触发渲染的方式
面试题参考答案

面试题:composition api相比于option api有哪些优势?

从两个方面回答:

  1. 为了更好的逻辑复用代码组织
  2. 更好的类型推导
有了composition api,配合reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。
相比于option api,composition api中没有了指向奇怪的this,所有的api变得更加函数式,这有利于和类型推断系统比如TS深度配合。

共享数据

vuex方案

安装vuex@4.x

两个重要变动:

  • 去掉了构造函数Vuex,而使用createStore创建仓库
  • 为了配合composition api,新增useStore函数获得仓库对象
global state

由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,轻松制造多个全局响应式数据

就是通过封装一个js文件,文件导出需要用到的数据或函数即可,如果数据不想被外部修改,那么就可以用 readonly 包装

// store/useLoginUser 提供当前登录用户的共享数据
// 以下代码仅供参考
import { reactive, readonly } from "vue";
import * as userServ from "../api/user"; // 导入api模块

// 创建默认的全局单例响应式数据,仅供该模块内部使用
const state = reactive({ user: null, loading: false });

// 对外暴露的数据是只读的,不能直接修改
// 也可以进一步使用toRefs进行封装,从而避免解构或展开后响应式丢失
export const loginUserStore = readonly(state);

//对外暴露函数
// 登录
export async function login(loginId, loginPwd) {
  state.loading = true;
  const user = await userServ.login(loginId, loginPwd);
  state.loginUser = user;
  state.loading = false;
}
// 退出
export async function loginOut() {
  state.loading = true;
  await userServ.loginOut();
  state.loading = false;
  state.loginUser = null;
}
// 恢复登录状态
export async function whoAmI() {
  state.loading = true;
  const user = await userServ.whoAmI();
  state.loading = false;
  state.loginUser = user;
}
Provide&Inject

vue2中,提供了provideinject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VdUoG80m-1627093459789)(https://camo.githubusercontent.com/e40f3619ad36455b409c74068ee29801584e7dab0f96f40322f3f4b8030d0d52/687474703a2f2f6d6472732e7975616e6a696e2e746563682f696d672f32303230313032363137333934392e706e67)]

除了兼容vue2的配置式注入,vue3composition api中添加了provideinject方法,可以在setup函数中注入和使用数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7mnHMTH-1627093459789)(https://camo.githubusercontent.com/aa092a872c541d03998533f57a3246e492e9c96664c018d98c4ea485a7b7f81d/687474703a2f2f6d6472732e7975616e6a696e2e746563682f696d672f32303230313032363137343033392e706e67)]

考虑到有些数据需要在整个vue应用中使用,vue3还在应用实例中加入了provide方法,用于提供整个应用的共享数据

creaetApp(App)
  .provide("foo", ref(1))
  .provide("bar", ref(2))
	.mount("#app");

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Le4t5MDt-1627093459789)(https://camo.githubusercontent.com/03e730cea3e471d094f80a7c4a0242a15a2476220ea96aa8dca9cb8196cb1c8a/687474703a2f2f6d6472732e7975616e6a696e2e746563682f696d672f32303230313032363137343631312e706e67)]

因此,我们可以利用这一点,在整个vue应用中提供共享数据

// store/useLoginUser 提供当前登录用户的共享数据
// 以下代码仅供参考
import { readonly, reactive, inject } from "vue";
const key = Symbol(); // Provide的key

// 在传入的vue应用实例中提供数据
export function provideStore(app) {
  // 创建默认的响应式数据
  const state = reactive({ user: null, loading: false });
  // 登录
  async function login(loginId, loginPwd) {
    state.loading = true;
    const user = await userServ.login(loginId, loginPwd);
    state.loginUser = user;
    state.loading = false;
  }
  // 退出
  async function loginOut() {
    state.loading = true;
    await userServ.loginOut();
    state.loading = false;
    state.loginUser = null;
  }
  // 恢复登录状态
  async function whoAmI() {
    state.loading = true;
    const user = await userServ.whoAmI();
    state.loading = false;
    state.loginUser = user;
  }
  // 提供全局数据
  app.provide(key, {
    state: readonly(state), // 对外只读
    login,
    loginOut,
    whoAmI,
  });
}

export function useStore(defaultValue = null) {
  return inject(key, defaultValue);
}

// store/index
// 应用所有store
import { provideStore as provideLoginUserStore } from "./useLoginUser";
// 继续导入其他共享数据模块...
// import { provideStore as provideNewsStore } from "./useNews"

// 提供统一的数据注入接口
export default function provideStore(app) {
  provideLoginUserStore(app);
  // 继续注入其他共享数据
  // provideNewsStore(app);
}

// main.js
import { createApp } from "vue";
import provideStore from "./store";
const app = createApp(App);
provideStore(app);
app.mount("#app");
对比
vuex(适合大型项目)global state(适合中小型项目)Provide&Inject(适合中小型项目)
组件数据共享
可否脱离组件
调试工具
状态树自行决定自行决定
ng = true;
const user = await userServ.whoAmI();
state.loading = false;
state.loginUser = user;

}
// 提供全局数据
app.provide(key, {
state: readonly(state), // 对外只读
login,
loginOut,
whoAmI,
});
}

export function useStore(defaultValue = null) {
return inject(key, defaultValue);
}

// store/index
// 应用所有store
import { provideStore as provideLoginUserStore } from “./useLoginUser”;
// 继续导入其他共享数据模块…
// import { provideStore as provideNewsStore } from “./useNews”

// 提供统一的数据注入接口
export default function provideStore(app) {
provideLoginUserStore(app);
// 继续注入其他共享数据
// provideNewsStore(app);
}

// main.js
import { createApp } from “vue”;
import provideStore from “./store”;
const app = createApp(App);
provideStore(app);
app.mount(“#app”);


#### 对比

|              | vuex(适合大型项目) | global state(适合中小型项目) | Provide&Inject(适合中小型项目) |
| ------------ | -------------------- | ------------------------------ | -------------------------------- |
| 组件数据共享 | ✅                    | ✅                              | ✅                                |
| 可否脱离组件 | ✅                    | ✅                              | ❌                                |
| 调试工具     | ✅                    | ❌                              | ✅                                |
| 状态树       | ✅                    | 自行决定                       | 自行决定                         |
| 量级         | 重                   | 轻                             | 轻                               |
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值