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
1
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>
1
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>
1
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);  // 删除索引,属性
1
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);
1
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() 
 * }
**/
1
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() }) 
 *  }
**/;
1
2
3
4
5
6

# v-for为什么要加key

为了在比对过程中进行复用

diff-key.5862ebbc

# 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)
}
1
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>
1
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)
    }
})
1
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)
})
1
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>
1
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)
        });
    }
}
1
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; // 获取链接
            });
        }
    }
}
1
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)
        }
    }
}
1
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');
        }); 
    }
}
1
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));
1
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
1
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
1
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
1
2
3
4
5

# Vue组件通信

# 常见组件通信方式

  • 1)props$emit 父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
    1. $attrs$listeners A->B->C。Vue 2.4 开始提供了$attrs$listeners来解决这个问题
    1. $parent,$children
    1. $refs 获取实例
    1. 父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。
    1. envetBus 平级组件数据传递 这种情况下可以使用中央事件总线的方式
    1. vuex状态管理

# 案例:Vue组件在线编辑器

  • 通过props、events 实现父子组件通信
  • 通过ref属性获取组件实例

img

# 掌握组件的基本概念

import Vue from 'vue'
import App from './App.vue'
new Vue({render: h => h(App)}).$mount('#app')
1
2
3

h我们一般称为createElement,这里我们可以用他来渲染组件,App其实就是一个组件 (就是一个对象而已)

<template>
  <div id="app"></div>
</template>

<script>
export default {
  name: 'App',
}
</script>
<style lang="stylus"></style>
1
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>
1
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>
1
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>
1
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
        }
    }
};
1
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 + '  ';
            }
        }
    }
};
1
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;
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11

这个其实就是典型的发布订阅模式,先在组件自己身上绑定事件(绑定的事件为父组件事件),稍后触发自己身上的事件,将数据传入给父组件的函数中,达到子父通信的效果

# 将数据传递给儿子组件

通过属性的方式将数据传递给儿子组件

<Show :code="code"></Show>
1

子组件接受传递过来的数据

export default {
    props:{
        code:{
            type:String,
            code:''
        }
    }
}
1
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>
1
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>
1
2
3
4
<Edit @input="handleInput" @run="handleRun"></Edit>
1

这里我们可以在父组件中监控到组件点击事件了。我们需要在父组件中调用Show组件中的run方法

<Show :code="code" ref="show"></Show>
//使用:this.$refs.show.run()  this.$refs[show].run()
1
2

# 解析代码

<div class="show-box" ref="display"></div>
1
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)
    }
}
1
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>
1
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>
1
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>
1
2
3
4
5
6
7
8

el-form-item

<template>
    <div><slot></slot></div>
</template>
<script>
export default {
    name:'elFormItem'
}
</script>
1
2
3
4
5
6
7
8

el-input

<template>
    <input type="text">
</template>
<script>
export default {
    name:'elInput'
}
</script>
1
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>
1
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>
1
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>
1
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
    }
}
1
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');
}
1
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>
1
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;
}
1
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>
1
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();
    });
}
1
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
      }
    }
}
1
2
3
4
5
6
7
8

# render函数的应用

# 模板缺陷

模板的最大特点是扩展难度大,不易扩展。可能会造成逻辑冗余

<Level :type="1">哈哈</Level>
<Level :type="2">哈哈</Level>
<Level :type="3">哈哈</Level>
1
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>
1
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
  }
 }
};
1
2
3
4
5
6
7
8
9
10

复杂的逻辑变得非常简单

# 函数式组件

如果只是接受一些 prop 的话可以标记为functionalroute-view组件;

Vue.component('my-component', {
  functional: true,
  props: {},
  render: function (createElement, context) {
    // ...
  }
})
1
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>
1
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>;
}
1
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>
1
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>
1
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>
1
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>
1
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>
    }
}
1
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

# 异步组件

上次更新: 2022/04/15, 05:41:33
×