Vue-3 props,$emit,slot,render,JSX和createElement

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/FlowGuanEr/article/details/98051069

Vue-3 props,$emit,slot,render,JSX和createElement

Props 和 $emit

使用 Vue 开发项目时,我们将项目中的内容按照模块划分,但是有时候模块和模块之间会存在数据交互。在真正的项目开发中,父子、兄弟组件之间需要互相传值。最传统的传值方式就是 props 和 $emit。

一、Props

Prop 是你可以在组件上注册的一些自定义特性。当一个值传递给一个 prop 特性的时候,它就变成了那个组件实例的一个属性。为了给博文组件传递一个标题,我们可以用一个 props 选项将其包含在该组件可接受的 prop 列表中。

1. props传值的示例

<!-- 父组件 -->
<template>
    <div id="app">
        <!-- 父组件将Home组件调用了三次,每次调用时传入不同的参数 -->
        <Home msg="hello world!" />
        <Home msg="tom" />
        <Home msg="lion king" />
    </div>
</template>

<script>
import Home from './components/Home'

export default {
    name: 'App',
    components: { Home },
    data() {
        return { }
    },
}
</script>
<!-- 子组件 -->
<template>
    <div class="home">
        <!-- props 中接受到的数据,直接渲染就好 -->
        <h3>{{ msg }}</h3>
    </div>
</template>

<script>
    export default {
        name: "Home",
        // 通过props接受外界在调用当前组建时,传入的参数
        props: ['msg']
    }
</script>

也可以向子组件传不同的参数:

<!-- ... -->
<Home msg="hello world!" />
<Home name="tom" />
<Home ani="lion king" />
<!-- ... -->
<script>
    export default {
        name: "Home",
        // props接受多个参数
        props: ['msg','name','ani']
    }
</script>

2. props类型验证
如果我们希望每个 prop 都有指定的值类型,并且以对象形式列出 prop,这些属性的名称和值分别是 prop 各自的名称和类型。

// ...
// 使用props接收数据并规定数据的类型
props: {
    title: String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise // or any other constructor
}
// ...

为了让传值变得更有灵活性,vue 提供了以下 props 的验证方式:

props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }

如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。

注:

  • 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
  • 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
  • 因为组件不能总是预见其他组件在调用当前组件时要传入的参数,所以 vue 也允许组件在被调用时传入一些非 props 特性。这些特性会自动添加到组件的根元素上。

二、$emit

一个子组件被一个父组件调用,当子组件触发某个函数时,需要父组件响应,就要使用到 $emit

1. $emit的简单示例

<!-- 子组件 -->
<template>
  <div class="child">
    <button @click="ev">子组件中的按钮</button>
  </div>
</template>
<script>
  export default {
      name: 'Child',
      data() {
          return {
              msg: 'I am a data from child component'
          }
      },
      methods: {
          ev() {
              // 当前组件的按钮被点击时,向外抛出一个事件,谁调用当前组件,谁就在当前组件的按钮被点击时响应这个事件
              this.$emit('clickBtn');
          }
      }
  }
</script>
<!-- 父组件 -->
<template>
  <div id="app">
    <!-- 父组件在调用子组件时绑定子组件中事件的响应程序 -->
    <Child v-on:clickBtn="evInFather" />
  </div>
</template>

<script>
  import Child from './components/Child'

  export default {
    name: 'App',
    components: {
        Child
    },
    data() {
      return {
        flag: true
      }
    },
    methods: {
       //当Child子组件中的 clickBtn 事件被emit时,evInFather 也会触发    
       evInFather() {
          console.log('father up');
       }
    }
  }
</script>

我们也可以在抛出事件时,同时向外界抛出一些数据。

// Child的methods
// ...
methods: {
    ev() {
        // 向外抛出数据
        this.$emit('clickBtn', this.msg);
    }
}
// ...
// 父组件中的methods
// ...
methods: {
    evInFather(data) {
        console.log(data);
    }
}
// ...

如果组件在抛出事件时抛出的数据不止一个,可以以参数列表的形式继续向 $emit 传值,然后在父组件中再以参数列表的形式调用。但是在数据很多的情况下会让代码显得和冗长,所以我们建议将所有要抛出的数据整合在一个对象中,向 $emit 函数的第二个参数传入一个对象。

slot 插槽

插槽是组件被调用的第二种方式,更好的提高了组件的复用性。

子组件在调用父组件并向其传值时,更偏向于数据层的传递,但是有时候我们希望子组件在结构渲染层也能更好的响应其某个父组件,可以通过父组件的数据控制让子组件中的内容渲染。

如我们需要一个操作结果提示框,弹框中的标题,图片和按钮栏全部根据操作结果渲染具体结果。使用 props 或者 $emit 也能实现这样的效果,但是如果多种渲染结果的差异很大,那代码冗余就又上去了,所以 Vue 提供了一种更简洁的方式,就是插槽。

插槽相当于将组件封装成一个大的框架,至于具体显示什么,怎么显示,可以在调用时传数据,通过数据控制,也可以在调用时传入内容,显示传入的内容。

1. 插槽的小示例

<!-- Alert.vue -->
<template>
    <div class="alert">
        <p>我是组件中本来就有的内容</p>
        <!-- 在组件中用slot预留即将被分发的内容,组件被调用时,分发的内容会被渲染在slot所在的位置 -->
        <slot></slot>
    </div>
</template>

<script>
    export default {
        name: "Alert"
    }
</script>
<template>
  <div id="app">   
     <Alert>
        <!-- 调用alert组件并向其中分发内容 -->
        <p>
            hello,我是father调用时分发给子组件的内容
        </p>
     </Alert>
  </div>
</template>
<script>
    // ...
</script>

注:
如果 Alert 组件中没有包含一个 <slot> 元素,则该组件被调用时起始标签和结束标签之间的任何内容都会被抛弃。

2. 为组件设置插槽的后备内容

如果一个组件中预留了插槽,但是在组件被调用时并没有为其分发内容,可能会导致结构、逻辑出现一些无法预知的错误,所以有时候为插槽设置一个默认内容是很有必要的。

<!-- Alert.vue -->
<div class="alert">
    <slot>
        <!-- 当前组件调用时,如果不分发内容,那就展示下面p标签中的内容 -->
        <p>default message</p>
    </slot>
</div>

3. 具名插槽

要想封装一个具有高复用性的组件,一个插槽可能还不够,我们可以在组件结构的不同位置为其预留多个插槽,为每个插槽命名,在调用组建时,向不同的插槽分发对应的内容。这就是具名插槽。

<!-- Alert.vue -->
<div class="alert">
    <div class="title">
        <!-- 具名插槽 title -->
        <slot name="title">温馨提示</slot>
    </div>
    <div class="content">
        <!-- 插槽 con -->
        <slot name="con">您确定要做这样的操作吗</slot>
    </div>
    <div class="btn">
        <!-- 插槽 btn -->
        <slot name="btn">
            <button>确定</button>
        </slot>
    </div>
</div>

调用 Alert 组件:

<Alert>
    <!-- 向不同的具名插槽分发对应的内容 -->
    <template v-slot:title>
        提示
    </template>
    <template v-slot:con>
        <p>这个操作很危险,是否要进行?</p>
    </template>
    <template v-slot:btn>
        <button>确定</button>
        <button>取消</button>
    </template>
</Alert>

具名插槽的简写:

<template #btn></template>

注:

  • 没有设置 name 属性的 <slot> ,默认 name 值是 default。
  • v-slot 属性只能添加在一个 <template> 上(除了独占默认插槽情况)。

4. 作用域插槽

有时我们需要在调用组件并向组件的插槽分发内容时,访问组件内部的数据。可以将组件中的数据绑定成 插槽 prop,然后就可以在组件被调用时,同时访问插槽 prop 上的数据。

<!-- Alert.vue -->
<template>
    <div class="Alert">
        <!-- 这个插槽中的数据是有关 user 的,父组件在调用时可能想要访问 user 中的其他数据,所以可以将user绑定成插槽 prop -->
        <slot name="userMsg" v-bind:user="user">{{ user.name }}</slot>
    </div>
</template>

<script>
    export default {
        name: "Alert",
        data() {
          return {
            user: {
                name: 'tom',
                age: 18
            }
          }
        }
    }
</script>

调用插槽并使用插槽 prop 的值:

<Alert>
    <!-- 通过v-slot指令接受插槽 prop -->
    <template v-slot:userMsg="um">
        <!-- 调用插槽 prop 的数据 -->
        <i>{{ um.user.name }}</i>
        今年
        <mark>{{ um.user.age }}</mark>
        岁啦
    </template>
</Alert>

可以为插槽设置多个插槽 prop:

<slot name="userMsg" 
    v-bind:user="user"
    v-bing:num="100000"
>
    {{ user.name }}
</slot>

接收:

<template v-slot:userMsg="um">
    <i>{{ um.user.name }}</i>
    今年
    <mark>{{ um.user.age }}</mark>
    岁啦
    <strong>{{ um.num }}</strong>
</template>

插槽 prop 也可以被解构使用:

<template v-slot:userMsg="{user}"></template>

注:
如果插槽 prop 很多,建议将所有数据整合成一个对象,传递一个插槽 prop 即可。

Render 函数,JSX 语法和 CreateElement 函数

render 函数 跟 template 一样都是创建 html 模板的,但是有些场景中用 template 实现起来代码冗长繁琐而且有大量重复,这时候就可以用 render 函数。

如果在组件中的使用 render 函数渲染,那就可以不使用 <template> 标签。组件文件中只需要 <script> 标签和 <style> 标签。

在了解 render 函数之前,我们要先明确一个 Vnode 的概念。

1. Vnode(虚拟节点)

在使用 Vue 开发项目时,渲染在浏览器上的结构是 Vue 通过底层机制的各种条件、循环、计算等操作之后最终渲染在浏览器上的,我们把浏览器上渲染的最终结果看做真实的 DOM 树。

但是 Vue 对于数据的响应很高效,面对这样高效的数据响应,也要同样高效的更新页面中的节点,但是 DOM 结构非常庞大且复杂,要完成所有 DOM 的更新特别困难。

比如我们渲染了一个商品管理列表,当某个商品的某个值发生变化时,页面的列表渲染也要更新,如果要使用原生JS去渲染,我们可能需要重新渲染整个表格,或者某一行。如果要精确地定位到某个单元格,对于代码的要求很高。

好在使用 Vue 时,我们不用手动的使用JS去更新DOM树,Vue 提供了一棵虚拟 DOM 树,它通过这个虚拟 DOM树 来追踪自己要如何改变真实 DOM。

我们在 vue 文件中写的 DOM 节点和 render 函数中的 DOM 都是虚拟 DOM,浏览器渲染时,会对所有的虚拟 DOM 进行计算,最终渲染在浏览器上。

我们在创建虚拟 DOM 时,包含了虚拟 DOM 的所有信息,如子元素,类名,样式,位置等等。

Vue 实例提供了 render 函数来渲染虚拟 DOM,render 函数的参数(也是一个函数)来创建虚拟DOM。

2. render 函数示例

<script>
    export default {
        name: "Home",
        data() {
          return {
          }
        },
        render(ce) {
            return ce(
                'div'
            )
        }
    }
</script>

<Home> 组件可以被正常调用,调用时拿到是 render 函数渲染出来的这个 <div>

render 函数:

  • 参数是 ce 函数(很多地方会将其写成createElement)。
  • 返回值是一个 VNode (即将要渲染的节点)。

3. render函数的参数-createElement函数

render 函数的参数就是 createElement 函数,它创建 VNode ,作为 render 的返回值。

createElement 函数接受三个参数:

  • 参数一:
    String | Object | Function
    一个 HTML 标签名、组件选项对象,或者 resolve 了上述任何一种的一个 async 函数。必填项。
  • 参数二:
    Object
    一个包含这个标签(或模板)所有相关属性的对象。可选。
  • 参数三:
    String | Array
    createElement 创建的子节点或列表。

下面是 createElement 第二个参数和第三个参数的详细介绍及示例:

// 参数二
{
    // 与 `v-bind:class` 的 API 相同,
    // 接受一个字符串、对象或字符串和对象组成的数组
    'class': {
        foo: true,
        bar: false
    },
    // 与 `v-bind:style` 的 API 相同,
    // 接受一个字符串、对象,或对象组成的数组
    style: {
        color: 'red',
        fontSize: '14px'
    },
    // 普通的 HTML 特性
    attrs: {
        id: 'foo'
    },
    // 组件 prop
    props: {
        myProp: 'bar'
    },
    // DOM 属性
    domProps: {
        innerHTML: 'baz'
    },
    // 事件监听器在 `on` 属性内,
    // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
    // 需要在处理函数中手动检查 keyCode。
    on: {
        click: this.clickHandler
    },
    // 仅用于组件,用于监听原生事件,而不是组件内部使用
    // `vm.$emit` 触发的事件。
    nativeOn: {
        click: this.nativeClickHandler
    },
    // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
    // 赋值,因为 Vue 已经自动为你进行了同步。
    directives: [
        {
        name: 'my-custom-directive',
        value: '2',
        expression: '1 + 1',
        arg: 'foo',
        modifiers: {
            bar: true
        }
        }
    ],
    // 作用域插槽的格式为
    // { name: props => VNode | Array<VNode> }
    scopedSlots: {
        default: props => createElement('span', props.text)
    },
    // 如果组件是其它组件的子组件,需为插槽指定名称
    slot: 'name-of-slot',
    // 其它特殊顶层属性
    key: 'myKey',
    ref: 'myRef',
    // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
    // 那么 `$refs.myRef` 会变成一个数组。
    refInFor: true
}
// 参数三:
// 如果参数三是一个字符串,会被渲染成元素的innerText
render(ce) {
    return ce(
        'div',
        {},
        '<p>hello world</p>'
    )
}
// 如果参数三是一个数组,那么里面就是元素的子内容列表,每个内容都是由createElement 创建的 VNode
render(ce) {
    return ce(
        'div',
        {},
        [ce('p'),ce('h4')]
    )
}
// 但是如果在参数三的数组中,罗列了字符串,那么这些字符串会被拼接在一起作为元素的innerText
render(ce) {
    return ce(
        'div',
        {},
        ['hello','world']
    )
}

4. JSX语法

如果一个模板中的结构较为简单,我们使用 createElement 函数就可以了,但是一旦结构稍稍复杂一点点,代码就会变得特别冗长。

如果想继续使用 render函数,就要使用到 JSX 语法。因为JSX 语法允许在JS代码中书写HTML结构

render(ce) {
    return (
        <div class="home">
            这里面可以任意写结构
        </div>
    );
}

JSX 语法在 React 中的使用可能会更广泛一点。

猜你喜欢

转载自blog.csdn.net/FlowGuanEr/article/details/98051069