Vue组件开发基础全面详解

8,541 阅读15分钟

前言

用Vue已经有2年多了,用本文详解讲解Vue组件开发过程中的方方面面,供大家学习,如果觉得还可以,点个赞收藏一下,持续更新中。

一、组件的注册

1、注册组件时,怎么给组件命名,要什么要求?

给组件命名有两种方式,一种是使用链式命名my-component,一种是使用大驼峰命名MyComponent

在字符串模板中<my-component></my-component><MyComponent></MyComponent>都可以使用,

在非字符串模板中最好使用<MyComponent></MyComponent>,因为要遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符),避免和当前以及未来的 HTML 元素相冲突。

2、怎么注册全局组件

Vue.component('my-component', {
    props:{
        title:{
            type: String,
            default:'全局组件',
        }
    },
    data(){
        return {}
    },
    created(){},
    mounted(){},
    methods:{
        handleClick(){
            alert(this.title)
        }
    },
    template: '<h3 @click="handleClick">{{ title }}</h3>',
})

3、如果template内容很多,那该怎么办?

在项目中static文件中建立my_component.html文件,文件内容如下

<h3 @click="handleClick">{{ title }}</h3>
Vue.component('my-component', (resolve, reject) =>{
    // 可以请求一个html文件,既然存放模板还是html文件存放比较好
    axios.get("../static/my_component.html").then(res =>{
        resolve({
            template: res,
            props: {
                title: {
                    type: String,
                    default: '全局组件',
                }
            },
            methods: {
                handleClick() {
                    alert(this.title)
                }
            },
        })
    });
});

4、局部组件怎么注册?

  1. 同步注册

    import myComponent from './my_component';
    export default {
        components: {
            myComponent,
        },
    }
    
  2. 异步注册(懒加载)

    export default{
        components:{
            myComponent: resolve => require(['./my_component'],resolve)
        }
    }
    

二、组件的props选项

1、props中每个prop命名有什么要注意的?

在非字符串模板中,如果prop是用小驼峰方式命名,在模板中要使用其等价的链式方式命名。

在字符串模板中就不要这么处理了。

index.vue文件

<template>
    <myComponent :my-title="my_title"></myComponent>
</template>
<script>
    export default{
        data(){
            return{
                my_title:"我的组件"
            }
        },
        components:{
            myComponent:resolve => require(['./my_component'],resolve)
        }
    }
</script>

my_component.vue文件

<template>
    <div>
        <h1>{{myTitle}}</h1>
    </div>
</template>
<script>
    export default{
        props:{
            myTitle:{
                type:String,
                default:'',
            }
        },
        data(){
            return{}
        },
    }
</script>

2、为什么在非字符串模板中要这么使用?

因为HTML(非字符串模板)中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。

3、怎么给prop传入一个静态值?

<myComponent my-title="我的组件"></myComponent>

4、怎么给prop传入一个数字和布尔值?

<myComponent :num="100"></myComponent>
<myComponent :isload="true"></myComponent>

5、在子组件中可以改变prop值吗?为什么

不能,防止从子组件意外改变父级组件的状态。

6、在子组件中怎么防止改变prop值?

  1. 在data中定义一个值,并将prop值赋值给它,如果prop值是对象和数组时,赋值时要深拷贝一下。

    <script>
        export default{
            props:{
                myTitle:{
                    type:String,
                    default:'',
                },
                myInfo:{
                    type:Object,
                    default(){
                        return:{}
                    }
                }
            },
            //this.deepClone为自定义的深拷贝方法
            data(){
                return{
                    title:this.myTitle,
                    info:this.deepClone(this.myInfo),
                }
            },
        }
    </script>
    

    2.如果prop以一种原始的值传入且需要进行转换。要使用这个 prop 的值来定义一个计算属性。

     <script>
         export default{
             props:{
                 myAchievement:{
                     type:Number,
                     default:0,
                 },
             },
             data(){
                 return{}
             },
             computed: {
                 result: function () {
                     return this.myAchievement*0.7+12;
                 }
             }
         }
     </script>
    

7、prop允许有哪些类型?

String、Number、Boolean、Array、Object、Date、Function、Symbol。 此外还可以是一个自定义的构造函数Personnel,并且通过 instanceof 来验证propwokrer的值是否是通过这个自定义的构造函数创建的。

function Personnel(name,age){
    this.name = name;
    this.age = age;
}
export default {
    props:{
        wokrer:Personnel
    }
}

8、怎么对prop进行类型验证拦截

export default {
    props:{
        propA:String,
        propB:{
            type:Number,
        },
        propC:Boolean,
    }
}

9、怎么对prop进行多个类型验证拦截

export default {
   props:{
       propA:[String,Number],
       propB:{
           type:[String,Number]
       }
   }
}

10、怎么对prop进行必填验证拦截

export default{
    props:{
        propA:{
            type: String,
            required: true,
        }
    }
}

11、怎么给对象和数组类型的prop设置默认值

export default{
    props:{
        propA:{
            type:Object,
            default(){
                return {
                    a: 1,
                }
            }
        },
        propB:{
            type:Array,
            default(){
                return [1,2,3]
            }
        }
        
    }
}

12、会自定义prop的验证吗?

export default{
    props:{
        propA:{
            validator(val){
                return 0 < val < 18
            }
        }
    }
}

13、prop在default和validator函数中能用data和computed的数据吗?

不会,因为prop会在一个组件实例创建之前进行验证,所以data和computed在default或validator函数中是不可用的。

14、怎么传入一个对象的所有属性,不是传入一个对像

使用不带参数的 v-bind,例:对于一个给定的对象 post

post: {
    id: 1,
    title: 'My Journey with Vue'
}
<myComponent v-bind="post"></myComponent>

相当

<myComponent :id="post.id" :title="post.title"></myComponent>

三、组件的model选项

先举个例子,用v-model来控制一个组件的显示隐藏

my_component.vue

<template>
    <div v-show="value">
        <span>我的组件</span>
        <button @click="$emit('input',false)">隐藏</button>
    </div>
</template>
<script>
    export default{
        props:{
            value:{
                type:Boolean,
                default:false,
            }
        },
        data(){
            return{}
        },
    }
</script>
<template>
    <div>
        <myComponent v-model="show"></myComponent>
        <button @click="show=true">显示</button>
    </div>
</template>
<script>
    export default{
        data(){
            return{
                show:false,
            }
        },
        components:{
            myComponent:resolve =>require(['./my_component'],resolve),
        }
    }
</script>

那为什么这么写?不明白的可以看的不是很清楚,那么在看下面代码就清楚。

<template>
    <div>
        <myComponent :value="show" @input="show=$event"></myComponent>
        <button @click="show=true">显示</button>
    </div>
</template>

在父组件中用$event访问通过$emit抛出的值。

这么写是不是很清楚了,组件上的v-model='show'相当 :value="show" @input="show=$event"

再举个例子

<template>
    <div>
        {{content}}
        <myInput v-model="content"></myInput>
        <!-- 等价于 -->
        <myInput :value="content" @input="content=$event"></myInput>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                content: '',
            }
        },
        components: {
            myInput: resolve => require(['./my_input'], resolve),
        }
    }
</script>

my_input.vue

<template>
    <input :value="value" @input="$emit('input',$event.target.value)"/>
</template>
<script>
    export default{
        props:['value'],
        data(){
            return{}
        },
    }
</script>

从以上代码可以看出,组件上的v-model='content'相当 :value="content" @input="content=$event"

其组件的自定义事件必须是input

既然后可以实现v-model,那么model选项是用来干嘛的,

因为一个组件上的v-model 默认会利用名为value的prop和名为input的事件,

但是像单选框、复选框等类型的输入控件可能会将value 特性用于不同的目的,

model 选项可以用来避免这样的冲突。

那么上面的例子也可以这么实现

my_input.vue

<template>
    <input :value="val" @input="$emit('change',$event.target.value)"/>
</template>
<script>
    export default{
        model:{
            prop:'val',
            event:'change'
        },
        props:['val'],
        data(){
            return{}
        },
    }
</script>

四、组件的插槽功能

1、插槽的写法

<myComponent>
    通过插槽传进去的内容
</myComponent>
<template>
    <div>
        <h1>我的组件</h1>
        <p>
            <slot></slot>
        </p>
    </div>
</template>

页面渲染出

我的组件
通过插槽传进去的内容

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

2、设置插槽的默认内容

<slot>默认内容</slot>

3、多个插槽使用(具名插槽)

<template>
    <div>
        <h2>默认插槽</h2>
        <slot>默认内容</slot>
        <h2>头部插槽</h2>
        <slot name="header"></slot>
        <h2>侧边栏插槽</h2>
        <slot name="sider"></slot>
    </div>
</template>
<template>
    <div>
        <myComponent>
            我是默认插槽
            <template v-slot:header>我是头部插槽</template>
            <template #sider>
                我是侧边栏插槽
            </template>
        </myComponent>
    </div>
</template>

v-slot 指令的缩写是 #v-slot只能添加在<template></template>

<slot></slot>上面没有name属性,会自动渲染成<slot name="default"></slot>

也可以这么调用<template v-slot:default></template>

4、父组件中使用插槽时怎么访问子组件的数据

在实际项目中,我们要获取子组件的数据在父组件中渲染插槽的内容。怎么办?

首先要在子组件中给slot绑定一个子组件的data中的一个对象childrenData,

例:<slot :toParent="childrenData"></slot> topParent是给父组件中调用的,以上称为插槽prop

然后在父组件中这样调用 <template v-slot:default="slotProps">{{slotProps.toParent}}</template>

其中slotProps是代表插槽prop的集合,其命名可以随便取的。此时slotProps.toParent的值就是childrenData的值

下面是完整例子

<template>
    <div>
        <h2>默认插槽</h2>
        <slot :toParent="childrenData" :toParent2="childrenData2">
            {{childrenData.data1}}
            {{childrenData2.data1}}
        </slot>
        <h2>头部插槽</h2>
        <slot name="header" :headerData="headerData">
            {{headerData.data1}}
        </slot>
    </div>
</template>
<script>
    export default{
        data(){
            return{
               childrenData:{
                   data1:'子组件数据一',
                   data2:'子组件数据二',
               },
               childrenData2:{
                   data1:'子组件数据三',
                   data2:'子组件数据四',
               },
               headerData:{
                   data1:'子组件头部数据一',
                   data2:'子组件头部数据二',
               },
            }
        },
    }
</script>
<template>
    <div>
        <myComponent>
            <template v-slot:default="slotProps">
                {{slotProps.toParent.data2}}
                {{slotProps.toParent2.data2}}
            </template>
            <template v-slot:header="headerProps">
                {{headerProps.headerData.data2}}
            </template>
        </myComponent>
    </div>
</template>

当有子组件slot上有多个插槽prop时,父组件调用时候可以ES6对象解构的方法,例:

<template>
    <div>
        <myComponent>
            <template v-slot:default="{toParent,toParent2}">
                {{toParent.data2}}
                {{toParent2.data2}}
            </template>
            <template v-slot:header="{headerData}">
                {{headerData.data2}}
            </template>
        </myComponent>
    </div>
</template>

也可以给子组件slot上绑定的值重新命名,例:

<template>
   <div>
   	<myComponent>
   		<template #default="{toParent:a,toParent2:b}">
   			{{a.data2}}
   			{{b.data2}}
   		</template>
   		<template #header="{headerData:c}">
   			{{c.data2}}
   		</template>
   	</myComponent>
   </div>
</template>

五、组件的非Prop特性

1、什么是组件的非Prop特性

在组件标签上添加的属性,没有在组件中props中定义,这些属性会被添加到这个组件的根元素上,这就是组件的非Prop特性,例:

<template>
    <div data="">
        <h2>我的组件</h2>
    </div>
</template>
<template>
    <div>
        <myComponent data="noProp"></myComponent>
    </div>
</template>

按F12打开调试工具可以看到

2、组件的非Prop特性的替换

如果组件的根节点已有data="children",那么在组件标签再定义data="noProp",这样根节点的data属性就会被替换。

按F12打开调试工具可以看到

3、组件的非Prop特性的合并

如果组件的根节点上有Class、Style属性,那么在组件标签再定义Class、Style,这样根节点的Class、Style是把两者合并后在赋值。例:

<template>
    <div class="children" style="font-size:16px;">
        <h2>我的组件</h2>
    </div>
</template>
<template>
    <div>
    	<myComponent class="parent" style="font-size:18px;color:red">
    	</myComponent>
    </div>
</template>

按F12打开调试工具可以看到

4、怎么指定组件中某个元素继承非Prop特性的属性

用实例属性$attrs,包含了组件标签上不作为 prop被识别(且获取)的特性绑定(class 和 style 除外)。

在写基础组件中经常用到,下面举个基础输入框组件为例子:

<template>
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        :value="value"
        @input="$emit('input', $event.target.value)"
      >
    </label>
</template>
<script>
    export default {
        inheritAttrs: false,
        props: ['label', 'value'],
        data(){
            return{}
        },
        mounted(){
            console.log(this.$attrs)
        }
    }
</script>
<template>
    <baseIinput class="input" style="color:red" v-model="val" placeholder="请输入" label="基础输入框"></baseInput>
</template>
<script>
export default {
    data() {
        return {
            val:'',
        };
    },
    components:{
        baseInput:resolve => require(['./base_input.vue'],resolve)
    }
};
</script>

例子中。mounted钩子函数打印出来是{placeholder:'请输入'},用v-bind指令将 $attrs绑定到要继承的元素上即可继承。

组件中的inheritAttrs选项为false时禁止组件的根节点继承非Prop特性,但不影响Class、Style

当inheritAttrs为true时,不禁用

六、组件上监听根元素的原生事件

v-on.native 修饰符,来实现。例:监听输入框获取焦点事件

<baseInput @focus.native="onFocus"></baseInput>

但是你会发现,监听失败,也不产生任何报错。

这是因为这个组件的根元素是<label></label>,而这标签是没有focus这个事件。

你可以用实例属性$listeners,其包含作用在这个组件上的所有监听器,配合v-on="$listeners" 将所有的事件监听器指向组件内部的任一个元素,如果改元素上还有其他监听器,可以用computed属性来合并监听器。

下面请看实例:

<template>
    <label>
        {{ label }}
        <input v-bind="$attrs" v-bind:value="value" v-on="inputListeners">
    </label>
</template>
<script>
export default {
    props: ['label', 'value'],
    data() {
        return {}
    },
    computed: {
        inputListeners: function(){
            return Object.assign(
                {},
                // 组件标签上添加所有的监听器
                this.$listeners,
                //组件内部原来的监听器
                {
                    input:(event) =>{
                        this.$emit('input',event.target.value)
                    }
                }
                
            )
        }
    }
}
</script>
<template>
    <div>
        {{val}}
        <baseInput v-model="val" @focus="onFocus"></baseInput>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                val:'',
            }
        },
        components: {
            baseInput: resolve => require(['./base_input'], resolve),
        },
        methods:{
            onFocus(){
                console.log('获取到焦点')
            }
        }
    }
</script>

七、component 标签和 is 特殊特性在组件上的应用

1、动态组件

<component :is="componentName"></component>

componentName可以是在本页面已经注册的局部组件名和全局组件名,也可以是一个组件的选项对象。

当控制componentName改变时就可以动态切换选择组件。

2、is的用法

有些HTML元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。

<ul>
    <card-list></card-list>
</ul>

所以上面<card-list></card-list>会被作为无效的内容提升到外部,并导致最终渲染结果出错。应该这么写

<ul>
    <li is="cardList"></li>
</ul>

八、组件的递归引用和组件的name选项

递归引用可以理解为组件调用自身,在开发多级菜单组件时就会用到,调用前要先设置组件的name选项。

注意一定要配合v-if使用,避免形成死循环

用element-vue组件库中NavMenu导航菜单组件开发多级菜单为例:

<template>
    <el-submenu :index="menu.id" popper-class="layout-sider-submenu" :key="menu.id">
        <template slot="title">
            <Icon :type="menu.icon" v-if="menu.icon"/>
            <span>{{menu.title}}</span>
        </template>
        <template v-for="(child,i) in menu.menus">
            <side-menu-item v-if="Array.isArray(child.menus) && child.menus.length" :menu="child"></side-menu-item>
            <el-menu-item :index="child.id" :key="child.id" v-else>
                <Icon :type="child.icon" v-if="child.icon"/>
                <span>{{child.title}}</span>
            </el-menu-item>
        </template>
    </el-submenu>
</template>
<script>
    export default{
        name: 'sideMenuItem',
        props: {
            menu: {
                type: Object,
                default(){
                    return {};
                }
            }
        }
    }
</script>

九、父子组件的通信

1、父组件向子组件通信

通过props来通信

2、子组件向父组件通信

通过在父组件上自定义一个监听事件<myComponent @diy="handleDiy"></myComponent> 在子组件用this.$emit('diy',data)来触发这个diy事件,其中data为子组件向父组件通信的数据, 在父组件中监听diy个事件时,可以通过$event访问data这个值。

3、父组件访问子组件的实例或者元素

  • 先用ref特性为子组件赋予一个ID引用<baseInput ref="myInput"></<baseInput>
  • 比如子组件有个focus的方法,可以这样调用this.$refs.myInput.focus()
  • 比如子组件有个value的数据,可以这样使用this.$refs.myInput.value
  • $refs 是在组件渲染完成之后生效,不是响应式的,所以要避免在模板和computed计算属性中使用

4、子组件访问父组件的实例或元素

this.$parent来访问

十、组件的依赖注入

上面写到,子组件可以用this.$parent访问父组件的实例,孙子组件可以用this.$parent.$parent来访问,那曾孙子组件呢,是不是要写很多个$parent

如果父组件下很多个子孙组件都要用祖先组件中的一个数据,这时候就要用到组件的依赖注入。

依赖注入,是通过provide inject 这两选项实现,provide是写在祖先组件中,inject是写在需要注入的子孙组件中。

provide选项应该是一个对象或返回一个对象的函数.

inject 选项应该是一个字符串数组或一个对象

对象的key是本地的绑定名,value是provide的 key (字符串或 Symbol)。

value也可以是个一个对象,该对象的:

from 属性是provide的 key (字符串或 Symbol)

default属性是在form定义的key在provide中未定义,使用的默认值。

祖先组件

<template>
    <div>
        <son></son>
    </div>
</template>
<script>
    export default{
        data(){
            return {}
        },
        provide(){
            return{
                top:20,
            }
        },
        components:{
            son:resolve => require(['./son.vue'],resolve)
        }
    }
</script>

son儿子组件

<template>
    <div>
        <grandson></grandson>
    </div>
</template>
<script>
    export default{
        data(){
            return {}
        },
        inject:['top'],
        components:{
            grandson:resolve => require(['./grandson.vue'],resolve)
        },
        mounted(){
            console.log(this.top)//20
        }
    }
</script>

grandson儿子组件

<template>
    <div></div>
</template>
<script>
    export default{
        data(){
            return {}
        },
        inject:{
            top:{
                from:'top',
                default:'30'
            },
            bottom:{
                from:'bottom',
                default:'30'
            }
            
        },
        components:{
            grandson:resolve => require(['./grandson.vue'],resolve)
        },
        mounted(){
            console.log(this.top)//20
            console.log(this.bottom)//30
        }
    }
</script>

十一、.sync修饰符在组件中应用

通常在子组件中是不允许对props中的值进行修改,但是有些情况下,要对一个 prop 进行“双向绑定”。真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

这时候我们可以用.sync修饰符来进行“双向绑定”,其原理是通过update:myPropName 的模式触发事件。

<template>
    <div>
        <button @click="show=true">显示</button>
        <myComponent :show="parentShow" v-on:update:show="parentShow = $event"></myComponent>
    </div>
</template>
<script>
    export default{
        data(){
            return{
                parentShow:false,
            }
        },
        components:{
            myComponent:resolve=>require(['./my_conponent'],resolve)
        }
    }
</script>
<template>
    <div v-show="show">
        <button @click="hide">隐藏</button>
    </div>
</template>
<script>
    export default{
        props:{
            show:{
                type:Boolean,
                default:false,
            }
        },
        data(){
            return{}
        },
        methods:{
            hide(){
                this.$emit('update:show',false)
            }
        }
    }
</script>

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

<my-component :show.sync="show"></my-component>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用,:show.sync="name=='小明'? true : false" 是无效的

将 v-bind.sync 用在一个对象上,例:v-bind.sync='{ title: doc.title }'也是无效的。

<myComponent 
    :show.sync="info.show" 
    :name.sync="info.name" 
    :age.sync="info.age"
></myComponent>

如果遇到上述情况可以怎么这么写

<myComponent v-bind.sync="info"></myComponent>

这样会把 info 对象中的每一个属性 (如 show) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

十二、组件的render选项

render选项的值是一个函数,通常叫渲染函数。

该函数接收一个createElement的方法作为第一参数来创建VNode(虚拟DOM),并返回。

先举个全局组件的例子

Vue.component('myComponent', {
    render: function (createElement) {
        return createElement(
            'h1',
            this.title
        )
    },
    props: {
        title: {
            type: String,
            default: '我是渲染出来的组件'
        }
    }
})

以上组件调用后渲染出<h1>我是渲染出来的组件</h1>

在单页面组件中使用要去掉<template></template>标签

<script>
export default {
    props: {
        title: {
            type: String,
            default: "我是渲染出来的组件"
        }
    },
    data() {
        return {};
    },
    render: function(createElement) {
        return createElement("h1", this.title);
    },
    
};
</script>

以上组件被调用后也渲染出<h1>我是渲染出来的组件</h1>

1、createElement函数的第一个参数,必填

接收一个String类型的元素标签如'div',或者接收一个Function类型的函数返回元素标签;

总之第一个参数是必填项,接收一个元素标签。

2、createElement函数的第二个参数用来接收组件的数据对象,类型为Object,可选

  • class项,绑定ClassName,对应:class,值为一个字符串、对象或字符串和对象组成的数组,例:

    值为对象:

    data(){
        classA:true
    },
    render: function (createElement) {
        return createElement('div',
            {
                class:{'classA':this.classA},
            },
            '我是渲染出来的组件',
        );
    },
    

    值为字符串:

    render: function (createElement) {
        return createElement('div',
            {
                class:'classA',
            },
            '我是渲染出来的组件',
        );
    },
    

    值为字符串和对象组成的数组

    data() {
        return {
            classB:true
        };
    },
    render: function (createElement) {
        return createElement('div',
            {
                class:['classA',{'classB':this.classB}],
            },
            '我是渲染出来的组件',
        );
    },
    
  • style项,绑定style,对应:style,接受一个字符串、对象,或对象组成的数组,例:

    值为字符串

    render: function (createElement) {
        return createElement('div',
            {
                style:'color:red'
            },
            '我是渲染出来的组件',
        );
    },
    

    值为对象

    render: function (createElement) {
        return createElement('div',
            {
                style:{
                    color:'red',
                    fontSize: '14px'
                }
            },
            '我是渲染出来的组件',
        );
    },
    

    值为对象组成的数组

    render: function (createElement) {
        return createElement('div',
            {
                style:[
                    {
                        color:'red',
                    },
                    {
                        fontSize: '14px'
                    }
                ]
            },
            '我是渲染出来的组件',
        );
    },
    
  • attrs项,普通的HTML特性,如id、title特性,例:

    render: function (createElement) {
        return createElement('div',
            {
                attrs:{
                    id:'idA',
                    title:'我是渲染出来的组件'
                }
            },
            '我是渲染出来的组件',
        );
    },
    

    渲染出<div id="idA" title="我是渲染出来的组件">我是渲染出来的组件</div>

  • domProps项,DOM属性,如innerHTML,列:

    render: function (createElement) {
        return createElement('div',
            {
                domProps: {
                    innerHTML: 'DOM属性内容'
                }
            },
            '我是渲染出来的组件',
        );
    },
    

    渲染出<div>DOM属性内容</div>我是渲染出来的组件

  • props项,若createElement函数的第一个参数是组件的话,且组件内有props属性,props选项相当给组件内的props传值

    Vue.component('myTitle', {
        render: function (createElement) {
            return createElement(
                'h1',
                this.title
            )
        },
        props: {
            title: {
                type: String,
                default: '我是渲染出来的组件'
            }
        }
    })
    Vue.component('myComponent', {
        render: function (createElement) {
            return createElement(
                'myTitle',
                {
                    props: {
                        title: '我是props传递出来的组件'
                    }
                },
            )
        },
    })
    
    <myComponent></myComponent>
    

    会渲染出来<h1>我是props传递出来的组件</h1>

  • on项,事件监听器,其它用法和v-on一样,例:

    render: function (createElement) {
        return createElement('div',
            {
                on: {
                    click: function(e){
                        console.log(e)
                    },
                }
            },
            '我是渲染出来的组件',
        );
    },
    
  • nativeOn项,只用于第一个参数为组件标签时,用来监听组件内根节点上的事件,列:

    components:{
        myComponent:resolve =>require(['./my_component'],resolve)
    },
    render: function (createElement) {
        return createElement('myComponent',
            {
                nativeOn: {
                    click: function(e){
                        console.log(e)
                    }
                }
            },
        );
    },
    
  • scopedSlots项,在父组件中用子组件的数据渲染插槽内容,例:

    子组件

    <template>
        <div>
            <slot :title="title"></slot>
            <slot name="content" :content="content"></slot>
        </div>
    </template>
    <script>
        export default {
            data() {
                return {
                    title: '我的组件',
                    content: '内容'
                }
            },
        }
    </script>
    

    父组件

    components: {
        myComponent: resolve => require(['./my_component'], resolve)
    },
    render: function (createElement) {
        return createElement('myComponent',
            {
                scopedSlots: {
                    default: ({ title }) => createElement('span', title),
                    content: ({ content }) => createElement('p', content)
                }
            },
        );
    },
    
  • slot项,如果组件是其它组件的子组件,需为插槽指定名称(demo待续);

  • directives项,自定义指令(demo待续);

  • key项,定义组件的key(demo待续);

  • ref项,给组件增加ref属性,通过this.$refs,在render函数组件中调用其子组件的方法,例:

    子组件

    <template>
        <div>{{title}}</div>
    </template>
    <script>
        export default {
            data() {
                return {
                    title: '我的组件',
                }
            },
            methods:{
                show(){
                    console.log(this.title)
                }
            }
        }
    </script>
    

    render函数组件

    <script>
        export default {
            data() {
                return {};
            },
            components: {
                myComponent: resolve => require(['./my_component'], resolve)
            },
            render: function (createElement) {
                var myChild = createElement('myComponent', { ref: 'myRef' });
                const _this = this;
                return createElement('div', {
                    on: {
                        click: function () {
                            _this.$refs.myRef.show();//我的组件
                        }
                    },
                    ref: 'myRef',
                }, [myChild])
            },
        };
    </script>
    

3、createElement函数的第三个参数用来接收子级虚拟节点,可选

  • 类型可以是String,可以是文字类型的节点,例:

    render: function (createElement) {
        return createElement('div', '我是渲染出来的组件');
    },
    

    或者

    <script>
        export default {
            props: {
                title: {
                    type: String,
                    default: "我是渲染出来的组件"
                }
            },
            data() {
                return {};
            },
            render: function (createElement) {
                return createElement('div', this.title);
            },
        };
    </script>
    
  • 类型可以是Array,其中值可为字符串、createElement函数,例:

    <script>
        export default {
            props: {
                title: {
                    type: Function,
                    default: "我是渲染出来的组件"
                }
            },
            data() {
                return {};
            },
            render: function (createElement) {
                return createElement('div', [this.title,'我是字符串',createElement('h1','我是子节点')]);
            },
        };
    </script>
    
  • 利用this.$slots在render函数组件中实现插槽功能,例:

    子组件

    <script>
        export default {
            data() {
                return {
                };
            },
            render: function (createElement) {
                return createElement(
                    'div',
                    [
                        createElement('h1',this.$slots.default),
                        createElement('h2',this.$slots.header)
                    ]
                )
            },
        };
    </script>
    

    父组件

    <template>
        <div>
            <myRender>
                父默认内容
                <template v-slot:header>父内容</template>
            </myRender>
        </div>
    </template>
    <script>
        export default {
            data() {
                return {}
            },
            components: {
                myRender: resolve => require(['./my_render.vue'], resolve),
            },
        }
    </script>
    

    渲染出

    <div>
        <h1>父默认内容</h1>
        <h2>父内容</h2>
    </div>
    
  • 利用this.$scopedSlots实现调用render函数组件时用render函数组件中的数据编辑插槽,例:

    子组件

    <script>
        export default {
            data() {
                return {
                    info:'我是render函数组件'
                };
            },
            render: function(createElement) {
                return createElement("div", [
                    this.$scopedSlots.header({
                        title: this.info
                    })
                ]);
            }
        };
    </script>
    

    父组件

    <template>
        <div>
            <myRender>
                <template v-slot:header="{title}">{{title}}</template>
            </myRender>
        </div>
    </template>
    <script>
        export default {
            data() {
                return {};
            },
            components: {
                myRender: resolve => require(["./my_render.vue"], resolve)
            }
        };
    </script>
    
  • 组件树中的所有 VNode 必须是唯一的,所以当参数类型为Array时,每个值不可重复,例:

    render: function (createElement) {
        var myChild = createElement('p', 'hi')
        return createElement('div', [
            // 错误 - 重复的 VNode
            myChild,myChild,myChild
        ])
    },
    

    应该这么做

    render: function (createElement) {
        return createElement('div',
            Array.from({ length: 20 }).map(function () {
                return createElement('p', 'hi')
            })
        )
    }
    

4、render函数中实现v-if v-else v-for 模板功能,例:

<ul v-if="lists.length">
    <li v-for="item in lists">{{ item.name }}</li>
</ul>
<p v-else>没有数据</p>
props: ['lists'],
render: function (h) {
    if (this.lists.length) {
        return h('ul', this.lists.map(function (item) {
            return h('li', item.name)
        }))
    } else {
        return h('p', '没有数据')
    }
}

5、render函数中实现v-model模板功能,例:

props: ["value"],
render: function(h) {
    var _this = this;
    return h("input", {
        domProps: {
            value: _this.value
        },
        on: {
            input: function(event) {
                _this.$emit("input", event.target.value);
            }
        }
    });
}

6、在父组件中使用render自定义子组件

在使用iview中Tree组件,你会发现组件提供了render属性让你可以自定义树节点。那是怎么实现的,下面举个简单列子:

子组件

<script>
    export default {
        props: {
            render: {
                type: Function,
            },
            data: {
                type: Object,
                default(){
                    return {}
                },
            }
        },
        render: function (h) {
            const params = {
                data: this.data,
            };
            return this.render(h,params);
        },
    };
</script>

父组件

<template>
    <div>
        <myRender :render="this.render" :data="this.data">
        </myRender>
    </div>
</template>
<script>
export default {
    data() {
        return {
            render: function(h,{data}) {
                const info= `我是${data.name},今年${data.age}岁`;
                return h('h1', info);
            },
            data:{
                name:'小明',
                age:18,
            },
        }
    },
    components: {
        myRender: resolve => require(['./my_render.vue'], resolve),
    },
}
</script>

渲染出

我是小明,今年18岁

注意 render值不能用箭头函数,否则this指向会出错。

7、函数式组件的应用

  • 什么是函数式组件,满足以下几个特点可以称为函数式组件

    • 没有管理任何状态
    • 没有监听任何传递给它的状态
    • 没有生命周期方法
    • 只是接收一些prop的函数
  • 函数式组件优点,渲染开销低

  • 怎么使用函数式组件

    functional选项标记为true,此时组件成为

    • 无状态 == 无响应式数据
    • 无实例 == 无this上下文

    那么上个例子的子组件可以这么写

    <script>
        export default {
            functional:true,
            render: function (h,context) {
                const params = {
                    data: context.props.data,
                };
                return context.props.render(h,params);
            },
        };
    </script>
    

    组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

    • props:提供所有 prop 的对象
    • children: VNode 子节点的数组
    • slots: 一个函数,返回了包含所有插槽的对象
    • scopedSlots: 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽
    • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
    • parent:对父组件的引用
    • listeners: 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名
    • injections: 如果使用了 inject 选项,则该对象包含了应当被注入的属性

    在模板中使用函数式组件这样声明:

    <template functional>
        <div>
            <p v-for="item in props.lists" @click="props.listClick(item);">
                {{ item }}
            </p>
        </div>
    </template>