# Vue 知识点

# vue 中 8 种组件通信方式

# props / $emit

父组件通过props的方式向子组件传递数据,而通过$emit子组件可以向父组件通信。

# 1. 父组件向子组件传值

demo:


// 父组件

<template>
	<div class='section'>
		<com-article :articles='articleList'></com-article>
	</div>
</template>

<script>
import comArticle from './test/article.vue';

export default {
	name: 'HelloWorld',
	component: {
		comArticle
	}
	data() {
		return {
			articleList: [1,2,3]
		}
	}

}

</script>

<template>
	<div>
		 <span v-for="(item, index) in articles" :key="index">{{item}}</span>
	</div>
</template>

<script>
export default {
  	props: {
		articles: {
			type: Array
		}
	}
}
</script>

总结: prop 只可以从上一级组件传递到下一级组件(父子组件), 即所谓的单向数据流。而且 prop 只读,不可修改,所有修改都会失效并且警告。

# 2. 子组件向父组件传值

$emit绑定一个自定义事件,当这个语句被执行时,就会将参数 arg 传递给父组件,父组件通过 v-on 监听并接受参数。

demo: 在上个例子的基础上, 点击页面渲染出来的 ariticle 的 item, 父组件中显示在数组中的下标


// 父组件

<template>
	<div class='section'>
		<com-article :articles='articleList' @onEmitIndex="onEmitIndex"></com-article>
	</div>
</template>

<script>
import comArticle from './test/article.vue';

export default {
	name: 'HelloWorld',
	component: {
		comArticle
	}
	data() {
		return {
			articleList: [1,2,3]
		}
	}

}

</script>

<template>
	<div>
		 <span v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</span>
	</div>
</template>

<script>
export default {
  	props: {
		articles: {
			type: Array
		}
	}
	methods: {
		emitIndex(index) {
			this.$emit('onEmitIndex',index)
		}
	}
}
</script>

# $chidlren / $parent

通过 $parent$children 就可以访问组件的实例,拿到实例代表什么? 代表就可以访问此组件的所有方法和data 。接下来就是怎么实现拿到指定组件的实例

demo:

// 父组件

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
    <button @click="handleChildren">++++</button>
  </div>
</template>

<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
  name: "home",
  components: {
    HelloWorld
  },
  data() {
    return {
      parentText: "父组件数据"
    };
  },
  methods: {
    handleChildren() {
      console.log(this.$children);
      this.$children[0].name = "hanzo";
    }
  }
};
</script>
// 子组件

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>{{ name }}</p>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      name: "ahao"
    };
  },
  created() {
    console.log(this.$parent.parentText); // 父组件数据
  }
};
</script>

# provide / inject

概念:

provid / injectvue2.2.0 新增的 api,简单来说就是父组件中通过provide来提供变量,然后再子组件中通过inject来注入变量

注意:这里不论子组件嵌套多深,只要调用了 inject 那么就可以注入 provide 中的数据,而不局限于只能从当前父组件的 props 属性中读取数据

demo:

// 父组件

<template>
  <div class="home">
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
  name: "home",
  components: {
    HelloWorld
  },
  provide: {
    name: "ahao"
  }
};
</script>
// 子组件
<template>
  <div class="hello">
    <p>{{ name }}</p>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  inject: ["name"]
};
</script>

# ref / refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据

demo:

// 父组件

<template>
  <div class="home">
    <HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
  name: "home",
  components: {
    HelloWorld
  },

  mounted() {
    console.log(this.$refs.HelloWorld.name); // i am HelloWorld
    this.$refs.HelloWorld.sayHello();
  }
};
</script>
// 子组件

<template>
  <div class="hello">
    <p>{{ name }}</p>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      name: "i am HelloWorld"
    };
  },
  methods: {
    sayHello() {
      console.log("hello");
    }
  }
};
</script>

# eventBus

eventBus 又称为事件总线,在 vue 中可以使用它来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以通知其他组件。

eventBus 也有不方便之处,当项目较大,就容易造成难以维护的灾难

在 Vue 的项目中怎么使用eventBus来实现组件之间的数据通信呢?具体通过下面几个步骤

  1. 初始化

首先需要创建一个事件总线并将其导出,以便其他模块可以使用或者监听它;

// 1. 创建eventBus.js
import Vue from "vue";
export const EventBus = new Vue();
  1. 创建 AB 两个组件,以兄弟组件为例
// 父组件

<template>
  <div class="home">
    <HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App"/>
    <A></A>
    <B></B>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'
import A from '@/components/A.vue';
import B from '@/components/B.vue';

export default {
  name: 'home',
  components: {
    HelloWorld,
    A,
    B
  },
}
</script>

  1. 发送事件
// A 组件 发送事件

<template>
    <div>
        <p>i am A</p>
        <button @click='add'>A+</button>
        <p>{{num}}</p>
    </div>
</template>

<script>
import { EventBus } from './../eventBus';
export default {
    name: 'A',
    data() {
        return {
            num: 0,
        }
    },
    methods: {
        add() {
            EventBus.$emit('addition',{
                num: this.num++
            })
        }
    },
}
</script>

  1. 接收事件
// B 组件  接收事件

<template>
    <div>
        <p>i am B</p>
        <p>{{count}}</p>
    </div>
</template>

<script>
import { EventBus } from './../eventBus';
export default {
    name: 'B',
    data() {
        return {
            count: 0,
        }
    },
    mounted() {
        EventBus.$on('addition', params => {
            console.log(params);
            this.count = params.num;
        })
    },
}
</script>

  1. 移除事件监听者
// 在组件销毁生命周期中 使用 EventBus.off('事件名称')

<template>
    <div>
        <p>i am B</p>
        <button>B+</button>
        <p>{{count}}</p>
    </div>
</template>

<script>
import { EventBus } from './../eventBus';

export default {
    name: 'B',
    data() {
        return {
            count: 0,
        }
    },
    mounted() {
        EventBus.$on('addition', params => {
            console.log(params);
            this.count = params.num;
        })
    },
    destroyed() {
        EventBus.$off('addition',{})
    }
}
</script>

# Vuex

vuex 就不概述了,最常用的状态管理方法

# LocalStorage / SessionStorage

这种通信比较简单,缺点就是数据和状态比较混乱,不太容易维护。通过 window.localStorage.getItem(key) 获取数据,通过window.localStorage.setIten(key,val)存储数据

注意用JSON.parse() / JSON.stringify()做数据格式转换 localStorage / SessionStorage 可以结合vuex,实现数据的持久保存,同时使用 vuex 解决数据和状态混乱问题.

# $attrs 与 $listeners

现在我们来讨论一种情况, 我们一开始给出的组件关系图中 A 组件与 D 组件是隔代关系, 那它们之前进行通信有哪些方式呢?

  1. 使用props绑定来进行一级一级的信息传递,如果 D 组件中状态改变需要传递数据给 A,使用事件系统一级级往上传递.
  2. 使用eventBus,这种情况下还是比较适合使用,但是碰到多人合作开发时,代码维护性比较低,可读性也比较低.
  3. 使用vuex来进行数据管理,但是如果仅仅是传递数据,而不做中间处理,使用vuex处理感觉有点大材小用了.

vue2.4中,为了解决改需求,引入了$attrs$listeners,新增了inheritAttrs选项。

在版本 2.4 以前,默认情况下,父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外),将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。接下来看一个跨级通信的

# Demo:

  • 父组件
// 父组件
<template>
  <div class="home">
    <HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App"/>
    <Child1
      :name='name'
      :age='age'
      :gender='gender'
    ></Child1>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'
import Child1 from '@/components/child1.vue'

export default {
  name: 'home',
  components: {
    HelloWorld,
    Child1
  },
  data() {
    return {
      name: 'ahao',
      age: 24,
      gender: '男',
    }
  }
}
</script>

  • Home 组件中的子组件 Child1
<template>
    <div>
        <p>i am child1</p>
        <Child2 v-bind="$attrs"></Child2>
    </div>
</template>

<script>
import Child2 from '@/components/child2.vue'
export default {
    name: 'child1',
    components: {
        Child2
    },
    created() {
        console.log(this.$attrs); // {name: "ahao", age: 24, gender: "男"}
    }
}
</script>

  • Child1 组件中的子组件 Child2
<template>
    <div>
        <p>i am child2</p>
        <p>{{$attrs.name}}</p>
        <p>{{$attrs.age}}</p>
        <p>{{$attrs.gender}}</p>
    </div>
</template>

<script>
export default {
    name: 'child2',
    created() {
        console.log(this.$attrs)
    }
}
</script>

# 总结

常见使用场景可以分为三类:

  • 父子组件通信:props $parent / $children provide / inject ref / refs $attrs / $listeners
  • 兄弟组件通信:eventBus Vuex
  • 跨级通信:eventBus Vuex provide / inject $attrs / $linsteners

# Vue 中 axios 的封装

# 采用 axios 官方推荐的,通过配置项创建 axios 实例的方式进行配置封装。

// http.js
import axios from "axios";

// 创建 axios 实例
const service = axios.create({
  // 配置项
});

# 根据环境设置 baseURL

baseURL属性是请求地址前缀,将自动加在 url 前面,除非 url 是个绝对地址。在正常情况下,在开发环境下和生产环境下有着不同的 baseURL,所以,我们需要根据不同的环境切换不同的 baseURL。

在开发模式下,由于有着 devServer 的存在,需要根据固定的 url 前缀进行请求地址重写,所以,在开发环境下,将 baseURL 设为某个固定的值,比如:/apis。 在生产模式下,根据 Java 模块的请求前缀的不同,可以设置不同的 baseURL。

// 根据 process.env.NODE_ENV 区分状态,切换不同的baseURL

const service = axios.create({
  baseURL: process.env.NODE_ENV === "production" ? "h5.mao.com" : "m.mao.com",
})

### 统一设置请求头

```js
const service = axios.create({
  ...
      headers: {
        get: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        },
        post: {
          'Content-Type': 'application/json;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        }
      }
})

# 跨域、超时、响应码处理

axios 中,提供是否允许跨域的属性——withCredentials,以及配置超时时间的属性——timeout,通过这两个属性,可以轻松处理跨域和超时的问题。

axios 提供了 validateStatus 属性,用于定义对于给定的 HTTP 响应状态是 resolve 或 reject promise. 所以,正常设置的情况下,我们会将状态 2 系列或者 304 的请求设为 reslove 状态,其余的位 reject 状态. 结果就是,我们可以在业务代码里,使用 catch 统一捕获响应错误的请求,从而进行统一处理。

const service = axios.create({
  // 跨域请求时是否需要使用凭证
  withCredentials: true
  // 请求30s超时
  timeout: 30000,
  validateStatus: function() {
    // 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
    return true
  }
});

# 请求、响应处理

在不使用axios的情况下,每次请求或者接受响应,都需要将请求或响应序列化

而在axios中, transformRequest允许在向服务器发送请求前,修改请求数据; transformResponse在传递给then/catch前,允许修改响应数据.

通过这两个钩子,可以省去大量重复的序列化代码.

const service = axios.create({
  // 在向服务器发送请求前,序列化请求数据
  transformResquest: [function(data) {
    data = JSON.stringify(data)
    return data
  }],
  // 在传递给 then/catch 前,修改响应数据
  transformResponse: [function(data) {
    if(typeof data === "string" && data.startsWith('{')) {
      data = JSON.parse(data)
    }
    return data
  }]
})

# 拦截器

拦截器,分为请求拦截器以及响应拦截器,分别在请求或响应被then或catch处理前拦截它们.

之前提到过,由于 async-await 中 catch 难以处理的问题,所以将出错的情况也作为 resolve 状态进行处理。但这带来了一个问题,请求或响应出错的情况下,结果没有数据协议中定义的 msg 字段(消息)。所以,我们需要在出错的时候,手动生成一个符合返回格式的返回数据。 由于,在业务中,没有需要在请求拦截器中做额外处理的需求,所以,请求拦截器的 resolve 状态,只需直接返回就可以了。

请求拦截器代码如下:

// 请求拦截器
service.interceptors.request.use((config) => {
	return config
}, (error) => {
	// 错误抛到业务代码
    error.data = {}
    error.data.msg = '服务器异常,请联系管理员!'
    return Promise.resolve(error)
})

再来看看响应拦截器,还是之前的那个问题,除了请求或响应错误,还有一种情况也会导致返回的消息体不符合协议规范,那就是状态码不为 2 系列或 304 时。此时,我们还是需要做一样的处理——手动生成一个符合返回格式的返回数据。但是,有一点不一样,我们还需要根据不同的状态码生成不同的提示信息,以方便处理上线后的问题

响应拦截器代码如下:

// 根据不同的状态码,生成不同的提示信息
const showStatus = (status) => {
    let message = ''
    // 这一坨代码可以使用策略模式进行优化
    switch (status) {
        case 400:
            message = '请求错误(400)'
            break
        case 401:
            message = '未授权,请重新登录(401)'
            break
        case 403:
            message = '拒绝访问(403)'
            break
        case 404:
            message = '请求出错(404)'
            break
        case 408:
            message = '请求超时(408)'
            break
        case 500:
            message = '服务器错误(500)'
            break
        case 501:
            message = '服务未实现(501)'
            break
        case 502:
            message = '网络错误(502)'
            break
        case 503:
            message = '服务不可用(503)'
            break
        case 504:
            message = '网络超时(504)'
            break
        case 505:
            message = 'HTTP版本不受支持(505)'
            break
        default:
            message = `连接出错(${status})!`
    }
    return `${message},请检查网络或联系管理员!`
}

// 响应拦截器
service.interceptors.response.use((response) => {
  const status = response.status
  let msg = '';
  if(status < 200 || status >= 300) {
    // 处理http错误,抛到业务代码
    msg = showStatus(status);
    if(typeof response.data === "string") {
      response.data = { msg }
    } else {
      response.data = msg;
    }
  }
  return response
},(error) => {
  // 错误抛到业务代码
  error.data = {}
  error.data.msg = '请求超时或服务器异常,请检查网络或联系管理员!'
  return Promise.resolve(err);
})
  • 有一些业务相关的需求,可以加在拦截器中,比如:loading、鉴权等~

# 完整代码

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const showStatus = (status) => {
  let message = ''
  switch (status) {
    case 400:
      message = '请求错误(400)'
      break
    case 401:
      message = '未授权,请重新登录(401)'
      break
    case 403:
      message = '拒绝访问(403)'
      break
    case 404:
      message = '请求出错(404)'
      break
    case 408:
      message = '请求超时(408)'
      break
    case 500:
      message = '服务器错误(500)'
      break
    case 501:
      message = '服务未实现(501)'
      break
    case 502:
      message = '网络错误(502)'
      break
    case 503:
      message = '服务不可用(503)'
      break
    case 504:
      message = '网络超时(504)'
      break
    case 505:
      message = 'HTTP版本不受支持(505)'
      break
    default:
      message = `连接出错(${status})!`
  }
  return `${message},请检查网络或联系管理员!`
}

const service = axios.create({
  // 联调
  baseURL: process.env.NODE_ENV === 'production' ? `/` : '/apis',
  headers: {
    get: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
    },
    post: {
      'Content-Type': 'application/json;charset=utf-8'
    }
  },
  // 是否跨站点访问控制请求
  withCredentials: true,
  timeout: 30000,
  transformRequest: [(data) => {
    data = JSON.stringify(data)
    return data
  }],
  validateStatus () {
    // 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
    return true
  },
  transformResponse: [(data) => {
    if (typeof data === 'string' && data.startsWith('{')) {
      data = JSON.parse(data)
    }
    return data
  }]
})

// 请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    return config
}, (error) => {
    // 错误抛到业务代码
    error.data = {}
    error.data.msg = '服务器异常,请联系管理员!'
    return Promise.resolve(error)
})

// 响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
    const status = response.status
    let msg = ''
    if (status < 200 || status >= 300) {
        // 处理http错误,抛到业务代码
        msg = showStatus(status)
        if (typeof response.data === 'string') {
            response.data = {msg}
        } else {
            response.data.msg = msg
        }
    }
    return response
}, (error) => {
    // 错误抛到业务代码
    error.data = {}
    error.data.msg = '请求超时或服务器异常,请检查网络或联系管理员!'
    return Promise.resolve(error)
})

export default service

最后直接在main.js 写入 Vue.prototype.$http = service

# Vue中封装一个Toast插件

# 实现Vue的MVVM

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{a.a}}</p>
        <p>{{b}}</p>
        <input type="text" v-model="b">
    </div>
    <script src="./vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                a: {
                    a: 1
                },
                b: 'hanzo'
            }
        })
    </script>
</body>
</html>
function Vue(options={}) {
    this.$options = options;
    let data = this.$data = this.$options.data;
    observe(data)
    // this代理了this.$data
    for(let key in data) {
        Object.defineProperty(this,key,{
            enumerable: true,
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }
    new Compile(this.$options.el,this)
}

function Compile(el,vm) {
    // el 表示 dom
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    while(child = vm.$el.firstChild) { // 将app的内存 移入到内存中
        fragment.appendChild(child);
    }
    console.log(fragment.childNodes)
    replace(fragment)
    function replace(fragment) {
        Array.from(fragment.childNodes).forEach(function(node) {
            let text = node.textContent
            let reg = /\{\{(.*)\}\}/;
            if(node.nodeType === 3 && reg.test(text)) {
                let arr = RegExp.$1.split('.');
                console.log(arr);
                let val = vm;
                arr.forEach(function(key) { // 取this.a.a
                    val = val[key]
                });
                new Watcher(vm,RegExp.$1,function(newVal) { // 函数里需要接受一个新值
                    node.textContent = text.replace(reg,newVal);
                })   
                // 替换的逻辑
                node.textContent = text.replace(reg,val);
            }
            if(node.nodeType === 1) {
                // 元素节点
                let nodeAttrs = node.attributes; // 获取当前dom节点属性
                console.log(nodeAttrs)
                Array.from(nodeAttrs).forEach(function(attr) {
                    let name = attr.name;
                    let exp = attr.value;
                    if(name.indexOf('v-') == 0) { // v-model
                        node.value = vm[exp]
                    }
                    new Watcher(vm,exp,function(newVal) {
                        node.value = vm[exp]; // 当wacther触发时 会自动将内容放到输入框内
                    });
                    node.addEventListener('input',function(e) {
                        let newVal = e.target.value;
                        vm[exp] = newVal;
                    })
                })


            }
            if(node.childNodes) {
                replace(node);
            }
        })
    }
    vm.$el.appendChild(fragment);
}


// 观察对象给对象增加Object.defineProperty
function Observe(data) {
    let dep = new Dep();
    for(let key in data) {
        let val = data[key];
        observe(val);
        Object.defineProperty(data,key,{
            enumerable: true,
            get() {
                Dep.target && dep.addSub(Dep.target);
                return val;
            },
            set(newVal) {
                if(newVal === val) {
                    return;
                }
                val = newVal;
                observe(newVal);
                dep.notify(); // 让所有的wacther的update方法执行即可
            }
        })
    }
}

function observe(data) {
    if(typeof data != 'object') return;
    return new Observe(data)
}

// 发布订阅模式
function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function(sub) { // 订阅
    this.subs.push(sub)
}

Dep.prototype.notify = function() {
    this.subs.forEach(function(sub) {
        sub.update();
    })
}

function Watcher(vm,expr,fn) {
    this.vm = vm;
    this.expr = expr; //添加到订阅中
    this.fn = fn;
    Dep.target = this;
    let val = vm;
    let arr = expr.split('.');
    arr.forEach(function(k) {
        val = val[k];
    })
    Dep.target = null;
}
Watcher.prototype.update = function() {
    let val = this.vm;
    let arr = this.expr.split('.');
    arr.forEach(function(k) {
        val = val[k];
    })
    this.fn(val);
}