多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
Vue还提供了很多好用的feature,如**event、v-model、slot、keep-alive,transition**等等。 ## event 处理**组件间的通讯,原生的交互**,都离不开事件。 对于一个组件元素,我们不仅仅可以绑定原生的DOM事件,还可以绑定自定义事件,非常灵活和方便。 ### 编译 ## v-model 很多同学在理解Vue的时候都把**Vue的数据响应原理理解为双向绑定,但实际上这是不准确的**。 我们提到的数据响应,都是通过数据的改变去驱动DOM视图的变化,而双向绑定除了数据驱动DOM外,DOM的变化反过来影响数据,是一个双向关系,在Vue中,我们可以通过v-model来实现双向绑定。 **v-model即可以作用在普通表单元素上,又可以作用在组件上**,其实是一个语法糖。在组件的实现中,我们是可以配置子组件接收的prop名称,以及派发的事件名称。 ## slot Vue的组件提供了**一个非常有用的特性 --slot插槽。它让组件的实现变得更加灵活**。 开发组件库的时候,为了让组件更加灵活可定制,经常用插槽的方式让用户可以自定义内容。插槽分为普通插槽和作用域插槽,它们可以解决不同的场景。 ### 普通插槽 ~~~ let AppLayout = { template: '<div class="container">' + '<header><slot name="header"></slot></header>' + '<main><slot>默认内容</slot></main>' + '<footer><slot name="footer"></slot></footer>' + '</div>' } let vm = new Vue({ el: '#app', template: '<div>' + '<app-layout>' + '<h1 slot="header">{{title}}</h1>' + '<p>{{msg}}</p>' + '<p slot="footer">{{desc}}</p>' + '</app-layout>' + '</div>', data() { return { title: '我是标题', msg: '我是内容', desc: '其它信息' } }, components: { AppLayout } }) ~~~ ~~~ <div> <div class="container"> <header><h1>我是标题</h1></header> <main><p>我是内容</p></main> <footer><p>其它信息</p></footer> </div> </div> ~~~ ### 编译 编译是发生在调用vm.$mount的时候,**所以编译的顺序是先编译父组件,再编译子组件**。 vm.$slots,const slotNodes = this.$slots[name],我们也能根据插槽名称获取到对应的vnode数组了。 在普通插槽里,**父组件应用到子组件插槽里的数据都是绑定到父组件的**,因为**他渲染成vnode的时机的上下文是父组件的实例**。但是一些实际开发中,我们想**通过子组件的一些数据来决定父组件实现插槽的逻辑**,Vue提供了另一种插槽。 ### 作用域插槽 ~~~ let Child = { template: '<div class="child">' + '<slot text="Hello " :msg="msg"></slot>' + '</div>', data() { return { msg: 'Vue' } } } let vm = new Vue({ el: '#app', template: '<div>' + '<child>' + '<template slot-scope="props">' + '<p>Hello from parent</p>' + '<p>{{ props.text + props.msg}}</p>' + '</template>' + '</child>' + '</div>', components: { Child } }) ~~~ 可以看到子组件的**slot标签多了个text属性,以及:msg属性**。父组件实现插槽的部分**多了一个template标签,以及scope-slot属性**。这些就是作用域插槽和普通插槽的写法上的区别。 我们可以通过插槽的名称拿到对应的scopedSlotFn,然后把相关的数据扩展到props上,作为函数的参数传入。 ### 总结 普通插槽和作用域插槽最大的差别是**数据作用域**,**普通插槽是在父组件编译和渲染阶段生成vnodes,**所以**数据作用域时父组件实例**,子组件渲染的时候直接拿到这些渲染好的vnodes。 对**作用域插槽,父组件在编译和渲染阶段不会直接生成vnodes**,而是在**父节点vnode的data中保留了一个scopeSlots对象**,存储着不同名称的插槽以及他们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成vnodes,由于是在子组件环境执行的,所以**对应的数据作用域时子组件实例**。 两种插槽目的:**让子组件slot占位符生成的内容由父组件来决定**,但数据的作用域会根据他们vnodes渲染时机不同而不同。 ## keep-alive 为了组件的缓存优化而使用<keep-alive>,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。 如果没有缓存,每点击一次,内容请就会创建一个组件,该组件会经理整个生命周期,比较浪费性能。 <keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁他们。和<transition>相似。是一个抽象组件,自身不会渲染一个DOM,也不会出现在父组件链接中。 ~~~ <keep-alive v-if="$route.meta.keepAlive"> <router-view></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"> <!-- 这里是不被缓存的视图组件,比如 Edit! --> </router-view> <HomeFooter/> ~~~ 常见用法: ~~~ // 组件 export default { name: 'test-keep-alive', data () { return { includedComponents: "test-keep-alive" } } } ~~~ ~~~ <keep-alive include="test-keep-alive"> <!-- 将缓存name为test-keep-alive的组件 --> <component></component> </keep-alive> <keep-alive include="a,b"> <!-- 将缓存name为a或者b的组件,结合动态组件使用 --> <component :is="view"></component> </keep-alive> <!-- 使用正则表达式,需使用v-bind --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- 动态判断 --> <keep-alive :include="includedComponents"> <router-view></router-view> </keep-alive> <keep-alive exclude="test-keep-alive"> <!-- 将不缓存name为test-keep-alive的组件 --> <component></component> </keep-alive> ~~~ 结合router,缓存部分页面 ~~~ <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view> ~~~ 需要在router中设置router的元信息meta ~~~ //...router.js export default new Router({ routes: [ { path: '/', name: 'Hello', component: Hello, meta: { keepAlive: false // 不需要缓存 } }, { path: '/page1', name: 'Page1', component: Page1, meta: { keepAlive: true // 需要被缓存 } } ] }) ~~~ ### 内置组件 <keep-alive>是Vue源码中实现的一个组件,Vue源码不仅实现了一套组件化的机制,也实现了一些内置组件。 ~~~ export default { name: 'keep-alive, abstract: true, props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, created () { this.cache = Object.create(null) this.keys = [] }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, ~~~ <keep-alive>在created钩子里定义了this.catche和this.keys,**本质上它就是去缓存已经创建过的vnode**,它的props定义了inclued,exclude,**include表示质疑匹配的组件会缓存**,**exclude表示任何匹配的组件都不会被缓存**。max表示缓存的大小。**因为我们是缓存的vnode对象,它也会持有DOM**,当我们缓存很多的时候,会比较占内存,所以可配置缓存大小。 <keep-alive>直接实现了render函数,不是常规的模板方式,执行<keep-alive>组件渲染的时候,就会执行这个render函数。 ~~~ const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) ~~~ 由于我们也是在<keep-alive>标签内部写DOM,所以可以先获取到它的默认插槽,然后在获取第一个节点,<keep-alive>只处理第一个子元素,**所以一般和它搭配使用的有component动态组件或者是router-view**。牢记!! **如果命中缓存,则直接从缓存中拿vnode的组件实例**,并且**重新渲染调整**了key的顺序放在最后一个,否则**把vnode设置进了缓存**(),如果超过max,则从缓存中删除第一个。 ### 动态组件 **让多个组件使用同一个挂载点,并动态切换,这就是动态组件**。 **通过使用保留的`<component>`元素,动态地绑定到它的is特性**,可以实现动态组件。 ~~~ <div id="example"> <button @click="change">切换页面</button> <component :is="currentView"></component> </div> ~~~ ~~~ new Vue({ el:'#example', components:{home,post,archive}, data:{index:0,arr:['home','post','archive']}, computed:{return this.arr[this.index]}, methods:{ change(){this.index = (++this.index)%3} } }) ~~~ 也可以直接绑定到组件对象上。 ### 组件渲染 关注2个方面,首次渲染和缓存渲染 #### 首次渲染 Vue的渲染最后都会到patch过程,而组件的patch过程会执行。createComponent方法。 对于首次渲染而言,除了在<keep-alive>中建立缓存,划分普通组件选人没什么区别 #### 缓存渲染 当我们从B组件再次点击switch切换到A组件,就会命中缓存渲染。 之前分析,当数据发生变化,在patch的过程中会执行patchVnode的逻辑,它会对比新旧vnode节点,甚至对比他们的子节点去做更新逻辑。**但对于组件vnode而言,是没有children的**,对于<keep-alive>如何包裹内容。 原来patchVnode在做各种diff之前,会先执行prepatch的钩子函数。 由于<keep-alive>组件本质上**支持了slot**。重新执行<keep-alive>的render方法,**如果包裹的第一个组件vnode命中缓存,则直接返回缓存**,接着又执行patch过程,再次执行到createComponent方法。 执行init钩子函数时**不会再执行组件的mount过程了**。 这也就是**被`<keep-alive`>包裹的组件在有缓存的时候就不会再执行组件的created、mounted等钩子函数的原因了**。 最后执行**instert(parentElem,vnode.elm,refElm)就把缓存的DOM对象直接插入到目标元素中,完成了在数据更新的情况下的渲染过程**。 ### 生命周期 组件一旦被<keep-alive>缓存,那么**再次渲染的时候就不会执行created、mounted等钩子函数了**。但是我们很多业务场景都是希望在我们被缓存的组件再次被缓存的时候做一些事情,好在Vue提供了activated钩子函数。 activated:执行时机是<keep-alive>包裹的组件渲染的时候。 虽然把所有的页面都基于缓存不用发起第二次请求,但是对于某些页面来说,如动态路由,需要根据接收的不同参数来获取不同的数据呢? 1. **页面第一次进入的时候,钩子触发顺序created->mounted=>actived** 2. **页面退出的时候触发deactived,当再次前进或者后退的时候只触发activated。** ### 总结 * <keep-alive>组件是一个**抽象组件** * 它的实现通过**自定义render函数并且利用了插槽**, * 并且知道了<keep-alive>**缓存vnode**,了解组件包裹的子元素--也就是插槽是如何做更新的。 * 且在patch过程中对于已缓存的组件**不会执行mounted**,所以不会有一般的组件的生命周期函数, * 但又提供了activated和deactivated构造函数。 * <keep-alive>的props除了include和exclude还有max,控制缓存的个数。 ## transition 需求:一个DOM节点的插入和删除或者是显示和隐藏,我们不想让它特别生硬,通常会考虑加一些过渡效果。 Vue.js除了实现了强大的数据驱动,组件化的能力,也给我们提供了一整套过渡的解决方案。 它内置了<transition>组件,我们可以**利用它配合一些css3样式**,**很方便的实现过渡动画**,也可以利用它**配合Javascript的钩子函数实现过渡动画**。 在下列情形中,可以给任何元素和组件添加**entering/leaving过渡**。 * 条件渲染(v-if) * 条件展示(v-show) * 动态组件 * 组件根节点 ~~~ let vm = new Vue({ el: '#app', template: '<div id="demo">' + '<button v-on:click="show = !show">' + 'Toggle' + '</button>' + '<transition :appear="true" name="fade">' + '<p v-if="show">hello</p>' + '</transition>' + '</div>', data() { return { show: true } } }) ~~~ ~~~ .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } ~~~ ## 总结 Vue的过渡实现: 1. 自动嗅探目标元素是否应用了css过渡或动画,如果是,在恰当的时机添加/删除css类名 2. 如果过渡组件提供了Javascript钩子函数,这些钩子函数会在恰当的时机被调用。 3. 如果没有找到Javascript钩子,并且也没有检测到css过渡/动画,DOM操作(插入/删除)在下一帧立即执行。 所以真正执行动画的是我们**写的 CSS 或者是 JavaScript 钩子函数**,而 Vue 的`<transition>`只是**帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机**。 <transition>组件的实现,它的render阶段只获取了一些数据,并且返回了渲染的vnode,并没有任何和动画相关,vnode patch的过程中,会执行很多钩子函数,那么对于过渡的实现,它只接收了**create和activate2个钩子函数**,**create钩子函数只有当节点的创建过程才会执行,而remove会在节点销毁的时候执行**,这就印证了<transition>必须要满足**v-if、动态组件、组件根节点条件之一**了。对于v-show在它的指令的钩子函数中也会执行相关逻辑。 **过渡动画提供了2个时机,一个是create和activate的时候提供了entering进入动画,一个是remove的时候提供了leaving离开动画**。 ## transition-group <transition>只能针对单一元素实现过渡效果,列表需求,对列表元素的添加和删除,有时也希望有过渡效果。 ~~~ let vm = new Vue({ el: '#app', template: '<div id="list-complete-demo" class="demo">' + '<button v-on:click="add">Add</button>' + '<button v-on:click="remove">Remove</button>' + '<transition-group name="list-complete" tag="p">' + '<span v-for="item in items" v-bind:key="item" class="list-complete-item">' + '{{ item }}' + '</span>' + '</transition-group>' + '</div>', data: { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) } } }) ~~~ ~~~ .list-complete-item { display: inline-block; margin-right: 10px; } .list-complete-move { transition: all 1s; } .list-complete-enter, .list-complete-leave-to { opacity: 0; transform: translateY(30px); } .list-complete-enter-active { transition: all 1s; } .list-complete-leave-active { transition: all 1s; position: absolute; } ~~~ ### render函数 <transition=group>组件也是由render函数渲染成vnode。 **<transition=group>组件非抽象组件,不同于<transition>组件**。 它会渲染成一个真实元素,默认tag是span。 如果`transition-group`只实现了这个`render`函数,那么每次插入和删除的元素的缓动动画是可以实现的,在我们的例子中,新增一个元素,它的插入的过渡动画是有的,但是剩余元素平移的过渡效果是出不来的,接下来我们来分析`<transition-group>`组件是如何实现剩余元素平移的过渡效果的。 ### move过渡实现 其实在实现元素的插入和删除,无非就是操作数据,控制它们的添加和删除。比如我们新增数据的时候,会添加一条数据,除了重新执行render函数渲染新的节点外,**还要触发updated钩子函数**, * 判断子元素是否定义move相关样式(克隆一个DOM节点,移除它所有其他的过渡Class;添加moveClass样式,设置display为none,添加到根节点上,然后从根节点上删除这个克隆节点子节点) * 子节点预处理 * 遍历子元素实现move过渡 ### 总结 实现了列表的过渡,以及它会渲染成真实的元素。 当我们去修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本身的过渡动画,这点和`<transition>`组件实现效果一样,除此之外`<transtion-group>`还实现了 move 的过渡效果,让我们的列表过渡动画更加丰富。