vue进阶相关整理
# 简介
记得跟vue面试题配合使用;
# 什么是库?什么是框架?
- 库是将代码集合成一个产品,库是我们调用库中的方法实现自己的功能。
- 框架则是为解决一类问题而开发的产品, 框架是我们在指定的位置编写好代码,框架帮我们调用。
# MVC 和 MVVM 区别
Vue并没有完全遵循MVVM模型,严格的MVVM模式中,View层不能直接和Model层通信,只能通过ViewModel来进行通信。
- 传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数 据。将结果返回给前端,页面重新渲染
- MVVM :传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户收到操作 dom 元素,将数据绑 定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。 ViewModel 就是我们 MVVM 模式中的桥梁.
# Vue的基本使用
# 快速安装
$ npm init -y
$ npm install vue
2
# Vue中的模板
<script src="node_modules/vue/dist/vue.js"></script>
<!-- 3.外部模板 -->
<div id="app">{{name}}</div>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'samy',
age: 22
},
// 2.内部模板
template:'<div>{{age}}</div>',
// 1.render函数
render(h){
return h('h1',['hello,',this.name,this.age])
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们默认使用的是
runtime-with-compiler
版本的vue,带compiler的版本才能使用template属性,内部会将template编译成render函数
- 渲染流程,会先查找用户传入的render
- 如果没有传入render则查找template属性
- 如果没有传入template则查找el属性,如果有el,则采用el的模板
# 模板语法
我们可以在vue中使用表达式语法,表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。
<div id="app">
<!-- 可以放入运算的结果 -->
{{ 1+ 1 }}
<!-- 当前这个表达式 最后会被编译成函数 _v(msg === 'hello'? true:false) -->
{{msg === 'hello'? true:false}}
<!-- 取值操作,函数返回结果 -->
{{obj.a}} {{fn()}}
</div>
2
3
4
5
6
7
8
这里不能使用js语句(
var a = 1
),带有返回值的都可以应用在模板语法中。
# 响应式原则
- Vue内部会递归的去循环vue中的data属性,会给每个属性都增加getter和setter,当属性值变化时会更新视图。
- 重写了数组中的方法,当调用数组方法时会触发更新,也会对数组中的数据(对象类型)进行了监控
通过以上两点可以发现Vue中的缺陷:
- 对象默认只监控自带的属性,新增的属性响应式不生效 (层级过深,性能差)
- 数组通过索引进行修改 或者 修改数组的长度,响应式不生效
Vue额外提供的API:
vm.$set(vm.arr,0,100); // 修改数组内部使用的是splice方法
vm.$set(vm.address,'number','6-301'); // 新增属性通过内部会将属性定义成响应式数据
vm.$delete(vm.arr,0); // 删除索引,属性
2
3
为了解决以上问题,Vue3.0使用Proxy来解决
let obj = {
name: {name: 'samy'},
arr: ['吃', '喝', '玩']
}
let handler = {
get(target,key){
if(typeof target[key] === 'object' && target[key] !== null){
return new Proxy(target[key],handler);
}
return Reflect.get(target,key);
},
set(target,key,value){
let oldValue = target[key];
if(!oldValue){
console.log('新增属性')
}else if(oldValue !== value){
console.log('修改属性')
}
return Reflect.set(target,key,value);
}
}
let proxy = new Proxy(obj,handler);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
代理 get、set方法,可以实现懒代理。并且兼容数组索引和长度变化
# 实例方法
- vm._uid (每个实例的唯一标识)
- vm.$data === vm._data (实例的数据源)
- vm.$options (用户传入的属性)
- vm.$el (当前组件的真实dom)
- vm.$nextTick (等待同步代码执行完毕)
- vm.$mount (手动挂载实例)
- vm.$watch (监控数据变化)
这些属性后续都会经常被应用,当然还有一些其他比较重要的属性,后续会在详细介绍。
# 指令的使用
vue中的指令,vue中都是以v-开头 (一般用来操作dom
)
# 常见指令
v-once
渲染一次 (可用作优化,但是使用频率极少)v-html
将字符串转化成dom
插入到标签中 (会导致xss攻击问题,并且覆盖子元素)v-if/v-else/v-else-if
不满足时dom
不存在(可以使用template标签)v-show
不满足时dom
隐藏 (不能使用template标签)v-for
循环字符串、对象、数字、数组 (循环时必须加key,尽量不采用索引)v-bind
可以简写成: 属性(style、class...)绑定v-on
可以简写成@ 给元素绑定事件 (常用修饰符 .stop、.prevent、.self、.once、.passive)v-model
双向绑定 (支持.trim、.number修饰符)
常考点:
# v-show和v-if区别
- v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素
- v-show 只是切换当前 dom 的显示或者隐藏;
v-show
会解析成指令,变为display:none
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`
<div v-if="true"><span v-for="i in 3">hello</span></div>`
);
/** with(this) {
* return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e()
* }
**/
2
3
4
5
6
7
8
# v-for和v-if连用问题
- v-for 会比 v-if 的优先级高一些,如果连用的话会把 v-if 给每个元素都添加一下,会造成性能问题 (使用计算属性优化)
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`);
/** with(this) {
* return _l((3), function (i) { return (false) ? _c('div', [_v("hello")]) : _e() })
* }
**/;
2
3
4
5
6
# v-for为什么要加key
为了在比对过程中进行复用
# v-model原理
内部会根据标签的不同解析出,不同的语法
- 例如 文本框会被解析成 value + input事件
- 例如 复选框会被解析成 checked + change事件
- ...
# 自定义指令
我们可以自定义Vue中的指令来实现功能的封装 (全局指令、局部指令)
# 钩子函数
指令定义对象可以提供如下几个钩子函数:
- bind:只调用一次,指令第一次绑定到元素时调用
- inserted:被绑定元素插入父节点时调用
- update:所在组件的 VNode 更新时调用,组件更新前状态
- componentUpdated:所在组件的 VNode 更新时调用,组件更新后的状态
- unbind:只调用一次,指令与元素解绑时调用。
// 1.el 指令所绑定的元素,可以用来直接操作 DOM
// 2.bindings 绑定的属性
// 3.Vue编译生成的虚拟节点 (context)当前指令所在的上下文
bind(el,bindings,vnode,oldVnode){ // 无法拿到父元素 父元素为null
console.log(el.parentNode,oldVnode)
},
inserted(el){ // 父元素已经存在
console.log(el.parentNode)
},
update(el){ // 组件更新前
console.log(el.innerHTML)
},
componentUpdated(el){ // 组件更新后
console.log(el.innerHTML)
},
unbind(el){ // 可用于解除事件绑定
console.log(el)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 练习1.clickOutSide
<div v-click-outside="hide">
<input type="text" @focus="show">
<div v-if="isShow">显示面板</div>
</div>
2
3
4
指令的编写
Vue.directive(clickOutside,{
bind(el,bindings,vnode){
el.handler = function (e) {
if(!el.contains(e.target)){
let method = bindings.expression;
vnode.context[method]();
}
}
document.addEventListener('click',el.handler)
},
unbind(el){
document.removeEventListener('click',el.handler)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 练习2.v-lazy
提供的server.js
const express =require('express');
const app = express();
app.use(express.static(__dirname+'\\images'))
app.listen(3000);
const arr = [];
for(let i = 10; i <=20;i++){
arr.push(`${i}.jpeg`)
}
app.get('/api/img',(req,res)=>{
res.json(arr)
})
2
3
4
5
6
7
8
9
10
11
插件使用
<script src="node_modules/vue/dist/vue.js"></script>
<script src="node_modules/axios/dist/axios.js"></script>
<script src="./vue-lazyload.js"></script>
<div id="app">
<div class="box">
<li v-for="img in imgs" :key="img">
<img v-lazy="img">
</li>
</div>
</div>
<script>
const loading = 'http://localhost:3000/images/1.gif';
Vue.use(VueLazyload,{
preLoad: 1.3, // 可见区域的1.3倍
loading, // loading图
})
const vm = new Vue({
el:'#app',
data() {
return {
imgs: []
}
},
created() {
axios.get('http://localhost:3000/api/img').then(({data})=>{
this.imgs = data;
})
}
});
</script>
<style>
.box {
height: 300px;
overflow: scroll;
width: 200px;
}
img {
width: 100px;
height: 100px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
定义插件
const Lazy = (Vue) => {
return class LazyClass {
constructor(options){
this.options = options;
}
add(el,bindings,vnode){}
}
}
const VueLazyload = {
install(Vue) {
const LazyClass = Lazy(Vue);
const lazy = new LazyClass(options);
Vue.directive('lazy', {
bind: lazy.add.bind(lazy)
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
获取滚动元素
const scrollParent = (el) =>{
let parent = el.parentNode;
while(parent){
if(/scroll/.test(getComputedStyle(parent)['overflow'])){
return parent;
}
parent = parent.parentNode;
}
return parent;
}
const Lazy = (Vue) => {
return class LazyClass {
constructor(options){
this.options = options;
}
add(el,bindings,vnode){
Vue.nextTick(()=>{
let parent = scrollParent(el);// 获取滚动元素
let src = bindings.value; // 获取链接
});
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
触发事件
const Lazy = (Vue) => {
class ReactiveListener {
constructor({el,src,elRenderer,options}){
this.el = el;
this.src = src;
this.elRenderer = elRenderer;
this.options = options;
// 定义状态
this.state = {loading:false}
}
}
return class LazyClass {
constructor(options) {
this.options = options;
this.listenerQueue = [];
this.bindHandler = false;
}
lazyLoadHandler() {
console.log('绑定')
}
add(el, bindings, vnode) {
Vue.nextTick(() => {
// 获取滚动元素
let parent = scrollParent(el);
// 获取链接
let src = bindings.value;
// 绑定事件
if (!this.bindHandler) {
this.bindHandler = true;
parent.addEventListener('scroll', this.lazyLoadHandler.bind(this))
}
// 给每个元素创建个实例,放到数组中
const listener = new ReactiveListener({
el, // 当前元素
src, // 真实路径
elRenderer: this.elRenderer.bind(this), // 传入渲染器
options: this.options
});
this.listenerQueue.push(listener);
// 检测需要默认加载哪些数据
this.lazyLoadHandler();
});
}
elRenderer(listener, state) {
let el = listener.el;
let src = '';
switch (state) {
case 'loading':
src = listener.options.loading || ''
break;
case 'error':
src = listener.options.error || ''
default:
src = listener.src;
break;
}
el.setAttribute('src',src)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
加载图片
const loadImageAsync = (src,resolve,reject) => {
let image = new Image();
image.src = src;
image.onload = resolve;
image.onerror = reject
}
class ReactiveListener {
constructor({el,src,elRenderer,options}){
this.el = el;
this.src = src;
this.elRenderer = elRenderer;
this.options = options;
// 定义状态
this.state = {loading:false}
}
checkInView(){
let {top} = this.el.getBoundingClientRect();
return top < window.innerHeight * this.options.preLoad
}
load(){
this.elRenderer(this,'loading');
loadImageAsync(this.src,()=>{
this.state.loading = true; // 加载完毕了
this.elRenderer(this,'loaded');
},()=>{
this.elRenderer(this,'error');
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
增加滚动节流
const throttle = (cb, delay) => {
let prev = Date.now();
return () => {
let now = Date.now();
if (now - prev >= delay) {
cb();
prev = Date.now();
}
}
}
this.lazyHandler = throttle(this.lazyLoadHandler.bind(this),500);
parent.addEventListener('scroll', this.lazyHandler.bind(this));
2
3
4
5
6
7
8
9
10
11
12
# 作业:
# 关于框架和库的说法正确的是:
- 框架中不能在使用库。x
- 框架则是为解决一类问题而开发的产品,库是将代码集合成一个产品
# 关于MVC 和 MVVM说法正确的是:
- React和Vue都是MVVM框架 x
- 前端既存在MVC框架也存在MVVM框架
# 关于render和template属性说法正确的是:
- 默认会先查找template,将template编译成render函数 x
- render函数的优先级高于template
# 响应式原理说法正确的是:
- vue中的属性对应的值是数组({arr:[1,2,3]}),当修改这个属性时不会导致视图更新 x
- Vue的响应式原理:对象通过defineProperty来实现,数组通过重写数组原型方法来实现
# v-if和v-show的区别
- v-show操作的是样式,内部采用的是opacity:0 + visibility:hidden x
- v-if操作的是dom是否存在,最终会编译成三元表达式
# 关于v-for说法正确的是?
- 如果是静态展示的属性可以使用索引作为key
- 循环出来的数据我们经常操作内部顺序 (倒序、正序、头部新增) 这时必须要采用索引作为key,可以提升性能;x
# 关于v-model说法正确的是?
- v-model只能使用在表单元素中x
- v-model可以理解成是语法糖形式
# vue-cli
项目创建
本节主要掌握Vue组件的应用及组件间的数据交互。
为什么要实现组件化开发? 可复用、方便维护、减少不必要的更新操作
# 安装
npm install -g @vue/cli
npm install -g @vue/cli-service-global
vue create vue-online-edit
2
3
# 初始化
? Check the features needed for your project:
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
>(*) CSS Pre-processors
( ) Linter / Formatter
( ) Unit Testing
( ) E2E Testing
2
3
4
5
6
7
8
9
10
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default)
Sass/SCSS (with dart-sass)
Sass/SCSS (with node-sass)
Less
> Stylus
2
3
4
5
# Vue组件通信
# 常见组件通信方式
- 1)
props
和$emit
父组件向子组件传递数据是通过prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的 $attrs
和$listeners
A->B->C。Vue 2.4 开始提供了$attrs
和$listeners
来解决这个问题
$parent
,$children
$refs
获取实例
- 父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。
envetBus
平级组件数据传递 这种情况下可以使用中央事件总线的方式
vuex
状态管理
# 案例:Vue
组件在线编辑器
- 通过props、events 实现父子组件通信
- 通过ref属性获取组件实例
# 掌握组件的基本概念
import Vue from 'vue'
import App from './App.vue'
new Vue({render: h => h(App)}).$mount('#app')
2
3
h我们一般称为
createElement
,这里我们可以用他来渲染组件,App
其实就是一个组件 (就是一个对象而已)
<template>
<div id="app"></div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style lang="stylus"></style>
2
3
4
5
6
7
8
9
10
为了编写组件方便,vue提供了
.vue
文件,最终这个对象会被解析为组件对象。一个组件由三部分组成:模板、逻辑、样式
# 划分组件结构
<template>
<div id="app">
<!-- 3.使用组件 -->
<Edit></Edit>
<Show></Show>
</div>
</template>
<script>
// 1.声明组件并引入
import Edit from '@/components/edit.vue';
import Show from '@/components/show.vue';
export default {
name: 'App',
// 2.组件的注册
components:{
Edit,
Show
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们将在线编辑器划分成左右两部分,左侧用于编辑操作、右侧用于展示操作。组件的使用有三步:声明导入、注册、通过标签形式使用组件
<style lang="stylus">
* {
margin: 0;
padding: 0;
}
html, body, #app {
width: 100%;
height: 100%;
}
#app {
display: flex;
& > div {
width: 50%;
height: 100%;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 编写Edit组件
<template>
<div class="edit">
<div class="edit-btn">
<button>代码运行</button>
<button>清空代码</button>
</div>
<div class="edit-box">
<textarea></textarea>
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="stylus">
.edit {
.edit-btn {
padding: 10px;
background: #ccc;
button {
width: 80px;
height: 40px;
margin-right: 5px;
}
}
.edit-box {
position: absolute;
top: 60px;
left: 0;
right: 0;
bottom: 0;
textarea {
width: 100%;
height: 100%;
outline: none;
border: none;
font-size 20px;
}
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 表单元素双向绑定
<textarea @input="handleInput" :value="code"></textarea>
export default {
data(){
return {code:''}
},
methods:{
handleInput(e){
this.code = e.target.value
}
}
};
2
3
4
5
6
7
8
9
10
11
这里我们将输入框的值映射到code数据中,当然也可以使用
v-model
来代替,但是在输入的过程中我们可能还要进行其他操作
思考:v-model是否真的等于input+value呢?
# 触发父组件事件
<textarea @input="handleInput" :value="code" @keydown.9.prevent="handleKeydown"></textarea>
export default {
methods:{
handleInput(e){
this.code = e.target.value;
this.$emit('input',this.code); //触发自己身上的事件
},
handleKeydown(e){
if(e.keyCode == 9){
this.code = e.target.value + ' ';
}
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
在父组件中,给当前组件绑定事件
<Edit @input="handleInput"></Edit>
export default {
data() {
return { code: "" };
},
methods: {
handleInput(code) {
this.code = code;
}
}
};
2
3
4
5
6
7
8
9
10
11
这个其实就是典型的发布订阅模式,先在组件自己身上绑定事件(绑定的事件为父组件事件),稍后触发自己身上的事件,将数据传入给父组件的函数中,达到子父通信的效果
# 将数据传递给儿子组件
通过属性的方式将数据传递给儿子组件
<Show :code="code"></Show>
子组件接受传递过来的数据
export default {
props:{
code:{
type:String,
code:''
}
}
}
2
3
4
5
6
7
8
# 定义show组件
<template>
<div class="show">
<h2 class="show-title">运行结果</h2>
<div class="show-box"></div>
</div>
</template>
<script>
export default {
props:{
code:{
type:String,
code:''
}
},
methods:{
run(){
// 运行代码
}
}
}
</script>
<style lang="stylus">
.show-title{
line-height: 40px;
padding-left:20px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 平级组件通信
最简单的方案可以找共同的父组件
<div class="edit-btn">
<button @click="$emit('run')">代码运行</button>
<button @click="code=''">清空代码</button>
</div>
2
3
4
<Edit @input="handleInput" @run="handleRun"></Edit>
这里我们可以在父组件中监控到组件点击事件了。我们需要在父组件中调用Show组件中的run方法
<Show :code="code" ref="show"></Show>
//使用:this.$refs.show.run() this.$refs[show].run()
2
# 解析代码
<div class="show-box" ref="display"></div>
getSource(type){
const reg = new RegExp(`<${type}[^>]*>`);
let code = this.code;
let matches = code.match(reg);
if(matches){
return code.slice(
code.indexOf(matches[0]) + matches[0].length,
code.lastIndexOf(`</${type}`)
)
}
return ''
},
run() {
// 运行代码
// 1.获取 js html css逻辑
const template = this.getSource("template")
const script = this.getSource("script").replace(/export default/,'return');
const style = this.getSource('style');
if(!template){
return alert('代码无法运行')
}
// 2.组合成组件
let component = new Function(script)();
component.template = template;
// 3.构造组件构造器
let instance = new (Vue.extend(component));
this.$refs.display.appendChild(instance.$mount().$el);
// 4.处理样式
if(style){
let styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.innerHTML = style;
document.getElementsByTagName("head")[0].appendChild(styleElement)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
解析出对应的内容,采用
Vue.extend
构造Vue组件,手动挂载到对应的元素上.当ref属性指定在DOM身上时,代表的是真实的DOM
元素
样式补充
<style lang="stylus">
.show {
padding-left: 20px;
.show-title {
line-height: 40px;
}
.show-box {
border-top: 10px solid blue;
position relative;
padding-top: 30px;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 案例:表单组件的封装
- 掌握插槽的应用
- $parent、$children、provide和inject的使用
- 组件的双向数据绑定
# 表单的使用
<template>
<div>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="ruleForm.password"></el-input>
</el-form-item>
<el-form-item>
<button @click="submitForm">提交表单</button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import elForm from "./components/el-form";
import elInput from "./components/el-input";
import elFormItem from "./components/el-form-item";
export default {
components: {
"el-form": elForm,
"el-input": elInput,
"el-form-item": elFormItem
},
data() {
return {
ruleForm: {
username: "",
password: ""
},
rules: {
username: [
{ required: true, message: "请输入用户名" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符" }
],
password: [{ required: true, message: "请输入密码" }]
}
};
},
methods: {
submitForm(formName) {
this.$refs["ruleForm"].validate(valid => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
return false;
}
});
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
这里我们参考
element-ui
表单组件的使用,自己动手实现下这三个组件。通过这三个组件的应用来掌握内部通信的机制。
# 编写组件
el-form
<template>
<form><slot></slot></form>
</template>
<script>
export default {
name:'elForm'
}
</script>
2
3
4
5
6
7
8
el-form-item
<template>
<div><slot></slot></div>
</template>
<script>
export default {
name:'elFormItem'
}
</script>
2
3
4
5
6
7
8
el-input
<template>
<input type="text">
</template>
<script>
export default {
name:'elInput'
}
</script>
2
3
4
5
6
7
8
先写出对应的基本组件结构
# Provide
的应用
<template>
<form><slot></slot></form>
</template>
<script>
export default {
name:'elForm',
provide(){
return {elForm:this}
},
props:{
model:{
type:Object,
default:()=>({})
},
rules:Object
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接收用户属性,并将当前组件提供出去,因为el-form 和 el-form-item 不一定是父子关系,可能是父孙关系
# inject
的应用
<template>
<div><slot></slot></div>
</template>
<script>
export default {
name:'elFormItem',
inject:['elForm'],
props:{
label:{
type:String,
default:''
},
prop:String
},
mounted(){
console.log(this.elForm)
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们通过inject将属性注入到组件中。从而解决了夸组件通信(provide和inject不要在业务逻辑中使用)
# 组件的双向绑定
<template>
<input type="text" :value="value" @input="handleInput">
</template>
<script>
export default {
name:'el-input',
props:{
value:String
},
methods:{
handleInput(e){
this.$emit('input',e.target.value);
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
组件的v-model其实就是 input + value的语法糖
# $parent
应用
Vue在组件初始化的过程中会创造父子关系,为了方便通信我们使用$parent属性来自己封装一个$dispatch方法,用于触发对应的祖先组件中的方法
Vue.prototype.$dispatch = function (componentName,eventName) {
const parent = this.$parent;
while (parent) {
let name = parent.$options.name;
if (name == componentName) {
break;
} else {
parent = parent.$parent;
}
}
if(parent){
if(eventName){
return parent.$emit(eventName)
}
return parent
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleInput(e){
this.$emit('input',e.target.value);
// 找到对应的formItem进行检测
this.$dispatch('elFormItem','validate');
}
2
3
4
5
# $on
手动绑定事件
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
{{errorMessage}}
</div>
</template>
<script>
import Schema from "async-validator";
export default {
name: "elFormItem",
inject: ["elForm"],
props: {
label: {
type: String,
default: ""
},
prop: String
},
data(){
return {errorMessage:''}
},
mounted() {
this.$on("validate", () => {
if (this.prop) {
let rule = this.elForm.rules[this.prop];
let newValue = this.elForm.model[this.prop];
let descriptor = {
[this.prop]: rule
};
let schema = new Schema(descriptor);
return schema.validate({[this.prop]:newValue},(err,res)=>{
if(err){
this.errorMessage = err[0].message;
}else{
this.errorMessage = ''
}
})
}
});
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# $children
应用
同理:Vue在组件初始化的过程中会创造父子关系,为了方便通信我们使用$children属性来自己封装一个$broadcast方法,用于触发对应后代组件中的方法
Vue.prototype.$broadcast = function (componentName,eventName) {
let children = this.$children;
let arr = [];
function findFormItem(children){
children.forEach(child => {
if(child.$options.name === componentName){
if(eventName){
arr.push(child.$emit('eventName'))
}else{
arr.push(child)
}
}
if(child.$children){
findFormItem(child.$children);
}
});
}
findFormItem(children);
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 调用校验方法
<template>
<form @submit.prevent>
<slot></slot>
</form>
</template>
<script>
export default {
name: "elForm",
provide() {
return { elForm: this };
},
props: {
model: {
type: Object,
default: () => ({})
},
rules: Object
},
methods: {
async validate(cb) {
let children = this.$broadcast("elFormItem");
try{
await Promise.all(children.map(child=>child.validate()));
cb(true);
}catch{
cb(false)
}
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 改写校验方法
methods: {
validate() {
if (this.prop) {
let rule = this.elForm.rules[this.prop];
let newValue = this.elForm.model[this.prop];
let descriptor = {
[this.prop]: rule
};
let schema = new Schema(descriptor);
return schema.validate({ [this.prop]: newValue }, (err, res) => {
if (err) {
this.errorMessage = err[0].message;
} else {
this.errorMessage = "";
}
});
}
}
},
mounted() {
this.$on("validate", () => {
this.validate();
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
补充:关闭
eslint
module.exports = {
devServer: {
overlay: {
warnings: false,
errors: false
}
}
}
2
3
4
5
6
7
8
# render
函数的应用
# 模板缺陷
模板的最大特点是扩展难度大,不易扩展。可能会造成逻辑冗余
<Level :type="1">哈哈</Level>
<Level :type="2">哈哈</Level>
<Level :type="3">哈哈</Level>
2
3
Level组件需要对不同的type产生不同的标签
<template>
<h1 v-if="type==1">
<slot></slot>
</h1>
<h2 v-else-if="type==2">
<slot></slot>
</h2>
<h3 v-else-if="type==3">
<slot></slot>
</h3>
</template>
<script>
export default {
props: {
type: {
type: Number
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用Render函数
export default {
render(h) {
return h("h" + this.type, {}, this.$slots.default);
},
props: {
type: {
type: Number
}
}
};
2
3
4
5
6
7
8
9
10
复杂的逻辑变得非常简单
# 函数式组件
如果只是接受一些 prop 的话可以标记为functional
; route-view
组件;
Vue.component('my-component', {
functional: true,
props: {},
render: function (createElement, context) {
// ...
}
})
2
3
4
5
6
7
函数式组件只是函数,所以渲染开销也低很多。 (没有this、没有状态、没有生命周期)
# 作用域插槽
# render函数的应用
如果我们想定制化一个列表的展现结构,我们可以使用render函数来实现
<List :data="data"></List>
<script>
import List from "./components/List";
export default {
data() {
return { data: ["苹果", "香蕉", "橘子"] };
},
components: {
List
}
};
</script>
<!-- List组件渲染列表 -->
<template>
<div class="list">
<div v-for="(item,index) in data" :key="index">
<li>{{item}}</li>
</div>
</div>
</template>
<script>
export default {
props: {
data: Array,
default: () => []
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
通过render方法来订制组件,在父组件中传入render方法
<List :data="data" :render="render"></List>
render(h, name) {
return <span>{name}</span>;
}
2
3
4
我们需要createElement方法,就会想到可以编写个render函数,将createElement方法传递出来
<template>
<div class="list">
<div v-for="(item,index) in data" :key="index">
<li v-if="!render">{{item}}</li>
<!-- 将render方法传到函数组件中,将渲染项传入到组件中,在内部回调这个render方法 -->
<ListItem v-else :item="item" :render="render"></ListItem>
</div>
</div>
</template>
<script>
import ListItem from "./ListItem";
export default {
components: {
ListItem
},
props: {
render: {
type: Function
},
data: Array,
default: () => []
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ListItem.vue调用最外层的render方法,将createElement和当前项传递出来
<script>
export default {
props: {
render: {
type: Function
},
item: {}
},
render(h) {
return this.render(h, this.item);
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
# 使用scope-slot
我们不难发现使用render函数确实可以大大提高灵活度,但是代码量偏多,这样我们可以使用作用域插槽来简化逻辑
<List :arr="arr">
<template v-slot="{item}">
{{item}}
</template>
</List>
<div v-for="(item,key) in arr" :key="key">
<slot :item="item"></slot>
</div>
2
3
4
5
6
7
8
9
目前像
iview
已经支持render函数和作用域插槽两种写法
# 递归组件的应用
# 案例:实现无限极菜单组件
# 使用模板来实现
<el-menu>
<template v-for="d in data">
<resub :data="d" :key="d.id"></resub>
</template>
</el-menu>
<el-submenu :key="data.id" v-if="data.children">
<template slot="title">{{data.title}}</template>
<template v-for="d in data.children">
<resub :key="d.id" :data="d"></resub>
</template>
</el-submenu>
<el-menu-item :key="data.id" v-else>{{data.title}}</el-menu-item>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用render函数来实现
import elMenu from "./components/el-menu.vue";
import elMenuItem from "./components/el-menu-item.vue";
import elSubmenu from "./components/el-submenu.vue";
export default {
props:{
data:{
type:Array,
default:()=>[]
}
},
render(){ // react语法
let renderChildren = (data) =>{
return data.map(child=>{
return child.children?
<elSubmenu>
<div slot="title">{child.title}</div>
{renderChildren(child.children)}
</elSubmenu>:
<elMenuItem nativeOnClick={()=>{
alert(1)
}}>{child.title}</elMenuItem>
})
}
return <elMenu>
{renderChildren(this.data)}
</elMenu>
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28