Vue3-拉开序幕的setup

Vue3 中的 setup 是一个新的配置项,值是一个函数。

export default {
  name: 'App',
  setup: function () {
    
  }
}
</script>

和 Vue2 中的 data 一样,我也可以将 setup 简写成为

export default {
  name: 'App',
  setup() {
    
  }
}

setup函数的使用

与 Vue2 不一样的是,setup 中包含了 data 数据,methods方法,computed计算属性,watch监听器等等一系列的属性,也就是说,在 Vue3 中,我们不会显式的把这些属性配置到 setup 中,而是直接将属性内部的数据或方法直接暴露在 setup 中

ps:本章所讲内容,不涉及响应式,也就是说 在 setup 中定义的数据,不是响应式的,如何转为响应式数据,下一章节 ref 会讲到。但是 props 传递的数据是响应式的。

setup() {
    // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
    let name = 'al'
    let age = 18

    // 类似于methods属性中的方法
    function hello() {
      // 因为这是直接在 setup 函数中访问变量,直接访问就行,不用 this
      alert(`我的名字是${name},我的年龄是${age}`) 
    }
  }

 setup 的参数

 setup 函数接收两个参数,分别是 props上下文 context

props 是响应式的数据对象,其中包含的是外部组件传递进来,且在组件内部通过顶层 props 属性接收过的属性。声明接收了的属性且在传入新的 props 时,会同步更新现有的 props。例如

export default {
  // 如果这里接收到的 props 改变了
  props: {
    title: String
  },

  // 那么这里的接使用的props也会同步改变
  setup(props) {
    console.log(props.title)
  }
}

同时需要注意的是,在使用props传递的数据时时,尽量采取 props.xxx的方式进行。

对于传递的 props尽量不要解构,因为解构得出的变量将会丢失响应式。如果确实需要解构,或者将 props中的某一项数据传递的同时保持响应式,可以采用 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')
  }
}

第二个参数则是上下文 context ,其中暴露了一些在setup 中可能会用到的值,例如:

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)
  }
}

这个 context 不是响应式的,可以直接解构,

export default {
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

attrs 类似于 Vue2组件实例对象上的 $attrs是一个有状态的对象,而且是一个 Proxy 代理对象。对象中包含的属性时组件外部传递进来,但是没有在顶层props中声明的属性。会随着组件自身更新而更新,但不是响应式数据,在使用时避免解构,而是尽量通过 attrs.x 的方式,如果想要基于 attrs 的改变来执行副作用,可以在 onBeforeUpdate 生命周期中编写逻辑。

父组件向子组件传递了 name和 age属性,按照Vue2的逻辑,我们需要再子组件中通过 props 接收参数,然后再去使用。

export default {
  name: "TestComponent",
  props: ['name','age'],
  setup(props, context) {
    
    console.log(props, context.attrs);    // Proxy(Object) {name: 'al', age: '28'} Proxy(Object) {}
    
    return {};
  },
};

但是在 Vue3 中,会有两种情况。第一种是 像 Vue2 一样,通过 props 接收,然后我们在 setup 中去查看 props 和 context.attrs,我们会发现 此时 props 数据就是父组件传递的数据,但是 attrs中是空的

export default {
  name: "TestComponent",
  props: ['name','age'],
  setup(props, context) {
    
    console.log(props);    // Proxy(Object) {name: 'al', age: '28'} 

    console.log(context.attrs);    // Proxy(Object) {}
    
    return {};
  },
};

但是,如果我们不通过 props 接收,而是直接在 setup 中使用,我们会发现 setup 中的 props是空的 ,attrs 则包含了父组件传递的完整数据

export default {
  name: "TestComponent",
  //props: ['name','age'],
  setup(props, context) {
    
    console.log(props);    //  Proxy(Object) {}

    console.log(context.attrs);    //  Proxy(Object) {name: 'al', age: '28'}
    
    return {};
  },
};

这说明了 attrs 其实是一个兜底属性,如果你自己接收了,那我就不管了。如果你自己没接收,但是你又想在 setup 中使用,那么你可以间接的使用 context.attrs 来获取。

slots 其实和 attrs 概念基本一致,使用方式上和 Vue2 基本没有区别:

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

  • 默认插槽
    // App.vue
    <template>
      <Test>
        <p>汤圆仔</p>
      </Test>
    </template>
    
    // test.vue
    <template>
      <p>这个是子组件test</p>
    
        <!-- 默认插槽 -->
       <slot></slot>  
    
    </template>
  • 具名插槽
    // App.vue
    <template>
      <Test>
        
        // 在 Vue3 中 已经移除了 slot='xxx' 的具名插槽写法,而是使用 v-solt
        <template v-slot:qqq>
          <p>汤圆仔</p>
        </template>
    
      </Test>
    </template>
    
    // test.vue
    <template>
      <p>这个是子组件test</p>
    
        <!-- 默认插槽 -->
      <slot name='qqq'></slot>
    
    </template>
  • 作用域插槽

    // test.vue
    <template>
      <Test>
        
        // 废弃了 slot-scope 作用域插槽语法,改为使用 v-slot,通过 v-bind 向父组件传递数据,不带 name 属性时,为默认作用域插槽
        <slot v-bind:user="user"></slot>
    
        
        // 如果带了 name 属性,那么在父组件中使用时,也需要 绑定 name 
        <slot name="testScope" v-bind:user="user"></slot>
    
    
      </Test>
    </template>
    
    <script>
    import {ref} from 'vue'
    export default {
      name: "TestComponent",
      setup(props, context) {
        console.log(context.slots);    //Proxy(Object) {_: 1, defult: ƒ,testScope:f}
        
        let user = ref({ name: 'al', age: 28 }) 
           
        return {
          user
        };
      },
    };
    </script>
    
    
    // App.vue
    <template>
      <Test>
        
        // 作用域插槽,在不带 name 时,默认为 defult 默认
        <template v-slot:default="userProps">
          <p>{{userProps.name}}</p>
        </template>
    
        // 作用域插槽,在带name时,需要通过 v-solt 区分
        <template v-slot:testScope="userProps">
          <p>{{userProps.name}}</p>
        </template>
      </Test>
    </template>
    
    
    

emit则是用来触发函数,和 Vue2 中作用一致。但是存在一点差异的就是,在Vue2中使用时,我们只需要在子组件中调用 this.$emit()  就可以触发父组件传递过来的方法。但是在Vue3 中,我们需要像 props一样先声明接收到了父组件传递过来的函数,然后再去setup 中调用该函数

export default {
  // 声明接收到了父组件传递的事件
  emits: ['hello'],

  // 那么这里的接使用的props也会同步改变
  setup(props,context) {
    function test() {
        context.emit('hello',传递给父组件的参数)
    }
  }
}

expose 的作用则是显式的限制向外暴露的属性。当父组件引用子组件时,只能访问到通过可 expose 暴露特定的方法。当 expose 不传递参数时,代表不暴露任何东西,当传递对象时,则有选择的暴露局部状态。

import { h, ref } from 'vue'

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

    expose({
      increment
    })

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

setup 的返回值

setup 存在两种返回值:

  1. 返回一个同步对象,对象中的属性、方法在模板中均可直接使用,例如
    export default {
      name: 'App',
      setup() {
        // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
        let name = 'al'
        let age = 18
    
        // 类似于methods属性中的方法
        function hello() {
          alert(`我的名字是${name},我的年龄是${age}`) 
        }
    
        return {
          name,
          age,
          hello
        }
      }
    }
    <template>
      <h1>我是app组件</h1>
      <p>姓名:{{ name }}</p>
      <p>年龄:{{ age }}</p>
      <button @click="hello()">个人信息</button>
    </template>

  2. 返回一个渲染函数 ,可以自定义渲染内容(
    <script>
    import { h } from 'vue'
    export default {
      name: 'App',
      setup() {
        // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
        let name = 'al'
        let age = 18
    
    
        // Vue2 中的 main.js 中,在使用 render 函数时,就使用了 h 这个方法,
        // 第一个参数是节点,第二个参数是渲染内容
        // 当使用 返回 render 函数之后,html 模板中的内容都会被忽略,只会执行 h 函数的渲染内容
        return () => h('div','我改名字了')
      }
    }
    </script>

ps:返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。因为此时除了渲染函数之外,暴露不了别的数据和方法了,为了解决这一问题,Vue3采用了 expose 来暴露方法或属性,然后返回渲染函数 

setup(props,{ expose }) {
  // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
  let name = 'al'
  let age = 18

  // 类似于methods属性中的方法
  function hello() {
    alert(`我的名字是${name},我的年龄是${age}`) 
  }

  // 通过 expose 暴露方法或属性
  expose({
    name,
    hello
  })

  // Vue2 中的 main.js 中,在使用 render 函数时,就使用了 h 这个方法,
  // 第一个参数是节点,第二个参数是渲染内容
  // 当使用 返回 render 函数之后,html 模板中的内容都会被忽略,只会执行 h 函数的渲染内容
  return () => h('div','我改名字了')
}
}

setup 执行时机

如果按照 Vue2的模式去实现数据定义,那么应该是在 beforeCreate 之后,create之前。

因为在 beforeCreate 之前,Vue只是实现了初始化( 生命周期、event事件,此时vm还不存在 )。

而在 create 之后,data数据,methods 等都可以访问,数据代理于数据监测已完成。

但是在 Vue3 中,我们可以发现 setup 其实是在 beforeCreate 钩子函数执行之前就已经完成了

export default {
  name: "App",
  beforeCreate() {
    console.log('beforeCreate');
  },
  setup() {
    console.log('setup');
    return {};
  },
};

 setup的简写形式

如果说我们在 setup 中定义了很多个变量或者,那我们就要在 setup 中一个个全部返回,这无疑是很痛苦的。所以 Vue3 提供了一种新的模式,那就是 <script setup> 模式:在 script 标签上 加上 setup 属性,然后在 script 标签中的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用,这是因为里面的代码会被编译成组件 setup() 函数的内容 。但是与正常的 script 相比的话,这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的代码会在每次组件实例被创建的时候执行

<script setup>
  // 变量
  const msg = 'Hello!'

  // 函数
  function log() {
    console.log(msg)
  }
</script>

<template>
  <button @click="log">{{ msg }}</button>
</template>

import 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper 函数,而不需要通过 methods 选项来暴露它:

<script setup>
import { capitalize } from './helpers'
</script>

<template>
  <div>{{ capitalize('hello') }}</div>
</template>

响应式状态需要明确使用响应式 API 来创建。和 setup() 函数的返回值一样,ref 在模板中使用的时候会自动解包

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

const count = ref(0)
</script>

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

除了引入外部方法,内部声明普通变量,响应式数据之外,还存在引入组件的情况。

<script setup> 范围里的值也能被直接作为自定义组件的标签名使用:

<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>

如果引入的是动态组件的话,由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup> 中要使用动态组件的时候,应该使用动态的 :is 来绑定:

<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>

还有一部分其他的要点,例如:使用自定义指令、defineProps、defineEmits、defineModel以及修饰符等等可以在官方文档上查看-- <script setup>

 注意事项

1、Vue2 和 Vue3 不要混用。Vue3 是向下兼容的,所以即使是在 Vue3 项目中,也可以使用 Vue2 的语法,但是也会带来一些问题。

  1. 兼容性:在 Vue3 项目中也可以使用 Vue2 的写法
    <template>
      <p>在Vue3中访问 Vue2 写法下的属性或方法</p>
      <p>姓名:{{ newName }}</p>
      <p>年龄:{{ newAge }}</p>
      <button @click="getVue3Data">点击获取Vue2写法中的数据</button>
    </template>
    
    
    <script>
    export default {
      name: 'App',
      data() {
        return {
          newName: '汤圆',
          newAge:1
        }
      },
      methods: { 
        testVue2(){
          alert(`我的名字是${this.newName},我的年龄是${this.newAge}`)
        }
      },
    }
    </script>
    

2、Vue2 的配置项( data、methods、computed... )中可以访问 Vue3 中 setup 中的属性、方法

<button @click="testVue3">点击后再Vue2写法中获取Vue3写法中setup的数据</button>

<script>
export default {
  name: 'App',
  methods: { 
    testVue3() {
      console.log(this.name);
      console.log(this.age);
      console.log(this.hello);
    }
  },
  setup() {
      // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
      let name = 'al'
      let age = 18

      // 类似于methods属性中的方法
      function hello() {
        alert(`我的名字是${name},我的年龄是${age}`) 
      }

      return {
        name,
        age,
        hello
      }
    }
}
</script>

点击按钮之后,控制台上打印出了 Vue3 setup 中定义的变量和方法。

3、但是在 Vue3 中的setup 中调用 Vue2 声明的变量或方法时,会无法找到

setup() {
  // 类似于data属性中的数据,不过不用显式的声明data,而是直接设置变量
  let name = 'al'
  let age = 18

  // 类似于methods属性中的方法
  function hello() {
    alert(`我的名字是${name},我的年龄是${age}`) 
  }

  function testVue4() {
    console.log(name);
    console.log(age);
    console.log(hello);

    // 因为这些属性或方法不是在 setup 内部定义的,而是按照Vue2 的配置语法定义的,所以按照Vue2的访问模式 通过 this.xxx 访问
    console.log(this.newName);
    console.log(this.newAge);
    console.log(this.testVue2);
  }
  return {
    name,
    age,
    hello,
    testVue4
  }
}

从这里可以看到,在 Vue3 的 setup 中访问 Vue2 中的方法或属性时,得到的是undefined,这是因为  setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值(在 Vue2 中访问 Vue3的属性或方法),但反过来则不行。

但是但是这里其实还存在一个问题,如果我直接在 setup 中访问 this,得到的是undefined,但是如果我是在 setup 中定义的函数中访问 this,得到的是一个 Proxy 代理对象

setup() {
  console.log(this, 'this111');

  function testVue4() {
    console.log(this,'this');
    console.log(this.newName);
    console.log(this.newAge);
    console.log(this.testVue2);
  }
  return {
    testVue4
  }
}

 首先说 直接在 setup 中访问 this,问什么会是undefined?这是因为setup函数执行时,不依赖于Vue实例,换句话说就是 setup 函数在Vue实例化之前就执行了,所以无法直接访问到this。

然后再说为什么setup 中的方法作为组件模板内容使用时(例如作为事件处理函数被调用时),那么Vue会将这个方法绑定到Vue实例上。当该函数被调用时,this指向的就是Vue组件实例。

也就是说 Vue3中无法在 setup 内部直接访问到 this,但是通过 return 出去的函数 或者绑定到模板的数据时可以访问到this。

4、如果 Vue2 和 Vue3 混用时,数据或方法出现重名情况,以 setup 中的数据优先

<template>
  <p>{{ name }}</p>
  <button @click="hello">点击查看</button>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      name: "aha",
    };
  },
  methods: {
    hello() {
      alert(`我的名字是${this.name}`);
    },
  },
  setup() {
    let name = "al";

    // 类似于methods属性中的方法
    function hello() {
      alert(`我的名字是${name}`) 
    }

    return {
      // eslint-disable-next-line vue/no-dupe-keys
      hello,
      // eslint-disable-next-line vue/no-dupe-keys
      name,
    };
  },
};
</script>

5、setup() 应该同步地返回一个对象。唯一可以使用 async setup() 的情况是,该组件是 Suspense 组件的后裔。

具体理解就是 setup 在一般情况下,不能使用  async 包裹,因为被 async 包裹之后,即使你返回的还是原来的对象,但是 async 会对象外部包裹一层 Promise,如果想要拿取数据的话,还需要通过.then() 来获取 ,模板中时无法直接看到并且使用 return 对象中的属性或方法的。而 Vue 是不会自动帮你做这件事的。

但是 也存在一直特殊情况,那就是  该组件是 Suspense 组件的后裔( 这个后在讲,我现在也没研究到这里来 )。

总结 

 1、Vue2 和 Vue3 的配置尽量不要混用

2、如果混用了,

  • Vue2 中的配置 (例如:data、methods、compute、watch等)可以访问到 setup 中的属性和方法
  • Vue3 中的setup 不能访问到 Vue2 中的配置项
  • 存在重名情况,以 Vue3 setup 中优先

3、setup函数的参数包含两个,分别是 props和 context上下文。props就是父组件传递的数据。而context则可以解构为四个参数,分别是:

  • attrs(类似于Vue2实例上的 this.$attrs,当没有声明props接收父组件数据时,可以通过 context.attrs 在 setup 中访问到)
  • slots(类似于Vue2实例上的 this.$slots,主要用于获取插槽内容)
  • emit(与Vue2中的$emit作用一致,用来触发父组件事件,但是需要先在组件内通过 emits:['xxx']接收事件,然后通过 context.emit()触发事件)
  • expose(显式的限制向外暴露属性,当父组件引用子组件时只能访问通过 expose暴露的属性或方法,不传递则代表不暴露任何东西)

4、setup 一般不用  async,因为 setup 需要同步的返回一个对象,以此来保证 模板中数据或方法正常绑定,如果用了 async 那么返回的数据对象 会被 Promise 包裹,模板中无法看到对象中的属性,无法绑定。但是 当该组件是 Suspense 组件的后裔时,可以使用  async setup()

5、setup的执行时机时在 beforeCreate 声明周期钩子函数执行之前,因为在 setup 函数顶层直接访问 this( 不是在 setup函数中定义的 方法内部访问this)返回的是 undefined

6、 setup的简写形式为 <script setup>:

  • 更少的样板内容,更简洁的模板:定义的变量、函数都可以直接使用而不用返回
  • 更好的运行时性能 :其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象。在Vue2或Vue3的普通script中模板中使用的数据( 模板中使用的数据(data、methods、computed 等等)通常通过渲染上下文对象进行访问,这个上下文对象充当了模板与组件实例之间的桥梁,使用 Proxy 代理对象进行访问,但是大量数据或深度嵌套情况下会对性能产生影响。而简写模式则表示 模板和脚本代码在编译过程中会被编译成同一个作用域下的渲染函数,这意味这模板中的变量和函数可以直接引用 setup 中声明的,而不需要通过代理对象访问)
    <script setup>
    import { ref } from 'vue';
    
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    </script>
    
    <template>
      <button @click="increment">Count is: {{ count }}</button>
    </template>
    
  • 能够使用纯 TypeScript 声明 props 和自定义事件:可以通过直接定义一个接口或类型来声明 props,然后使用 defineProps 函数将其与组件的 props 关联。同时也可以通过 defineEmits 函数来声明自定义事件,并结合 TypeScript 的类型系统来确保事件的类型安全。
    // defineProps
    <script setup lang="ts">
    interface Props {
      title: string;
      count: number;
      isActive?: boolean;
    }
    
    const props = defineProps<Props>();
    </script>
    
    <template>
      <h1>{{ props.title }}</h1>
      <p>Count: {{ props.count }}</p>
      <p v-if="props.isActive">Active</p>
    </template>
    
    
    // defineEmits
    <script setup lang="ts">
    interface Emits {
      (event: 'increment', value: number): void;
      (event: 'toggle', isActive: boolean): void;
    }
    
    const emit = defineEmits<Emits>();
    
    function handleClick() {
      emit('increment', 1);
    }
    
    function handleToggle() {
      emit('toggle', true);
    }
    </script>
    
    <template>
      <button @click="handleClick">Increment</button>
      <button @click="handleToggle">Toggle</button>
    </template>
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值