Vue3 setup详解

setup执行的时机

  1. 在beforeCreate之前执行(一次),此时组件对象还没创建;
  2. this是undefined,不能通过this来访问data/computed/methods/props;
  3. 其实所有的composition API相关回调函数中也都不可以;

setup的返回值

  1. 一般都返回一个对象:为模板提供数据,也就是模板中可以直接使用此对象中的所有属性/方法
  2. 返回对象中的属性会与data函数返回对象合并成为组件对象的属性
  3. 返回对象中的方法会与methods中的方法合并成功组件对象的方法
  4. 如果有重名,setup优先
  5. 注意:一般不要混合使用:methods中可以访问setup提供的属性和方法,但在setup方法中不能访问data和methods;setup不能是async函数:因为返回值不再是return的对象,而不是promise,模板看不到return对象中的属性数据

setup参数

  1. setup(props,context)/setup(props,{attrs,slots,emit})
  2. props:包含props配置声明且传入了所有属性的对象
  3. attrs:包含没有在props配置中声明的属性的对象,相当于this.$attrs
  4. slots:包含所有传入的插槽内容的对象,相当于this.$slots
  5. emit:用来分发自定义事件的函数,相当于this.$emit

演示代码

  1. 父组件
	<template>
  		<h2>App</h2>
  		<p>msg: {{msg}}</p>
  		<button @click="fn('--')">更新</button>
		<child :msg="msg" msg2="cba" @fn="fn"/>
	</template>
	<script lang="ts">
		import {
  			reactive,
  			ref,
		} from 'vue'
		import child from './child.vue'

		export default {
  			components: {
    			child
  			},

  			setup () {
    			const msg = ref('abc')
    			function fn (content: string) {
      			msg.value += content
    		}
    		return {
      			msg,
      			fn
    		}
  		}
	}
</script>
  1. 子组件
<template>
  <div>
    <h3>{{n}}</h3>
    <h3>{{m}}</h3>
	<h3>msg: {{msg}}</h3>
    <h3>msg2: {{$attrs.msg2}}</h3>
    <slot name="xxx"></slot>
    <button @click="update">更新</button>
  </div>
</template>
<script lang="ts">
	import {
  		ref,
  		defineComponent
	} from 'vue'

	export default defineComponent({
  		name: 'child',
	  	props: ['msg'],
  		emits: ['fn'], // 可选的, 声明了更利于程序员阅读, 且可以对分发的事件数据进行校验
  		data () {
    		console.log('data', this)
    		return {
      			// n: 1
    	}
  	},

  	beforeCreate () {
    	console.log('beforeCreate', this)
  	},

  	methods: {
    	// update () {
    	//   this.n++
    	//   this.m++
    // }
  },

  // setup (props, context) {
  setup (props, {attrs, emit, slots}) {
    console.log('setup', this)
    console.log(props.msg, attrs.msg2, slots, emit)

    const m = ref(2)
    const n = ref(3)

    function update () {
      // console.log('--', this)
      // this.n += 2 
      // this.m += 2
      m.value += 2
      n.value += 2

      // 分发自定义事件
      emit('fn', '++')
    }

    return {
      m,
      n,
      update,
    }
  },
})
</script>

reactive与ref细节

  1. 是vue3的composition API中最重要的响应式API
  2. ref用来处理基本类型的数据,reactive用来处理对象(递归深度响应式)
  3. 如果用ref对象/数组,内部会自动将对象/数组转换为reactive的代理对象
  4. ref内部:通过给value属性添加getter/setter来实现对数据的劫持
  5. reactive内部:通过使用Proxy来实现对对象内部所有数据的劫持,并通过Reflect操作对象内部数据
  6. ref的数据操作:在js中要.value,在模板中不需要(内部解析模板时会自动添加.value)
<template>
  <h2>App</h2>
  <p>m1: {{m1}}</p>
  <p>m2: {{m2}}</p>
  <p>m3: {{m3}}</p>
  <button @click="update">更新</button>
</template>
<script lang="ts">
import {
  reactive,
  ref
} from 'vue'

export default {

  setup () {
    const m1 = ref('abc')
    const m2 = reactive({x: 1, y: {z: 'abc'}})

    // 使用ref处理对象  ==> 对象会被自动reactive为proxy对象
    const m3 = ref({a1: 2, a2: {a3: 'abc'}})
    console.log(m1, m2, m3)
    console.log(m3.value.a2) // 也是一个proxy对象

    function update() {
      m1.value += '--'
      m2.x += 1
      m2.y.z += '++'

      m3.value = {a1: 3, a2: {a3: 'abc---'}}
      m3.value.a2.a3 += '==' // reactive对对象进行了深度数据劫持
      console.log(m3.value.a2)
    }

    return {
      m1,
      m2,
      m3,
      update
    }
  }
}
</script>

计算属性和监视属性

<template>
  <h2>App</h2>
  fistName: <input v-model="user.firstName"/><br>
  lastName: <input v-model="user.lastName"/><br>
  fullName1: <input v-model="fullName1"/><br>
  fullName2: <input v-model="fullName2"><br>
  fullName3: <input v-model="fullName3"><br>

</template>
<script lang="ts">
/*
计算属性与监视
1. computed函数: 
  与computed配置功能一致
  只有getter
  有getter和setter
2. watch函数
  与watch配置功能一致
  监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
  默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
  通过配置deep为true, 来指定深度监视
3. watchEffect函数
  不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
  默认初始时就会执行第一次, 从而可以收集需要监视的数据
  监视数据发生变化时回调
*/

import {
  reactive,
  ref,
  computed,
  watch,
  watchEffect
} from 'vue'

export default {

  setup () {
    const user = reactive({
      firstName: 'A',
      lastName: 'B'
    })

    // 只有getter的计算属性
    const fullName1 = computed(() => {
      console.log('fullName1')
      return user.firstName + '-' + user.lastName
    })

    // 有getter与setter的计算属性
    const fullName2 = computed({
      get () {
        console.log('fullName2 get')
        return user.firstName + '-' + user.lastName
      },

      set (value: string) {
        console.log('fullName2 set')
        const names = value.split('-')
        user.firstName = names[0]
        user.lastName = names[1]
      }
    })

    const fullName3 = ref('')

    /* 
    watchEffect: 监视所有回调中使用的数据
    */
    /* 
    watchEffect(() => {
      console.log('watchEffect')
      fullName3.value = user.firstName + '-' + user.lastName
    }) 
    */

    /* 
    使用watch的2个特性:
      深度监视
      初始化立即执行
    */
    watch(user, () => {
      fullName3.value = user.firstName + '-' + user.lastName
    }, {
      immediate: true,  // 是否初始化立即执行一次, 默认是false
      deep: true, // 是否是深度监视, 默认是false
    })

    /* 
    watch一个数据
      默认在数据发生改变时执行回调
    */
    watch(fullName3, (value) => {
      console.log('watch')
      const names = value.split('-')
      user.firstName = names[0]
      user.lastName = names[1]
    })

    /* 
    watch多个数据: 
      使用数组来指定
      如果是ref对象, 直接指定
      如果是reactive对象中的属性,  必须通过函数来指定
    */
    watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
      console.log('监视多个数据', values)
    })

    return {
      user,
      fullName1,
      fullName2,
      fullName3
    }
  }
}
</script>

生命周期

与2.x版本生命周期相对应的组合API
。beforeCreate=>使用setup()
。create=>使用setup()
。beforeMount=>onBeforeMount
。mounted=>onMounted
。beforeUpdate=>onBeforeUpdate
。updated=>onUpdated
。beforeDestroy=>onBeforeUnmount
。destroyed=>onUnmounted
。errorCaptured=>onErrorCaptured

toRefs

  1. 把一个响应式对象转换成普通对象,该普通对象的每个property都是一个ref
  2. 应用:当从合成函数返回响应式对象时,toRefs非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
  3. 问题:reactive对象取出的所有属性值都是非响应式的
  4. 解决:利用toRefs可以将一个响应式reactive对象的所有原始属性转换为响应式的ref属性
<template>
  <h2>App</h2>
  <h3>foo: {{foo}}</h3>
  <h3>bar: {{bar}}</h3>
  <h3>foo2: {{foo2}}</h3>
  <h3>bar2: {{bar2}}</h3>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
  将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
  应用: 当从合成函数返回响应式对象时,toRefs 非常有用,
        这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
*/
export default {

  setup () {

    const state = reactive({
      foo: 'a',
      bar: 'b',
    })

    const stateAsRefs = toRefs(state)

    setTimeout(() => {
      state.foo += '++'
      state.bar += '++'
    }, 2000);

    const {foo2, bar2} = useReatureX()

    return {
      // ...state,
      ...stateAsRefs,
      foo2, 
      bar2
    }
  },
}

function useReatureX() {
  const state = reactive({
    foo2: 'a',
    bar2: 'b',
  })

  setTimeout(() => {
    state.foo2 += '++'
    state.bar2 += '++'
  }, 2000);

  return toRefs(state)
}

</script>

ref获取元素

  1. 利用ref函数获取组件中的标签元素
  2. 功能需求:让输入框自动获取焦点
<script lang="ts">
import { onMounted, ref } from 'vue'
/* 
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/
export default {
  setup() {
    const inputRef = ref<HTMLElement|null>(null)

    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })

    return {
      inputRef
    }
  },
}
</script>

自定义hook函数

与vue2中mixins作用差不多,使用方法是直接在页面中引用即可

import { ref,onMounted } from "vue";
export default function() {
    const x = ref(0);
    const y = ref(1);
    const clickHandler = (event:MouseEvent) => {
        x.value = event.pageX;
        y.value = event.pageY;
    }

    onMounted(() => {
        window.addEventListener('click',clickHandler);
    })

    return {
        x,
        y
    }
}

toRefs的使用

可以将响应式的对象转换成一个个ref变量

 const person = reactive({
    name: 'guozhiqiang',
    age: '20',
    sex: '男',
    tag: '帅哥'
  })

  const a = toRefs(person);//返回一个ref变量组成的数组
  a.tag.value = '大帅哥';
  console.log(a.tag.value);//大帅哥

  console.log(person.tag.value)//大帅哥

ref获取标签元素

const InputText = ref<HTMLElement | null>();//获取ref为InputText的元素

shallowReactive与shallowRef

浅拷贝与深拷贝的区别,shallowReactive只有对象第一层的数据是响应式的,可以引起视图更新
shallowRef其实就是特殊的shallowReactive,也是只监听第一层数据
这两个适用于层数较深的对象但只需要对象第一层需要响应式的需求,可以减少浏览器开销

const person = shallowReactive({
    name: 'guozhiqiang',
    age: '20',
    sex: '男',
    tag: '帅哥',
    job: {
      name: 'web 开发工程师'
    }
  })

  person.name = 'shuaige';//数据更新时页面会变化
  person.job.name = 'aa';//数据更新时页面不会变化
  
   //shallowRef生成非递归响应数据,只监听第一层数据的变化
    let state= shallowRef({
        gf: {
            f: {
                s: {
                    d: 4
                },
                c: 3
            },
            b: 2
        },
        a: 1
    })

readOnly 和 shallowReadonly

用于设置数据只读,readonly和shallowReadonly的区别就是shallowReadonly只有第一层数据只读,其他数据可改变

const person = readonly({
	name: 'guozhiqiang',
	age: '20',
	sex: '男',
	job: {
		name: 'web 开发工程师'
	}
})

person.name = 'a'//错误,目标代理对象为只读

const person2 = shallowreadonly({
	name: 'guozhiqiang',
	age: '20',
	sex: '男',
	job: {
		name: 'web 开发工程师'
	}
})

person.name = 'a'//错误,只有第一层数据为只读
person.job.name = 'a'//可以

toRaw 和 markRaw

  • toRaw:
    作用:将一个由reactive生成的响应式对象转为普通对象。

使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

  • markRaw:
    作用:标记一个对象,使其永远不会再成为响应式对象。

应用场景:

有些值不应被设置为响应式的,例如复杂的第三方类库等。

当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

 let data = {
      name: "张三",
      age: 18,
      likeFood: {
        fruits: {
          apple: "苹果",
        },
      },
    }
    // 将普通对象变为响应对象
    let person = reactive(data);
    // 将响应对象变为普通对象
    let p = toRaw(person)
	//标记p,永远不能成为响应对象
	markRaw(p)

toRef的使用

ref:是复制,修改响应式数据不会影响以前的数据,数据发生变化,界面会自动更新。
toRef:是引用,修改响应式数据会影响以前的数据,数据发生变化,界面不会更新。
应用场景

如果想让响应式数据和以前的数据关联起来,并且更新响应式数据之后还不想更新UI,那么就可以使用toRef

setup() {
const state = reactive({
 age:5,
 money:100
})
//把响应式数据state对象中的某个属性age变成了ref对象了
const age = toRef(state,'age')
//把响应式对象中的某个属性使用了ref进行包装,变成了一个ref对象
const money = ref(state.money)
}

customRef的使用

自定义hook防抖的函数,value传入的数据,将来数据的类型不确定,所以,用泛型delay防抖的间隔事件,默认是200毫秒

<template>
    <!-- 实现一个效果,input输入数据,然后输入框下面同时显示数据但是,是延迟一秒之后 -->
    <input type="text" v-model="word">
    <span>{{ word }}</span>
</template>
<script>
import { customRef } from 'vue';
export default {
    name: 'toRef',
    setup() {
        function myRef(value, delay) {
            return customRef((track, trigger) => { // 这里有两个参数
                let timer;// 用于接收定时器
                return {
                    get() {
                        console.log('get调用了');
                        // 读取数据时会自动调用该方法
                        track();// 这个方法会通知vue追踪value的变化
                        return value;
                    },
                    set(newValue) {// 有个参数,是新修改的数据我们在set里进行赋值
                        console.log('set调用了');
                        clearTimeout(timer);// 使用前先清空定时器,免得创建很多定时器,在输入时就会出现抖动的效果
                        // 修改数据时会自动调用该方法
                        timer = setTimeout(() => {
                            value = newValue;
                            trigger(); // 这个方法会通知vue去解析模版(解析就会调用get去获取新的数据了)
                        }, delay);
                    }
                }
            })
        };
        let word = myRef('a', 1000);
        return {
            word
        };
    }
}
</script>

Provide 和 inject实现祖孙级组件通信

在setup中使用

   provide('color','red');
   
   const w = inject('color');

响应式数据的判断

//isRef: 检查一个值是否为一个ref对象
  console.log(isRef({}))
  //isReactive: 检查一个值是否为一个reactive对象
  console.log(isReactive({}))
  //isReadonly: 检查一个值是否为一个readonly对象
  console.log(isReadonly({}))
  //isProxy: 检查一个值是否为reactive和readonly代理的对象
  console.log(isProxy({}))

手写组合api

shallowReactive 和 Reactive

const reactiveHandler = {
    get(target: any,prop: any):any {
		if(prop === 'is_reactive') return true;
        const result = Reflect.get(target,prop);
        console.log('拦截了读取数据',prop,target);
        return result;
    },
    set(target: any,prop: any,val:any):any {
        const result = Reflect.set(target,prop,val);
        console.log('拦截了读取数据',prop,target);
        return result;
    },
    deleteProperty(target: any, prop: any):any {
        const result = Reflect.deleteProperty(target,prop);
        console.log('拦截了删除属性',prop,target);
        return result;
    }
}
function shallowReactive<T>(target:T):T {
    if(target && typeof target === 'object') {
        console.log(1);
        return new Proxy(target,reactiveHandler);
    } else {
        return target;
    }
}

function reactive<T>(target: T):T {
    if(target && typeof target === 'object') {
        if(target instanceof Array) {
            target.forEach((item,index) => {
                target[index] = reactive(item);
            })
        } else {
            Object.keys(target).forEach(key => {
                target[key] = reactive(target[key]);
            })
        }
        return new Proxy(target,reactiveHandler);
    } else {
        return target;
    }
    
}

export {
    shallowReactive,
    reactive
}

shallowReadonly 和 readonly

const readonlyHandler = {
    get(target: any,prop: any):any {
		if(prop ==== 'is_readonly') return true;
        const result = Reflect.get(target,prop);
        console.log('拦截了读取数据',prop,target);
        return result;
    },
    set(target: any,prop: any,val:any):any {
        console.log('只读不能设置属性',prop,target);
        return true;
    },
    deleteProperty(target: any, prop: any):any {
        console.log('只读不能删除属性',prop,target);
        return true;
    }
}
function shallowReadonly<T>(target:T):T {
    if(target && typeof target === 'object') {
        return new Proxy(target,readonlyHandler);
    } else {
        return target;
    }
}

function readonly<T>(target: T):T {
    if(target && typeof target === 'object') {
        if(target instanceof Array) {
            target.forEach((item,index) => {
                target[index] = readonly(item);
            })
        } else {
            Object.keys(target).forEach(key => {
                target[key] = readonly(target[key]);
            })
        }
        return new Proxy(target,readonlyHandler);
    } else {
        return target;
    }
    
}

export {
    shallowReadonly,
    readonly
}

shallowRef 和 ref

function shallowRef(target) {
    return {
		is_shallowRef: true,
        _value: target,
        get value(){
            console.log('劫持到了读取');
            return this._value
        },
        set value(val) {
            console.log('劫持到了设置')
            this._value = val;
        }
    }
}

function ref(target) {
    target = reactive(target);
    return {
		isRef: true,
        _value: target,
        get value(){
            console.log('劫持到了读取');
            return this._value
        },
        set value(val) {
            console.log('劫持到了设置')
            this._value = val;
        }
    }
}

export {
    shallowRef,
    ref
}

isRef 和 isReactive 和 isReadonly

function isRef(target) {
    return target&&target.isRef;
}

function isReactive(target) {
    return target&&target.isReactive;
}

function isReadonly(target) {
    return target&&target.isReadonly;
}

export {
    isReactive,
    isRef,
    isReadonly
}

Fragment(片断)

  • 在vue2中,组件必须有一个根标签
  • 在vue3中, 组件可以没有根标签,内部会将多个标签包含在一个Fragment标签中
  • 减少标签层级,减小内存占用
<Fragment>
	<div>111</div>
	<div>222</div>
</Fragment>

Teleport(瞬移)

  • Teleport提供了一种干净的方法,让组件的html在父组件界面外的特定标签插入显示
    index.html
  <div id="app"></div>
  <div id="teleport-target"></div>
  <script type="module" src="/src/main.js"></script>

src/components/HelloWorld.vue 中,添加如下,留意 to 属性跟上面的 id 选择器一致

  <button @click="showToast" class="btn">打开 toast</button>
  <!-- to 属性就是目标位置 -->
  <teleport to="#teleport-target">
    <div v-if="visible" class="toast-wrap">
      <div class="toast-msg">我是一个 Toast 文案</div>
    </div>
  </teleport>
import { ref } from 'vue';
export default {
  setup() {
    // toast 的封装
    const visible = ref(false);
    let timer;
    const showToast = () => {
      visible.value = true;
      clearTimeout(timer);
      timer = setTimeout(() => {
        visible.value = false;
      }, 2000);
    }
    return {
      visible,
      showToast
    }
  }
}

效果如下
image

  • 可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 teleport 的状态 visible 又是完全由内部 Vue 组件控制

  • 与 Vue components 一起使用 —— modal

  • 如果 包含 Vue 组件,则它仍将是 父组件的逻辑子组件

  • 接下来我们以一个 modal 组件为例

  <div id="app"></div>
  <div id="teleport-target"></div>
+  <div id="modal-container"></div>
  <script type="module" src="/src/main.js"></script>
  <teleport to="#modal-container">
    <!-- use the modal component, pass in the prop -->
    <modal :show="showModal" @close="showModal = false">
      <template #header>
        <h3>custom header</h3>
      </template>
    </modal>
  </teleport>
  • JS 核心代码如下:
import { ref } from 'vue';
import Modal from './Modal.vue';
export default {
  components: {
    Modal
  },
  setup() {
    // modal 的封装
    const showModal = ref(false);
    return {
      showModal
    }
  }
}
  • 在这种情况下,即使在不同的地方渲染 Modal,它仍将是当前组件(调用 Modal 的组件)的子级,并将从中接收 show prop

  • 这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置

  • 看实际效果以及在 Vue Devtool 中

posted @ 2022-03-20 12:21  ajajaz  阅读(1939)  评论(0编辑  收藏  举报