Skip to content

Vue3

官方文档: https://cn.vuejs.org/

特性

  • 也称组合 API
  • setup
  • ref 和 reactive
  • computed 和 watch
  • 新的生命周期函数
  • 自定义hooks函数
  • 区别:相当于ES5和ES6

安装脚手架

tsx
vue create first
安装时选择 Babel TS Linter 一路回车

Vue3脚手架启动

基本使用

  1. 修改配置文件:vue.config.js (与原来相同)
js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
	transpileDependencies: true,
	lintOnSave:false,// 关闭语法检查
	devServer:{
		open:true,// 自动打开浏览器
		port:80,// 指定端口号
		host:"127.0.0.1",// 指定host
	},
    // 关闭编译时的特征标志
    chainWebpack: (config) => {
        config.plugin('define').tap((definitions) => {
            Object.assign(definitions[0], {
                __VUE_OPTIONS_API__: 'true',
                __VUE_PROD_DEVTOOLS__: 'false',
                __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
            })
            return definitions
        })
    }
})
  1. 创建应用实例并挂载:src->main.ts

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例app

应用实例必须在调用了 .mount() 方法后才会渲染出来。

该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串

js
// 在Vue3中不需要实例化Vue
// 可以通过createApp函数创建单页面应用的一个对象,类型是一个函数。
import {createApp} from "vue";
// console.log(createApp);// 是一个函数
// 引入根组件App,根组件不允许省略扩展名.vue(main.ts中不支持省略.vue)
import App from "@/App.vue";
// 运行该函数必须指定一个根组件。
// 该函数返回的是一个对象,称该对象为app对象。

// 方式一
// const app = createApp(App);
// console.log(app);// 输出的app对象只是一个普通对象
// 指定挂载的容器。(将id为app的标签作为应用的容器)
// app.mount("#app");

// 方式二
createApp(App).mount("#app");
  1. 入口APP组件:src->App.vue
vue
<template>
    <!-- Vue3中可以拥有多个根元素   -->
    <h3>App组件1</h3>
    <h3>App组件2</h3>
    <h3>App组件3</h3>
</template>

<script>
import {defineComponent} from "vue";
// defineComponent是一个在定义 Vue 组件时提供对象类型推导的辅助函数。
// defineComponent作用对对象类型的推断辅助。由于ts拥有推断机制,所以也可以省略。
export default defineComponent({
    name:"App"
})
</script>

<style scoped>

</style>
  1. shims-vue.d.ts
tsx
// 这个是ts的类型声明文件

应用配置

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误

js
app.config.errorHandler = (err) => {
  /* 处理错误 */
}

应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:

js
app.component('TodoDeleteButton', TodoDeleteButton)

多应用实例

应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。

js
const app1 = createApp({
  /* ... */
})
app1.mount('#container-1')

const app2 = createApp({
  /* ... */
})
app2.mount('#container-2')

如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。

Vue3中使用Vue2语法

  1. 入口APP文件:src->App.vue
vue
<template>
    <h3>App组件</h3>
    <p>{{ num }}</p>
    <p>{{ arr }}</p>
    <button @click="num++">{{ num }}</button>
    <button @click="changeNum">{{ num }}</button>
    <button @click="changeNum2(100)">{{ num }}</button>
    <p>{{sum}}</p>
    <hr/>
    <button @click="isShow=!isShow">{{isShow}}</button>
    <Child v-if="isShow"></Child>
    <p v-for="item in arr" :key="item">{{item}}</p>
</template>

<script>
// 支持Vue2的基本语法,不支持销毁生命周期钩子函数
import Child from "@/components/Child";
const arr = [1, 2, 3, 4, 5];
export default {
    name: "App",
    components: {Child},
    data() {
        return {
            num: 1,
            arr,
            isShow:true
        }
    },
    computed:{
        sum(){
            // return this.arr.length;
            return this.arr.reduce((sum,item)=>{
                sum+=item;
                return sum;
            },0)
        }
    },
    methods: {
        changeNum() {
            // 1  false
            // console.log(this.num,this.arr===arr);
            this.num++;
            this.arr.push(this.arr.length + 1);
        },
        changeNum2(n) {
            this.num += n;
        }
    },
    // 挂载阶段
    beforeCreate() {
        console.log("1-beforeCreate");
    },
    created(){
        console.log("2-created");
    },
    beforeMount() {
        console.log("3-beforeMount");
    },
    mounted(){
        console.log("4-mounted");
    },
    // 更新
    beforeUpdate() {
        console.log("1-beforeUpdate");
    },
    updated() {
        console.log("2-updated");
    }
}
</script>

<style scoped>

</style>
  1. 组件:src->components->Child.vue
vue
<template>
    <h3>Child</h3>
</template>

<script>
export default {
    name: "Child",
    beforeDestroy() {
        console.log("1-beforeDestroy");
    },
    destroyed() {
        console.log("2-destroyed");
    }
}
</script>

<style scoped>

</style>

组合式API

setup函数

setup是组合API的入口的钩子函数,在beforeCreate之前调用。

setup是一个钩子函数。

基本使用

js
1. setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined
2. 你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。
3. setup() 应该同步地返回一个对象。
4. 唯一可以使用 async setup() 的情况是,该组件是 Suspense 组件的后裔。
5. 在语法糖中可以直接定义数据,直接定义的数据可以在模板中直接使用。
注意:如果语法糖写法与过去的写法同时出现(出现两个script),语法糖写法优先

访问 Props

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新

js
export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}
请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。
因此我们推荐通过 props.xxx 的形式来使用其中的 props
-----
如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:

import { toRefs, toRef } from 'vue'

export default {
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props)
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value)

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, 'title')
  }
}

Setup 上下文

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值

js
export default {
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

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

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }
}
该上下文对象是非响应式的,可以安全地解构
export default {
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

注意

js
attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。
这意味着你应当避免解构它们,并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。
此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。
如果你想要基于 attrs 或 slots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑

暴露公共属性

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

js
export default {
  setup(props, { expose }) {
    // 让组件实例处于 “关闭状态”
    // 即不向父组件暴露任何东西
    expose()

    const publicCount = ref(0)
    const privateCount = ref(0)
    // 有选择地暴露局部状态
    expose({ count: publicCount })
  }
}

与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

js
import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return () => h('div', count.value)
  }
}

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。

我们可以通过调用 expose() 解决这个问题:

js
import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}

此时父组件可以通过模板引用来访问这个 increment 方法。

使用案例

通过选项时API创建Vue3

vue
<template>
    <h3>App</h3>
    <p>userName:{{userName}}</p>
    <p>age:{{age}}</p>
	
    <button @click="changeAge">changeAge{{age}}</button>
</template>
<script>
    import {defineComponent} from "vue";

    export default defineComponent({
        beforeCreate() {
            console.log("beforeCreate");
        },
        // 1- setup是一个钩子函数。
        // 2- 在beforeCreate之前执行。
        // 3- this指向的undefined.
        // 4- setup如果要使用return进行返回,要求必须是一个对象
        // 5- setup不允许设置为async函数。
        // 6- 返回对象的属性可以直接被模板调用
        // 7- 如果data与setup拥有相同名的数据,那么以setup为准
        // 8- data中的数据是响应式的,而setup中的数据不是响应式。
        data(){
            return{
                userName:"Fusang",
                age:18
            }
    	},
        setup(){
            console.log("setup->",this); //undefined
            return{
                userName:"Marrin",
                age:26,
                changeAge(){
                    // Proxy对象中存在的是data中返回的 userName:"Fusang",age:18,changeAge
                    console.log("setup->return->", this) // Proxy对象
                }
            }
        }
    })
</script>
<style>

</style>

setup 标签语法糖

使用 script setup 标签简化代码

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

const count = ref(0)

function increment() {
  	count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

说明

js
<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用,不需要return。
你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数——它自然可以访问与它一起声明的所有内容。
如果语法糖写法与过去的写法同时出现(出现两个script),语法糖写法优先

使用案例

vue
<template>
    <h3>setup语法糖</h3>
    <p>{{num}}</p>
    <button @click="changeNum">修改num,修改不了,因为直接定义的数据不支持响应式</button>
    <hr/>
    <p @click="count++">{{count}}</p>
    <button @click="setCount">修改count</button>
    <hr/>
    <p>{{obj.userName}}</p>
    <button @click="obj.userName+='!'">修改obj->userName</button>
</template>

<script setup lang="ts">
    // 之前的写法可以将Vue2语法与Vue3语法结合使用。如果使用语法糖只支持Vue3
    // 语法糖即是在script标签中增加setup
    // 之前要在setup函数中定义数据,需要返回。
    // 在语法糖中可以直接定义数据,直接定义的数据可以在模板中直接使用。
    // 注意:如果语法糖写法与过去的写法同时出现(出现两个script),语法糖写法优先
    import {reactive, ref} from "vue";

    let num = 100;
    // 配合ref支持响应式
    let count = ref(9);
    let obj = reactive({
        userName:"zhangsan"
    })
    // 直接定义的基本数据类型不支持响应式
    const changeNum = function(){
        num++;
    }
    const setCount = function(){
        count.value+=2;
    }
</script>

<script lang="ts">
export default {
    setup() {
        return {
            num: 102
        }
    }
}
</script>

ref函数

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

在组合式 API 中,推荐使用ref()函数来声明响应式状态

特性

  1. 可以通过ref实现数据的响应式。

  2. ref需要通过import {ref} from "vue"导入

  3. ref是一个函数

  4. ref的参数可以是任意类型,同时也是ref函数返回的RefImpl的实例对象的value属性值

  5. ref的返回值是一个拥有value属性的RefImpl的实例对象

  6. ref在setup中调用,其调用结果作为setup返回对象的属性

  7. setup返回的对象中的属性如果是RefImpl的实例,在模版中可以直接书写实例

  8. 在Template模板中事件监听中可以直接操作RefImpl对象的属性,在script中必须要使用.value

  9. 如果ref接收的是一个非原始类型(对象),那么其响应式原理底层用的是reactive

注意

js
setup返回的对象中的属性值如果是RefImpl的实例,那么在模块中可以直接被解包。

解包:可以不需要写 RefImpl实例.value,可以直接书写实例即可。

示例:
const count = ref(0)
// 比如实例为count,在模板中可以直接写count
<div>{{ count }}</div>
// 在setup中不会被解包,必须要写.value
console.log(count.value)

基本使用

js
import { ref } from 'vue'

const count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

js
const count = ref(0)

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

count.value++
console.log(count.value) // 1

要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:

js
import { ref } from 'vue'

export default {
  // `setup` 是一个特殊的钩子,专门用于组合式 API。
  setup() {
    const count = ref(0)

    // 将 ref 暴露给模板
    return {
      count
    }
  }
}
vue
<div>{{ count }}</div>

注意

在模板中使用 ref 时,我们不需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包

你也可以直接在事件监听器中改变一个 ref

js
<button @click="count++">
  {{ count }}
</button>

暴漏方法

对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开

js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }

    // 不要忘记同时暴露 increment 函数
    return {
      count,
      increment
    }
  }
}

然后,暴露的方法可以被用作事件监听器

js
<button @click="increment">
  {{ count }}
</button>

响应式原理-官方

你可能会好奇:为什么我们需要使用带有 .value 的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。

当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。

在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

该 .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:

js
// 伪代码,不是真正的实现
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用

深层响应式-官方

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到

js
import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。

也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

响应式原理

当传入的参数是一个直接量时,ref实现响应式的原理是调用Object.defineProperty函数

Object.defineProperty示例

js
// 定义一个对象
const obj = {
    userName:"zhangsan"
};
let value = obj.userName;
// 1- defineProperty是属性Object对象下的函数。
// 2- 接收三个参数:第一个参数是操作的对象,第二个参数是拦截的属性名字,第三个参数是描述对象。
// 对对象中的属性进行拦截处理
Object.defineProperty(obj,"userName",{
    // 获取时userName时调用,返回值即为返回的结果。
    get(){
    	return "《"+value+"》";
	},
    // 设置userName时调用,接收的参数即是最新赋予的值。
    set(v){
        console.log("set",v);
        value = v;
    }
});
obj.userName = "lisi";
console.log(obj.userName);

当传入的参数是一个对象时,使用的原理是JS中内置的构造函数:Proxy

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Proxy示例1
js
<script>
const target = {
  message1: "hello",
  message2: "everyone",
};

const handler = {
  get(target, prop, receiver) {
    // 返回值即为返回的结果
    return "world";
  },
};
// 创建proxy实例 Proxy构造函数的参数 :第一个参数是一个对象(目标对象),第二个参数是描述对象
const proxy = new Proxy(target, handler);

console.log(proxy.message1); // world
console.log(proxy.message2); // world
<script>
Proxy示例2
js
<script>
// 定义一个对象
	let obj = {
		userName:"zhangsan"
	};
// 创建proxy实例 p
// Proxy构造函数的参数 :第一个参数是一个对象(目标对象),第二个参数是描述对象
// p是Proxy实例,相当于对obj进行了一个深度复制。
// 所有对obj的操作可以通过p来进行。即是代理。
const p = new Proxy(obj,{
		// 当获取p的属性时执行,不是获取obj的属性时执行
		// 第一个参数是目标对象 obj
		// 第二个参数是操作的属性名
		// 第三个参数是Proxy实例 p
		get(target,key,proxy){
			console.log("proxy->get->userName",target===obj);// true
			console.log("proxy->get->key",key);// userName
			console.log("proxy->get->proxy",proxy===p);// true
			return "《"+target[key]+"》";
		},
		// 当修改p属性时执行。不是设置obj的属性时执行
		// 第一个参数是目标对象target
		// 第二个参数是操作的属性名key
		// 第三个参数是要修改的值
		// 第四个参数是Proxy实例p
		set(target,key,newValue,proxy){
			console.log("proxy-set->target",target===obj);// true
			console.log("proxy-set->key",key);// userName
			console.log("proxy-set->newValue",newValue);// lisi
			console.log("proxy-set->proxy",proxy===p);// true
			target[key] = newValue;
		}
	});
	console.log(p.userName);// 《zhangsan》原因:获取p执行get
	console.log(obj.userName);// zhangsan 原因:获取obj不执行get
	p.userName = "lisi"; 
	console.log(p.userName);// 《lisi》
	console.log(obj.userName);// lisi
<script>

实现响应式

ref的特性

ref()是一个函数

ref()参数可以接收任意类型的数据

ref()的返回值可以得到一个RefImpl实例

RefImpl实例中拥有属性value

若ref接收的参数是一个基本数据类型是通过defineProperty实现的响应式

若ref接收的参数是一个引用类型,那么返回的RefImpl实例中的value值是proxy对象

ref响应式代码实现方式1

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h3></h3>
    <p></p>
    <button>点我</button>
</body>
<script>
    
    const btn = document.querySelector("button");
    const p = document.querySelector("p");
    const h3 = document.querySelector("h3");

    const Qua = function (value) {
        // 若ref接收的参数的数据类型是一个对象
        if (typeof value === "object") {
            //在Qua的实例上增加一个value属性,类型是一个对象类型的Proxy
            this.value = new Proxy(value, {
                get(target, key) {
                    return target[key];
                },
                set(target, key, newValue) {
                    // 更新视图
                    target[key] = p.innerText = newValue;
                }
            })
            // 若ref接收的参数的数据类型是一个基本数据类型
        } else {
            // 把value定义为私有变量
            let _value = value;
            // 使用defineProperty函数实现响应式
            Object.defineProperty(this, "value", {
                get() {
                    return _value;
                },
                set(v) {
                    _value = h3.innerText= v;
                }
            })
        }
    }

    // 创建一个qua函数
    const qua = function (value) {
        return new Qua(value);
    }
    // 创建一个qua对象
    const count = qua(666);
    const han = qua({
            userName: "hanser"
    })
    // 修改qua对象
    btn.onclick = function () {
        count.value++;
        han.value.userName += "!";
    }
    // 绑定到视图上
    h3.innerText = count.value;
    p.innerText = han.value.userName;
</script>
</html>

ref响应式代码方式2

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h3></h3>
    <p></p>
    <button>点我</button>
</body>
<script>
    
    const btn = document.querySelector("button");
    const p = document.querySelector("p");
    const h3 = document.querySelector("h3");

    // 创建一个函数叫做reactive返回一个Proxy类型的对象
    function reactive(value){
        return new Proxy(value, {
            get(target, key) {
                return target[key];
            },
            set(target, key, newValue) {
                target[key] = p.innerText = newValue;
            }
        })
    }
    // 创建一个Qua对应RefImpl的构造函数
    const Qua = function (value) {
        if(typeof value === 'object'){
            // 在实例上增加一个value属性,类型是一个对象类型的Proxy
            this.value = reactive(value); 
            console.log(this) // 这里的this指向 Qua的实例 qua
        }else{
            // 使用defineProperty函数对Qua的实例进行拦截,同时在实例上增加一个"value"属性,属性值是Qua类型的实例
            // let _value = value
            let obj = Object.defineProperty(this, "value", {
                get() {
                    return value;
                },
                set(newValue) {
                    // 更新视图
                    value = h3.innerText = newValue;
                }
            })
            
        }
    }
    // 创建一个qua函数对应ref函数
    const qua = function (value) {
        return new Qua(value);
    }
    // 创建一个qua对象对应ref对象
    const count = qua(666);
    let han = qua({
        age:18,
        userName:"yousa"
    })
    // 修改传入ref的参数
    btn.onclick = function () {
        count.value++;
        // 输出han对象
        console.log(han) // 这里的han是一个Qua的实例
        console.log(count) // 这里的han是一个Qua的实例
        console.log(han.value) // 这里的han.value是一个对象类型的Proxy
        console.log(count.value) // 这里的count.value是一个基本数据类型 667
        // 更改han
        han.value.age += 1;
        // 更改han的value属性为一个对象
        han.value = {
            userName: "Hanser",
            age: 23,
        }
        console.log(han.value); // 这里的han.value是一个一般的对象,不是一个Proxy对象了
    }
    // 输出值
    h3.innerText = count.value;
    p.innerText = han.value.age;
</script>
</html>

ref函数使用案例

vue
<template>
    <h3>App</h3>
    <p>count:{{count}}</p>
    <button @click="suibian">{{count}}</button>
    <hr/>
    <p>{{obj}}</p>
    <button @click="obj.userName+='!'">更改userName</button>
    <button @click="changeAge">更改age</button>
</template>
<script lang="ts">
    import {defineComponent,ref} from "vue";

    export default defineComponent({
        setup(){
            // setup是组合API的入口
            // 1- 可以通过ref实现数据的响应式。
            // 2- ref需要通过import {ref} from "vue"导入
            // 3- ref是一个函数。
            // 4- 返回的是一个拥有value属性的RefImpl的实例对象。
            // 5- ref接收的值即是ref函数返回的RefImpl的实例对象的value属性值。
            // 6- 常见操作:ref在setup中调用,其调用结果作为setup返回对象的属性。
            // 7- setup返回的对象中的属性值如果是RefImpl的实例,那么在模块中可以直接被解包。
            //    解包:可以不需要写 RefImpl实例.value,可以直接书写实例即可。
            //         比如实例为count,在模板中可以直接写count
            //    在setup中不会被解包,必须要写.value
            // 8- ref可以接收任意类型的数据,并实现响应式。
            // 9- 如果ref接收的是一个非原始类型(对象),那么其底层用的是reactive.(下午详聊)
            // 10- 在模板中事件监听中可以直接操作refImpl属性,在script中必须要使用.value
            
            const num = ref(); // num指向的是一个RefImpl对象,对象中有value属性,对应就是ref的参数
            console.log(num.value);// undefined

            let count = ref(100);
            
            let obj = ref({
                userName:"zhangsan",
                age:12
            })
            
            console.log(count.value);
            return {
                count,
                obj,
                suibian(){
                    this.count = 200;// 可以实现响应式,this指向Proxy对象,对象中存在setup return的属性。(不推荐这种写法)
                    count.value = 200;// 可以实现响应式,count指向RefImpl对象,对象中存在属性名为value的ref参数
                },
                changeAge(){
                    obj.value.age+=10;
                    // 基本数据类型-ref接收的类型
                    console.log("count",count);
                    // 引用类型-ref接收的类型
                    console.log("obj",obj)
                }
            }
        }
    })
</script>

ref和普通元素结合使用

  1. 在标签中作ref属性,其值为ref对象。那么当组件挂载完成后得到的value值为真实DOM元素对象。

  2. ref的属性值userName可以作为DOM元素的对象,可以用于获取真实DOM元素。

js
<template>
    <div>
        <h3 ref="h3Ref">ref</h3>
        <input value="100" ref="userName" type="text">
    </div>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
// 定义ref对象
const userName = ref<HTMLInputElement | number>();
const h3Ref = ref();
// 与普通标签结合使用:在标签中作用ref属性,其值为元素对象。

// 还未实现挂载,值为初始的1
console.log(userName.value); // 1
// 挂载完成后 真实DOM元素对象中的值和ref对象的值相同
onMounted(()=>{
    console.log("mounted",userName.value===document.querySelector("input")); // true
    console.log("mounted",(document.querySelector("input") as  HTMLInputElement).value); // 挂载完成后真实DOM元素对象的值为 100
    console.log("mounted",(userName.value as  HTMLInputElement).value);  // 100
    console.log("mounted",h3Ref.value.innerText) // 获取真实DOM元素对象 ref
})
</script>

ref和v-for结合使用

ref与v-for结合使用(pRef.value)得到的是一个数组,数组内的元素是真实的DOM元素

js
<template>
    <div>
        <p ref="pRef" v-for="item in arr" :key="item">{{item}}</p>
    </div>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
// ref与v-for结合使用得到的是一个数组,数组内的元素是真实的DOM
let arr = [1, 2, 3, 4, 5, 6];
// 创建一个ref对象
let pRef = ref();
// 未挂载前 beforeMounted
console.log(pRef.value); // undefined
// 挂载后
onMounted(function(){
    console.log("mounted",pRef.value) // Proxy对象,对象中的Target属性中是一个数组,每个数组元素是DOM元素对象。
    // 隔行变色
    for(let index = 0;index<pRef.value.length;index++){
        if(index % 2 === 1){
            pRef.value[index].style.color="red";
        }else  pRef.value[index].style.color="green";
    }
})
</script>

ref和组件结合使用

ref与组件结合使用,childRef.value可以获取组件proxy实例

如果要在父组件使用子组件的数据状态,必须在子组件中暴露数据。

js
在Vue3中,子组件不需要显式地在模板中使用components标签进行注册

在Vue3中,组件的注册和使用方式与Vue2有所不同。
特别是在使用组合式API(Composition API)时,子组件可以通过import语句直接引入,而无需使用components选项进行注册。
这意味着,当使用组合式API时,可以在模板中直接使用这些已引入的组件,而无需在setup函数或defineComponent函数中进行额外的注册。
这种变化使得代码更加简洁和模块化,同时也减少了模板和逻辑代码之间的耦合
  1. 父文件:src->App.vue
js
<template>
    <h3>与组件结合使用</h3>
    <button @click="childRef.setCount(200)">更改子组件中的count</button>
    <hr/>
    <child ref="childRef"/>

</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import Child from "./components/Child.vue";
// ref与组件结合使用,可以获取组件proxy实例。
const childRef = ref();

onMounted(function(){
    console.log(childRef.value) // Proxy对象,Proxy对象的Target属性中是,组件的实例。
})
</script>
  1. 子组件:src->components->Child.vue
vue
<template>
    <div>Child</div>
    <p @click="setCount(50)">count:{{count}}</p>
</template>

<script setup lang="ts">
   import {ref} from "vue";

   let a = 1;
   let count = ref(100);
   let setCount = function(n:number){
       count.value = n;
   }
   // 如果要在组件外部使用当前组件的数据状态,必须暴露。
   defineExpose({a,count,setCount})
</script>

<style scoped>

</style>

defineExpose()

如果要在组件外部使用当前组件的数据状态,必须暴露数据

js
import {ref} from "vue";

   let a = 1;
   let count = ref(100);
   let setCount = function(n:number){
       count.value = n;
   }
defineExpose({a,count,setCount})

如果要在组件外部使用当前组件的数据状态,必须暴露

reactive函数

返回一个对象的响应式代理。

特性

  1. reactive也是来自于vue
  2. reactive是一个函数
  3. reactive接收的是一个对象。
  4. reactive返回的是一个Proxy对象。
  5. 在setup中声明并返回,在模板中可以直接使用,在script中可以对其进行修改并实现响应式。

创建一个响应式对象

js
const obj = reactive({ count: 0 })
obj.count++

ref 对象作为属性,ref会自动解包

js
const count = ref(1)
const obj = reactive({ count })

// ref 会被解包
console.log(obj.count === count.value) // true

// 会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

注意当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包

js
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)

将一个 ref 赋值给一个 reactive 属性时,该 ref 会被自动解包

js
const count = ref(1)
const obj = reactive({})

obj.count = count

console.log(obj.count) // 1
console.log(obj.count === count.value) // true

reactive响应式原理

伪代码

html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h3></h3>
    <p></p>
    <button>点我</button>
</body>
<script>
    const btn = document.querySelector("button");
    const h3 = document.querySelector("h3");
    // 1- reactive是一个函数
    // 2- 返回一个Proxy实例
    // 3- 接收的是一个对象

    // 创建一个函数
    function reactive(value) {
        // 返回一个Proxy实例
        return new Proxy(value, {
            get(target, key) {
                return target[key];
            },
            set(target, key, newValue) {
                h3.innerText = target[key] = newValue;
            }
        })
    }
    const obj = reactive({
        age: 100
    })
    h3.innerText = obj.age;
    btn.onclick = function () {
        obj.age++;
        console.log(obj.age);
    }
</script>

</html>

使用案例

html
<template>
    <h3>reactive组合API</h3>
    <button @click="count++">{{count}}</button>
    <hr/>
    <p>{{obj.userName}}</p>
    <p>{{obj.age}}</p>
    <button @click="obj.userName+='!'">修改userName</button>
    <button @click="obj.age++">修改age</button>
    <button @click="objHandler">修改userName以及age</button>
</template>
<script lang="ts">
    import {defineComponent,ref,reactive} from "vue";
    // reactive也可以实现数据的响应式。
    // 1- reactive也是来自于vue
    // 2- reactive是一个函数
    // 3- reactive接收的是一个对象。
    // 4- reactive返回的是一个Proxy实例。
    // 5- 常用:在setup中声明并返回,在模板中可以直接使用,在script中可以对其进行修改并实现响应式。
    export default defineComponent({
        name:"App",
        setup(){
            const count = ref(1);
            // setup中声明 声明reactive中return返回的函数中可以使用count和obj这些变量,这些案例就是闭包的案例
            const obj = reactive({
                userName:"zhangsan",
                age:12
            })
            return {
                count,
                // 返回
                obj,
                // 在return中写函数方法,闭包的场景vue3中可以
                objHandler(){
                    obj.userName+="!";
                    obj.age ++;
                }
            }
        }
    })
</script>

ref和reactive区别

相同点:都是函数,都是来自于vue,都可以实现响应式。

不同点:

  • 数据定义方面:
    • ref常用于声明基本数据类型
    • reactive用于声明引用类型(对象,数组)
    • 注意:ref也可以声明引用类型。ref声明的引用类型的本质是通过reactive创建的。
  • 响应原理方面:
    • ref是通过Object.defineProperty实现的(基本类型)。引用类型是通过reactive创建的。
    • reactive是通过Proxy内置构造函数实现的。
  • 使用方面:
    • ref在模块中不需要value,在JS中需要写value
    • reactive不需要value.

ref和reactive注意点

  1. 通过reactive创建了一个引用类型的数据,那么如果在使用时,直接更改其引用地址,那么就不支持响应式。
  2. 通过ref创建了数据状态,将其value值设置为引用类型,仍然是响应式。(原因:ref.value属性的值数据类型是Proxy实例)
js
通过reactive创建的数据 写成这样才是响应式:原因如上所述
const state = reactive({
    // 定义数据
    info:[]
})
vue
<template>
    <h3>App</h3>
    <p @click="num++">num:{{ num }}</p>
    <p @click="count++">ref->count:{{ count }}</p>
    <p @click="setCount(2)">ref->count:{{ count }}</p>
    <p @click="obj.age++">reactive->obj.age:{{ obj.age }}</p>
    <p @click="upObjAge">reactive->obj.age:{{ obj.age }}</p>
    <p @click="upInfoAge">ref->info.age:{{ info.age }}</p>
</template>

<script setup lang="ts">
// 注意1:通过reactive创建了一个引用类型的数据,那么如果在使用时,直接更改其引用地址,那么就不支持响应式。
// 注意2:通过ref创建了数据状态,将其value值设置为引用类型,仍然是响应式(原因:其value属性的值数据类型仍是Proxy实例)
    
// 可以通过ref,以及reactive实现响应式。
import {ref,reactive} from "vue";
// 可以渲染,但不支持响应式
let num = 100;
// ref 指定ref参数的类型 <number> 
let count = ref<number>(1);
let info = ref({
    age:90
})
// reactive obj对象是一个Proxy对象
let obj = reactive({
    age:100
});
// 指定n的类型为number
const setCount = (n:number)=> {
    count.value += n;
}
const upObjAge = function(){
    // 如果通过reactive创建了一个引用类型的数据,那么如果在使用时,
    // 更改obj为一个普通的对象,引用地址改变,不支持响应式
    obj = {
         age:200
    };
	// 更改obj为一个Proxy对象,更改其引用地址,不支持响应式
    obj = reactive({
         age:200
    })
    console.log(obj);
}
function upInfoAge(){
    // 更改ref的value值,支持响应式
    info.value.age++;
    // 更改ref的value值为引用类型,仍然支持响应式
    info.value = {
        age:87
    };
    //原因:通过ref创建了数据状态,将其value值设置为引用类型,那么在使用时其value属性的值数据类型仍是Proxy实例。仍然是响应式
    console.log(info.value)
}
</script>

<style scoped>

</style>