本篇主要介绍非单文件组件( Vue.component创建的组件其中包括函数式组件)在项目中的使用,会用到以下api:Vue.component()、Vue.extend()、$createElement、patch()。
单文件组件:文件扩展名为 .vue 的 single-file components (单文件组件)。
从事vue开发的小伙伴,平时组件化的过程中大多都采用 单文件组件+模块化系统的方式吧。例如:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
如果你看过官方文档,了解过vue的组件化,会发现除了.vue结尾的单文件组件,vue还提供创建组件的另一种思路:Vue.component、Vue.extend,其中函数式组件也正是利用这种方式来创建的。
函数式组件:没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this上下文),组件需要的一切都是通过 context参数传递:
- props:提供所有 prop 的对象
-
children
:VNode 子节点的数组 -
slots
:一个函数,返回了包含所有插槽的对象 -
scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。 -
data
:传递给组件的整个数据对象,作为 createElement的第二个参数传入组件 -
parent
:对父组件的引用 -
listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。 -
injections
:(2.3.0+) 如果使用了 inject选项,则该对象包含了应当被注入的 property。
官网的函数式组件示例:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
将以上示例适当修改,并引入到我们项目中:
child.js
import Vue from 'vue';
export default Vue.component('my-component',{ // 该组件抽成js文件,
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
return createElement('h1', '我是函数式子组件')
}
})
这里我将该组件抽成单独的js文件,便于复用和维护。在父组件引入该组件:
parent.vue:
<template>
<div>
<h1>我是父组件</h1>
<child />
<my-component />
</div>
</template>
<script>
import child from './chid.js'
export default {
name: "parent",
components: {
child
},
data() {
return {
};
},
mounted(){
},
methods:{
}
};
</script>
效果:
!
我们再增加一个子组件(跟上面的组件同名):
import Vue from 'vue';
//这是函数式组件2
export default Vue.component('my-component',{ // 同名
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
return createElement('h1', '我是函数式子组件2')
}
})
现在看看运行效果:
结果会发现,"函数式组件2"被覆盖了!由于Vue.component()同名的组件会覆盖,也因为全局组件不好辨别当前的组件名是否已经注册,所以建议使用Vue.extend()来新建函数式组件(不想创建全局组件的情况下)。
Vue.extend使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
Vue.extend相当于一个扩展实例构造器,用于创建一个具有初始化选项的Vue子类,在实例化时可以进行扩展选项,最后引入该函数式组件(components或者$mount手动挂载),$mounte会替换被挂载节点下的内容!
Vue.extend和Vue.component之间的关系:
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
<com />
</div>
</template>
<script>
import child from "./child.js";
import Vue from 'vue';
Vue.component('com', child)
...
Vue.extend可以当做Vue.component的组件选项。
因此如果希望创建的组件为全局组件, 可用Vue.component(),否则用Vue.extend(),之后在当前组件的components中引入或者可以手动挂载。
下面用Vue.extend()创建组件:
child.js:
import Vue from 'vue';
export default Vue.extend({,
// Props 是可选的
props: {
},
template: `<div>我是extend函数式子组件</div>`
})
这里使用的template写法,vue底层执行的时候会将template解析成AST,然后将AST转化为render函数,render的过程vue帮我们处理就好了,所以不习惯写render函数的同学可以用template语法(用模板语法则该组件将不再是函数式组件,并拥有实例this)。
parent.vue:
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
</div>
</template>
<script>
import child from './child.js'
export default {
name: "parent",
components: {
child // 引入函数式组件
},
data() {
return {
};
},
created(){
},
mounted(){
new child(
{
props: {
val:{
default:6
}
},
methods:{
func1(){
console.log("我是方法")
}
}
}
).$mount("#parent") //手动挂载: 用$mount()将child产生的实例挂载到id为parent的dom下
},
methods:{
}
};
</script>
child.js:
import Vue from 'vue';
export default Vue.extend({
template: `<div>我是extend<span></span>子组件{{val}}</div>`,
mounted(){
this.func1()
}
})
效果:
tips: Vue.extend和Vue.component都各有利弊,前者需要手动挂载,后者是全局组件,下面有个中间方案(直接导出它们的options):
test.js
const test = {
data () {
return {
haha: '我是哈哈'
}
},
template: `
<div>{{haha}}</div>
`
}
export default test
App.vue
<template>
<test />
</template>
import test from './test'
export default {
components:{
test
}
}
最终效果:
以上不管是Vue.component()还是Vue.extend()最终都是创建Vue的组件实例,它既不是虚拟dom也不是真实的dom节点。
业务中,有些ui库要求我们传入vNode或者真实的dom,例如element UI中的$confirm弹框中的message属性,既可以传普通的字符串,也可以传vNode。下面来手写一段vNode:
...
const h = this.$createElement; // 对$createElement不熟悉的可以查看vue文档
const vNode = h('p', null, [
h('span', null, '内容可以是 '),
h('i', { style: 'color: teal' }, 'VNode')
]);
console.log(vNode);
打印:
上面的vNode结构非常简单,h函数的children参数可以手写,但如果vNode结构很复杂的话,手写就显得很凌乱。因此在h函数的第一个参数,我们可以传一个component组件。
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
</div>
</template>
<script>
import child from './child.js'
export default {
name: "parent",
components: {
},
data() {
return {
};
},
created(){
},
mounted(){
const h = this.$createElement;
let vNode = h(child,{
props: {
val:8,
func1: ()=>{console.log('哈哈哈哈')}
}
})
console.log(vNode)
},
methods:{
}
};
</script>
接下来在elementUI中使用:
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
</div>
</template>
<script>
import child from "./child.js";
export default {
name: "parent",
components: {},
data() {
return {};
},
created() {},
mounted() {
const h = this.$createElement;
let vNode = h(child, {
props: {
val: 8,
func1: () => {
console.log("哈哈哈哈");
},
},
});
this.$confirm("提示", {
title: "提示",
message: vNode,
showCancelButton: true,
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
});
console.log(vNode);
},
methods: {},
};
</script>
继续思考,上面ui组件会帮我们将vNode虚拟节点渲染到页面中,如果我想将vNode渲染到我们页面中的某个节点,该怎么实现呢?
其实vue的实例原型上有一个方法:__patch__,而patch函数就是vue中diff算法的核心函数,可以利用它来帮我们完成vNode虚拟节点的"上树" !
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
</div>
</template>
<script>
import child from "./child.js";
export default {
name: "parent",
components: {},
data() {
return {};
},
created() {},
mounted() {
const h = this.$createElement;
let vNode = h(child, {
props: {
val: 8,
func1: () => {
console.log("哈哈哈哈");
},
},
});
const dom = document.getElementById("parent")
this.__patch__(dom,vNode) //dom是老节点(id为parent),vNode是我们即将渲染的新节点,通过diff算法重新渲染parent节点
},
methods: {},
};
</script>
最后封装一个render函数版的原汁原味的函数式组件,render函数并没有使用createElement,而是使用jsx,书写更方便,上代码!
functionalComponent.js:
import Vue from 'vue';
const functionalComponent = Vue.extend(
{
functional: true,
props: {
render: Function
},
render(h, ctx) {
return ctx.props.render.call(ctx, h);
}
}
)
export default functionalComponent
parent.js:
<template>
<div>
<h1>我是父组件</h1>
<div id="parent"></div>
</div>
</template>
<script>
import functionalComponent from "./functionalComponent";
export default {
name: "parent",
components: {},
data() {
return {};
},
created() {},
mounted() {
const _h = this.$createElement;
let vNode = _h(functionalComponent, {
props: {
render: (createElement, ctx) => {
return <div>123</div>; // jsx
},
},
});
const dom = document.getElementById("parent");
this.__patch__(dom, vNode);
},
methods: {},
};
</script>
总结,以上就是我对vue中创建非单文件组件以及函数式组件的理解,如果有什么意见和建议,欢迎留言~