vue函数式组件详解
本篇将详细介绍vue组件化之函数式组件,会用到以下api:
Vue.component()、Vue.extend()、$createElement、patch()。
从事vue开发的小伙伴,平时组件化的过程中大多都采用的vue文件+模块化系统的方式吧。例如:
import ComponentA from './ComponentA.vue' export default { components: { ComponentA }, // ... }
如果你看过官方文档,了解过vue的组件化,你会发现vue提供创建组件的另一种思路:函数式组件。我身边有从事vue开发的朋友,他们有的对函数式组件并没什么概念,也没有在项目中实际的使用过,下面将和大家一起复习函数式组件的创建和使用。篇幅较长,结尾会有彩蛋哦~
官网的函数式组件示例:
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>
效果:
你会发现 <child />和<my-component />都能引入到父组件中,前者好理解,import引入后component中注册,后者为啥能直接用呢?是因为Vue.component()注册的是全局组件!
我们再增加一个子组件(跟上面的组件同名):
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子类,在实例化时可以进行扩展选项,最后使用$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.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。
parent.vue:
<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(){ 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函数式子组件{{val}}</div>`, mounted(){ this.func1() } })
效果:
以上不管是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算法的核心函数,可以利用它来帮我们完成dom节点的”上树” !
<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函数版的原汁原味的函数式组件,上代码!
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>; }, }, }); const dom = document.getElementById("parent"); this.__patch__(dom, vNode); }, methods: {}, }; </script>
总结,以上就是我对vue中创建函数式组件的理解,如果还有更佳的实现方式,欢迎留言~
脚踏实地行,海阔天空飞~