这文章在一年前已经写出来了。今天仍是决定放出来供全部人学习。为何我会写vue组件通讯全揭秘,由于不管任何组件模式的框架。组件是核心,只有把组件写组件之间能理顺了。项目也就天然顺了。内容很是多,并且一年后我以为组件的通讯部分的没有任何变化。没有任何一点过期。但愿对你们有帮助前端
Vue 是尤雨溪一我的的项目,是一套构建用户界面的渐进式框架。与其余重量级框架不一样的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不只易于上手,还便于与第三方库或既有项目整合。另外一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也彻底可以为复杂的单页应用程序提供驱动。vue
Vue 经过 Api 来进行统一性的管理,可让整个团队的代码都用统一的风格和方法标准去运做,并且对组件系统也有强大的支持,在封装组件时经过 Props 和 Event 两个标准性的原则去调用,可让开发更加驾轻就熟。node
若是非要让我说一个不学 Vue 的理由,多是它的写法太方便了……你也可能以为它借鉴的太多,没有亮眼的地方,那我只能说一样实现的东西就是那么方便,简洁的教程和 Api 文档接入整个开发体系,至关符合中国市场的开发——业务变更大、版本要求上线快、需求改动频繁、学习成本低……相比之下,一样有着高效的功能,集成了组件系统和 Virtual DOM。webpack
掌握 Vue 主要就是正确理解教程和深刻掌握 Api 的用法,不但要会用,更重要的是学会对症下药,在任何一种场景下使用最简洁、最正确、最合理的代码才是关键。只有对 Api 和教程有了必定程度的项目实战和组件库实战经验才能把它用的游刃有余。程序员
在 Vue2.0 起步的时候我在掘金上进行了 Vue 课程的一系列套课的讲解,从基础到 Vuex,最后到组件库的实战都进行了简单的讲解。期间也通过大量的项目实战和组件库的实践,经过一步步总结,对 Api 文档的深刻理解和测试性模拟,总结了一些真实场景的正确用法和经常使用案例需求,让你在开发中少走弯路,少刨坑。es6
在 Vue 开发中,咱们不但要准确的运用 Api,还要结合 es6 的新语法,用更深更强大的新特性来组织代码,这一样也是下一代 Javascript 的标准:web
在这次教程中将会展现 es6 大量的新语法进行,只有不断的进行尝试,才能有不一样的成效。vue-router
若是你想快速上手进行一个特别面向 C 端的 Mobile 产品开发,甚至是一个中大型的项目开发,若是你能彻底阅读完全部课程,而且跟着一步步实践,那么你一样也能给本身的C端产品设计一套属于本身的组件库,毕竟通用型的组件库仍然具有面对市场竞争需求的独特性。vuex
本课程分享的内容是 Vue 的最新版本,能够说这是一套独一无二的教程,不但会结合官方教程和 Api,最主要的是告诉你们在什么场景用什么方式组织代码,避开没必要要的坑。vue-cli
数据驱动架构体系永远离不开组件模式。在这里我会给你们分享级别组件的划份内部原则性,在本身打造组件库的同时,也大量借鉴了各大厂商团队的优秀组件写法,进行比对优缺点,总结相应的理论。
$on
, $emit
, v-on 三者关系$attrs
,$listeners
深组件通讯当我在掘金写下第一篇文章的时候,虽然只是很基础的部分,但文章在两天内得到了大量关注,这充分显示出了中国市场的开发者们对 Vue 的渴望程度。与此同时,我也收集到了一些批评意见,对于读者的反馈能及时作出响应才更能体现出一个课程的价值。
不是能写出源码的教程就是对你有帮助,也并非写的很基础就对你没有帮助。不是每一个人都能当大上牛、进入大公司的研发团队,大多数程序员都是面对业务层面的开发。所以如何在市场上有立锥之地,能快速接手项目,这才是大部分人应该最须要发力的地方。
学习本课程的同窗须要对 Html 和 JavaScript 的基础知识有必定了解,理解 es6 基础新特性,了解 npm 和 node 的基本用法。
推荐:
别浪费时间看别的了,若是你能静下来看完整本书,比任何 es6 其它书籍都好,为何呢?平民化,就像 Vue 同样,很容易让人理解。
同时在学习本教程的时候,尽可能跑一遍 Vue 中文官网结合 Api 你能看懂的示例。
可能有些 Api 或者教程只有一个简单的解释,还特别官方话,不要紧,跟着我一步一步敲遍全部的 Demo。
在整理好心情开始旅程之时,咱们每每都会带上许多必备工具,一样 Vue 在面向开源之时,周边的身态也向其靠拢。
vue-devtool
以往 Dom 操做的时候,咱们都是经过 dubger 断点来进行错点查找和基础数据驱动,dubger 已经派不上什么用场了,只有经过观察数据的变化,才能准确的定位到错识变化的数据和是否执行了须要的事件。
就用商店输入vue自行安装
vue-cli
Vue.js
提供一个官方命令行工具,可用于快速搭建大型单页应用。该工具提供开箱即用的构建工具配置,带来现代化的前端开发流程。只需几分钟便可建立并启动一个带热重载、保存时静态检查以及可用于生产环境的构建配置的项目:
进入 Node.js 官网,下载 Node.js 安装包;
为了下载安装包快速一点,走淘宝源进入 cli 终端;
运行npm install -g cnpm --registry=https://registry.npm.taobao.org 全局安装 vue-cli $ npm install --global vue-cli 建立一个基于 webpack 模板的新项目 $ vue init webpack my-project 安装依赖,走你 $ cd my-project $ npm install $ npm run dev 复制代码
打开文件夹,本次教程的示例所有经过 Components 文件夹来定义单个组件,进行 SPA 的应用开发,用单 .vue 文件也更加直观,一个文夹多是一个 Page,也多是一个 Component;
在开启 Vue 的旅程之时,拿 todo-list 尝试一下它的神奇魔法,经过 Vue 实例和模板进行数据与行为的交互绑定;
实例的每一个选项如何与定义的模板值进行一一对应,经过数据驱动、事件绑定,来轻松高效的实现一个 todoList 应用。相比 Juqery 这种操做 Dom 的冷兵器时代,给开发者的感受是彻底变了一种模式,延续着 Html 写法的友好性和适应度,一样还提供了 JSX 语法,Vue 官网说是一个渐进式框加,写法也一样是渐近式,让开发者以不畏惧的心态使用,并且 Vue 的数据驱动模式提供了大量的 Api,每一个 Api 不管是实例选项仍是实例属性都负责着本身的职责,它们就像五金店的零件同样,只有正确的使用每一个 Api 特性而且做用到恰当的地方,Vue 工程代码组织结构和后续的维护才会显得易如反掌。在组件化工程化没到来的时候,业务的实现复杂度并非最难的,反而使人头疼的是对代码后续的版本迭代、重构、复用等一系列问题,但愿经过简单的 todo-list 应用,能够对前端开发革命有新的认识!
<template> <div> <input type="text" v-model.trim="msg" @keyup.enter="push"> <ul> <li v-for="(item,index) in list" :key="index" @click="deleteItem(index)"> {{index}} {{item.name}} </li> </ul> </div> </template> <script> export default { name: 'todo-list', data () { return { msg: "", list: [] } }, methods: { push () { this.list.push({name:this.msg}) this.msg = "" }, deleteItem (index) { this.list.splice(index,1) } } } </script> 复制代码
本章经过这个示例 Demo 表现 Vue 数据驱动式框架运做是如何简单到使人窒息。
一个 todo-list 应用集成了两个事件,两条 data 数据就完成了!
经过 Template 里的 Html 模版能清楚的观察到绑定信息,数据联动和时时改动:
对于往时操做dom写法和当前的数据驱动有什么区别?
以上只是一个简单 todo-list Demo 总结出来的例子,文中所提到的也只是部分功能优点,还有不少功能可让开发路径更加快速。重点在于数据驱动的模式,只要把组件与组件之间的通讯掌握了,也就至关于你就手握大半江山,由于一切的一切都是基于组件通讯模式和结构用法来的。
下篇课程导读:
数据驱动一切都是一数据,只有灵活把控对数据的理解,才能自如的运用,在 Vue 里灵活的 data,死板的 props,是存放数据的和传递数据的基点。
在前端来讲数据驱动式框架,必然离不开事件驱动,事件驱动必定程度上弥补了数据驱动的不足,在dom操做
的时代一般都是这样操做:
经过特定的选择器查找到须要操做的节点 -> 给节点添加相应的事件监听
响应用户操做,效果是这样:
用户执行某事件(点击,输入,后退等等) -> 调用 JavaScript 来修改节点
这种模式对业务来讲是没有什么问题,可是从开发成本和效率来讲会比较力不从心,在业务系统愈来愈庞大的时候,就显得复杂了。另外一方面,找节点和修改节点这件事,效率自己就很低,所以出现了数据驱动模式。
读取模板,同时得到数据,并创建 VM( view-model ) 的抽象层 -> 在页面进行填充
要注意的是,MVVM
对应了三个层,M - Model
,能够简单的理解为数据层;V - View
,能够理解为视图,或者网页界面;VM - ViewModel
,一个抽象层,简单来讲能够认为是 V 层中抽象出的数据对象,而且能够与V 和 M 双向互动
(通常实现是基于双向绑定,双向绑定的处理方式在不一样框架中不尽相同)。
用户执行某个操做 -> 反馈到 VM 处理(能够致使 Model 变更) -> VM 层改变,经过绑定关系直接更新页面对应位置的数据
能够简单地理解:数据驱动不是操做节点的,而是经过虚拟的抽象数据层来直接更新页面。主要就是由于这一点,数据驱动框架才得以有较快的运行速度(由于不须要去折腾节点),而且能够应用到大型项目。
Vue 经过{{}}
绑定文本节点,data
里动态数据与Props
静态数据进行一个映射关系,当data
中的属性或者props
中的属性有变更,以上二者里的每一个数据都是行为操做须要的数据或者模板 view 须要渲染的数据,一旦其中一个属性发生变化,则全部关联的行为操做和数据渲染的模板上的数据同一时间进行同步变化,这种基于数据驱动的模式更简便于大型应用开发。只要合理的组织数据和代码,就不会显得后续皮软。
二者选项里均可以存放各类类型的数据,当行为操做改变时,全部行为操做所用到和模板所渲染的数据同时都会发生同步变化。
Data 被称之为动态数据的缘由,在各自实例中,在任何状况下,咱们均可以随意改变它的数据类型和数据结构,不会被任何环境所影响。
Props 被称之为静态数据的缘由,在各自实例中,一旦在初始化被定义好类型时,基于 Vue 是单向数据流,在数据传递时始终不能改变它的数据类型。
更为关键地是,对数据单向流的理解,props
的数据都是经过父组件或者更高层级的组件数据或者字面量的方式进行传递的,不容许直接操做改变各自实例中的props
数据,而是须要经过别的手段,改变传递源中的数据。
当一个实例建立的时候,Vue
会将其响应系统的数据放在data选项中
,当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。初始定行的行为代码也都会随着响应系统进行一个映射。
而 data 选项中的数据在实例中能够任意改变,不受任何影响,前提必须数据要跟逻辑相辅相成。
<template> <div> <p v-if='boolean'>true</p> <p v-for='value in obj'>{{value}}</p> <p v-for='item in list'>{{item}}</p> <p>{{StringMsg}}</p> <p>{{NumberMsg}}</p> </div> </template> <script> export default { data () { return { obj : {a:'1',b:'2',c:'3'}, list:['a','b','c'], boolean : true, StringMsg : 'hello vue', NumberMsg : 2.4, } } } </script> 复制代码
运行代码时,在data选项
里定义了五种数据类型,经过指令和{{}}
进行渲染,证明了data选项
里能够定义任何数据类型
。
<template> <div> <p>{{StringMsg}}</p> <p>{{NumberMsg}}</p> <button @click='changeData'>改变数据</button> </div> </template> <script> export default { data () { return { StringMsg : 'hello vue', NumberMsg : 2.4 } }, methods: { changeData () { this.StringMsg = 2.4; this. NumberMsg = 'hello vue' } } } </script> 复制代码
每一个.vue 的文件则就是一个实例,在 data 中定义了两种数据:
同时还定义了一个 changeData 事件。
在运行代码时候,data选项
已经进入了Vue的响应系统
里,model层
(数据层)与view层
(视图层)进行了对应的映射,任何数据类型均可以定义。
当用户发生点击操做的时候,同时能够把 StringMsg, NumberMsg 的数据对调,充分说明了,不管值和类形均可以进行随意转换
。
<template> <div> <p>{{StringMsg}}</p> <p>{{NumberMsg}}</p> <button @click='changeData'>改变数据</button> <button @click='findData'>查看数据</button> </div> </template> <script> export default { data () { return { StringMsg : 'hello vue', NumberMsg : 2.4 } }, methods: { changeData () { this.StringMsg = 2.4; this.NumberMsg = 'hello vue' }, findData () { console.log(`StringMsg: ${this.StringMsg}`) console.log(`NumberMsg: ${this.NumberMsg}`) } } } </script> 复制代码
改变数据之后,经过点击 findData 事件来进行验证,虽然在初始化定义好了行为数据的检测代码,可是当数据在执行 findData 以前先执行 changeData,一旦改变 data 选项里的数据时,findData 里对应的数据同时也会进行相应的映射。
this.StringMsg //=> 2.4
this.NumberMsg //=>'hello vue'
总结:
使用props
传递数据做用域是孤立的,它是父组件经过模板传递而来,想接收到父组件传来的数据,须要经过props选项
来进行接收。
子组件须要显示的声明接收父组件传递来的数据的数量
,类型
,初始值
。
简单的接收能够经过数组的形式来进行接收。
<template> <div> <demo :msg='msgData' :math = 'mathData' ></demo> </div> </template> <script> import Demo from './Demo.vue' export default { data () { return { msgData:'从父组件接收来的数据', mathData : 2 } }, components : { Demo } } </script> 复制代码
<template> <div> <p>{{msg}}</p> <p>{{math}}</p> </div> </template> <script> export default { name: 'demo', props: [ 'msg' , 'math'], } </script> 复制代码
在子组件中须要经过显示定义好须要从父组件中接收那些数据。
一样的在父组件中在子组件模板中过v-bind
来传递子组件中须要显示接收的数据。
语法: :== v-bind(是封装的语法糖) :msg = msgData
props 能够显示定义一个或一个以上的数据,对于接收的数据,能够是各类数据类型,一样也能够传递一个函数。
<template> <div> <demo :fn = 'myFunction' ></demo> </div> </template> <script> import Demo from './Demo.vue' export default { components : { Demo }, methods: { myFunction () { console.log('vue') } } } </script> 复制代码
<template> <div> <button @click='fn'>按钮</button> </div> </template> <script> export default { name: 'demo', props: [ 'fn' ], } </script> 复制代码
一样,在父组件中也能够向子组件中传递一个function
,在子组件一样也能够执行父组件传递过来的 myFunction 这个函数。
对于字面量语法和动态语法,初学者在父组件模板中向子组件中传递数据时加和不加 v-bind 有什么区别,同时会引发什么错语等问题会感受迷惑。
v-bind:msg = 'msg'
经过 v-bind 进行传递数据而且传递的数据并非一个字面量,双引号里的解析的是一个表达式
,一样也能够是实例上定义的数据和方法(其实就是引用一个变量)"。
msg='11111'
没有 v-bind 的模式下只能传递一个字面量,这个字面量只限于 String 类量,字符串类型。
注意:
虽然经过字面量模式下,传任何类型都会被转成字符串类型,可是在子件接收的时候能够经过 typeof 去进行类型检测。
想经过字面量进行数据传递时,若是想传递非String类型
,必须props
名前要加上v-bind
,内部经过实例寻找,若是实例方没有此属性和方法,则默认为对应的数据类型
。
:msg='11111'
//number :msg='true'
//bootlean :msg='()=>{console.log(1)}
//function :msg='{a:1}
//object
HTML 特性是不区分大小写的,因此当使用的不是字符串模板,camelCased (驼峰式) 命名的 prop 须要转换为相对应的 kebab-case (短横线隔开式) 命名。
因为文档上仍然有这句话,通过测试后,不管是否是字符串模板,camelCased (驼峰式) 和 kebab-case (短横线隔开式) 二者均可以。
为了直观性,规范性仍是推荐 kebab-case (短横线隔开式)。
props 原子化可让总体代码逻辑和向外暴露须要传递数据的接口很是清晰,可是一样能够把子组件须要接收的 props 在父组件中以一个对象进行传递。
当传递的数量一旦多到已经让原子化再也不结构清晰的时候,经过一个对象传递显得更为简洁明了。
<template> <div> <demo v-bind= 'msg' ></demo> </div> </template> <script> import Demo from './Demo.vue' export default { components : { Demo }, data () { return { msg : {a:1,b:2} } } } </script> 复制代码
<template> <div> <button>按钮</button> </div> </template> <script> export default { name: 'demo', props: ['a','b'], created () { console.log(this.a) console.log(this.b) }, } </script> 复制代码
<demo v-bind= 'msg' ></demo>
内部发生了什么?在子组件模板内部对其进行了一个封装
,把其展开则跟 props 原子化原理是一个原理
<demo :a='a' :b='b' ></demo>
一般状况下建议使用第二种,props 原子化。
在 data 选项中,当前实例(当前组件中改动)能够任意改变data选项里的数据
,Vue
传递数据时是基于数据单向流动
,子组件不能改变当前实例中的 props 任何属性,须要通知父组件改变相应的值,从新改变。
<template> <div> <button @click='changeProps'>按钮</button> </div> </template> <script> export default { name: 'demo', props: ['msg'], methods: { changeProps () { this.msg = 'new msg' } } } </script> 复制代码
直接改变 props 时会发生一个警告报错
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "msg"
防止数据的不可控性
,不能显示的直接改变,父组件的传递来的数据和子组接 props 接收的数据也是同步响应的,一旦父组件向下传递的数据改变时,prop 接收的数据值也会一样发生变化。
单向数据流的缘由也是如此,就像河流同样,水只会从高向低流,想让水的质量改变,只有从源头改变。
<template> <div> <demo :msg = 'msg' ></demo> <button @click='msg = "new vue"'>按钮</button> </div> </template> <script> import Demo from './Demo.vue' export default { components : { Demo }, data () { return { msg : 'vue' } } } </script> 复制代码
在父组件中初始化传递事后,想要改变子组件的数据,能够经过再次改变向子组件传递的 msg 数据,子组件渲染的视图一样会跟着同步改动。
虽然 props 是不可改动的,上面的 case 是父组件进行改变自身实例的的数据,这个实现很简单,有时通过一次数据传递,不需用父组件再次传递,由于一些需求须要改动 props 数据,能够用过渡的方法,让其转换为一个可变的数据。
props: ['msg'], data: function () { return { myMsg: this.msg } } 复制代码
在 data 选项里经过 myMsg 接收 props msg 数据,至关于对 myMsg = msg 进行一个赋值操做,不只拿到了 myMsg 的数据,并且也能够改变 myMsg 数据。
this.myMsg = 'new Vue' myMsg 会发生相应的改变。
依然是经过 props 一次性接收,想对接收的 prop 进行一些过滤操做再次进行视图渲染,能够在一些计算属性中进行操做,能够 computed 监听 props 里的数据变化,通过过滤操做返回一个须要的值。
props:['msg'] computed : { computedMsg () { return this.msg + 1 } } 复制代码
注意: 在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,若是 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。不要对父组件传递来的引用类型数据进行过滤。
下篇导读
本章对 props 和 data 的用法理解已经进行了全面的讲解,经过再次改变传递数据时是在父组件的实例里进行实施的。每每特定的需求和一些组件封装触发传递的命令并不能直接在父组件执行,须要子组件通知上层组件。
再近一步说,子组件改变不了父组件传递的数据,可是子组件能够用通讯的方式,通知子组件改动,所以 $on
,$emit
,v-on 深刻理解这三者关系尤其重要!
$emit
,$on
的关系$on(eventName)
监听事件$emit(eventName)
触发事件若是把Vue
当作一个家庭(至关于一个单独的components
),女主人一直在家里指派($emit)
男人作事,而男人则一直监听($on)
着女士的指派($emit)里eventName
所触发的事件消息,一旦 $emit
事件一触发,$on
则监听到 $emit
所派发的事件,派发出的命令和执行派执命令所要作的事都是一一对应的。
Api 中的解释:
vm.$emit( event, […args] )
参数:
{string} event
[...args]
触发当前实例上的事件。附加参数都会传给监听器回调。
vm.$on( event, callback )
参数:
{string | Array<string>}
event (数组只在 2.2.0+
中支持) {Function} callback
用法:
监听当前实例上的自定义事件。事件能够由 vm.$emit
触发。回调函数会接收全部传入事件触发函数的额外参数。
<template> <div> <p @click='emit'>{{msg}}</p> </div> </template> <script> export default { name: 'demo', data () { return { msg : '点击后女人派发事件' } }, created () { this.$on('wash_Goods',(arg)=> { console.log(arg) }) }, methods : { emit () { this.$emit('wash_Goods',['fish',true,{name:'vue',verison:'2.4'}]) } } } </script> 复制代码
以上案例说了什么呢?在文章开始的时候说了 $emit
的(eventName)是与 $on(eventName)
是一一对应的,再结合以上两人在组成家庭的以前,女人会给男人列一个手册,告诉男人我会派发 $(emit)
那些事情,男人则会在家庭组成以前 $on(eventName)
后应该如何作那些事情。
经过以上说明我来进一步解释一下官方 Api 的意思。
vm.$emit( event, […args] )
参数:
{string} event
[...args]
this.$emit('wash_Goods','fish') 复制代码
vm.$on( event, callback )
参数:
{string | Array<string>}
event (数组只在 2.2.0+
中支持)
第一个参数是相对于 $emit (eventName)
一一对应的 $on (eventName)
,二者是并存的、必须是 String 类型的。
(数组只在
2.2.0+中支持)
或者是Array<String>
数组中必须包含的是 String 项,后面再具体说。
故事中就是男人在组件一个家庭 (components) 的时候所监听的事件名。
{Function} callback
第二个参数则是一个 function,一样也被叫做以前回调函数,里面能够接收到由 $emit 触发时所传入的参数(若是是单个参数)。
故事中是男人在接收到女人派发的事情该去作那些事情。
{string | Array} event (数组只在
2.2.0+
中支持)
在2.2中新增这个 Api 牵扯了另外一种方式,也存在这其它的独特用法。
继续延续故事,当女人派发的事情多了,我相信做为男人也会以为很烦,一旦听到事件的时候确定会很烦躁,总会抱怨两句。
若是女人在组成家庭以前,告诉男人将要监听那些事情,若是作一件事就抱怨一次,启不是画蛇添足,因此咱们能够经过Array<string> event
把事件名写成一个数组,在数组里写入你所想监听的那些事件,使用共享原则去执行某些派发事件。
<template> <div> <p @click='emit'>{{msg}}</p> <p @click='emitOther'>{{msg2}}</p> </div> </template> <script> export default { name: 'demo', data () { return { msg : '点击后女人派发事件', msg2 : '点击后女人派发事件2', } }, created () { this.$on(['wash_Goods','drive_Car'],(arg)=> { console.log('事真多') }) this.$on('wash_Goods',(arg)=> { console.log(arg) }) this.$on('drive_Car',(...arg)=> { console.log(BMW,Ferrari) }) }, methods : { emit () { this.$emit('wash_Goods','fish') }, emitOther () { this.$emit('drive_Car',['BMW','Ferrari']) } } } </script> 复制代码
以上案例说明了当女人不管是派发drive_Car
或者是wash_Goods
事件,都会打印出事真多
,再执行一一对应监听的事件。
一般状况下,以上用法是毫无心思的。在日常业务中,这种用法也用不到,一般在写组件的时候,让$emit在父级做用域中
进行一个触发,通知子组件的进行执行事情。接下来,能够看一个经过在父级组件中,拿到子组件的实例进行派发事件,然而在子组件中事先进行好派好事件监听的准备,接收到一一对应的事件进行一个回调,一样也能够称之为封装组件向父组件暴露的接口。
<template> <div> <slot name="list"></slot> <div class="list-donetip" v-show="!isLoading && isDone"> <slot>没有更多数据了</slot> </div> <div class="list-loading" v-show="isLoading"> <slot>加载中</slot> </div> </div> </template> <script type="text/babel"> export default { data() { return { isLoading: false, isDone: false, } }, props: { onInfinite: { type: Function, required: true }, distance : { type : Number, default:100 } }, methods: { init() { this.$on('loadedDone', () => { this.isLoading = false; this.isDone = true; }); this.$on('finishLoad', () => { this.isLoading = false; }); }, scrollHandler() { if (this.isLoading || this.isDone) return; let baseHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight let moreHeight = this.scrollview == window ? document.body.scrollHeight : this.scrollview.scrollHeight; let scrollTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop if (baseHeight + scrollTop + this.distance > moreHeight) { this.isLoading = true; this.onInfinite() } } }, mounted() { this.scrollview = window this.scrollview.addEventListener('scroll', this.scrollHandler, false); this.$nextTick(this.init); }, } </script> 复制代码
对下拉组件加载加更的组件进行了一个简单的封装:
data 参数解释:
false 表明正在执行下拉加载获取更多数据的标识
,true表明数据加载完毕
false 表明数据没有全完加载完毕
,true 表明数据已经所有加载完毕
props 参数解释:
父组件向子组件传入当滚动到底部时执行加载数据的函数
距离滚动到底部的设定值
mounted
的时候,对window
对像进行了一个滚动监听,监听的函数为scrollHandler
isLoading,isDone
任何一个为true时则退出
isloading
为true
时防止屡次一样加载,必须等待加载完毕isDone
为true
时说明全部数据已经加载完成,没有必要再执行scrollHandler
loadedDone
一旦组件实例$emit('loadedDone')事件时,执行回调,放开加载权限finishLoad
一旦组件实例$emit('finishLoad')事件时,执行回调,放开加载权限if (this.isLoading || this.isDone) return;
一旦一者为true,则退出,缘由在mounted已经叙述过了if (baseHeight + scrollTop + this.distance > moreHeight)
当在window对象上监听scroll事件时,当滚动到底部的时候执行
this.isLoading = true;
防止重复监听this.onInfinite()
执行加载数据函数父组件中调用 infinite-scroll 组件
<template> <div> <infinite-scroll :on-infinite='loadData' ref='infinite'> <ul slot='list'> <li v-for='n in Number'></li> </ul> </infinite-scroll> </div> </template> <script type="text/babel"> import 'InfiniteScroll' from '.......' //引入infinitescroll.vue文件 export default { data () { return { Number : 10 } }, methods : { loadData () { setTimeout(()=>{ this.Number = 20 this.$refs.infinite.$emit('loadDone') },1000) } } } </script> 复制代码
在父组件中引入 infinite-scroll 组件
当滑到底部的时候,infinite-scroll 组件组件内部会执行传入的:on-infinite='loadData'
函数 同时在内部也会把 Loading 设置为 true,防止重复执行。
在这里用this.$refs.infinite
拿到infinite-scroll
组件的实例,同时触发事件以前在组件中 $on
已经监听着的事件,在一秒后进行改变数据,同时发出loadDone
事情,告诉组件内部去执行loadDone
的监听回调,数据已经所有加载完毕,设置this.isDone = true;
一旦isDone
或者isLoading
一者为true
,则一直保持return退出状态
。
$emit
和 $on
必须都在实例上进行触发和监听。
第一阶段 $emit
和 $on
的二者之间的关系讲完了,接下来该说说 v-on 与 $emit
的关系。
另外,父组件能够在使用子组件的引入模板直接用 v-on 来监听子组件触发的事件。
v-on 用接着故事直观的说法就是,在家里装了一个电话,父母随一直听着电话,一样也有一本小册子,在组成家庭以前,也知识要去监听那些事。
不能用 $on 侦听子组件释放的事件,而必须在模板里直接用 v-on 绑定
。
上面 Warn 的意思是$emit和$on只能做用在一一对应的同一个组件实例
,而v-on只能做用在父组件引入子组件后的模板上
。
就像下面这样: <children v-on:eventName="callback"></children>
就拿官方的这个例子说吧,其实仍是很直观的:
<div id="counter-event-example"> <p>{{ total }}</p> <button-counter v-on:increment="incrementTotal"></button-counter> <button-counter v-on:increment="incrementTotal"></button-counter> </div> Vue.component('button-counter', { template: '<button v-on:click="incrementCounter">{{ counter }}</button>', data: function () { return { counter: 0 } }, methods: { incrementCounter: function () { this.counter += 1 this.$emit('increment') } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, methods: { incrementTotal: function () { this.total += 1 } } }) 复制代码
这样的好处在哪里?虽然 Vue
是进行数据单向流的,可是子组件不能直接改变父组件的数据,(也不是彻底不能,但不推荐用),标准通用明了的用法,则是经过父组件在子组件模板上进行一个 v-on
的绑定监听事件,同时再写入监听后所要执行的回调。
在counter-event-example
父组件里,声明了两个button-count
的实列,经过 data
用闭包的形式,让二者的数据都是单独享用的,并且v-on
所监听的 eventName
都是当前本身实列中的 $emit
触发的事件,可是回调都是公用的一个 incrementTotal
函数,由于个实例所触发后都是执行一种操做!
若是你只是想进行简单的进行父子组件基础单个数据进行双向通讯的话,在模板上经过 v-on
和所在监听的模板实例上进行 $emit
触发事件的话,未免有点多余。一般来讲经过 v-on
来进行监听子组件的触发事件的话,咱们会进行一些多步操做。
<template> <div> <p @click='emit'>{{msg}}</p> </div> </template> <script> export default { name: 'demo', data () { return { msg : '点击后改变数据', } }, methods : { emit () { this.$emit('fromDemo') }, } } </script> 复制代码
<template> <div class="hello"> <p>hello {{msg}}</p> <demo v-on:fromDemo='Fdemo'></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', data () { return { msg: '数据将在一秒后改变' } }, methods: { waitTime() { return new Promise(resolve=>{ setTimeout(()=> { this.msg = '数据一秒后改变了' resolve(1) },1000) }) }, async Fdemo () { let a = await this.waitTime(); console.log(a) } }, components : { Demo } } </script> 复制代码
从上面 demo 能够看出当子组件触发了 fromDemo
事件,同时父组件也进行着监听。
当父组件接收到子组件的事件触发的时候,执行了async
的异步事件,经过一秒钟的等秒改变 msg
,再打印出回调后经过 promise
返回的值。
接下来想通的此例子告诉你们,这种方法一般是经过监听子组件的事件,让父组件去执行一些多步操做,若是咱们只是简单的示意父组件改变传递过来的值用此方法就显的多余了。
咱们进行一些的改动:
children
<template> <div> <p @click='emit'>{{msg}}</p> </div> </template> <script> export default { name: 'demo', props: [ 'msg' ], methods : { emit () { this.$emit('fromDemo','数据改变了') }, } } </script> 复制代码
parent
<template> <div class="hello"> <demo v-on:fromDemo='Fdemo' :msg='msg'></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', data () { return { msg: '数据没有改变' } }, methods: { Fdemo (arg) { this.msg = arg } }, components : { Demo } } </script> 复制代码
上面 demo
中子组件从父组件接收一个 msg
数据,可是想点击按钮的时候,改变父组件的 msg
,进行父组件的数据改动,同时再次改变子组件的 msg
,可是最简便的方法则是直接改变 prop
里 msg
的数据。可是数据驱动都是单向数据流,为了避免形成数据传递的混乱,咱们只能依靠一些其它手段去完成,一个小小的传递数据就显得很复杂的了,因此后续咱们会讲讲如何去用更简便的 Api
作对应的事。
下篇课程导读:
在2.0初期 .sync
被砍了,v-model
承担起了双向绑定的职责,毕竟 v-model
不是为组件与组件之间数据双向绑定而设计的,用起来总有蹩脚的时候。2.3
版本的回归,启用了显示通知的形式让双向绑定又活了,.sync
或者 v-model
比$emit
与 v-on
只是进行简单的父子组件数据交互更加便捷。
上一章咱们已经对$emit和v-on
如何进行数据和行为的交互作了讲解,但若是只是简单用来数据传递改变的话.sync和v-model
再适合不过了。若是用过1.0的 Vue 的开发者,我相信 .sync 会让你用起来很是便捷,经过双向绑定很简单就能实双,父子组件的双向绑定,2.0为了保持单向数据流的良好性,去除了 .sync 的功能。
官方解释:
1.0 Props 如今只能单向传递。为了对父组件产生反向影响,子组件须要显式地传递一个事件而不是依赖于隐式地双向绑定。
推荐使用
经过大量观察,在初期2.0版本中,由于 .sync 并无回归,只是在2.3进行回归,在组件库中进行数据双向绑定,几乎都是经过 v-model 来进行的。可是不管从语意上仍是感观上,给代码维护的感就是不直观,v-model 在开发一般都是结合 Input 输入框来结合进行一个数据绑定,进行父子组件双向绑定,可是相比自定义 v-on 组件事件,不管从代码量,仍是用法上更加简洁。
在 Vue 中,有许多方法和 Angular 类似,这主要是由于 Angular 是 Vue 早期开发的灵感来源。然而 Angular 中存在许多问题,在 Vue 中已经获得解决。
官方解释
自定义事件能够用来建立自定义的表单输入组件,使用 v-model 来进行数据双向绑定。
<input v-model="something"> 复制代码
这不过是如下示例的语法糖:
<input v-bind:value="something" v-on:input="something = $event.target.value"> 复制代码
v-model 其实也是一个语法糖,想要理解这些代码,你要先知道Input元素
上自己有个oninput事件
,这是HTML5新增长
的,相似 onchange,每当输入框内容发生变化的时候,就会触发Input事件
,而后把 Input 输入框中 value 值再次传递给 something。
此时 value 运用在一个 Input 元素上,用:v-bind:value='something',意义上面只是把 Input 输入框中的 value 值与 something 做为一一对应的双向绑定,这就像一个循环操做,当再次触发 Input 事件时,input($event.target)对象中的value值会再次改变something
。
这里咱们对 v-model 绑定在 Input 元素上进行语法糖上的解析。
既然在元素上能进行双向绑定,那在组件中进行双向绑定又如何实现,原理其实都是同样的,只是应用在自定义的组件上时,拿的并非$event.target.value
,由于我此时不做用在 Input 输入框上。
<custom-input v-bind:value="something" v-on:input="something = arguments[0]"> </custom-input> 复制代码
经过以上简写,经过自定事件让 v-model 进行一个父子组件双向绑定的话。
接收的只能是 value 吗?必须是,由于 v-model 是基于 Input 输入框定制的,其中value 值是为 Input 内部定制的
v-on:input="something = arguments[0]" 复制代码
此时做用在组件上时,v-on 监听的语法糖也会有所改动,监听的并非$event.target.value
,而是回调函数中的第一个参数
。
<template> <div class="hello"> <button @click="show=true">打开model</button> <demo v-model="show"></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { show: false } } } </script> 复制代码
<template> <div v-show="value"> <div> <p>这是一个Model框</p> <button @click="close">关闭model</button> </div> </div> </template> <script> export default { props: ['value'], methods: { close () { this.$emit('input',false) } } } </script> 复制代码
这是一个模态框的基本雏形,能够在父组件经过 v-model 来进行 model 框和父组件之间的显示交互。
经过子组件看出经过props接收了value值
,当点击关闭的时候仍是经过$emit事件触发input事件
,而后经过传入 false 参数。
父组件隐式 v-on:input="something = arguments[0]" 进行了监听,一但 Input 事件触发,父组件就会执行监听回调,从而作到了双向绑定。
checkbox 和 radio 原理
<input type="checkbox" :checked="status" @change="status = $event.target.checked" /> <input type="radio" :checked="status" @change="status = $event.target.checked" /> 复制代码
经过绑定 checked 属性,一样的监听的是 change 事件,不管是 checkbox 仍是 radio 在操做的时候都会隐式自动触发一个 change 事件,跟 Input 经过 value 值,Input 触发事件原理绑定是同样的。
定制组件,咱们就能够重写v-model里的Props 和 event
,默认状况下,一个组件的 v-model 会使用 value 属性
和 input 事件
,每每有些时候,value 值被占用了,或者表单的和自定议v-model的$emit('input')事件发生冲突
,为了不这种冲突,能够定制组件 v-model,冲突示例。
<template> <div v-show="value"> <div> <p>这是一个Model框</p> <input type="text" v-model="value"> {{value}} <button @click="close">关闭model</button> </div> </div> </template> <script> export default { props: ['value'], methods: { close () { this.$emit('input',false) } } } </script> 复制代码
<template> <div class="hello"> <button @click="show=true">打开model</button> <demo v-model="show"></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { show: false } } } </script> 复制代码
上面例子能够发现,在子组件中input中v-model
和model显示的操做数据共同占用的 props 中的(value)
,一样二者也共同占用了 emit('input') 触发事件,Input 输入框的事件是自动出发,而 model 显示消失是手动触发。
初始化的时候,Input 输入框的值的会被 value 传入的 false 值给自动加上,当改变 Input 输入框的时候,由于冲突而致使报错。
定制 v-model, 经过 model 选项改变 props 和 event 的值,从而解除二者的冲突。
<template> <div v-show="show"> <div> <p>这是一个Model框</p> <input type="text" v-model="value"> {{value}} <button @click="closeModel">关闭model</button> </div> </div> </template> <script> export default { model: { prop: 'show', event: 'close' }, props: ['show'], data () { return { value: 10 } }, methods: { closeModel () { this.$emit('close',false) } } } </script> 复制代码
<template> <div class="hello"> <button @click="show=true">打开model</button> <demo v-model="show" ></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { show: false } } } </script> 复制代码
经过 model 选项的改变,把 props 从本来的value换成了show
,input触发的事件换成了close
,从而二者都不相互依赖,解决了冲突的问题。
有些时候经过父组件中的子组件模板中想传递 value 值,也会致使一样的冲突。
在不用定制组件的状况下,如下的写法,也会一样致使冲突,致使同用一个 value。
<demo v-model="show" value="some value"></demo> 复制代码
props:['value'] 复制代码
在一些状况下,咱们可能会须要对一个 prop 进行『双向绑定』。事实上,这正是 Vue 1.x 中的 .sync 修饰符所提供的功能。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会致使问题,由于它破坏了『单向数据流』的假设。因为子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你彻底不知道它什么时候悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。
在2.0发布一段以后,不管在业务组件仍是在功能组件库上面的,大量的子组件改变父子组件的数据和组件库中可能达到大功率的复用,可是在2.3中回归,从新引入了 .sync 修饰符,此次它只是做为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 侦听器。
以前的例子中,v-model 毕竟不是给组件与组件之间通讯而设计的双向绑定,不管从语意上和代码写法上都没有 .sync 直观和方便。
不管从 v-model 仍是 .sync 修饰符来看,都离不开 $emit v-on 语法糖的封装,主要目的仍是为了保证数据的正确单向流动与显示流动。
<demo :foo.sync="something"></demo> 复制代码
语法糖的扩展:
<demo :foo="something" @update:foo="val => something = val"></demo> 复制代码
当子组件须要更新 foo 的值时,它须要显式地触发一个更新事件:
this.$emit('update:foo', newValue) 复制代码
同时父组件@update:foo
也是依赖于子组件的显示触发,这样就能够很轻松的捕捉到了数据的正确的流动
。
第一个参数则是 update 是显示更新的事件,跟在后面的:foo
则是须要改变对应的props值
。
第二个参数传入的是你但愿父组件foo数据里将要变化的值
,以用于父组件接收update时更新数据
。
<template> <div v-show="show"> <p>这是一个Model框</p> <button @click="closeModel">关闭model</button> </div> </template> <script> export default { props: ['show'], methods: { closeModel () { this.$emit('update:show',false) } } } </script> 复制代码
<template> <div class="hello"> <button @click="show=true">打开model</button> <demo :show.sync="show" ></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { show: false } } } </script> 复制代码
上面的 case 一样也解决了 model 显示交互操做,从代码的语意上看上去让开发者一目了然,一样也作了 v-model 作不了的事,基于 props 的原子化,对传入的 props 进行多个数据双向绑定
.sync 也能轻松作到。
<template> <div class="hello"> <button @click="show=true">打开model</button> <demo :show.sync="show" :msg.sync="msg"></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { show: false, msg: '这是一个model' } } } </script> 复制代码
<template> <div v-show="show"> <p>{{msg}}</p> <button @click="closeModel">关闭model</button> <button @click="$emit('update:msg','改变了model文案')">改变文案</button> </div> </template> <script> export default { props: ['show', 'msg'], methods: { closeModel () { this.$emit('update:show',false) } } } </script> 复制代码
父组件向子组件 props 里传递了 msg 和 show 两个值,都用了.sync 修饰符,进行双向绑定。
子组件改变父组件的数据时,update 冒号后面的参数和父组件传递进来的值是同步的,想改变那个,则冒号后面的值对应的那个,二者是一一对应的,同时也是必填的。
一样还能够在组件 template 里点击执行 click 后不但能够支持回调函数,还能够写入表达式,只是一种直观的表现仍是推荐这种写法的。
.sync 修饰符给咱们开发中带来了很大的方便,同时在2.0的初期的组件库中大量的 v-model 给开发者用起来仍是很别扭,在.sync 回归后同时也会慢慢向.sync 进行一个版本的迁移。
下篇课程导读:
不基于大量行为操做,只是进行一个或多个数据双向组件的时候,能够轻松用 .sync 与 v-model 去化解,每每组件通讯并非你想像的那么轻松简单,在项目复杂的时候,组件如何合理的拆分,会让业务代码的清晰度
,复用率
,后续维护都会下降成本
,有利必有困难,一样会形成组件与组件的深层次传递,那咱们如何进行通讯呢?第一个想到的办法必然是 Vuex。Vuex 理解其实本质上并非处理跨度深层次组件而使用的,每每这样会致使你们会滥用 vuex,而 $attrs
$listeners
这对兄弟能够很好的帮助你进行深组件的通讯。
在2.4版本中,有关$attrs
和$listeners
这两个实例属性用法仍是比模糊,深层次挖掘将会很是有用,由于在项目中深层次组件交互的话可能就须要 Vuex 助力了,可是若是只是一个简单的深层次数据传递,或者进行某种交互时须要向上通知顶层或父层组件数据改变时,杀鸡用牛 VUX 可能未免有点多余!
什么状况才会显得多余,若是咱们纯经过 props 一层一层向下传递,再经过 watch 或者 data 进行过渡,若是只是单向数据深层能传递,进行监听改变深传递的数据,不进行跨路由之间页面的共享的话,用这两个属性很是便捷。
有些开发者,特别对 Vuex 没有深刻理解和实战经验的时候,同时对组件与组件多层传递时,不敢大胆的解耦组件,只能进行到父子组件这个层面,并且组件复用率层面上也有所降低。
$attr
与 interitAttrs 之间的关系默认状况下父做用域的不被认做 props 的特性绑定 (attribute bindings)
将会“回退”且做为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另外一个组件的组件时,这可能不会老是符合预期行为。经过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而经过 (一样是 2.4 新增的) 实例属性 $attrs
可让这些特性生效,且能够经过 v-bind 显性的绑定到非根元素上。
注意:这个选项不影响 class 和 style 绑定。
官网上并无给出一点 demo,语意上看起来仍是比较官方的,理解起来老是有点不太友好,经过一些 demo 来看看发生了什么。
<template> <div> {{first}} </div> </template> <script> export default { name: 'demo', props: ['first'] } </script> 复制代码
<template> <div class="hello"> <demo :first="firstMsg" :second="secondMessage"></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { firstMsg: 'first props', secondMessage: 'second props' } }, } </script> 复制代码
父组件在子组件中进行传递 firstMsg 和secondMsg 两个数据,在子组件中,应该有相对应的 props 定义的接收点,若是在 props 中定义了,你会发现不管是 firstMsg 和 secondMsg 都成了子组件的接收来的数据了,能够用来进行数据展现和行为操做。
虽然在父组件中在子组件模版上经过 props 定义了两个数据,可是子组件中的 props 只接收了一个,只接收了 firstMsg,并无接收 secondMsg,没有进行接收的此时就会成为子组件根无素的属性节点。
<div class="hello" second="secondMessage"></div>
当咱们用 v-for 渲染大量的一样的 DOM 结构时,可是每一个上面都加一个点击事件,这个会致使性能问题,那咱们能够经过 HTML5 的 data 的自定义属性作事件代理。
<template> <div class="hello" @click="ff"> <demo :first="firstMsg" :data-second="secondMsg"></demo> <demo :first="firstMsg" :data-second="secondMsg"></demo> <demo :first="firstMsg" :data-second="secondMsg"></demo> <demo :first="firstMsg" :data-second="secondMsg"></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { firstMsg: 'first props', secondMsg: 'secondMsg' } }, methods: { ff (e) { if(e.target.dataset.second == 'secondMsg') { console.log('经过事件委托拿到了自定义属性') } } } } </script> 复制代码
通过改动以后,在父组件中,把向子组件传递的参数名改为了 HTML 自定义的 data-second 属性,一样在子组件中不进行 props 接收,就顺其天然的成为了子组件每个根节点的自定义属性。
经过事件冒泡的原理,然而能够从e.target.dataset.second 就能找对应的 Dom 节点进行逻辑操做。
一样,在子组件模版上能够绑定多个自定义属性,在子组件包裹的外层进行一次监听,经过 data 自定义属性拿到循环出来组件的对应的数据,进行逻辑操做。
interitAttrs = false 发生了什么 ?
<template> <div> {{first}} </div> </template> <script> export default { name: 'demo', props: ['first'], inheritAttrs: false, } </script> 复制代码
对子组件进行一个改动,咱们加上 inheritAttrs: false,从字面上的翻译的意思,取消继承的属性,然而 props 里仍然没有接收 seconed,发现就算 props 里没有接收 seconed,在子组件的根元素上并无绑定任何属性。
$attrs
包含了父做用域中不被认为 (且不预期为) props 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含全部父做用域的绑定 (class 和 style 除外),而且能够经过 v-bind="$attrs" 传入内部组件——在建立更高层次的组件时很是有用。
在前面的例子中,子组件props中并无接受seconed
,设置选项 inheritAttrs: false
,一样也不会做为根元素的属性节点,整个没有接收的数据都被 $attr
实例属性给接收,里面包含着全部父组件传入而子组件并无在 Props里显示接收的数据。
为了验证事实,能够在子组件中加上
created () { console.log(this.$attrs) } 复制代码
打印出来则是一个对象 {second: "secondMsg", third: "thirdMsg"}
想要通 $attr
接收,但必需要保证设置选项 inheritAttrs: false,否则会默认变成根元素的属性节点。
开头说了,最有用的状况则是在深层次组件运用的时候,建立第三层孙子组件,做为第二层父组件的子组件,在子组件引入的孙子组件,在模版上把整个 $attr
当数做数据传递下去,中间则并不用经过任何方法去手动转换数据。
<template> <div> <next-demo v-bind="$attrs"></next-demo> </div> </template> 复制代码
<template> <div> {{second}}{{third}} </div> </template> <script> export default { props : [ 'second' , 'third'] } </script> 复制代码
孙子组件在 props 接收子组件中经过 $attr
包裹传来的数据,一样是经过父组件传来的数据,只是在子组件用了$attrs
进行了统一接收,再往下传递,最后经过孙子组件进行接收。
以此类推孙子组件仍然不想接收,再传入下级组件,咱们仍然须要对孙子组件实力选项进行设置选项 inheritAttrs: false,不然仍然会成为孙子组件根元素的属性节点。
从而利用 $attrs
来接收 props 为接收的数据再次向下传递是一件很方便的事件,深层次接收数据咱们理解了,那从深层次向层请求改变数据如何实现。意思就是让顶层数据和最底层数据进行一个双向绑定。
listeners 能够认为是监听者。
向下如何传递数据已经了解了,面临的问题是如何向顶层的组件改变数据,父子组件能够经过 v-model,.sync,v-on 等一系列方法,深层及的组件能够经过 $listeners
去管理。
$listeners
和 $attrs
二者表面层都是一个意思,$attrs
是向下传递数据,$listeners
是向下传递方法,经过手动去调用 $listeners
对象里的方法,原理就是 $emit
监听事件,$listeners
也能够当作一个包裹监听事件的一个对象。
<template> <div class="hello"> {{firstMsg}} <demo v-on:changeData="changeData" v-on:another = 'another'></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { firstMsg: '父组件', } }, methods: { changeData (params) { this.firstMsg = params }, another () { alert(2) } } } </script> 复制代码
在父组件中引入子组件,在子组件模板上面进行 changeData 和 another 两个事件监听,其它这两个监听事件并不打算被触发,而是直接被调用,再简单的理解则是向下传递两个函数。
<template> <div> <p @click="$emit('another')">子组件</p> <next-demo v-on='$listeners'></next-demo> </div> </template> <script> import NextDemo from './nextDemo.vue' export default { name: 'demo', components: { NextDemo }, created () { console.log(this.$listeners) }, } </script> 复制代码
在子组件中,引入孙子组件 nextDemo。在子组件中像 $attrs
同样,能够用 $listeners
去总体接收监听的事件,{changeData: ƒ, another: ƒ}
以一个对象去接收,此时在父组件中子组件模板上监听的两个事件不但能够被子组件实例属性 $listeners
去总体接收,而且同时能够在子组件进行触发。
一样在孙子 nextDemo 组件中,继续向下传递,经过 v-on 把整个 $listeners
所接收的事件传递到孙子组件中,只是经过 $listeners
把其全部在父组件拿到监听事件一并经过 $listeners
一块儿传递到孙子组件里。
<template> <div class="hello"> <p @click='$listeners.changeData("change")'>孙子组件</p> </div> </template> <script> export default { name: 'demo', created () { console.log(this.$listeners) }, } </script> 复制代码
依然能拿到从子组中传递过来的$listeners
全部的监听事件,此时并非经过$emit
去触发,而是像调用函数同样,$emit
只是针对于父子组件的双向通讯,$listeners
包了一个对象,分别是 changeData 和 another,经过$listeners.changeData('change')等于直接触发了事件,执行监听后的回调函数,就是经过函数的传递,调用了父组件的函数。
经过 $attrs
和 $listeners
能够很愉快地解决深层次组件的通讯问题,更加合理的组织你的代码。
下篇导读
以上介绍了如何在高层组件向下传递数据,在底层组件向上通知改变数据或者进行一些行为操做,而$listeners
就像是调用了父组件的函数同样,看上去根本没有什么区别,你可能会想用$parents,$children
同样能作到。不是不可用,而是在什么状况下适合用,经过下篇介绍木偶组建和智能组件好好理一下正确场景下如何准确利用 Api 进行行为交互、数据交互。
Vue 中在组件层面的数据和行为通讯,前五章经过一些 demo 和进行了深刻总结,包括如下几点:
$emit
与 $on
的通讯,父子组件 v-on 与 $emit
的通讯$attrs
与 $listeners
深层次数据传递与行为交互的运用模式以上涵盖了大量组件与组件之间的通讯模式,只有能熟练掌握以上知识点,接下来才能对智能组件与木偶组件写法和封装有准确用法。
智能组件能够称为第三方通用组件,也能够称之为业务型公用组件,与父组件之间的关系是彻底解耦的,只能经过 props 进行数据传递,event 进行事件传递,不依赖于任何环境,只须要传递相应的数据和事件,就能获得你想要的操做。
木偶组件是为了业务页面进行拆分而造成的组件模式。好比一个页面,能够分多个模块,而每个模块与其他页面并无公用性,只是纯粹拆分。
还有一个方面则是复合组件的联动用法。当一个智能组件是由两个组件组成的一个复合智能组件,而它的子组件与父组件之间就有一个木偶的原理,由于二者是相互的,在开发者调用并需保持它们的关系性、规范性,一旦改变其自己的模式则会无效。
木偶组件的拆分简便用法
对于每个木偶组件在定义以前,你必然会知道它将做用于哪一个页面,在哪一层,都是有一个准确的不变性,取决于你对页面的拆分深度和数量。
$parent
指向当前组件的父组件,能够拿到父组件的整个实例。前面已经说了,木偶组件能够明确的知道运用在每一个 spa 页面对应路由的第几层组件,多是当前页面的子组件,孙子组件,或者更深的层次。而想和父组件进行通讯的话,在不考虑复用的前题下,能够明确如何与父组件进行数据通讯或者行为通讯。
<template> <div class="hello"> {{msg}} <demo></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, data () { return { msg: '父组件', } } } </script> 复制代码
<template> <div> <p>{{demoMsg}}</p> <p @click="handleClick">子组件</p> </div> </template> <script> export default { name: 'demo', data () { return { demoMsg : '' } }, methods: { handleClick () { let msg = this.$parent.msg this.demoMsg = msg this.$parent.msg = '父组件数据被改了' } } } </script> 复制代码
demo 组件已经明确的知道是 Hello 组件的子组件,也能够是 demo 组件是 Hello 组件的木偶组件,经过 $parent
就能够随意取到和改动父组件实例的属性(数据)。一样这也并不违反数据的单向流的原则,能够对比一下经过 v-on 和 $emit
或者 v-model,.sync 这几种方法,不但方便不少,还更加快捷,而且明确了组件的位置,就像木偶同样,永远不会变,它的父组件永远只会是同一个。
父组件
... methods : { parentMethods () { console.log('调用父组件的方法') } } 复制代码
this.$parent.parentMethods() 复制代码
一样能够调用父件的方法,经过子组的调用去执行父组件的方法。此方法是在父组件内部执行的,在某些场景下就会显得很便捷,后面会给出例子。
$children
也是针对于木偶组件的应用,它和$parent
相反,此 Api是对于一个组件来讲,已经明确知道它的子组件,也多是一个子组件集,准确地拿到想要的子组件实例,或者子组件集实列$children
能够经过父组件拿到子组件的实例,它是以一个数组的形式包裹。
<template> <div class="hello"> <p @click='handlerClick'>父组件</p> <demo></demo> <demo></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo }, methods: { handlerClick () { console.log(this.$children) this.$children.forEach(item => { item.demoMsg = '经过$children改变' }) } } } </script> 复制代码
<template> <div> <p>{{demoMsg}}</p> </div> </template> <script> export default { name: 'demo', data () { return { demoMsg : '' } } } </script> 复制代码
此时已经不是经过子组件去与父组件通讯,而是用父组件与子组件通讯,$parent
与$children
就造成了一个父子组件互相通讯的机制,仍是那句重点一句只适合木偶组件的模式
。
在父组件中明确 demo 组件是子组件,经过$children
拿到全部 demo 组件的实例,经过 forEach 循环改变每一个子组件的实例属。由于 data 里全部属性(数据)都是经过 object.defineproperty 来进行数据劫持,把 data 里的属性都绑到 Vue 实例上。从中咱们能够垂手可得的获得它。
智能组件的运用
智能组件多是业务组件也多是第三方通用组件,总归是多个组件公用的子组件,由于它可能服务多个组件或者页面,当嵌入不一样组件里,所须要展求的业务能力也是有所区别的,所以称之为智能组件。
比方说一个智能组件 A,将嵌入 B,C 组件作为子组件:
当A嵌入到B中须要显示文案嵌入B组件中
当A嵌入到C中须要显示文案嵌入C组件中
经过向智能传递一个数据和标识,告诉它我须要你展现什么?
<template> <div class="hello"> <p>父组件</p> <demo type='A'></demo> </div> </template> <script> import Demo from './Demo.vue' export default { name: 'hello', components: { Demo } } </script> 复制代码
<template> <div> <p>{{type==='B'?'嵌入B的组件':'嵌入C的组件'}}</p> </div> </template> <script> export default { name: 'demo', props: ['type'] } </script> 复制代码
对于智能组件你永远不知道你将做用于哪一个组件之下,这自己就是一个不定因素,特别对于通用组件,这将会暴露各类方法和 props 数据,只有传递数据传递事件去作本身想作的事件,智能组件(也是一个封装模块),会根据传入的数据和事件去作内部封装后所作的事情,而你并不能够轻意的随便改动它。
智能组件与木偶组件同时能够相互嵌套,能够做用在复合组件上
。通常复合组件是都是通三方通用组件称之为智能组件,可是复合组件的父组件和子组件一样能够互相成为对方的木偶组件
,二者能够成为相互依赖的关系
。不管从代码量和理解,调用都会很方便,木偶组件相比智能组件更方便理解和简洁
,可是功能上就比较单一
。
accordion属于第三方通用组件,一样也是一个复合组件。
<template> <div> <slot></slot> </div> </template> <script> export default { props : ['repeat'], methods : { open (uid) { this.$children.forEach(item => { if(item._uid != uid){ item.close = false } }) } } } </script> 复制代码
<template> <div> <p @click='handleClick'>{{title}}</p> <div v-show='close' > <slot></slot> </div> </div> </template> <script> export default { props : ['title'], data () { return { close : false } }, created () { if(this.$parent.repeat === true) { this.close = true } }, methods : { handleClick () { this.$parent.open(this._uid) this.close = !this.close } } } </script> 复制代码
<template> <div class="hello"> <accordion :repeat='true'> <accordion-item title='vueTitle'>vue</accordion-item> <accordion-item title='vue-routerTitle'>vue-router</accordion-item> <accordion-item title='vuex-Title'>vuex</accordion-item> </accordion> </div> </template> <script> import Accordion from './accordion.vue' import AccordionItem from './accordion-item.vue' export default { name: 'hello', components: { Accordion, AccordionItem } } </script> 复制代码
先从智能组件这个方面提及,不管是 accordion 仍是 accordion-item 同向外暴露一个 props进行你但愿的操做。
accordion 暴露了一个 repeat,当 repeat 为 true 的时候则把全部 item 项初始化都进行展开。
accordion-item 暴露了一个 title,能够随意传入你想设计的标题。
以上这些每每都是一些不定因素,也不知道它可能会嵌套在哪一个页面组件的哪一层,这就是复合组件的智能方面。
再从木偶组件这个方面论一下。accordion 与 accordion-item 二者是父子组件关系,这种关系是不可变的,想要用到这个复合组件,accordion 与 accordion-item 必须保证肯定的父子组件关系,而且缺一不可,从中就能体现出二者的木偶性。
accordion-item 经过$parent
调用 accordion 父组件的 open 方法, 而 accordion 经过$children
拿到每个 accordion-item 子组件的实例,进行显示隐藏的转换。二者很充分造成了一个对木偶关系,这种父子关系是永远断不了的。
总结: 木偶组件:子组件只能有一个爹,必须是惟一的,并且父子俩长得一模一模,谁离开谁都活不了。
智能组件:子组件能够有N个爹,非惟一性,并且父子长得不必定要同样,子组件可能会有N个爹的特性,子组件离开哪一个爹都能继续生存。
中央事件通讯,就像一根线同样,把两个组件的通讯用一根线链接起来。前面几节课讲了父子组件通讯与深层次嵌套组件通讯,而且已经经过各类 Api 和良好的解决方案,可是同级组件怎么办,不管用$emit
v-on
v-model
.sync
$attr与$listeners
都不适用,以上这些都是基于嵌套的父子组件进行通讯
。
同级组件通讯,也是一种常见的通讯模式,在一个大的容器下(父组件)底下有两个平级的组件,两个组件进行数据交或者行为交互,在 Api 的方法里也没有专门的设计。
$emit
,v-on,$on
三者结合使用这种操做是很是复杂的,若是你能良好掌握以上三个 Api 进行同级组件的通讯,那你对这三个 Api 已经彻底掌握了。这种方法是一种过渡方法,b->a
a->c
,意思是a
去通知b
,b
对a
进行一个监听,当a
监听到件事,在进行向c
触发,c
的内部再进行监听,这样就造成了一个过渡链条。可是代码上就不显的那么直观了,多个触发事件,多个监听事件,一旦这种平级组件须要通讯多了,那么代码就有一种很难维护的感受。
实例demo
<template> <div> <p @click="$emit('fromFirst','来自A组件')">first组件</p> </div> </template> <script> export default { name: 'first' } </script> 复制代码
按着上面的讲解的顺序,先定义一个同级子组件,当点击的时候向外触发一个eventName
为fromFirst
的事件,传递一个来自A组件
的参数这就造成了b->a
让a
去监听事件,让b
去触发事件。
<template> <div> <p>父组件</p> <first v-on:fromFirst="hanlderFromA"></first> <second ref="second"></second> </div> <template> <script> import First from './first.vue' import Second from './second.vue' export default { name: 'login', components: { First, Second }, methods: { hanlderFromA (Bmsg) { let second = this.$refs.second second.$emit('fromLogin', Bmsg) } } } <script> 复制代码
First
/Second
,仍是延续b->a
。此时a
就是这个父组件,再梳理一下知识点,v-on与$emit
是进行父子组件事件通讯
,做用在父子组件两个层面上,在First
组件模版上进行一个v-on监听
,一旦监听到触发fromFirst
事件,则进行hanlderFromA
函数。a->c
这个阶段,$emit与$on
都是做用在同一个组件的实列上,经过this.$refs
拿到Second
组件的实列,在执行hanlderFromA函数
时再告诉c
组件进行通讯,同时把从b
接收到的参数再次传入。以上很明显能看出 A(父组件)只是一个过渡体,也能够说是一个真实的中央体,进行中央事件的派发。
<template> <div> <p>{{Bmsg}}</p> <p>second组件</p> </div> </template> <script> export default { name: 'second', created () { this.$on('fromLogin', (Bmsg) => { this.Bmsg = Bmsg console.log('通讯成功') }) }, data () { return { Bmsg: '' } } } </script> 复制代码
Second 组件是被通讯的一方,在a(父组件)
进行触发,然而在c(second)
组件中进行监听,一旦监听到了fromLogin
事件,能够作你想作得改变数据,行为操做都不是问题了。
这就是b->a a->c
的模式,我只能用一句话说,复杂!实在是复杂,那必然有简单的方法。在了解更简单的方法以前,先了解一下ES6 模块的运行机制
。
ES6 模块的运行机制
JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用
。ES6 export 的原始值变了,import加载
的值也会跟着变。所以,ES6
模块是动态引用,而且不会缓存值,模块里面的变量绑定其所在的模块。
举个例子
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 复制代码
虽然在main.js
执行程序的时候加载了count
,可是count
在lib.js
和在main.js
里造成了一个引用关系,一旦libs内部的export
导出的counter
发生变化时,main.js
中一样会发生变化。
经过额外的实例进行简单的中央事件处理
定义一个额外的实例进行一个事件的中转,对于ES6
模块的运行机制已经有了一个讲解,当模块内部发生变化的时候,引入模块的部分一样会发生变化,当又一个额外的实例对加载机制进行引入进行$emit与$on
进行绑定通讯,能垂手可得解决问题,经过b->a->c
的模式直接过渡。
定义一个中央事件实例
import Vue from 'vue' export default new Vue() 复制代码
new 一个 Vue 的实例,而后把这个实例能过 es6 模块机制导出。
<template> <div> <p>父组件</p> <first></first> <second></second> </div> </template> <script> import First from './first.vue' import Second from './second.vue' export default { name: 'login', components: { First, Second } } </script> 复制代码
在父这里只须要进行两个同组件的引入,能够删除任何过渡的方式。
<template> <div> <p @click="handleClick">first组件</p> </div> </template> <script> import Bus from './bus.js' export default { name: 'first', methods: { handleClick () { Bus.$emit('fromFirst', '来自A的组件') } } } </script> 复制代码
在first同级
组件中把bus
实例引入,点击时让bus
实例触发一个fromFirst
事件,这里你可能已经理解 module 加载机制配合在单个实例上用$emit和$on
进行通讯绑定,往下看。
<template> <div> <p>{{Bmsg}}</p> <p>second组件</p> </div> </template> <script> import Bus from './bus.js' export default { name: 'second', created () { Bus.$on('fromFirst', ( Amsg )=> { this.Bmsg = Amsg console.log('同级组件交互成功') }) }, data () { return { Bmsg: '' } } } </script> 复制代码
一样也引入bus
实列,经过bus
用$on
监听fromFirst
事件,由于bus实例
与bus.js
里的export defalt new Vue
关系是一个引用关系,当代码执行后,不管first
或者second
组件经过bus实例
造成了一个中央事件链条
,这种方法不但直观,也更加便捷。
中央事件的延生 跨组件深层次交互
既然同级组件能够用中央事件去过渡,那深层次嵌套不一样级组件能够吗?那你确定第一时间用到了 Vuex,但我一直认为 Vuex 操做大量的数据联动性很是有用,可是若是只是一个改变数据,或者执行事件,用起来反而更加直观。
将要模拟的方案:
当firstInner组件
可能会与second组件
或者secondInner组件
发生跨组件深层次交互也一样能够用中央事件去进行过渡,若是说 vuex 是顶层共享数据源,那么中央事件就是顶层共享通讯网
。
demo 示例
前面的全部父组件都不写代码了,只展现一下firstInner 组件、secondInner 组件。
<template> <div> <p @click="handleClick">firstInner组件</p> </div> </template> <script> import Bus from './bus.js' export default { name: 'first', methods: { handleClick () { Bus.$emit('fromFirstInner', '来自firstInner组件') } } } </script> 复制代码
<template> <div> <p>secondInner组件</p> </div> </template> <script> import Bus from './bus.js' export default { name: 'secondInner', created () { Bus.$on('fromFirstInner',(msg) => { console.log(msg) }) } } </script> 复制代码
不管你想通讯的两个组件嵌到在任何地方,它们的关系是如何的,只须要经过中央事件的处理,都能完成,同时还能够进行一对多的中央事件处理方式。在程序代码可控的状况下,没有什么是不可行的,只要数据量的变更是在可控范围以内,作一个中央事件网去行成一个通讯网络,也是一个不错的选择。