VUE组件化

思维导图参考:

https://www.processon.com/view/link/5d430271e4b01ed2c6aa4171#map

第一个词:

组件化:

组件化到底是什么?
重点词:抽象、独立、可复用
官方回答:vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护性,便于多人协同开发
第二个词:

组件通信常用方式

常用
props:需要在子组件里定义一个props即参数,一般是[’’,’’]数组,但如果你想对参数进行验证,他就是一个对象,{canshuA:{type:,default:}},在父组件中调用该子组件时进行传参,参数名就是定义的props名
$emit/$on

var Event = new Vue(); 
相当于又new了一个vue实例,Event中含有vue的全部方法;
可在main.js里全局注册
Event.$emit('msg',this.msg); 
发送数据,第一个参数是发送数据的名称,接收时还用这个名字接收,
第二个参数是这个数据现在的位置,即参数本身;
Event.$on('msg',function(msg){  
接收数据,第一个参数是数据的名字,与发送时的名字对应,
第二个参数是一个方法,要对数据的操作
/这里是对数据的操作
 })

event bus

//vue原型链挂载总线(main.js)
Vue.prototype.bus = new Vue();
//子组件发送数据
this.bus.$emit("change",data);
//子组件接收数据
this.bus.$on("change",function(data){
})

vuex

Vuex 主要有四部分:
    state:包含了store中存储的各个状态。
    	Vuex 使用 state 来存储应用中需要共享的状态。为了能让 Vue 组件在 state更改后也随着更改,需要基于state 创建计算属性。
    	return this.$store.state.count  // count 为某个状态
    	
    getter: 类似于 Vue 中的计算属性,根据其他 getter 或 state 计算返回值。
    	类似于 Vue 中的 计算属性(可以认为是 store 的计算属性),getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
    	Getter 会暴露为 store.getters 对象,可以以属性的形式访问这些值:
		store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
		store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
		
    mutation: 一组方法,是改变store中状态的执行者,只能是同步操作。
    更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。也就是说,前面两个都是状态值本身,mutations才是改变状态的执行者。
    注意:mutations只能是同步地更改状态。
    mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
  store.commit('increment')

    action: 一组方法,其中可以包含异步操作。
    	想要异步地更改状态,就需要使用action。action并不直接改变state,而是发起mutation

参考自博客,本身还是没有太了解vuex,会在日后的学习中继续补充。
不推荐使用
$parent:用法与event bus 一致 ,this.$parent.$on,this.$parent.$emit 就是一个相同的中间人就可以了 即都是$parent
$children:可以获得全部孩子,访问到孩子 然后去调用孩子的方法(只会有自定义组件)$children不能保证⼦元素顺序,只能根据同步组件排序异步组件不行
$root
不推荐原因:耦合性(模块间关联程度的度量)高–>复用性低
$refs:给组件写一个类似ref="a",就可以通过this.$refs.a访问到该组件,任意组件都可
provide/inject隔层传参:即跨很多代:index.vue里 provide() { return { bar: 'bar' } }这里面return的对象不管隔多少代都可以拿出来,如:在children2中inject: { bar1: { from: 'bar',这里是防止命名冲突,所以采用取别名的形式,名字叫bar1但其实来源于bar default: 'barrrrrrrr' } },注入一下,就可以用了<p>{{bar1}}</p>
非prop特性(props中未声明)
$attrs:就比如<Child2 ref="child2" msg="some message">,使用了msg属性,但在Child2 组件内并没有定义msg的props属性就可以在Child2 组件内利用<p>{{$attrs.msg}}</p>使用了
$attrs:重点使用 :爷孙通信,例如:index.vue是爷爷,有一个parent.vue父组件,children2.vue是孙组件,parent里引入children2,index里引入parent,<Parent msg="msg from grandpa"> 从爷爷辈传来的信息在老爹辈原封不动的转给孙子,即children2里如果想访问爷爷里msg属性,就需要在parent里进行属性展开,<Child2 v-bind="$attrs" > v-bind就会把这个对象通过键值对的方式把它展开了
$listeners:事件转发,隔代转发,与attrs同理,index里<Parent msg="msg from grandpa" @foo="onFoo"> parent里<Child2 v-bind="$attrs" v-on="$listeners"> 在children2里触发一下this.$emit('foo'),就会调到index的onFoo方法
第三个词:

插槽

说到插槽 就要想到 内容分发,即实际上内容是在子组件里面,但其实我们是在父组件里面传递内容,在子组件显示
匿名插槽:

// comp1 <div>  <slot></slot> </div>
// parent <comp>hello</comp>

具名插槽
将内容分发到⼦组件指定位置

// comp2 <div>  <slot></slot>  <slot name="content"></slot> </div>
// parent <Comp2>    <!-- 默认插槽⽤default做参数 -->    <template v-slot:default>具名插槽</template>    <!-- 具名插槽⽤插槽名做参数 -->    <template v-slot:content>内容...</template> </Comp2>

作⽤域插槽
分发内容要⽤到⼦组件中的数据

// comp3 <div>  <slot :foo="foo"></slot> </div>
// parent <Comp3>    <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->    <template v-slot:default="slotProps">    来⾃⼦组件数据:{{slotProps.foo}}    </template> </Comp3>

组件化实战 仿Element-ui的表单案例

<!-- KInput -->

<template>
  <div>
    <!-- 管理数据,实现双向绑定 -->
    <!-- :value, @input -->
    <input :type="type" :value="value" @input="onInput"
      v-bind="$attrs">
  </div>
</template>
<script>
  export default {
    inheritAttrs: false,//根元素上就不会显示了,如果你不希望组件的根元素继承特性,你可以在组件的选项中设置
    props: {
      value: {
        type: String,
        default: ''
      },
      type: {
        type: String,
        default: 'text'
      }
    },
    methods: {
		// 把值派发出去,实现双绑
      onInput(e) {
        this.$emit('input', e.target.value)
        
        // 触发校验  Input通知校验  $parent指FormItem 
        // 这里使用$parent会产生耦合,如果KInput的老爹不是KFormItem就无效了
        this.$parent.$emit('validate')
      }
    },
  }
</script>

<!-- KFormItem -->

<template>
  <div>
    <!-- 显示label -->
    <label v-if="label">{{label}}</label>
    <!-- 显示内部表单元素 -->
    <!-- 占一个坑位,有内容就显示,例如表单项 -->
    <slot></slot>
    <!-- 错误提示信息 -->
    <p v-if="error" class="error">{{error}}</p>
    <!-- <p>{{form.rules[prop]}}</p> -->
  </div>
</template>

<script>
import Schema from "async-validator";

export default {
  inject: ["form"],//注⼊ 
  props: {
    label: {
      type: String,
      default: ""
    },
    prop: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      error: 'some error'
    }
  },
  mounted() {// 监听校验事件
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      // 当前表单项校验
      // element使用的是async-validator需要安装 npm i async-validator
      // 获取校验规则和当前数据  // 获取对应FormItem校验规则  
      const rules = this.form.rules[this.prop]
      // 获取校验值 
      const value = this.form.model[this.prop]
      // 创建校验器  // 校验描述对象 
      const schema = new Schema({[this.prop]: rules})//用这个规则去校验这个值
      // 返回promise,全局可以统一处理 // 返回Promise,没有触发catch就说明验证通过 
      return schema.validate({[this.prop]: value}, errors => {
        // errors存在则校验失败
        if (errors) {
          this.error = errors[0].message
        } else {
          // 校验通过
          this.error = ''
        }
      })
    }
  }
};
</script>

<style scoped>
.error {
  color: red;
}
</style>

<!-- KForm -->

<template>
	<div>
		<slot></slot>
	</div>
</template>

<script>
	export default {
	//隔代传参,传好几代
		provide(){
			return {
				form:this// 将组件实例作为提供者,⼦代组件可⽅便获取 
			}
		},
		props: {
			model: {
				type: Object,
				required:true
			},
			rules:Object
		},
		methods:{
			validate(cb){
			// 调⽤所有含有prop属性的⼦组件的validate⽅法并得到Promise数组 
				const tasks=this.$children.filter(item=>item.prop).map(item=>item.validate())
				 // 所有任务必须全部成功才算校验通过,任⼀失败则校验失败 
				Promise.all(tasks).then(()=>cb(true)).catch(()=>cb(false))
			}
		}
		
	}
</script>

<!-- index -->

<template>
	<div>
		<KForm :model="model" :rules="rules" ref="loginForm">
			<KFormItem label="用户名" prop="username">
				<KInput v-model="model.username" placeholder="请输入用户名"></KInput>
			</KFormItem>
			<KFormItem label="密码" prop="password">
				<KInput v-model="model.password" placeholder="请输入密码"></KInput>
			</KFormItem>
			<KFormItem>
				<button @click="login">登录</button>
			</KFormItem>
		</KForm>
	</div>
</template>

<script>
	import KInput from './KInput.vue'
	import KFormItem from './KFormItem.vue'
	import KForm from './KForm.vue'
	export default {
		components:{
			KInput,
			KFormItem,
			KForm
		},
		data(){
			return {
				model:{
					username:'tom',
					password:''
				},
				rules:{
					username:[{required:true,message:"yhm不能为空"}],
					password:[{required:true,message:"mm不能为空"}]
				}
			}
		},
		methods:{
			login(){
				this.$refs.loginForm.validate((isValidate)=>{
					if(isValidate){
						console.log('login')
					}else{
						alert('校验失败')
					}
				})
			}
		}
	}
</script>

<style scoped>

</style>

为什么要用自定义组件?----> 由此引入 vue.extend及$mount的使用
因为当前项目使用的ui框架不能满足需求,由此产生自定义组件的念头。自定义组件写完后,又想能够自定义创建组件实例。这样一步一步,接触到了vue.extend。

vue.extend(简洁,重点)

1.vue.extend的作用(官方文档API)
vue.extend(option) :

  • option的参数是一个包含组件选项的对象
  • 返回一个Vue构造器
import AComponent from path; // 自定义的组件//此组件已存在,并已创建
// 第一种,option为自定义
// 此时baseExtend为构造函数
let baseExtend = vue.extend({
    template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
    data: function () {
        return {
          firstName: 'Walter',
          lastName: 'White',
          alias: 'Heisenberg'
        }
  }
});
let baseInstance = new baseExtend(); // 创建组件实例
baseInstance.$mount('#mount-point');// 将实例挂载到id位mount-point的元素上
 
// 第二种,option为导入的组件对象
// 此时baseExtendTwo为AComponent组件的构造函数
let baseExtendTwo = vue.extend(AComponent);
let baseInstanceTwo = new baseExtendTwo(); // 创建AComponent组件实例
baseInstanceTwo.$mount('#mount-point');// 将实例挂载到id位mount-point的元素上

动态生成一个自定义组件与实例不同。官方文档中是事先定义一个id为“mount-point”的元素,然后挂载上去。没有举例如何动态生成一个元素并挂载。所以下一步,是查看如果将组件实例挂载到动态生成的元素上。
2.查看了$mount的用法vm.$mount( [elementOrSelector] ) 返回值:vm - 实例自身
如果Vue实例在实例化时没有收到el选项,则它处于“未挂载"状态,没有关联的DOM元素。可以使用vm.$mount()手动地挂载一个未挂载的实例。如果没有提供element0rSelector参数,模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把它插入文档中。

//官方API使用介绍
var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')

// 同上
new MyComponent({ el: '#app' })

// 或者,在文档之外渲染并且随后挂载 //本次需要使用
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

$mount()中不传参数,将实例渲染为文档之外的元素。渲染为文档之外的元素就相当于createElement了,但是还需要将元素插入到文档中,因此可做以下需求:

import AComponent from path; // 自定义的组件
 
// 第一种,option为自定义
// 此时baseExtend为构造函数
let baseExtend = vue.extend({
    template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
    data: function () {
        return {
          firstName: 'Walter',
          lastName: 'White',
          alias: 'Heisenberg'
        }
  }
});
 
let baseInstance = new baseExtend(); // 创建组件实例
 
baseInstance.$mount();// 将实例挂载到id位mount-point的元素上
 
document.body.appenChild(baseInstance.$el); // 将实例元素插入body最后
 
// 第二种,option为导入的组件对象
// 此时baseExtendTwo为AComponent组件的构造函数
let baseExtendTwo = vue.extend(AComponent);
 
let baseInstanceTwo = new baseExtendTwo(); // 创建AComponent组件实例
 
baseInstanceTwo.$mount();// 将实例挂载到id位mount-point的元素上
 
document.body.appenChild(baseInstanceTwo.$el); // 将实例元素插入body最后

问题探究:元素动态挂载方法一二,$parent,$children解耦

问题1:需求:将弹窗由alert变成自定义弹窗组件,并挂载,步骤如下
1.在网上拷的不知名弹窗组件

<template>
	<div class="box" v-if="isShow">
		<h3>{{title}}</h3>
		<p class="box-content">{{message}}</p>
	</div>
</template>

<script>
	export default {
		props: {
			title: {
				type: String,
				default: ""
			},
			message: {
				type: String,
				default: ""
			},
			// 持续几秒钟
			duration: {
				type: Number,
				default: 1000
			}
		},
		data() {
			return {
				isShow: false
			};
		},
		methods: {
			show() {
				this.isShow = true;
				//定时器
				setTimeout(this.hide, this.duration);
			},
			hide() {
				this.isShow = false;
				//时间到了自动remove
				this.remove();
			}
		}
	};
</script>

<style>
	.box {
		position: fixed;
		width: 100%;
		top: 16px;
		left: 0;
		text-align: center;
		pointer-events: none;
		background-color: #fff;
		border: grey 3px solid;
		box-sizing: border-box;
	}

	.box-content {
		width: 200px;
		margin: 10px auto;
		font-size: 14px;
		padding: 8px 16px;
		background: #fff;
		border-radius: 3px;
		margin-bottom: 8px;
	}
</style>

2.写$mount挂载
方法一:

import Vue from 'vue'

// 传入一个组件配置
// 创建它的实例,并且将它挂载到body上
// 返回组件实例
export default function create(Component, props) {
  // 实例创建
  // 方式一:借鸡生蛋
  const vm = new Vue({
    render(h) {
      return h(Component, { props })
    }
  }).$mount() // $mount()本质上将vdom=》dom//挂载的本质 将虚拟的dom变成真实的dom

  // 通过vm.$el获取生成的dom //vm.$el就是自己,vue实例中使用的根dom元素
  document.body.appendChild(vm.$el)

  // 删除函数
  // 获取组件实例
  const comp = vm.$children[0]
// 未来我不需要你的时候,要把你干掉
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }

  return comp
}

方法二:

import Vue from 'vue'
export default function exCreate(Component,props){
	var baseExtend = Vue.extend(Component)
	//propsData选项传递属性
	//此时拿到的直接就是组件实例 ,和借鸡生蛋不同,借鸡生蛋外面还有一层老母鸡
	var baseInstance = new baseExtend({propsData:props})
	baseInstance.$mount()
	//输出$el,它是一个注释(标签),即console.log(baseInstance.$el)
	//组件里面v-if导致的
	//patch算法,diff一样的
	//当后面执行show()即展示该对话框时,就会变成组件里的div了
	document.body.appendChild(baseInstance.$el)
	baseInstance.remove=()=>{
	//移除dom
		document.body.removeChild(baseInstance.$el)
		//销毁组件
		baseInstance.$destroy()
	}
	return baseInstance
} 

3 .index.vue里的使用

methods:{
			login(){
				// this.$refs.loginForm.validate((isValidate)=>{
				// 	if(isValidate){
				// 		console.log('login')
				// 	}else{
				// 		alert('校验失败')
				// 	}
				// })
				this.$refs.loginForm.validate((isValid)=>{
					exCreate(Notice,{
						title:'我是标题',
						message:isValid?'登陆成功':'登陆失败',
						duration:3000
					}).show()
				})
			}
		}

问题2:使用$parent会产生耦合,如果KInput的老爹不是KFormItem就无效了
由此去看源码:需要“混入(mixin)”
作为混入被引用,一般是为了复用
什么是混入?
官方解释: 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

类型:Array<Object>
详细:
mixins 选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中,使用的是和 Vue.extend() 一样的选项合并逻辑。也就是说,如果你的混入包含一个 created 钩子,而创建组件本身也有一个,那么两个函数都会被调用。
Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
示例:
var mixin = {
created: function () { console.log(1) }
}
var vm = new Vue({
created: function () { console.log(2) },
mixins: [mixin]
})
// => 1
// => 2

通俗理解: 它可以制作一个可以复用的功能,可以复用到各个组件中,同时呢这个mixin功能可以使用vue组件里任意组件选项,比如data,method,watch,和各个生命周期函数。当需要运用的组件,引用它时,相当于把mixin的各个组件选项合并到引用的组件中。
boardcast及dispatch的mixin(来源:VUE源码)
广播boardcast:由上而下派发事件,而dispatch类似于冒泡,是由下向上的去查找componentName相同的组件名并派发事件
apply
apply:方法能劫持另外一个对象的方法,继承另外一个对象的属性
到底怎么理解这句话呢?
apply() 方法接受数组形式的参数,即apply(对象,参数)时,使用了apply,无论是哪个对象使用,对象都变成apply(的这个对象)
因此使用此解决$parent耦合性问题,解决方案如下:
1.参考Elment-UI源码 混入(mixin)的使用,粘贴混入
emitter.js

	// 广播:由上而下派发事件 (组件名,事件名,参数数组)
function broadcast(componentName, eventName, params) {
	// 遍历所有子元素,如果componentName与传入的相同 则派发事件
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
	  // dispatch类似于冒泡,是由下向上的去查找componentName相同的组件名并派发事件
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
// 向上查找 直到找到相同名称的组件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
	  // 如果找到就派发事件
      if (parent) {
		  // 使用apply:为了打散后面的数组,使后面数组的每一个元素对应一个参数
		  // apply作用:劫持另外一个对象的方法,继承另外一个对象的属性
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

2.修改KInput.vue里有关$parent派发事件的问题

//需要增加修改的代码
import emitter from '../../mixins/emitter.js'
export default {
	mixins:[emitter],
	methods:{
		onInput(e){
		
			// this.$parent.$emit('validate');
			//需要找到的老爹组件是KFormItem
			this.dispatch('KFormItem','validate');
			}
	}
}

3 .由于老爹组件是KFormItem,为了找到他 需要在老爹组件是KFormItem给他定义一个名字

export default {
	name:"KFormItem",//这个名字一般用于路由,路由缓存
	componentName:"KFormItem",//这个才是本次需要使用的名字,但他不属于vue,是自定义的
}

问题3:同理解决$children耦合性问题,解决方案如下:
由于广播会造成性能损失(所以还是使用了dispatch),因为他会不断的向下寻找知道找到叶子节点,因此查阅Element-UI的源码(form.vue)发现使用 this.fields这个字段,遍历字段调用validate,因此猜测所有KFormItem的实例所组成的数组。
探索 this.fields的产生发现 ,created()里面的addField事件,因此他与FormItem是有一个通信的,当FormItem挂载的时候会派发一个事件叫addField,会告诉当前的表单实例 我已经来了,你把我注册一下,加到这个数组保存一下,这样将来校验的时候就知道去调谁的校验方法了。
为什么FormItem要在mounted里去派发事件,因为这个时候他的老爹就确确实实存在了,这个时候派发事件通知KForm,新增一个字段 即 FormItem实例,引入混入emitter去派发事件,然后在KForm里监听该事件,一派发就监听(同源码一样在created里监听)
补充:什么是响应式数据? “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。
具体步骤如下:
1.在KFormItem里引用emitter.js

import emitter from '@/mixins/emitter'//@表示src
export default {
	mixins:[emitter],
	mounted() {
	//在mounted里派发事件
		//kkb.form.addField:kkb的库,表单里要新增一个字段(KForm派发)
		//参数是自己 且要是一个数组(通过源码得)
		//if(this.prop),过滤掉没有prop的KFormItem
		if(this.prop){
			this.dispatch('KForm','kkb.form.addField',[this])
		}
	}
}

2.在KForm里监听事件

export default {
	name:"KForm",
	componentName:"KForm",//注意一定不要拼错,不然找不到派发事件
	created() {
			// 不是响应式可写在这,响应式数据需写在data里面
			this.fields=[],
			//监听事件,事件名称,第二个参数是每一个formItem,将所有的item推进数组中
			this.$on('kkb.form.addField',(item) => {
				this.fields.push(item)
			})
		},
	methods:{
			validate(cb){
				// const tasks=this.$children.filter(item=>item.prop).map(item=>item.validate())
				// 上面的filter过滤属性也就可以在派发事件时做判断了,即:
				const tasks = this.fields.map(item=>item.validate())
			}
		}
}

$children解耦
全部基于 vue-study-web20案例

知识点补充

补充1:使用插件进⼀步封装便于使用Notice <~~~> create.js

import Notice from '@/components/Notice.vue' 
//... 
export default {  
install(Vue) {
    Vue.prototype.$notice = function (options) {
          return create(Notice, options)
              }
      }
  }

使用:
this.$notice({title:'xx'})

补充2:递归组件(一般用于多级菜单以及数据有孩子的)

递归组件是可以在它们⾃⼰模板中调⽤⾃身的组件。
例如,下⾯是Node.vue

<template>
  <div>
      <h3>{{data.name}}</h3>
          <!-- 有条件嵌套 -->
       <Node v-for="n in data.children" :key="n.name" :data="n">
       </Node>
  </div>
</template>
<script>
    export default { 
       name: 'Node', // name对递归组件是必要的
        props: {
            data: { 
               type: Object,
                require: true 
             },
        },
    } 
</script>

递归组件的数据通常是⼀棵树,NodeTest.vue

<template>
  <div>
    <Node :data="folder"></Node>
  </div>
</template>
<script>
 import Node from '@/components/recursion/Node.vue';
export default {
  components: {
    Node,
  },
  data() {
    return { 
     // 树形结构的数据
      folder: {
        name: "vue-study",
        children: [
          { name: "src", children: [{ name: "main.js" }] },
          { name: "package.json" }
        ]
      }
    };
  }
 };
</script>

适当添加⼀些缩进

<h3 :style="{'text-indent': indent+'em'}">{{data.name}}</h3>
export default {
  props: {
    // 添加⼀个层级属性
    level: {
      type: Number,
      default: 0
    }
  },
  computed: {
    indent() {
      // 缩进两个字的宽度
      return this.level * 2
     }
  },
}

实现Tree组件
Tree组件是典型的递归组件,其他的诸如菜单组件都属于这⼀类,也是相当常⻅的。
实现tree-node组件,recursion/TreeNode.vue

<template>
  <div>
    <div @click="toggle" :style="{paddingLeft: (level-1)+'em'}">
      <label>{{model.title}}</label>
            <span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
    </div>
    <div v-show="open" v-if="isFolder"> 
     <tree-node class="item" v-for="model in model.children"         :model="model" :key="model.title"        :level="level + 1"></tree-node>
    </div>
  </div>
</template>
<script>
 export default {
  name: "tree-node",
  props: {
    model: Object,
    level: {
      type: Number,
      default: 1
    }
  },
  data: function() {
     return {
      open: false
    };
  },
  computed: {
    isFolder: function() {
      return this.model.children && this.model.children.length;
    }
  },
  methods: {
    toggle: function() {
      if (this.isFolder) {
        this.open = !this.open;
      }
    },
  }
 };
 </script>

实现tree组件,recursion/Tree.vue

<template>
  <div class="kkb-tree">
    <TreeNode v-for="item in data" :key="item.title" :model="item"></TreeNode>
  </div>
</template>
<script>
  import TreeNode from '@/components/recursion/TreeNode.vue';
  export default {
    props: {
      data: {
        type: Array, 
        required: true
       },
    },
    components: {
      TreeNode,
    },
  }
</script>
<style scoped>
 .kkb-tree {
  text-align: left;
 }
</style>

使⽤,recursion/index.vue

<template>
  <Tree :data="treeData"></Tree> 
</template>
<script>
 import Tree from '@/components/recursion/Tree.vue';
  export default {
  data() {
    return {
      treeData: [{
        title: "Web全栈架构师",
        children: [
          {
            title: "Java架构师"
          },
          {
            title: "JS⾼级",
            children: [
              {
                title: "ES6"
              },
              {
                title: "动效"
              }
            ]
          },
          {
            title: "Web全栈",
            children: [
              {
                title: "Vue训练营",
                expand: true,
                children: [
                   {
                    title: "组件化"
                  },
                  {
                    title: "源码"
                  },
                  {
                    title: "docker部署"
                  }
                ]
              },
              {
                title: "React",
                children: [
                  {
                    title: "JSX"
                   },
                  {
                    title: "虚拟DOM"
                  }
                ]
              },
              {
                title: "Node"
              }
            ]
          }
        ]
      }]
    };
  },
  components: {
    Tree,
  },
 };
</script>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值