Vue 3.0 新特性与使用 二

1,396 阅读12分钟

image

setUp 执行时机

执行时机

  • setup:执行,组件实例尚未被创建
  • beforeCreate:表示组件刚刚被创建出来,组件的 data 和 methods 还没有初始化好
  • created:表示组件刚刚被创建出来,并且组件的 data 和 methods 已经初始化好

setup 注意点

  • 由于在执行 setup 函数的时候,还没有执行 beforeCreate、created 生命周期方法,所以在 setup 函数中,是无法使用 data 和 methods
  • 由于我们不能在 setup 函数中使用 data 和 methods,所以 Vue 为了避免我们错误的使用,它直接将 setup 函数中 this 修改成了 undefined
  • setup 函数只能同步而不能异步的

组合 Api 本质

export default {
  props: {
    title: String
  },
  data() {
      return {
          name: 'Benson'
      }
  },
  methods: {
      setName() {
          console.log(this.name);
      }
  }
  setup(props) {
    const sex = '男';
    
    function setSex() {
        console.log(this.sex);
    }
    return { sex, setSex };
  }
}

composition Api 实际上最终会把 setup return 的变量和方法会放到和 option Api 的 data 和 methods 一样的地方,所以我们能后直接 this.xx 去使用。

reactive 理解

  1. 什么是 reactive?
  • reactive 是 Vue3 中提供的实现响应式数据的方法
  • 在 Vue2 中响应式数据是通过 defineProperty 来实现的,而在 Vue3 中响应式数据是通过 ES6 的 Proxy 来实现的
  1. reactive 注意点:
  • reactive 参数必须是对象(json/arr)
  • 如果给 rective 传递了其他对象
    • 默认情况下使用该对象的方法来修改对象,界面不会自动更新
    • 如果想更新,可以通过重新赋值的方式

例如使用了 data 对象:

import { reactive } from 'vue';

export default {
    name: 'App',
    setup(props, context) {
        // 创建一个响应式数据
        // 本质:就是将传入的数据包装成一个 Propxy 对象
        const state = reactive({
            time: new Date();
        })
        
        function myFn() {
            // 直接修改以前的,界面是不会更新
            state.time.setDate(state.time.getDate() + 1);
            // 重新赋值方式才能响应式变更界面
            const newTime = new Date(state.time.getTime());
            newTime.setDate(state.time.getDate() + 1);
            state.time = newTime;
            console.log(state.time);
        }
        return {state, myFn}
    }
}

ref 理解

  1. 什么是 ref?
  • ref 和 reactive 一样,也是用来实现响应式数据的方法
  • 由于 reactive 必须传递一个对象,所以导致在企业开发中,如果我们只想让某个变量实现响应式的时候会非常麻烦,所以 Vue3 就给我们提供了 ref 方法,实现对简单值的监听
  1. ref 本质:
  • ref 在源码中会对传入的数据进行类型判断,如果判断为对象数据类型会使用 reactive 去进行响应式分装的;
  • 对于非对象类型 ref 底层的会 new 一个 RefImpl 对象,该对象会定义 get 和 set 方法去取值赋值监听,这点类似于 Vue2 的 Object.definePropert
  1. ref 注意点:
  • 在 Vue template 中使用 ref 的值不用通过 value 获取,因为 Vue 会在编译 {{ age }} 的时候自动加上变成 {{ age.value }}
  • 在 JS 中使用 ref 的值必须通过 value 获取,因为 ref 传入的值会报存在 RefImpl 对象的 _value 和 value 中

ref 和 reactive 的区别

  1. ref 和 reactive 区别:
  • 如果在 template 里使用的是 ref 类型的数据,那么 Vue 会自动帮我们添加 .value
  • 如果在 template 里使用的是 reactive 类型的数据,那么 Vue 不会自动帮我们添加 .value
  1. Vue 是如何确定是否需要添加 .value 的?
  • Vue 在解析数据之前,会自动判断这个数据是否是 ref 数据,如果是就自动添加 .value, 如果不是就不自动添加 .value。
  • 那 Vue 是如何判断当前的数据是否为 ref 类型的?,Vue 是通过当前数据的 __v_ref 类判断的,如果有这个私有的属性,并且取值为 true,那么就代表是一个 ref 类型的数据
  1. ref 和 reactive 输出
  • ref 是一个 RefImpl 对象,包含了__value 和 __v_isRef
  • reactive 是一个 Proxy 对象
  1. Vue 提供了 2 个方法来判断 Ref 和 Reactive
import {ref, reactive, isRef, isReactive} from 'vue';

export default {
    name: 'App',
    setup() {
        const age = ref(18);
        const state = reactive({name: 'Benson'});
        console.log(isRef(age));
        console.log(isReactive(state));
        return {age, state};
    }
}

递归监听

<template>
	<div>
		<p>State1: {{ state1 }}</p>
		<p>State2: {{ state2 }}</p>
		<button @click="fn">变更</button>
	</div>
</template>
<script>
import { ref, reactive } from 'vue';

export default {
	name: 'App',
	setup() {
		let state1 = reactive({
			a: 'a',
			gf: {
				b: 'b',
				f: {
					c: 'c',
					s: {
						d: 'd',
					},
				},
			},
		});
		let state2 = ref({
			a: 'a',
			gf: {
				b: 'b',
				f: {
					c: 'c',
					s: {
						d: 'd',
					},
				},
			},
		});
		function fn() {
			state1.a = '1';
			state1.gf.b = '2';
			state1.gf.f.c = '3';
			state1.gf.f.s.d = '4';
			console.log('-----------State1-----------');
			console.log(state1);
			console.log(state1.gf);
			console.log(state1.gf.f);
			console.log(state1.gf.f.s);

			state2.value.a = '1';
			state2.value.gf.b = '2';
			state2.value.gf.f.c = '3';
			state2.value.gf.f.s.d = '4';

			console.log('-----------State2-----------');
			console.log(state2.value);
			console.log(state2.value.gf);
			console.log(state2.value.gf.f);
			console.log(state2.value.gf.f.s);
		}
		return { state1, state2, fn };
	},
};
</script>

变更前:

image

变更后:

image

image

上述代码在执行 fn 方法的时候,会发现页面同时发生了变化,并且在查看控制台的时候,你会发现输出来的全是进行过 Proxy 封装过的数据。

这就是 Vue3 的递归监听,reactive 和 ref 会递归循环数据,为每一层数据进行 Proxy 封装。

默认情况下,无论是通过 ref 还是 reactive 都是递归监听的。

递归监听存在一定的问题,如果数据量比较大,是非常消耗性能的

非递归监听

一般情况下我们使用 ref 和 reactive 即可

只有在需要监听的数据量比较大的时候,我们才使用 shallowRef /shallowReactive

<template>
	<div>
		<p>State1: {{ state1 }}</p>
		<p>State2: {{ state2 }}</p>
		<button @click="fn">变更</button>
	</div>
</template>
<script>
import { shallowRef, shallowReactive, triggerRef } from 'vue';

export default {
	name: 'App',
	setup() {
		const data = {
			a: 'a',
			gf: {
				b: 'b',
				f: {
					c: 'c',
					s: {
						d: 'd',
					},
				},
			},
		};
		// 注意点:shallowReactive 创建的数据,只进行第一层的数据监听
		let state1 = shallowReactive(JSON.parse(JSON.stringify(data)));
		// shallowRef 创建的数据,只进行第一层的数据监听
		let state2 = shallowRef(JSON.parse(JSON.stringify(data)));
		function fn() {
			// 只有修改这个第一层才能监听到数据变更UI
			// state1.a = '1';
			state1.gf.b = '2';
			state1.gf.f.c = '3';
			state1.gf.f.s.d = '4';
			console.log('-----------State1-----------');
			console.log(state1);
			console.log(state1.gf);
			console.log(state1.gf.f);
			console.log(state1.gf.f.s);

			// 只有修改这个第一层才能监听到数据变更UI
			// state2.value = JSON.parse(JSON.stringify(data));
			state2.value.a = '1';
			state2.value.gf.b = '2';
			state2.value.gf.f.c = '3';
			state2.value.gf.f.s.d = '4';
			// Vue3 只提供了 triggerRef 方法,没有提供 triggerReactive 方法
			// 所以如果是 reative 类型的数据,那么是无法主动触发界面更新的
			triggerRef(state2);

			console.log('-----------State2-----------');
			console.log(state2);
			console.log(state2.value);
			console.log(state2.value.gf);
			console.log(state2.value.gf.f);
			console.log(state2.value.gf.f.s);
		}
		return { state1, state2, fn };
	},
};
</script>
<style></style>

读者可自行拷贝代码进行演示

shallowRef 本质

ref -> refImpl(如果传入 object -> reactive)
ref(10) -> refImpl{value: 10}
shallowRef -> shallowReactive
shallowRef(10) -> refImpl(如果传入 object -> shallowReactive)

左边标识我们在使用 API 的样子

右边为 Vue3 实际使用的样子

toRaw

toRaw 从 Reactive 或 Ref 中得到原始数据

toRaw 作用就是做一些不想被监听的事情(提升性能)

setup() {
    let obj = {name: 'Benson', age 18};
    /**
     * ref / reactive 数据类型的特点
     * 每次修改都会被追踪,都会更新 UI 界面,但是这样其实是非常消耗性能的
     * 所有如果我们有一些操作不需要追踪,不需要更新 UI 界面,那么这个时候,
     * 我们就可以通过 toRaw 方法拿到它的原始数据,对原始数据进行修改
     * 这样就不会被追踪,这样就不会更新 UI 界面,这样性能就好了
     */
    let state = reactive(obj);
    let obj2 = toRaw(state);
    console.log(obj === obj2); // true
    console.log(obj === state); // false

    // state 和 obj 的关系:
    // 引用关系,state 的本质是一个 Proxy 对象,在这个 Proxy 对象引用了 obj
    
    function fn() {
        // 如果直接修改 obj,那么是无法触发界面更新的
        // 只有通过包装后的 Proxy 对象来修,才会触发界面的更新
        obj.name = 'HAHA';
    }
}

markRaw

import { reactive, markRaw } from 'vue';

export defalut {
    name: 'App',
    setup() {
        let obj = {name: 'Benson', age: 18};
        obj = markRow(obj);
        let state = reactive(obj);
        function fn() {
            state.name = 'zs';
        }
        return { state, fn }
    }
}

markRaw 的作用就是禁止源数据不能被用于监听,经过上述处理后,reactive 处理 obj 进行响应式数据的封装将不在其作用了。

toRef

可以用来为源响应式对象上的 property 性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

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

const fooRef = toRef(state, 'foo')

// 往 fooRef property 找,fooRef property 指向 state property 的 ref
fooRef.value++;
console.log(state.foo) // 2

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

当您要将 prop 的 ref 传递给复合函数时,toRef 很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo')); // 将 foo 属性传下去
  }
}

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref。

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

const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和 原始property “链接”
state.foo++
console.log(stateAsRefs.foo.value) // 2

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

当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

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

  // 逻辑运行状态

  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar
    }
  }
}

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并应返回一个带有 get 和 set 的对象。

使用 v-model 使用自定义 ref 实现 debounce 的示例:

Html

<input v-model="text" />

Js

function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}

ref 获取元素

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

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM元素将在初始渲染后分配给ref
        console.log(root.value) // <div>这是根元素</div>
      })

      return { root }
    }
  }
</script>

setup return 响应式变量 root,template 中,ref="root"onMounted 挂载之后就会将相应的元素或者组件实例将分配给该 root 变量。

readonly

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 适用于响应性追踪
  console.log(copy.count)
})

// 变更 original 会触发 watchEffect
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!

// 通过 isReadonly 判断是否为 readonly 对象
console.log(isReadonly(copy)); // true
console.log(isReadonly(original)); // false

shallowReadonly

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
});
// 改变状态本身的 property 将失败
state.foo++;
// ...但适用于嵌套对象
isReadonly(state.nested); // false
state.nested.bar++; // 适用

只读效果,仅仅控制在 第一层,深层的属性还是可以变更新的

还有一个注意点,ES6 的 const 也能控制只读,但请看下面例子:

const argu_1 = {name: '小明', attrs: {sex: 18}};
const argu_2 = readonly({name: '小红'}, attrs: {sex: 16});
const argu_3 = shallowReadonly({name: '小夏'}, attrs: {sex: 17});
  • argu_1 控制这该变量不可变更,但是可以变更里面的属性
  • argu_2 控制着每一层属性都不可变更,但是 argu_2 可以重新赋值
  • argu_3 控制着第一层属性不可变更,深层的属性可以变更,比如 sex 是可以变更的,argu_3 可以重新赋值

emits 定义自定义事件

这个语法类似于 vue 组件中使用 props 校验传入的参数。

例子:

app.component('custom-form', {
  // 数组方式, 只对自定义事件名称校验
  emits: ['click', 'submit'],
  // 对象方式
  emits: {
    // 没有验证
    click: null,

    // 验证submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password })
    }
  }
})

script scoped 支持全局规则或只针对插槽内容的规则

deep 选择器

<style scoped>
    /* deep selectors */
    ::v-deep(.foo) {}
    /* shorthand */
    :deep(.foo) {}
</style>

最初,支持>>>组合器以使选择器为“ deep”。但是,某些CSS预处理器(例如SASS)在解析它时会遇到问题,因为这不是官方的CSS组合器。

后来切换到/ deep /,这曾经是CSS的实际建议添加(甚至是Chrome本身提供的),但后来删除了。这引起了一些用户的困惑,因为他们担心在Vue SFC中使用/ deep /会导致在删除该功能的浏览器中不支持其代码。但是,就像>>>一样,/ deep /仅由Vue的SFC编译器用作编译时提示以重写选择器,并在最终CSS中被删除。

为了避免丢失的/ deep /组合器造成混乱,引入了另一个自定义组合器:: v-deep,这次更加明确地表明这是Vue特定的扩展,并使用伪元素语法,以便任何-处理器应该能够解析它。

由于当前Vue 2 SFC编译器的兼容性原因,仍支持深度组合器的先前版本,这又可能使用户感到困惑。在v3中,我们不再支持>>>和/ deep /。

在研究适用于v3的新SFC编译器时,我们注意到CSS伪元素实际上在语义上不是组合符。伪元素改为接受参数,这与惯用CSS更加一致,因此,我们也使:: v-deep()那样工作。如果您不关心显式的v-前缀,也可以使用更短的:deep()变体,其工作原理完全相同。

当前仍支持:: v-deep作为组合器,但已将其弃用,并会发出警告。

插槽样式

<style scoped>
    /* targeting slot content */
    ::v-slotted(.foo) {}
    /* shorthand */
    :slotted(.foo) {}
</style>

当前,从父级传入的 slot 内容受父级的作用域样式和子级的作用域样式的影响。无法编写仅明确指定 slot 内容或不影响 slot 内容的规则。

在v3中,我们打算默认使子范围的样式不影响 slot 的内容。为了显式地指定插槽内容,可以使用:: v-slotted()(简写为:: slotted())伪元素。

scoped 情况下定义全局样式

<style scoped>
    /* one-off global rule */
    ::v-global(.foo) {}
    /* shorthand */
    :global(.foo) {}
</style>

当前,要添加全局CSS规则,我们需要使用单独的无作用域块。我们将引入一个新的:: v-global()(简写为:: global())伪元素,以用于一次性全局规则。

实验状态的特性

script setup

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

<script setup="props, { emit }">
  import { ref, onMounted } from 'vue'

  export const count = ref(0)
  export const inc = () => count.value++
  
  onMounted(() => {
    emit('foo');
  )
</script>

最新方案:

import { useContext, defineProps, defineEmit } from 'vue'

const emit = defineEmit(['onClick'])
const ctx = useContext()
const props = defineProps({
  msg: String
})

其中 ctx 包含了 attrs、emit、props、slots、expose 属性

script 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>

注意的一点,目前 Vue SFC 样式提供了直接的 CS 配置和封装,但是它是纯静态的-这意味着到目前为止,尚无法根据组件的状态在运行时动态更新样式。

当样式存在 scoped 局部样式和又要使用全局 var 的情况:

<style scoped vars="{ color }">
h1 {
  color: var(--color);
  font-size: var(--global:fontSize);
}
</style>

这里的 fontSize 是全局的 var css 变量

经过 compiles 后:

h1 {
  color: var(--6b53742-color);
  font-size: var(--fontSize);
}

参考文献

系列